Skip to content

Commit 352e1a9

Browse files
authored
quoted-strings: Add allow-quoted-quotes option
Allows strings like `'foo"bar'` on `quote-type: double` and vice versa.
1 parent e319a17 commit 352e1a9

File tree

2 files changed

+146
-5
lines changed

2 files changed

+146
-5
lines changed

tests/rules/test_quoted_strings.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,3 +453,106 @@ def test_octal_values(self):
453453
'- "0o800"\n',
454454
conf,
455455
problem1=(9, 3), problem2=(10, 3))
456+
457+
def test_allow_quoted_quotes(self):
458+
conf = ('quoted-strings: {quote-type: single,\n'
459+
' required: false,\n'
460+
' allow-quoted-quotes: false}\n')
461+
self.check('---\n'
462+
'foo1: "[barbaz]"\n' # fails
463+
'foo2: "[bar\'baz]"\n', # fails
464+
conf, problem1=(2, 7), problem2=(3, 7))
465+
466+
conf = ('quoted-strings: {quote-type: single,\n'
467+
' required: false,\n'
468+
' allow-quoted-quotes: true}\n')
469+
self.check('---\n'
470+
'foo1: "[barbaz]"\n' # fails
471+
'foo2: "[bar\'baz]"\n',
472+
conf, problem1=(2, 7))
473+
474+
conf = ('quoted-strings: {quote-type: single,\n'
475+
' required: true,\n'
476+
' allow-quoted-quotes: false}\n')
477+
self.check('---\n'
478+
'foo1: "[barbaz]"\n' # fails
479+
'foo2: "[bar\'baz]"\n', # fails
480+
conf, problem1=(2, 7), problem2=(3, 7))
481+
482+
conf = ('quoted-strings: {quote-type: single,\n'
483+
' required: true,\n'
484+
' allow-quoted-quotes: true}\n')
485+
self.check('---\n'
486+
'foo1: "[barbaz]"\n' # fails
487+
'foo2: "[bar\'baz]"\n',
488+
conf, problem1=(2, 7))
489+
490+
conf = ('quoted-strings: {quote-type: single,\n'
491+
' required: only-when-needed,\n'
492+
' allow-quoted-quotes: false}\n')
493+
self.check('---\n'
494+
'foo1: "[barbaz]"\n' # fails
495+
'foo2: "[bar\'baz]"\n', # fails
496+
conf, problem1=(2, 7), problem2=(3, 7))
497+
498+
conf = ('quoted-strings: {quote-type: single,\n'
499+
' required: only-when-needed,\n'
500+
' allow-quoted-quotes: true}\n')
501+
self.check('---\n'
502+
'foo1: "[barbaz]"\n' # fails
503+
'foo2: "[bar\'baz]"\n',
504+
conf, problem1=(2, 7))
505+
506+
conf = ('quoted-strings: {quote-type: double,\n'
507+
' required: false,\n'
508+
' allow-quoted-quotes: false}\n')
509+
self.check("---\n"
510+
"foo1: '[barbaz]'\n" # fails
511+
"foo2: '[bar\"baz]'\n", # fails
512+
conf, problem1=(2, 7), problem2=(3, 7))
513+
514+
conf = ('quoted-strings: {quote-type: double,\n'
515+
' required: false,\n'
516+
' allow-quoted-quotes: true}\n')
517+
self.check("---\n"
518+
"foo1: '[barbaz]'\n" # fails
519+
"foo2: '[bar\"baz]'\n",
520+
conf, problem1=(2, 7))
521+
522+
conf = ('quoted-strings: {quote-type: double,\n'
523+
' required: true,\n'
524+
' allow-quoted-quotes: false}\n')
525+
self.check("---\n"
526+
"foo1: '[barbaz]'\n" # fails
527+
"foo2: '[bar\"baz]'\n", # fails
528+
conf, problem1=(2, 7), problem2=(3, 7))
529+
530+
conf = ('quoted-strings: {quote-type: double,\n'
531+
' required: true,\n'
532+
' allow-quoted-quotes: true}\n')
533+
self.check("---\n"
534+
"foo1: '[barbaz]'\n" # fails
535+
"foo2: '[bar\"baz]'\n",
536+
conf, problem1=(2, 7))
537+
538+
conf = ('quoted-strings: {quote-type: double,\n'
539+
' required: only-when-needed,\n'
540+
' allow-quoted-quotes: false}\n')
541+
self.check("---\n"
542+
"foo1: '[barbaz]'\n" # fails
543+
"foo2: '[bar\"baz]'\n", # fails
544+
conf, problem1=(2, 7), problem2=(3, 7))
545+
546+
conf = ('quoted-strings: {quote-type: double,\n'
547+
' required: only-when-needed,\n'
548+
' allow-quoted-quotes: true}\n')
549+
self.check("---\n"
550+
"foo1: '[barbaz]'\n" # fails
551+
"foo2: '[bar\"baz]'\n",
552+
conf, problem1=(2, 7))
553+
554+
conf = ('quoted-strings: {quote-type: any}\n')
555+
self.check("---\n"
556+
"foo1: '[barbaz]'\n"
557+
"foo2: '[bar\"baz]'\n",
558+
conf)

