Skip to content

Commit 6fcdc82

Browse files
Timo Schraderkleinschrader
authored andcommitted
quoted-strings: Add allow-double-quotes-for-escaping
1 parent e3b72f5 commit 6fcdc82

File tree

2 files changed

+101
-8
lines changed

2 files changed

+101
-8
lines changed

tests/rules/test_quoted_strings.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,55 @@ def test_quote_type_consistent(self):
227227
conf, problem1=(3, 10), problem2=(5, 18), problem3=(6, 11),
228228
problem4=(8, 10), problem5=(10, 3), problem6=(12, 3))
229229

230+
def test_allow_double_quotes_for_escaping(self):
231+
conf = ('quoted-strings:\n'
232+
' quote-type: single\n'
233+
' allow-double-quotes-for-escaping: true\n')
234+
self.check('---\n'
235+
"string1: 'foo'\n"
236+
'string2: "bar\\nstool"\n'
237+
'string3: "baz"\n' # fails
238+
"string4: 'quux'\n",
239+
conf, problem1=(4, 10))
240+
241+
conf = ('quoted-strings:\n'
242+
' quote-type: single\n'
243+
' allow-double-quotes-for-escaping: true\n'
244+
' check-keys: true\n')
245+
246+
self.check('---\n'
247+
"'string1': 'foo'\n"
248+
"\"string2\\tsplit\": 'foo'\n"
249+
"'string3': \"foo\\nbar\"\n"
250+
"string4: 'bar'\n" # fails
251+
"\"string5\": 'baz'\n" # fails
252+
"'string6': {'key': 'val'}\n"
253+
"'string7': \"foo\"\n",
254+
conf, problem1=(5, 1), problem2=(6, 1), problem3=(8, 12))
255+
256+
conf = ('quoted-strings:\n'
257+
' quote-type: single\n'
258+
' allow-double-quotes-for-escaping: true\n'
259+
' check-keys: true\n'
260+
' required: false\n')
261+
self.check('---\n'
262+
'string1: \'foo\'\n'
263+
'string2: "bar"\n' # fails
264+
'string3: \'baz\'\n'
265+
'string4: {\'key\': "val"}\n' # fails
266+
'string5: {"key": \'val\'}\n' # fails
267+
'string6:\n'
268+
' \'key\': "val"\n' # fails
269+
'string7:\n'
270+
' "key": \'val\'\n' # fails
271+
'string8:\n'
272+
' "string"\n' # fails
273+
'string9: >\n'
274+
' "string"\n'
275+
'string10: "bar\\nstool"\n',
276+
conf, problem1=(3, 10), problem2=(5, 18), problem3=(6, 11),
277+
problem4=(8, 10), problem5=(10, 3), problem6=(12, 3))
278+
230279
def test_any_quotes_not_required(self):
231280
conf = 'quoted-strings: {quote-type: any, required: false}\n'
232281

