From 6ce11dedb422e083cab52f87f76b0129a28fbc64 Mon Sep 17 00:00:00 2001 From: ilyam8 Date: Sat, 4 Apr 2020 23:44:55 +0300 Subject: [PATCH 01/14] truthy: add `check-keys` option --- tests/rules/test_truthy.py | 26 ++++++++++++++++++++++++++ yamllint/rules/truthy.py | 27 +++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py index 82e4f6c..1c87184 100644 --- a/tests/rules/test_truthy.py +++ b/tests/rules/test_truthy.py @@ -114,3 +114,29 @@ class TruthyTestCase(RuleTestCase): 'boolean5: !!bool off\n' 'boolean6: !!bool NO\n', conf) + + def test_check_keys_disabled(self): + conf = ('truthy:\n' + ' allowed-values: []\n' + ' check-keys: false\n' + 'key-duplicates: disable\n') + self.check('---\n' + 'YES: 0\n' + 'Yes: 0\n' + 'yes: 0\n' + 'No: 0\n' + 'No: 0\n' + 'no: 0\n' + 'TRUE: 0\n' + 'True: 0\n' + 'true: 0\n' + 'FALSE: 0\n' + 'False: 0\n' + 'false: 0\n' + 'ON: 0\n' + 'On: 0\n' + 'on: 0\n' + 'OFF: 0\n' + 'Off: 0\n' + 'off: 0\n', + conf) diff --git a/yamllint/rules/truthy.py b/yamllint/rules/truthy.py index 7dd778f..64ccaaa 100644 --- a/yamllint/rules/truthy.py +++ b/yamllint/rules/truthy.py @@ -30,6 +30,9 @@ This can be useful to prevent surprises from YAML parsers transforming ``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``, ``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'Off'``, ``'off'``. +* ``check-keys`` disables verification for keys in mappings. By default, + ``truthy`` rule applies to both keys and values. Set this option to ``false`` + to prevent this. .. rubric:: Examples @@ -92,6 +95,22 @@ This can be useful to prevent surprises from YAML parsers transforming - false - on - off + +#. With ``truthy: {check-keys: false}`` + + the following code snippet would **PASS**: + :: + + yes: 1 + on: 2 + true: 3 + + the following code snippet would **FAIL**: + :: + + yes: Yes + on: On + true: True """ import yaml @@ -109,14 +128,18 @@ TRUTHY = ['YES', 'Yes', 'yes', ID = 'truthy' TYPE = 'token' -CONF = {'allowed-values': list(TRUTHY)} -DEFAULT = {'allowed-values': ['true', 'false']} +CONF = {'allowed-values': list(TRUTHY), 'check-keys': bool} +DEFAULT = {'allowed-values': ['true', 'false'], 'check-keys': True} def check(conf, token, prev, next, nextnext, context): if prev and isinstance(prev, yaml.tokens.TagToken): return + if (not conf['check-keys'] and isinstance(prev, yaml.tokens.KeyToken) and + isinstance(token, yaml.tokens.ScalarToken)): + return + if isinstance(token, yaml.tokens.ScalarToken): if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and token.style is None): From 46ed0c02be46324cb74df31b35bcc2b05b5d5bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 8 Apr 2020 12:31:12 +0200 Subject: [PATCH 02/14] truthy: Add missing test removed from PR See https://github.com/adrienverge/yamllint/pull/247#discussion_r405421376. --- tests/rules/test_truthy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py index 1c87184..9229253 100644 --- a/tests/rules/test_truthy.py +++ b/tests/rules/test_truthy.py @@ -138,5 +138,9 @@ class TruthyTestCase(RuleTestCase): 'on: 0\n' 'OFF: 0\n' 'Off: 0\n' - 'off: 0\n', + 'off: 0\n' + 'YES:\n' + ' Yes:\n' + ' yes:\n' + ' on: 0\n', conf) From 1a13837e84431f0fe6823cb655886daec0211eb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Thu, 9 Apr 2020 16:25:00 +0200 Subject: [PATCH 03/14] docs: Sunset Python 2 Keep supporting Python 2.7 for one extra year after upstream dropped it: https://www.python.org/doc/sunset-python-2/ --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 0bebb78..89bc0e8 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,10 @@ indentation, etc. Written in Python (compatible with Python 2 & 3). +⚠ Python 2 upstream support stopped on January 1, 2020. yamllint will keep +best-effort support for Python 2.7 until January 1, 2021. Passed that date, +yamllint will drop all Python 2-related code. + Documentation ------------- From e284d74be1695f96a5abcff271814e50b883a78a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 17:54:58 +0200 Subject: [PATCH 04/14] quoted-strings: Rename tests names for clarity And move only-when-needed tests at the end for readability. --- tests/rules/test_quoted_strings.py | 100 ++++++++++++++--------------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 0fdba71..3f0d8a8 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -137,17 +137,17 @@ class QuotedTestCase(RuleTestCase): ' word 2"\n', conf, problem1=(9, 3)) - def test_disallow_redundant_quotes(self): - conf = 'quoted-strings: {required: only-when-needed}\n' + def test_single_quotes_required(self): + conf = 'quoted-strings: {quote-type: single, required: true}\n' self.check('---\n' 'boolean1: true\n' 'number1: 123\n' - 'string1: foo\n' + 'string1: foo\n' # fails 'string2: "foo"\n' # fails - 'string3: "true"\n' - 'string4: "123"\n' - 'string5: \'bar\'\n' # fails + 'string3: "true"\n' # fails + 'string4: "123"\n' # fails + 'string5: \'bar\'\n' 'string6: !!str genericstring\n' 'string7: !!str 456\n' 'string8: !!str "quotedgenericstring"\n' @@ -155,7 +155,8 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(5, 10), problem2=(8, 10)) + conf, problem1=(4, 10), problem2=(5, 10), + problem3=(6, 10), problem4=(7, 10)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -164,25 +165,24 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 3:\n' - ' word 1\n' + ' word 1\n' # fails ' word 2\n' 'multiline string 4:\n' ' "word 1\\\n' # fails ' word 2"\n', - conf, problem1=(12, 3)) + conf, problem1=(9, 3), problem2=(12, 3)) - def test_disallow_redundant_single_quotes(self): - conf = 'quoted-strings: {quote-type: single, ' + \ - 'required: only-when-needed}\n' + def test_any_quotes_not_required(self): + conf = 'quoted-strings: {quote-type: any, required: false}\n' self.check('---\n' 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' - 'string2: "foo"\n' # fails - 'string3: "true"\n' # fails - 'string4: "123"\n' # fails - 'string5: \'bar\'\n' # fails + 'string2: "foo"\n' + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' 'string6: !!str genericstring\n' 'string7: !!str 456\n' 'string8: !!str "quotedgenericstring"\n' @@ -190,8 +190,7 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(5, 10), problem2=(6, 10), - problem3=(7, 10), problem4=(8, 10)) + conf) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -203,17 +202,17 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 4:\n' - ' "word 1\\\n' # fails + ' "word 1\\\n' ' word 2"\n', - conf, problem1=(12, 3)) + conf) - def test_single_quotes_required(self): - conf = 'quoted-strings: {quote-type: single, required: true}\n' + def test_single_quotes_not_required(self): + conf = 'quoted-strings: {quote-type: single, required: false}\n' self.check('---\n' 'boolean1: true\n' 'number1: 123\n' - 'string1: foo\n' # fails + 'string1: foo\n' 'string2: "foo"\n' # fails 'string3: "true"\n' # fails 'string4: "123"\n' # fails @@ -225,7 +224,7 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(5, 10), + conf, problem2=(5, 10), problem3=(6, 10), problem4=(7, 10)) self.check('---\n' 'multiline string 1: |\n' @@ -235,20 +234,20 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 3:\n' - ' word 1\n' # fails + ' word 1\n' ' word 2\n' 'multiline string 4:\n' ' "word 1\\\n' # fails ' word 2"\n', - conf, problem1=(9, 3), problem2=(12, 3)) + conf, problem1=(12, 3)) - def test_any_quotes_relaxed(self): - conf = 'quoted-strings: {quote-type: any, required: false}\n' + def test_quotes_required(self): + conf = 'quoted-strings: {quote-type: any, required: true}\n' self.check('---\n' 'boolean1: true\n' 'number1: 123\n' - 'string1: foo\n' + 'string1: foo\n' # fails 'string2: "foo"\n' 'string3: "true"\n' 'string4: "123"\n' @@ -260,7 +259,7 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf) + conf, problem2=(4, 10)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -269,24 +268,24 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 3:\n' - ' word 1\n' + ' word 1\n' # fails ' word 2\n' 'multiline string 4:\n' ' "word 1\\\n' ' word 2"\n', - conf) + conf, problem1=(9, 3)) - def test_single_quotes_relaxed(self): - conf = 'quoted-strings: {quote-type: single, required: false}\n' + def test_only_when_needed(self): + conf = 'quoted-strings: {required: only-when-needed}\n' self.check('---\n' 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' 'string2: "foo"\n' # fails - 'string3: "true"\n' # fails - 'string4: "123"\n' # fails - 'string5: \'bar\'\n' + 'string3: "true"\n' + 'string4: "123"\n' + 'string5: \'bar\'\n' # fails 'string6: !!str genericstring\n' 'string7: !!str 456\n' 'string8: !!str "quotedgenericstring"\n' @@ -294,8 +293,7 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem2=(5, 10), - problem3=(6, 10), problem4=(7, 10)) + conf, problem1=(5, 10), problem2=(8, 10)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -311,17 +309,18 @@ class QuotedTestCase(RuleTestCase): ' word 2"\n', conf, problem1=(12, 3)) - def test_quotes_required(self): - conf = 'quoted-strings: {quote-type: any, required: true}\n' + def test_only_when_needed_single_quotes(self): + conf = ('quoted-strings: {quote-type: single,\n' + ' required: only-when-needed}\n') self.check('---\n' 'boolean1: true\n' 'number1: 123\n' - 'string1: foo\n' # fails - 'string2: "foo"\n' - 'string3: "true"\n' - 'string4: "123"\n' - 'string5: \'bar\'\n' + 'string1: foo\n' + 'string2: "foo"\n' # fails + 'string3: "true"\n' # fails + 'string4: "123"\n' # fails + 'string5: \'bar\'\n' # fails 'string6: !!str genericstring\n' 'string7: !!str 456\n' 'string8: !!str "quotedgenericstring"\n' @@ -329,7 +328,8 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem2=(4, 10)) + conf, problem1=(5, 10), problem2=(6, 10), + problem3=(7, 10), problem4=(8, 10)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -338,9 +338,9 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 3:\n' - ' word 1\n' # fails + ' word 1\n' ' word 2\n' 'multiline string 4:\n' - ' "word 1\\\n' + ' "word 1\\\n' # fails ' word 2"\n', - conf, problem1=(9, 3)) + conf, problem1=(12, 3)) From 2d8639c3a18fa66fe348e4d3eb1e1435b970a1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 19:06:59 +0200 Subject: [PATCH 05/14] quoted-strings: Fix broken rule for list items The rule worked for values like: flow-map: {a: foo, b: "bar"} block-map: a: foo b: "bar" But not for: flow-seq: [foo, "bar"] block-seq: - foo - "bar" Also add tests to make sure there will be no regression. Fixes: #208. --- tests/rules/test_quoted_strings.py | 73 +++++++++++++++++++++++------- yamllint/rules/quoted_strings.py | 5 +- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 3f0d8a8..c0c0ff3 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -51,8 +51,14 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem=(4, 10)) + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - 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)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -85,9 +91,16 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(5, 10), - problem3=(6, 10), problem4=(7, 10)) + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - foo\n' # fails + ' - "foo"\n' # fails + 'flow-seq: [foo, "foo"]\n' # fails + 'flow-map: {a: foo, b: "foo"}\n', # fails + 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)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -120,8 +133,14 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(8, 10)) + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - foo\n' # fails + ' - "foo"\n' + 'flow-seq: [foo, "foo"]\n' # fails + 'flow-map: {a: foo, b: "foo"}\n', # fails + conf, problem1=(4, 10), problem2=(8, 10), problem3=(17, 5), + problem4=(19, 12), problem5=(20, 15)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -189,7 +208,12 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - foo\n' # fails + ' - "foo"\n' + 'flow-seq: [foo, "foo"]\n' # fails + 'flow-map: {a: foo, b: "foo"}\n', # fails conf) self.check('---\n' 'multiline string 1: |\n' @@ -223,9 +247,14 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem2=(5, 10), - problem3=(6, 10), problem4=(7, 10)) + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - foo\n' # fails + ' - "foo"\n' + 'flow-seq: [foo, "foo"]\n' # fails + 'flow-map: {a: foo, b: "foo"}\n', # fails + conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), + problem4=(18, 5), problem5=(19, 17), problem6=(20, 23)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -292,8 +321,14 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(5, 10), problem2=(8, 10)) + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - foo\n' + ' - "foo"\n' # fails + 'flow-seq: [foo, "foo"]\n' # fails + 'flow-map: {a: foo, b: "foo"}\n', # fails + conf, problem1=(5, 10), problem2=(8, 10), problem3=(18, 5), + problem4=(19, 17), problem5=(20, 23)) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -327,9 +362,15 @@ class QuotedTestCase(RuleTestCase): 'binary: !!binary binstring\n' 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(5, 10), problem2=(6, 10), - problem3=(7, 10), problem4=(8, 10)) + 'boolean3: !!bool "quotedboolstring"\n' + 'block-seq:\n' + ' - foo\n' + ' - "foo"\n' # fails + 'flow-seq: [foo, "foo"]\n' # fails + 'flow-map: {a: foo, b: "foo"}\n', # fails + conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), + problem4=(8, 10), problem5=(18, 5), problem6=(19, 17), + problem7=(20, 23)) 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 aaa635d..df675a6 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -89,7 +89,10 @@ def quote_match(quote_type, token_style): def check(conf, token, prev, next, nextnext, context): if not (isinstance(token, yaml.tokens.ScalarToken) and - isinstance(prev, (yaml.ValueToken, yaml.TagToken))): + isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken, + yaml.FlowSequenceStartToken, yaml.TagToken, + yaml.ValueToken))): + return # Ignore explicit types, e.g. !!str testtest or !!int 42 From 0bffba1e13a302cec71ec7e61ea3bae8280d83bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 18:56:25 +0200 Subject: [PATCH 06/14] quoted-strings: Remove test_single_quotes_required() It is exactly the same tests as `test_quote_type_single()`. --- tests/rules/test_quoted_strings.py | 35 ------------------------------ 1 file changed, 35 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index c0c0ff3..9391302 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -156,41 +156,6 @@ class QuotedTestCase(RuleTestCase): ' word 2"\n', conf, problem1=(9, 3)) - def test_single_quotes_required(self): - conf = 'quoted-strings: {quote-type: single, required: true}\n' - - self.check('---\n' - 'boolean1: true\n' - 'number1: 123\n' - 'string1: foo\n' # fails - 'string2: "foo"\n' # fails - 'string3: "true"\n' # fails - 'string4: "123"\n' # fails - 'string5: \'bar\'\n' - 'string6: !!str genericstring\n' - 'string7: !!str 456\n' - 'string8: !!str "quotedgenericstring"\n' - 'binary: !!binary binstring\n' - 'integer: !!int intstring\n' - 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(5, 10), - problem3=(6, 10), problem4=(7, 10)) - self.check('---\n' - 'multiline string 1: |\n' - ' line 1\n' - ' line 2\n' - 'multiline string 2: >\n' - ' word 1\n' - ' word 2\n' - 'multiline string 3:\n' - ' word 1\n' # fails - ' word 2\n' - 'multiline string 4:\n' - ' "word 1\\\n' # fails - ' word 2"\n', - conf, problem1=(9, 3), problem2=(12, 3)) - def test_any_quotes_not_required(self): conf = 'quoted-strings: {quote-type: any, required: false}\n' From ce7d3fcc7b0b5d602d116eb331f26e02c70dfbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 18:58:40 +0200 Subject: [PATCH 07/14] quoted-strings: Remove test_quotes_required() It is exactly the same tests as `test_quote_type_any()`. --- tests/rules/test_quoted_strings.py | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 9391302..0352d0d 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -235,40 +235,6 @@ class QuotedTestCase(RuleTestCase): ' word 2"\n', conf, problem1=(12, 3)) - def test_quotes_required(self): - conf = 'quoted-strings: {quote-type: any, required: true}\n' - - self.check('---\n' - 'boolean1: true\n' - 'number1: 123\n' - 'string1: foo\n' # fails - 'string2: "foo"\n' - 'string3: "true"\n' - 'string4: "123"\n' - 'string5: \'bar\'\n' - 'string6: !!str genericstring\n' - 'string7: !!str 456\n' - 'string8: !!str "quotedgenericstring"\n' - 'binary: !!binary binstring\n' - 'integer: !!int intstring\n' - 'boolean2: !!bool boolstring\n' - 'boolean3: !!bool "quotedboolstring"\n', - conf, problem2=(4, 10)) - self.check('---\n' - 'multiline string 1: |\n' - ' line 1\n' - ' line 2\n' - 'multiline string 2: >\n' - ' word 1\n' - ' word 2\n' - 'multiline string 3:\n' - ' word 1\n' # fails - ' word 2\n' - 'multiline string 4:\n' - ' "word 1\\\n' - ' word 2"\n', - conf, problem1=(9, 3)) - def test_only_when_needed(self): conf = 'quoted-strings: {required: only-when-needed}\n' From 961c496b4f8cf8213b0f67b8137bd3e7c243b5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Mon, 13 Apr 2020 14:32:08 +0200 Subject: [PATCH 08/14] yamllint version 1.22.0 --- CHANGELOG.rst | 7 +++++++ yamllint/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e56ab6..87e1a6e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,13 @@ Changelog ========= +1.22.0 (2020-04-13) +------------------- + +- Add ``check-keys`` option to the ``truthy`` rule +- Fix ``quoted-strings`` rule not working on sequences items +- Sunset Python 2 + 1.21.0 (2020-03-24) ------------------- diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 5b04ef6..18184f2 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.21.0' +APP_VERSION = '1.22.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' From fa87913566b9881e1a5ba91bb0d27ac29def4437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Mon, 13 Apr 2020 15:24:13 +0200 Subject: [PATCH 09/14] quoted-strings: Fix only-when-needed on corner cases Change implementation of `required: only-when-needed`, because maintaining a list of `START_TOKENS` and just looking at the first character of string values has proven to be partially broken. Cf. discussion at https://github.com/adrienverge/yamllint/pull/246#issuecomment-612354097. Fixes https://github.com/adrienverge/yamllint/issues/242 and https://github.com/adrienverge/yamllint/pull/244. --- tests/rules/test_quoted_strings.py | 41 ++++++++++++++++++++++++++++++ yamllint/rules/quoted_strings.py | 30 ++++++++++++++++------ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 0352d0d..25cc0f6 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -316,3 +316,44 @@ class QuotedTestCase(RuleTestCase): ' "word 1\\\n' # fails ' word 2"\n', conf, problem1=(12, 3)) + + def test_only_when_needed_corner_cases(self): + conf = 'quoted-strings: {required: only-when-needed}\n' + + self.check('---\n' + '- ""\n' + '- "- item"\n' + '- "key: value"\n' + '- "%H:%M:%S"\n' + '- "%wheel ALL=(ALL) NOPASSWD: ALL"\n' + '- \'"quoted"\'\n' + '- "\'foo\' == \'bar\'"\n' + '- "\'Mac\' in ansible_facts.product_name"\n', + conf) + self.check('---\n' + 'k1: ""\n' + 'k2: "- item"\n' + 'k3: "key: value"\n' + 'k4: "%H:%M:%S"\n' + 'k5: "%wheel ALL=(ALL) NOPASSWD: ALL"\n' + 'k6: \'"quoted"\'\n' + 'k7: "\'foo\' == \'bar\'"\n' + 'k8: "\'Mac\' in ansible_facts.product_name"\n', + conf) + + self.check('---\n' + '- ---\n' + '- "---"\n' # fails + '- ----------\n' + '- "----------"\n' # fails + '- :wq\n' + '- ":wq"\n', # fails + conf, problem1=(3, 3), problem2=(5, 3), problem3=(7, 3)) + self.check('---\n' + 'k1: ---\n' + 'k2: "---"\n' # fails + 'k3: ----------\n' + 'k4: "----------"\n' # fails + 'k5: :wq\n' + 'k6: ":wq"\n', # fails + conf, problem1=(3, 5), problem2=(5, 5), problem3=(7, 5)) diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index df675a6..46d3d45 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -77,16 +77,30 @@ DEFAULT = {'quote-type': 'any', 'required': True} DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' -START_TOKENS = {'#', '*', '!', '?', '@', '`', '&', - ',', '-', '{', '}', '[', ']', ':'} -def quote_match(quote_type, token_style): +def _quote_match(quote_type, token_style): return ((quote_type == 'any') or (quote_type == 'single' and token_style == "'") or (quote_type == 'double' and token_style == '"')) +def _quotes_are_needed(string): + loader = yaml.BaseLoader('key: ' + string) + # Remove the 5 first tokens corresponding to 'key: ' (StreamStartToken, + # BlockMappingStartToken, KeyToken, ScalarToken(value=key), ValueToken) + for _ in range(5): + loader.get_token() + try: + a, b = loader.get_token(), loader.get_token() + if (isinstance(a, yaml.ScalarToken) and a.style is None and + isinstance(b, yaml.BlockEndToken)): + return False + return True + except yaml.scanner.ScannerError: + return True + + def check(conf, token, prev, next, nextnext, context): if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken, @@ -121,25 +135,25 @@ def check(conf, token, prev, next, nextnext, context): if required is True: # Quotes are mandatory and need to match config - if token.style is None or not quote_match(quote_type, token.style): + if token.style is None or not _quote_match(quote_type, token.style): msg = "string value is not quoted with %s quotes" % (quote_type) elif required is False: # Quotes are not mandatory but when used need to match config - if token.style and not quote_match(quote_type, token.style): + if token.style and not _quote_match(quote_type, token.style): msg = "string value is not quoted with %s quotes" % (quote_type) elif not token.plain: # Quotes are disallowed when not needed - if (tag == DEFAULT_SCALAR_TAG and token.value - and token.value[0] not in START_TOKENS): + if (tag == DEFAULT_SCALAR_TAG and token.value and + not _quotes_are_needed(token.value)): msg = "string value is redundantly quoted with %s quotes" % ( quote_type) # But when used need to match config - elif token.style and not quote_match(quote_type, token.style): + elif token.style and not _quote_match(quote_type, token.style): msg = "string value is not quoted with %s quotes" % (quote_type) if msg is not None: From 483a8d89a557f57b3a782314875b0401b2c4d5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 15 Apr 2020 07:55:57 +0200 Subject: [PATCH 10/14] yamllint version 1.22.1 --- CHANGELOG.rst | 5 +++++ yamllint/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 87e1a6e..b44cbb7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,11 @@ Changelog ========= +1.22.1 (2020-04-15) +------------------- + +- Fix ``quoted-strings`` rule with ``only-when-needed`` on corner cases + 1.22.0 (2020-04-13) ------------------- diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 18184f2..50fee4d 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.22.0' +APP_VERSION = '1.22.1' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' From 851d34b9fdb6ff934a2d40fb9831844fe459f175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 18:25:25 +0200 Subject: [PATCH 11/14] config: Allow rules to validate their configuration --- yamllint/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/yamllint/config.py b/yamllint/config.py index 0837799..b72644c 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -177,6 +177,12 @@ def validate_rule_conf(rule, conf): for optkey in options: if optkey not in conf: conf[optkey] = options_default[optkey] + + if hasattr(rule, 'VALIDATE'): + res = rule.VALIDATE(conf) + if res: + raise YamlLintConfigError('invalid config: %s: %s' % + (rule.ID, res)) else: raise YamlLintConfigError(('invalid config: rule "%s": should be ' 'either "enable", "disable" or a dict') From d68022b846b0c2376f465706cbd1804408b83959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 18:26:06 +0200 Subject: [PATCH 12/14] config: Allow generic types inside lists For example it's possible to define a conf like: rule: foo: [str], bar: [int, bool, 'magic'], --- yamllint/config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/yamllint/config.py b/yamllint/config.py index b72644c..a955d8e 100644 --- a/yamllint/config.py +++ b/yamllint/config.py @@ -157,11 +157,12 @@ def validate_rule_conf(rule, conf): raise YamlLintConfigError( 'invalid config: option "%s" of "%s" should be in %s' % (optkey, rule.ID, options[optkey])) - # Example: CONF = {option: ['flag1', 'flag2']} - # → {option: [flag1]} → {option: [flag1, flag2]} + # Example: CONF = {option: ['flag1', 'flag2', int]} + # → {option: [flag1]} → {option: [42, flag1, flag2]} elif isinstance(options[optkey], list): if (type(conf[optkey]) is not list or - any(flag not in options[optkey] + any(flag not in options[optkey] and + type(flag) not in options[optkey] for flag in conf[optkey])): raise YamlLintConfigError( ('invalid config: option "%s" of "%s" should only ' From b711fd993e794dc88179fcb8b45b7cb8455419f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 10 Apr 2020 19:57:51 +0200 Subject: [PATCH 13/14] quoted-strings: Add options extra-required and extra-allowed Add ability to: - require strings to be quoted if they match a pattern (PCRE regex) - allow quoted strings if they match a pattern, while `require: only-when-needed` is enforced. Co-Authored-By: Leo Feyer (https://github.com/adrienverge/yamllint/pull/246) --- tests/rules/test_quoted_strings.py | 78 ++++++++++++++++++++++ yamllint/rules/quoted_strings.py | 101 ++++++++++++++++++++++++----- 2 files changed, 162 insertions(+), 17 deletions(-) diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 25cc0f6..d66cb0b 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -16,6 +16,8 @@ from tests.common import RuleTestCase +from yamllint import config + class QuotedTestCase(RuleTestCase): rule_id = 'quoted-strings' @@ -357,3 +359,79 @@ class QuotedTestCase(RuleTestCase): 'k5: :wq\n' 'k6: ":wq"\n', # fails conf, problem1=(3, 5), problem2=(5, 5), problem3=(7, 5)) + + def test_only_when_needed_extras(self): + conf = ('quoted-strings:\n' + ' required: true\n' + ' extra-allowed: [^http://]\n') + self.assertRaises(config.YamlLintConfigError, self.check, '', conf) + + conf = ('quoted-strings:\n' + ' required: true\n' + ' extra-required: [^http://]\n') + self.assertRaises(config.YamlLintConfigError, self.check, '', conf) + + conf = ('quoted-strings:\n' + ' required: false\n' + ' extra-allowed: [^http://]\n') + self.assertRaises(config.YamlLintConfigError, self.check, '', conf) + + conf = ('quoted-strings:\n' + ' required: true\n') + self.check('---\n' + '- 123\n' + '- "123"\n' + '- localhost\n' # fails + '- "localhost"\n' + '- http://localhost\n' # fails + '- "http://localhost"\n' + '- ftp://localhost\n' # fails + '- "ftp://localhost"\n', + conf, problem1=(4, 3), problem2=(6, 3), problem3=(8, 3)) + + conf = ('quoted-strings:\n' + ' required: only-when-needed\n' + ' extra-allowed: [^ftp://]\n' + ' extra-required: [^http://]\n') + self.check('---\n' + '- 123\n' + '- "123"\n' + '- localhost\n' + '- "localhost"\n' # fails + '- http://localhost\n' # fails + '- "http://localhost"\n' + '- ftp://localhost\n' + '- "ftp://localhost"\n', + conf, problem1=(5, 3), problem2=(6, 3)) + + conf = ('quoted-strings:\n' + ' required: false\n' + ' extra-required: [^http://, ^ftp://]\n') + self.check('---\n' + '- 123\n' + '- "123"\n' + '- localhost\n' + '- "localhost"\n' + '- http://localhost\n' # fails + '- "http://localhost"\n' + '- ftp://localhost\n' # fails + '- "ftp://localhost"\n', + conf, problem1=(6, 3), problem2=(8, 3)) + + conf = ('quoted-strings:\n' + ' required: only-when-needed\n' + ' extra-allowed: [^ftp://, ";$", " "]\n') + self.check('---\n' + '- localhost\n' + '- "localhost"\n' # fails + '- ftp://localhost\n' + '- "ftp://localhost"\n' + '- i=i+1\n' + '- "i=i+1"\n' # fails + '- i=i+2;\n' + '- "i=i+2;"\n' + '- foo\n' + '- "foo"\n' # fails + '- foo bar\n' + '- "foo bar"\n', + conf, problem1=(3, 3), problem2=(7, 3), problem3=(11, 3)) diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 46d3d45..1d99729 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -26,6 +26,11 @@ used. * ``required`` defines whether using quotes in string values is required (``true``, default) or not (``false``), or only allowed when really needed (``only-when-needed``). +* ``extra-required`` is a list of PCRE regexes to force string values to be + quoted, if they match any regex. This option can only be used with + ``required: false`` and ``required: only-when-needed``. +* ``extra-allowed`` is a list of PCRE regexes to allow quoted string values, + even if ``required: only-when-needed`` is set. **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked. @@ -63,8 +68,44 @@ used. :: foo: 'bar' + +#. With ``quoted-strings: {required: false, extra-required: [^http://, + ^ftp://]}`` + + the following code snippet would **PASS**: + :: + + - localhost + - "localhost" + - "http://localhost" + - "ftp://localhost" + + the following code snippet would **FAIL**: + :: + + - http://localhost + - ftp://localhost + +#. With ``quoted-strings: {required: only-when-needed, extra-allowed: + [^http://, ^ftp://], extra-required: [QUOTED]}`` + + the following code snippet would **PASS**: + :: + + - localhost + - "http://localhost" + - "ftp://localhost" + - "this is a string that needs to be QUOTED" + + the following code snippet would **FAIL**: + :: + + - "localhost" + - this is a string that needs to be QUOTED """ +import re + import yaml from yamllint.linter import LintProblem @@ -72,9 +113,23 @@ from yamllint.linter import LintProblem ID = 'quoted-strings' TYPE = 'token' CONF = {'quote-type': ('any', 'single', 'double'), - 'required': (True, False, 'only-when-needed')} + 'required': (True, False, 'only-when-needed'), + 'extra-required': [str], + 'extra-allowed': [str]} DEFAULT = {'quote-type': 'any', - 'required': True} + 'required': True, + 'extra-required': [], + 'extra-allowed': []} + + +def VALIDATE(conf): + if conf['required'] is True and len(conf['extra-allowed']) > 0: + return 'cannot use both "required: true" and "extra-allowed"' + if conf['required'] is True and len(conf['extra-required']) > 0: + return 'cannot use both "required: true" and "extra-required"' + if conf['required'] is False and len(conf['extra-allowed']) > 0: + return 'cannot use both "required: false" and "extra-allowed"' + DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' @@ -125,36 +180,48 @@ def check(conf, token, prev, next, nextnext, context): return quote_type = conf['quote-type'] - required = conf['required'] - - # Completely relaxed about quotes (same as the rule being disabled) - if required is False and quote_type == 'any': - return msg = None - if required is True: + if conf['required'] is True: # Quotes are mandatory and need to match config if token.style is None or not _quote_match(quote_type, token.style): - msg = "string value is not quoted with %s quotes" % (quote_type) + msg = "string value is not quoted with %s quotes" % quote_type - elif required is False: + elif conf['required'] is False: # Quotes are not mandatory but when used need to match config if token.style and not _quote_match(quote_type, token.style): - msg = "string value is not quoted with %s quotes" % (quote_type) + msg = "string value is not quoted with %s quotes" % quote_type + + elif not token.style: + is_extra_required = any(re.search(r, token.value) + for r in conf['extra-required']) + if is_extra_required: + msg = "string value is not quoted" - elif not token.plain: + elif conf['required'] == 'only-when-needed': - # Quotes are disallowed when not needed - if (tag == DEFAULT_SCALAR_TAG and token.value and + # Quotes are not strictly needed here + if (token.style and tag == DEFAULT_SCALAR_TAG and token.value and not _quotes_are_needed(token.value)): - msg = "string value is redundantly quoted with %s quotes" % ( - quote_type) + is_extra_required = any(re.search(r, token.value) + for r in conf['extra-required']) + is_extra_allowed = any(re.search(r, token.value) + for r in conf['extra-allowed']) + if not (is_extra_required or is_extra_allowed): + msg = "string value is redundantly quoted with %s quotes" % ( + quote_type) # But when used need to match config elif token.style and not _quote_match(quote_type, token.style): - msg = "string value is not quoted with %s quotes" % (quote_type) + msg = "string value is not quoted with %s quotes" % quote_type + + elif not token.style: + is_extra_required = len(conf['extra-required']) and any( + re.search(r, token.value) for r in conf['extra-required']) + if is_extra_required: + msg = "string value is not quoted" if msg is not None: yield LintProblem( From a54cbce1b62a37b76eb07479a16dd761d476990c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Fri, 17 Apr 2020 10:31:52 +0200 Subject: [PATCH 14/14] yamllint version 1.23.0 --- CHANGELOG.rst | 6 ++++++ yamllint/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b44cbb7..4e97957 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,12 @@ Changelog ========= +1.23.0 (2020-04-17) +------------------- + +- Allow rules to validate their configuration +- Add options ``extra-required`` and ``extra-allowed`` to ``quoted-strings`` + 1.22.1 (2020-04-15) ------------------- diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 50fee4d..b78fe9c 100644 --- a/yamllint/__init__.py +++ b/yamllint/__init__.py @@ -22,7 +22,7 @@ indentation, etc.""" APP_NAME = 'yamllint' -APP_VERSION = '1.22.1' +APP_VERSION = '1.23.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé'