Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 84 additions & 17 deletions tests/rules/test_quoted_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ def test_quote_type_any(self):
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\n'
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n'
' word 2"\n',
conf, problem1=(9, 3))
Expand Down Expand Up @@ -127,9 +130,12 @@ def test_quote_type_single(self):
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n'
' "word 1\n' # fails
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(9, 3), problem2=(12, 3))
conf, problem1=(9, 3), problem2=(12, 3), problem3=(15, 3))

def test_quote_type_double(self):
conf = 'quoted-strings: {quote-type: double}\n'
Expand Down Expand Up @@ -173,6 +179,9 @@ def test_quote_type_double(self):
' word 1\n' # fails
' word 2\n'
'multiline string 4:\n'
' "word 1\n'
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n'
' word 2"\n',
conf, problem1=(9, 3))
Expand Down Expand Up @@ -216,6 +225,9 @@ def test_any_quotes_not_required(self):
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\n'
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n'
' word 2"\n',
conf)
Expand Down Expand Up @@ -263,9 +275,12 @@ def test_single_quotes_not_required(self):
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\n' # fails
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
conf, problem1=(12, 3), problem2=(15, 3))

def test_only_when_needed(self):
conf = 'quoted-strings: {required: only-when-needed}\n'
Expand Down Expand Up @@ -307,7 +322,10 @@ def test_only_when_needed(self):
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\\\n' # fails
' "word 1\n' # fails
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n'
' word 2"\n',
conf, problem1=(12, 3))

Expand Down Expand Up @@ -354,9 +372,12 @@ def test_only_when_needed_single_quotes(self):
' word 1\n'
' word 2\n'
'multiline string 4:\n'
' "word 1\n'
' word 2"\n'
'multiline string 5:\n'
' "word 1\\\n' # fails
' word 2"\n',
conf, problem1=(12, 3))
conf, problem1=(12, 3), problem2=(15, 3))

def test_only_when_needed_corner_cases(self):
conf = 'quoted-strings: {required: only-when-needed}\n'
Expand Down Expand Up @@ -626,7 +647,8 @@ class QuotedKeysTestCase(RuleTestCase):
rule_id = 'quoted-strings'

def test_disabled(self):
conf_disabled = "quoted-strings: {}"
conf_disabled = ('quoted-strings: {}\n'
'key-duplicates: disable\n')
key_strings = ('---\n'
'true: 2\n'
'123: 3\n'
Expand Down Expand Up @@ -665,6 +687,10 @@ def test_disabled(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
Expand All @@ -673,7 +699,8 @@ def test_disabled(self):
def test_default(self):
# Default configuration, but with check-keys
conf_default = ("quoted-strings:\n"
" check-keys: true\n")
" check-keys: true\n"
"key-duplicates: disable\n")
key_strings = ('---\n'
'true: 2\n'
'123: 3\n'
Expand Down Expand Up @@ -712,6 +739,10 @@ def test_default(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
Expand All @@ -721,7 +752,8 @@ def test_default(self):
def test_quote_type_any(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' quote-type: any\n')
' quote-type: any\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -761,6 +793,10 @@ def test_quote_type_any(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
Expand All @@ -771,7 +807,8 @@ def test_quote_type_any(self):
def test_quote_type_single(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' quote-type: single\n')
' quote-type: single\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -811,19 +848,24 @@ def test_quote_type_single(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
self.check(key_strings, conf,
problem1=(4, 1), problem2=(5, 1), problem3=(6, 1),
problem4=(7, 1), problem5=(20, 3), problem6=(21, 3),
problem7=(23, 2), problem8=(23, 10), problem9=(33, 3),
problem10=(37, 3))
problem10=(37, 3), problem11=(41, 3))

def test_quote_type_double(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' quote-type: double\n')
' quote-type: double\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -863,6 +905,10 @@ def test_quote_type_double(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
Expand All @@ -874,7 +920,8 @@ def test_any_quotes_not_required(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' quote-type: any\n'
' required: false\n')
' required: false\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -914,6 +961,10 @@ def test_any_quotes_not_required(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
Expand All @@ -923,7 +974,8 @@ def test_single_quotes_not_required(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' quote-type: single\n'
' required: false\n')
' required: false\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -963,17 +1015,23 @@ def test_single_quotes_not_required(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
self.check(key_strings, conf,
problem1=(5, 1), problem2=(6, 1), problem3=(7, 1),
problem4=(21, 3), problem5=(23, 10), problem6=(37, 3))
problem4=(21, 3), problem5=(23, 10), problem6=(37, 3),
problem7=(41, 3))

def test_only_when_needed(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' required: only-when-needed\n')
' required: only-when-needed\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -1013,6 +1071,10 @@ def test_only_when_needed(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
Expand All @@ -1024,7 +1086,8 @@ def test_only_when_needed_single_quotes(self):
conf = ('quoted-strings:\n'
' check-keys: true\n'
' quote-type: single\n'
' required: only-when-needed\n')
' required: only-when-needed\n'
'key-duplicates: disable\n')