yamllint/rules/quoted_strings.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
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-double-quotes-for-escaping` defines whether or not to allow
34+
double quotes even if ``quote-type: single`` or
35+
``required: only-when-needed`` when the quoted
36+
string contains escape sequences.
3337
* ``allow-quoted-quotes`` allows (``true``) using disallowed quotes for strings
3438
with allowed quotes inside. Default ``false``.
3539
* ``check-keys`` defines whether to apply the rules to keys in mappings. By
@@ -165,6 +169,20 @@
165169
::
166170
167171
"foo:bar": baz
172+
#. With ``quoted-strings: {quote-type: singe,
173+
allow-double-quotes-for-escaping: true}``
174+
175+
the following code snippet would **PASS**:
176+
::
177+
178+
foo: 'bar'
179+
baz: "quux\\nquux 2"
180+
181+
the following code snippet would **FAIL**:
182+
::
183+
184+
foo: 'bar'
185+
baz: "quux quux 2"
168186
"""
169187

170188
import re
@@ -175,16 +193,24 @@
175193

176194
ID = 'quoted-strings'
177195
TYPE = 'token'
178-
CONF = {'quote-type': ('any', 'single', 'double', 'consistent'),
196+
CONF = {'quote-type':
197+
(
198+
'any',
199+
'single',
200+
'double',
201+
'consistent',
202+
),
179203
'required': (True, False, 'only-when-needed'),
180204
'extra-required': [str],
181205
'extra-allowed': [str],
206+
'allow-double-quotes-for-escaping': bool,
182207
'allow-quoted-quotes': bool,
183208
'check-keys': bool}
184209
DEFAULT = {'quote-type': 'any',
185210
'required': True,
186211
'extra-required': [],
187212
'extra-allowed': [],
213+
'allow-double-quotes-for-escaping': False,
188214
'allow-quoted-quotes': False,
189215
'check-keys': False}
190216

@@ -212,7 +238,9 @@ def VALIDATE(conf):
212238
list('-+0123456789'))
213239

214240

215-
def _quote_match(quote_type, token_style, context):
241+
def _quote_match(quote_type, token, context):
242+
token_style = token.style
243+
216244
if quote_type == 'consistent' and token_style is not None:
217245
# The canonical token style in a document is assumed to be the first
218246
# one found for the purpose of 'consistent'
@@ -263,6 +291,17 @@ def _has_quoted_quotes(token):
263291
(token.style == '"' and "'" in token.value)))
264292

265293

294+
def _has_escaping_in_double_quotes(token):
295+
if token.style != '"':
296+
return False
297+
298+
plain_value = token.start_mark.buffer[
299+
token.start_mark.pointer:token.end_mark.pointer
300+
]
301+
302+
return ('\\' in plain_value)
303+
304+
266305
def _has_backslash_on_at_least_one_line_ending(token):
267306
if token.start_mark.line == token.end_mark.line:
268307
return False
@@ -315,17 +354,21 @@ def check(conf, token, prev, next, nextnext, context):
315354

316355
# Quotes are mandatory and need to match config
317356
if (token.style is None or
318-
not (_quote_match(quote_type, token.style, context) or
319-
(conf['allow-quoted-quotes'] and _has_quoted_quotes(token)))):
357+
not (_quote_match(quote_type, token, context) or
358+
(conf['allow-quoted-quotes'] and _has_quoted_quotes(token)) or
359+
(conf['allow-double-quotes-for-escaping'] and
360+
_has_escaping_in_double_quotes(token)))):
320361
msg = f"string {node} is not quoted with {quote_type} quotes"
321362

322363
elif conf['required'] is False:
323364

324365
# Quotes are not mandatory but when used need to match config
325366
if (token.style and
326-
not _quote_match(quote_type, token.style, context) and
367+
not _quote_match(quote_type, token, context) and
327368
not (conf['allow-quoted-quotes'] and
328-
_has_quoted_quotes(token))):
369+
_has_quoted_quotes(token)) and
370+
not (conf['allow-double-quotes-for-escaping'] and
371+
_has_escaping_in_double_quotes(token))):
329372
msg = f"string {node} is not quoted with {quote_type} quotes"
330373

331374
elif not token.style:
@@ -343,13 +386,14 @@ def check(conf, token, prev, next, nextnext, context):
343386
for r in conf['extra-required'])
344387
is_extra_allowed = any(re.search(r, token.value)
345388
for r in conf['extra-allowed'])
346-
if not (is_extra_required or is_extra_allowed):
389+
contains_escape = _has_escaping_in_double_quotes(token)
390+
if not (is_extra_required or is_extra_allowed or contains_escape):
347391
msg = (f"string {node} is redundantly quoted with "
348392
f"{quote_type} quotes")
349393

350394
# But when used need to match config
351395
elif (token.style and
352-
not _quote_match(quote_type, token.style, context) and
396+
not _quote_match(quote_type, token, context) and
353397
not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))):
354398
msg = f"string {node} is not quoted with {quote_type} quotes"
355399

0 commit comments

Comments
 (0)