diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4e56ab6..4e97957 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,24 @@ 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) +------------------- + +- Fix ``quoted-strings`` rule with ``only-when-needed`` on corner cases + +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/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 ------------- diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 0fdba71..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' @@ -51,8 +53,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 +93,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 +135,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' @@ -137,25 +158,30 @@ 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_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 + 'string2: "foo"\n' 'string3: "true"\n' 'string4: "123"\n' - 'string5: \'bar\'\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=(5, 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) self.check('---\n' 'multiline string 1: |\n' ' line 1\n' @@ -167,13 +193,12 @@ 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_disallow_redundant_single_quotes(self): - conf = 'quoted-strings: {quote-type: single, ' + \ - 'required: only-when-needed}\n' + def test_single_quotes_not_required(self): + conf = 'quoted-strings: {quote-type: single, required: false}\n' self.check('---\n' 'boolean1: true\n' @@ -182,16 +207,21 @@ class QuotedTestCase(RuleTestCase): 'string2: "foo"\n' # fails 'string3: "true"\n' # fails 'string4: "123"\n' # fails - 'string5: \'bar\'\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=(5, 10), problem2=(6, 10), - problem3=(7, 10), problem4=(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=(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' @@ -207,60 +237,31 @@ class QuotedTestCase(RuleTestCase): ' word 2"\n', conf, problem1=(12, 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_relaxed(self): - conf = 'quoted-strings: {quote-type: any, 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' + 'string2: "foo"\n' # fails 'string3: "true"\n' 'string4: "123"\n' - 'string5: \'bar\'\n' + 'string5: \'bar\'\n' # fails '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) + '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' @@ -272,12 +273,13 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 4:\n' - ' "word 1\\\n' + ' "word 1\\\n' # fails ' word 2"\n', - conf) + conf, problem1=(12, 3)) - def test_single_quotes_relaxed(self): - conf = 'quoted-strings: {quote-type: single, required: false}\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' @@ -286,16 +288,22 @@ class QuotedTestCase(RuleTestCase): 'string2: "foo"\n' # fails 'string3: "true"\n' # fails 'string4: "123"\n' # fails - 'string5: \'bar\'\n' + 'string5: \'bar\'\n' # fails '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=(5, 10), - problem3=(6, 10), problem4=(7, 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' @@ -311,36 +319,119 @@ 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_corner_cases(self): + conf = 'quoted-strings: {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' - '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)) + '- ""\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' - '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)) + '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)) + + 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/tests/rules/test_truthy.py b/tests/rules/test_truthy.py index 82e4f6c..9229253 100644 --- a/tests/rules/test_truthy.py +++ b/tests/rules/test_truthy.py @@ -114,3 +114,33 @@ 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' + 'YES:\n' + ' Yes:\n' + ' yes:\n' + ' on: 0\n', + conf) diff --git a/yamllint/__init__.py b/yamllint/__init__.py index 5b04ef6..b78fe9c 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.23.0' APP_DESCRIPTION = __doc__ __author__ = u'Adrien Vergé' diff --git a/yamllint/config.py b/yamllint/config.py index 0837799..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 ' @@ -177,6 +178,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') diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index aaa635d..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,24 +113,55 @@ 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' -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.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 @@ -108,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) + 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: + 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) - - 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): - msg = "string value is redundantly quoted with %s quotes" % ( - quote_type) + 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.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 conf['required'] == 'only-when-needed': + + # Quotes are not strictly needed here + if (token.style and tag == DEFAULT_SCALAR_TAG and token.value and + not _quotes_are_needed(token.value)): + 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) + elif token.style and not _quote_match(quote_type, token.style): + 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( 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):