key_strings = ('---\n'
'true: 2\n'
Expand Down Expand Up @@ -1064,13 +1127,17 @@ def test_only_when_needed_single_quotes(self):
' line 2\n'
': 35\n'
'?\n'
' "line 1\n'
' line 2"\n'
': 37\n'
'?\n'
' "line 1\\\n'
' line 2"\n'
': 39\n')
self.check(key_strings, conf,
problem1=(5, 1), problem2=(6, 1), problem3=(7, 1),
problem4=(8, 1), problem5=(21, 3), problem6=(23, 10),
problem7=(37, 3))
problem7=(37, 3), problem8=(41, 3))

def test_only_when_needed_corner_cases(self):
conf = ('quoted-strings:\n'
Expand Down
27 changes: 18 additions & 9 deletions yamllint/rules/quoted_strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,20 +204,23 @@ def _quote_match(quote_type, token_style):
(quote_type == 'double' and token_style == '"'))


def _quotes_are_needed(string, style, is_inside_a_flow):
def _quotes_are_needed(token, is_inside_a_flow):
# Quotes needed on strings containing flow tokens
if is_inside_a_flow and set(string) & {',', '[', ']', '{', '}'}:
if is_inside_a_flow and set(token.value) & {',', '[', ']', '{', '}'}:
return True

if style == '"':
if token.style == '"':
try:
yaml.reader.Reader('').check_printable('key: ' + string)
yaml.reader.Reader('').check_printable('key: ' + token.value)
except yaml.reader.ReaderError:
# Special characters in a double-quoted string are assumed to have
# been backslash-escaped
return True

loader = yaml.BaseLoader('key: ' + string)
if _has_backslash_on_at_least_one_line_ending(token):
return True

loader = yaml.BaseLoader('key: ' + token.value)
# Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken,
# BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken)
for _ in range(5):
Expand All @@ -228,7 +231,7 @@ def _quotes_are_needed(string, style, is_inside_a_flow):
return True
else:
if (isinstance(a, yaml.ScalarToken) and a.style is None and
isinstance(b, yaml.BlockEndToken) and a.value == string):
isinstance(b, yaml.BlockEndToken) and a.value == token.value):
return False
return True

Expand All @@ -239,6 +242,14 @@ def _has_quoted_quotes(token):
(token.style == '"' and "'" in token.value)))


def _has_backslash_on_at_least_one_line_ending(token):
if token.start_mark.line == token.end_mark.line:
return False
buffer = token.start_mark.buffer[
token.start_mark.index + 1:token.end_mark.index - 1]
return '\\\n' in buffer or '\\\r\n' in buffer


def check(conf, token, prev, next, nextnext, context):
if 'flow_nest_count' not in context:
context['flow_nest_count'] = 0
Expand Down Expand Up @@ -306,9 +317,7 @@ def check(conf, token, prev, next, nextnext, context):

# Quotes are not strictly needed here
if (token.style and tag == DEFAULT_SCALAR_TAG and token.value and
not _quotes_are_needed(token.value,
token.style,
context['flow_nest_count'] > 0)):
not _quotes_are_needed(token, context['flow_nest_count'] > 0)):
is_extra_required = any(re.search(r, token.value)
for r in conf['extra-required'])
is_extra_allowed = any(re.search(r, token.value)
Expand Down
Loading