From a962b4101ade797eaae75d746f31f3463278181d Mon Sep 17 00:00:00 2001 From: Rob Herring Date: Tue, 21 Nov 2023 13:15:18 -0700 Subject: [PATCH] quoted-strings: Fix only-when-needed in flow maps and sequences Flow maps and sequences need quotes if the values contain any of the flow tokens ({}, [], ','). However, yamllint generates false positives in these cases: $ yamllint -d 'rules: {quoted-strings: {required: only-when-needed}}' - <<<'field: ["string[bracket]"]' 1:9 error string value is redundantly quoted with any quotes (quoted-strings) To fix this, track when inside a flow map/sequence and skip the quoting checks except for the quoting type. Closes #516 --- tests/rules/test_quoted_strings.py | 65 ++++++++++++++++++++++++------ yamllint/rules/quoted_strings.py | 19 ++++++++- 2 files changed, 69 insertions(+), 15 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 543cc0d1..040ea793 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -57,9 +57,14 @@ def test_quote_type_any(self): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails - conf, problem1=(4, 10), problem2=(17, 5), - problem3=(19, 12), problem4=(20, 15)) + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', + conf, problem1=(4, 10), problem2=(17, 5), problem3=(19, 12), + problem4=(20, 15), problem5=(21, 13), problem6=(22, 16), + problem7=(23, 19), problem8=(23, 28), problem9=(24, 20)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -97,11 +102,19 @@ def test_quote_type_single(self): ' - foo\n' # fails ' - "foo"\n' # fails 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(4, 10), problem2=(5, 10), problem3=(6, 10), problem4=(7, 10), problem5=(17, 5), problem6=(18, 5), problem7=(19, 12), problem8=(19, 17), problem9=(20, 15), - problem10=(20, 23)) + problem10=(20, 23), problem11=(21, 13), problem12=(21, 18), + problem13=(21, 29), problem14=(21, 41), problem15=(22, 16), + problem16=(22, 24), problem17=(23, 19), problem18=(23, 28), + problem19=(23, 33), problem20=(24, 20), problem21=(24, 30), + problem22=(24, 45)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -139,9 +152,15 @@ def test_quote_type_double(self): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(4, 10), problem2=(8, 10), problem3=(17, 5), - problem4=(19, 12), problem5=(20, 15)) + problem4=(19, 12), problem5=(20, 15), problem6=(21, 13), + problem7=(22, 16), problem8=(23, 19), problem9=(23, 28), + problem10=(24, 20)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -179,7 +198,11 @@ def test_any_quotes_not_required(self): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf) self.check('---\n' 'multiline string 1: |\n' @@ -218,9 +241,16 @@ def test_single_quotes_not_required(self): ' - foo\n' # fails ' - "foo"\n' 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), - problem4=(18, 5), problem5=(19, 17), problem6=(20, 23)) + problem4=(18, 5), problem5=(19, 17), problem6=(20, 23), + problem7=(21, 18), problem8=(21, 29), problem9=(21, 41), + problem10=(22, 24), problem11=(23, 33), problem12=(24, 30), + problem13=(24, 45)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -258,7 +288,11 @@ def test_only_when_needed(self): ' - foo\n' ' - "foo"\n' # fails 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar", "foo[bar]", "foo{bar}"]\n' + 'flow-map2: {a: foo, b: "foo,bar"}\n' + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(5, 10), problem2=(8, 10), problem3=(18, 5), problem4=(19, 17), problem5=(20, 23)) self.check('---\n' @@ -299,10 +333,15 @@ def test_only_when_needed_single_quotes(self): ' - foo\n' ' - "foo"\n' # fails 'flow-seq: [foo, "foo"]\n' # fails - 'flow-map: {a: foo, b: "foo"}\n', # fails + 'flow-map: {a: foo, b: "foo"}\n' # fails + 'flow-seq2: [foo, "foo,bar"]\n' # fails + 'flow-map2: {a: foo, b: "foo,bar"}\n' # fails + 'nested-flow1: {a: foo, b: [foo, "foo,bar"]}\n' + 'nested-flow2: [{a: foo}, {b: "foo,bar", c: ["d[e]"]}]\n', conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), problem4=(8, 10), problem5=(18, 5), problem6=(19, 17), - problem7=(20, 23)) + problem7=(20, 23), problem8=(21, 18), problem9=(22, 24), + problem10=(23, 33), problem11=(24, 30), problem12=(24, 45)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 9380ae57..86b825e0 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -186,7 +186,11 @@ def _quote_match(quote_type, token_style): (quote_type == 'double' and token_style == '"')) -def _quotes_are_needed(string): +def _quotes_are_needed(string, is_inside_a_flow): + # Quotes needed on strings containing flow tokens + if is_inside_a_flow and set(string) & {',', '[', ']', '{', '}'}: + return True + loader = yaml.BaseLoader('key: ' + string) # Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken, # BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken) @@ -209,6 +213,16 @@ def _has_quoted_quotes(token): def check(conf, token, prev, next, nextnext, context): + if 'flow_nest_count' not in context: + context['flow_nest_count'] = 0 + + if isinstance(token, (yaml.FlowMappingStartToken, + yaml.FlowSequenceStartToken)): + context['flow_nest_count'] += 1 + elif isinstance(token, (yaml.FlowMappingEndToken, + yaml.FlowSequenceEndToken)): + context['flow_nest_count'] -= 1 + if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken, yaml.FlowSequenceStartToken, yaml.TagToken, @@ -261,7 +275,8 @@ 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)): + not _quotes_are_needed(token.value, + 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)