Skip to content

Commit 510c6c8

Browse files
author
Timo Schrader
committed
Feature: Added single-unless-contains-escaped quote type to quoted strings.
1 parent e3d54cc commit 510c6c8

File tree

2 files changed

+82
-6
lines changed

2 files changed

+82
-6
lines changed

tests/rules/test_quoted_strings.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,51 @@ 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_quote_type_single_unless_contains_escaped(self):
231+
conf = 'quoted-strings: {quote-type: single-unless-contains-escaped}'
232+
self.check('---\n'
233+
"string1: 'foo'\n"
234+
'string2: "bar\\nstool"\n'
235+
'string3: "baz"\n' # fails
236+
"string4: 'quux'\n",
237+
conf, problem1=(4, 10))
238+
239+
conf = ('quoted-strings:\n'
240+
' quote-type: single-unless-contains-escaped\n'
241+
' check-keys: true\n')
242+
243+
self.check('---\n'
244+
"'string1': 'foo'\n"
245+
"\"string2\\tsplit\": 'foo'\n"
246+
"'string3': \"foo\\nbar\"\n"
247+
"string4: 'bar'\n" # fails
248+
"\"string5\": 'baz'\n" # fails
249+
"'string6': {'key': 'val'}\n"
250+
"'string7': \"foo\"\n",
251+
conf, problem1=(5, 1), problem2=(6, 1), problem3=(8, 12))
252+
253+
conf = ('quoted-strings:\n'
254+
' quote-type: single-unless-contains-escaped\n'
255+
' check-keys: true\n'
256+
' required: false\n')
257+
self.check('---\n'
258+
'string1: \'foo\'\n'
259+
'string2: "bar"\n' # fails
260+
'string3: \'baz\'\n'
261+
'string4: {\'key\': "val"}\n' # fails
262+
'string5: {"key": \'val\'}\n' # fails
263+
'string6:\n'
264+
' \'key\': "val"\n' # fails
265+
'string7:\n'
266+
' "key": \'val\'\n' # fails
267+
'string8:\n'
268+
' "string"\n' # fails
269+
'string9: >\n'
270+
' "string"\n'
271+
'string10: "bar\\nstool"\n',
272+
conf, problem1=(3, 10), problem2=(5, 18), problem3=(6, 11),
273+
problem4=(8, 10), problem5=(10, 3), problem6=(12, 3))
274+
230275
def test_any_quotes_not_required(self):
231276
conf = 'quoted-strings: {quote-type: any, required: false}\n'
232277

yamllint/rules/quoted_strings.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
2121
.. rubric:: Options
2222
23-
* ``quote-type`` defines allowed quotes: ``single``, ``double`` or ``any``
23+
* ``quote-type`` defines allowed quotes: ``single``, ``double``,
24+
``single-unless-contains-escaped``, or ``any``
2425
(default).
2526
* ``required`` defines whether using quotes in string values is required
2627
(``true``, default) or not (``false``), or only allowed when really needed
@@ -165,6 +166,19 @@
165166
::
166167
167168
"foo:bar": baz
169+
#. With ``quoted-strings: {quote-type: single-unless-contains-escaped}``
170+
171+
the following code snippet would **PASS**:
172+
::
173+
174+
foo: 'bar'
175+
baz: "quux\\nquux 2"
176+
177+
the following code snippet would **FAIL**:
178+
::
179+
180+
foo: 'bar'
181+
baz: "quux quux 2"
168182
"""
169183

170184
import re
@@ -175,7 +189,14 @@
175189

176190
ID = 'quoted-strings'
177191
TYPE = 'token'
178-
CONF = {'quote-type': ('any', 'single', 'double', 'consistent'),
192+
CONF = {'quote-type':
193+
(
194+
'any',
195+
'single',
196+
'double',
197+
'consistent',
198+
'single-unless-contains-escaped'
199+
),
179200
'required': (True, False, 'only-when-needed'),
180201
'extra-required': [str],
181202
'extra-allowed': [str],
@@ -212,14 +233,24 @@ def VALIDATE(conf):
212233
list('-+0123456789'))
213234

214235

215-
def _quote_match(quote_type, token_style, context):
236+
def _quote_match(quote_type, token, context):
237+
token_style = token.style
238+
216239
if quote_type == 'consistent' and token_style is not None:
217240
# The canonical token style in a document is assumed to be the first
218241
# one found for the purpose of 'consistent'
219242
if 'quoted_strings_consistent_token_style' not in context:
220243
context['quoted_strings_consistent_token_style'] = token_style
221244
return context['quoted_strings_consistent_token_style'] == token_style
222245

246+
if (quote_type == 'single-unless-contains-escaped' and
247+
token_style is not None):
248+
plain_value = token.start_mark.buffer[
249+
token.start_mark.pointer:token.end_mark.pointer
250+
]
251+
return ((token_style == "'") or
252+
(token_style == '"' and '\\' in plain_value))
253+
223254
return ((quote_type == 'any') or
224255
(quote_type == 'single' and token_style == "'") or
225256
(quote_type == 'double' and token_style == '"'))
@@ -315,15 +346,15 @@ def check(conf, token, prev, next, nextnext, context):
315346

316347
# Quotes are mandatory and need to match config
317348
if (token.style is None or
318-
not (_quote_match(quote_type, token.style, context) or
349+
not (_quote_match(quote_type, token, context) or
319350
(conf['allow-quoted-quotes'] and _has_quoted_quotes(token)))):
320351
msg = f"string {node} is not quoted with {quote_type} quotes"
321352

322353
elif conf['required'] is False:
323354

324355
# Quotes are not mandatory but when used need to match config
325356
if (token.style and
326-
not _quote_match(quote_type, token.style, context) and
357+
not _quote_match(quote_type, token, context) and
327358
not (conf['allow-quoted-quotes'] and
328359
_has_quoted_quotes(token))):
329360
msg = f"string {node} is not quoted with {quote_type} quotes"
@@ -349,7 +380,7 @@ def check(conf, token, prev, next, nextnext, context):
349380

350381
# But when used need to match config
351382
elif (token.style and
352-
not _quote_match(quote_type, token.style, context) and
383+
not _quote_match(quote_type, token, context) and
353384
not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))):
354385
msg = f"string {node} is not quoted with {quote_type} quotes"
355386

0 commit comments

Comments
 (0)