yamllint/rules/quoted_strings.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
``required: false`` and ``required: only-when-needed``.
3131
* ``extra-allowed`` is a list of PCRE regexes to allow quoted string values,
3232
even if ``required: only-when-needed`` is set.
33+
* ``allow-quoted-quotes`` allows (``true``) using disallowed quotes for strings
34+
with allowed quotes inside. Default ``false``.
3335
3436
**Note**: Multi-line strings (with ``|`` or ``>``) will not be checked.
3537
@@ -43,6 +45,7 @@
4345
required: true
4446
extra-required: []
4547
extra-allowed: []
48+
allow-quoted-quotes: false
4649
4750
.. rubric:: Examples
4851
@@ -112,6 +115,26 @@
112115
113116
- "localhost"
114117
- this is a string that needs to be QUOTED
118+
119+
#. With ``quoted-strings: {quote-type: double, allow-quoted-quotes: false}``
120+
121+
the following code snippet would **PASS**:
122+
::
123+
124+
foo: "bar\"baz"
125+
126+
the following code snippet would **FAIL**:
127+
::
128+
129+
foo: 'bar"baz'
130+
131+
#. With ``quoted-strings: {quote-type: double, allow-quoted-quotes: true}``
132+
133+
the following code snippet would **PASS**:
134+
::
135+
136+
foo: 'bar"baz'
137+
115138
"""
116139

117140
import re
@@ -125,11 +148,13 @@
125148
CONF = {'quote-type': ('any', 'single', 'double'),
126149
'required': (True, False, 'only-when-needed'),
127150
'extra-required': [str],
128-
'extra-allowed': [str]}
151+
'extra-allowed': [str],
152+
'allow-quoted-quotes': bool}
129153
DEFAULT = {'quote-type': 'any',
130154
'required': True,
131155
'extra-required': [],
132-
'extra-allowed': []}
156+
'extra-allowed': [],
157+
'allow-quoted-quotes': False}
133158

134159

135160
def VALIDATE(conf):
@@ -177,6 +202,12 @@ def _quotes_are_needed(string):
177202
return True
178203

179204

205+
def _has_quoted_quotes(token):
206+
return ((not token.plain) and
207+
((token.style == "'" and '"' in token.value) or
208+
(token.style == '"' and "'" in token.value)))
209+
210+
180211
def check(conf, token, prev, next, nextnext, context):
181212
if not (isinstance(token, yaml.tokens.ScalarToken) and
182213
isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken,
@@ -206,13 +237,18 @@ def check(conf, token, prev, next, nextnext, context):
206237
if conf['required'] is True:
207238

208239
# Quotes are mandatory and need to match config
209-
if token.style is None or not _quote_match(quote_type, token.style):
240+
if (token.style is None or
241+
not (_quote_match(quote_type, token.style) or
242+
(conf['allow-quoted-quotes'] and _has_quoted_quotes(token)))):
210243
msg = "string value is not quoted with %s quotes" % quote_type
211244

212245
elif conf['required'] is False:
213246

214247
# Quotes are not mandatory but when used need to match config
215-
if token.style and not _quote_match(quote_type, token.style):
248+
if (token.style and
249+
not _quote_match(quote_type, token.style) and
250+
not (conf['allow-quoted-quotes'] and
251+
_has_quoted_quotes(token))):
216252
msg = "string value is not quoted with %s quotes" % quote_type
217253

218254
elif not token.style:
@@ -235,7 +271,9 @@ def check(conf, token, prev, next, nextnext, context):
235271
quote_type)
236272

237273
# But when used need to match config
238-
elif token.style and not _quote_match(quote_type, token.style):
274+
elif (token.style and
275+
not _quote_match(quote_type, token.style) and
276+
not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))):
239277
msg = "string value is not quoted with %s quotes" % quote_type
240278

241279
elif not token.style:

0 commit comments

Comments
 (0)