Merge tag 'v1.23.0' into packaging

packaging
Philipp Huebner 5 years ago
commit f5d8c765c7

@ -1,6 +1,24 @@
Changelog 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) 1.21.0 (2020-03-24)
------------------- -------------------

@ -21,6 +21,10 @@ indentation, etc.
Written in Python (compatible with Python 2 & 3). 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 Documentation
------------- -------------

@ -16,6 +16,8 @@
from tests.common import RuleTestCase from tests.common import RuleTestCase
from yamllint import config
class QuotedTestCase(RuleTestCase): class QuotedTestCase(RuleTestCase):
rule_id = 'quoted-strings' rule_id = 'quoted-strings'
@ -51,8 +53,14 @@ class QuotedTestCase(RuleTestCase):
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem=(4, 10)) '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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -85,9 +93,16 @@ class QuotedTestCase(RuleTestCase):
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(4, 10), problem2=(5, 10), 'block-seq:\n'
problem3=(6, 10), problem4=(7, 10)) ' - 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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -120,8 +135,14 @@ class QuotedTestCase(RuleTestCase):
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(4, 10), problem2=(8, 10)) '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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -137,25 +158,30 @@ class QuotedTestCase(RuleTestCase):
' word 2"\n', ' word 2"\n',
conf, problem1=(9, 3)) conf, problem1=(9, 3))
def test_disallow_redundant_quotes(self): def test_any_quotes_not_required(self):
conf = 'quoted-strings: {required: only-when-needed}\n' conf = 'quoted-strings: {quote-type: any, required: false}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' 'string1: foo\n'
'string2: "foo"\n' # fails 'string2: "foo"\n'
'string3: "true"\n' 'string3: "true"\n'
'string4: "123"\n' 'string4: "123"\n'
'string5: \'bar\'\n' # fails 'string5: \'bar\'\n'
'string6: !!str genericstring\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n' 'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n' 'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(5, 10), problem2=(8, 10)) '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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -167,13 +193,12 @@ class QuotedTestCase(RuleTestCase):
' word 1\n' ' word 1\n'
' word 2\n' ' word 2\n'
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' # fails ' "word 1\\\n'
' word 2"\n', ' word 2"\n',
conf, problem1=(12, 3)) conf)
def test_disallow_redundant_single_quotes(self): def test_single_quotes_not_required(self):
conf = 'quoted-strings: {quote-type: single, ' + \ conf = 'quoted-strings: {quote-type: single, required: false}\n'
'required: only-when-needed}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
@ -182,16 +207,21 @@ class QuotedTestCase(RuleTestCase):
'string2: "foo"\n' # fails 'string2: "foo"\n' # fails
'string3: "true"\n' # fails 'string3: "true"\n' # fails
'string4: "123"\n' # fails 'string4: "123"\n' # fails
'string5: \'bar\'\n' # fails 'string5: \'bar\'\n'
'string6: !!str genericstring\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n' 'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n' 'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem1=(5, 10), problem2=(6, 10), 'block-seq:\n'
problem3=(7, 10), problem4=(8, 10)) ' - 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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -207,60 +237,31 @@ class QuotedTestCase(RuleTestCase):
' word 2"\n', ' word 2"\n',
conf, problem1=(12, 3)) conf, problem1=(12, 3))
def test_single_quotes_required(self): def test_only_when_needed(self):
conf = 'quoted-strings: {quote-type: single, required: true}\n' conf = 'quoted-strings: {required: only-when-needed}\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'
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
'number1: 123\n' 'number1: 123\n'
'string1: foo\n' 'string1: foo\n'
'string2: "foo"\n' 'string2: "foo"\n' # fails
'string3: "true"\n' 'string3: "true"\n'
'string4: "123"\n' 'string4: "123"\n'
'string5: \'bar\'\n' 'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n' 'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n' 'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf) '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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -272,12 +273,13 @@ class QuotedTestCase(RuleTestCase):
' word 1\n' ' word 1\n'
' word 2\n' ' word 2\n'
'multiline string 4:\n' 'multiline string 4:\n'
' "word 1\\\n' ' "word 1\\\n' # fails
' word 2"\n', ' word 2"\n',
conf) conf, problem1=(12, 3))
def test_single_quotes_relaxed(self): def test_only_when_needed_single_quotes(self):
conf = 'quoted-strings: {quote-type: single, required: false}\n' conf = ('quoted-strings: {quote-type: single,\n'
' required: only-when-needed}\n')
self.check('---\n' self.check('---\n'
'boolean1: true\n' 'boolean1: true\n'
@ -286,16 +288,22 @@ class QuotedTestCase(RuleTestCase):
'string2: "foo"\n' # fails 'string2: "foo"\n' # fails
'string3: "true"\n' # fails 'string3: "true"\n' # fails
'string4: "123"\n' # fails 'string4: "123"\n' # fails
'string5: \'bar\'\n' 'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n' 'string7: !!str 456\n'
'string8: !!str "quotedgenericstring"\n' 'string8: !!str "quotedgenericstring"\n'
'binary: !!binary binstring\n' 'binary: !!binary binstring\n'
'integer: !!int intstring\n' 'integer: !!int intstring\n'
'boolean2: !!bool boolstring\n' 'boolean2: !!bool boolstring\n'
'boolean3: !!bool "quotedboolstring"\n', 'boolean3: !!bool "quotedboolstring"\n'
conf, problem2=(5, 10), 'block-seq:\n'
problem3=(6, 10), problem4=(7, 10)) ' - 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' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
' line 1\n' ' line 1\n'
@ -311,36 +319,119 @@ class QuotedTestCase(RuleTestCase):
' word 2"\n', ' word 2"\n',
conf, problem1=(12, 3)) conf, problem1=(12, 3))
def test_quotes_required(self): def test_only_when_needed_corner_cases(self):
conf = 'quoted-strings: {quote-type: any, required: true}\n' conf = 'quoted-strings: {required: only-when-needed}\n'
self.check('---\n' self.check('---\n'
'boolean1: true\n' '- ""\n'
'number1: 123\n' '- "- item"\n'
'string1: foo\n' # fails '- "key: value"\n'
'string2: "foo"\n' '- "%H:%M:%S"\n'
'string3: "true"\n' '- "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
'string4: "123"\n' '- \'"quoted"\'\n'
'string5: \'bar\'\n' '- "\'foo\' == \'bar\'"\n'
'string6: !!str genericstring\n' '- "\'Mac\' in ansible_facts.product_name"\n',
'string7: !!str 456\n' conf)
'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' self.check('---\n'
'multiline string 1: |\n' 'k1: ""\n'
' line 1\n' 'k2: "- item"\n'
' line 2\n' 'k3: "key: value"\n'
'multiline string 2: >\n' 'k4: "%H:%M:%S"\n'
' word 1\n' 'k5: "%wheel ALL=(ALL) NOPASSWD: ALL"\n'
' word 2\n' 'k6: \'"quoted"\'\n'
'multiline string 3:\n' 'k7: "\'foo\' == \'bar\'"\n'
' word 1\n' # fails 'k8: "\'Mac\' in ansible_facts.product_name"\n',
' word 2\n' conf)
'multiline string 4:\n'
' "word 1\\\n' self.check('---\n'
' word 2"\n', '- ---\n'
conf, problem1=(9, 3)) '- "---"\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))

@ -114,3 +114,33 @@ class TruthyTestCase(RuleTestCase):
'boolean5: !!bool off\n' 'boolean5: !!bool off\n'
'boolean6: !!bool NO\n', 'boolean6: !!bool NO\n',
conf) 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)

@ -22,7 +22,7 @@ indentation, etc."""
APP_NAME = 'yamllint' APP_NAME = 'yamllint'
APP_VERSION = '1.21.0' APP_VERSION = '1.23.0'
APP_DESCRIPTION = __doc__ APP_DESCRIPTION = __doc__
__author__ = u'Adrien Vergé' __author__ = u'Adrien Vergé'

@ -157,11 +157,12 @@ def validate_rule_conf(rule, conf):
raise YamlLintConfigError( raise YamlLintConfigError(
'invalid config: option "%s" of "%s" should be in %s' 'invalid config: option "%s" of "%s" should be in %s'
% (optkey, rule.ID, options[optkey])) % (optkey, rule.ID, options[optkey]))
# Example: CONF = {option: ['flag1', 'flag2']} # Example: CONF = {option: ['flag1', 'flag2', int]}
# → {option: [flag1]} → {option: [flag1, flag2]} # → {option: [flag1]} → {option: [42, flag1, flag2]}
elif isinstance(options[optkey], list): elif isinstance(options[optkey], list):
if (type(conf[optkey]) is not list or 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])): for flag in conf[optkey])):
raise YamlLintConfigError( raise YamlLintConfigError(
('invalid config: option "%s" of "%s" should only ' ('invalid config: option "%s" of "%s" should only '
@ -177,6 +178,12 @@ def validate_rule_conf(rule, conf):
for optkey in options: for optkey in options:
if optkey not in conf: if optkey not in conf:
conf[optkey] = options_default[optkey] 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: else:
raise YamlLintConfigError(('invalid config: rule "%s": should be ' raise YamlLintConfigError(('invalid config: rule "%s": should be '
'either "enable", "disable" or a dict') 'either "enable", "disable" or a dict')

@ -26,6 +26,11 @@ used.
* ``required`` defines whether using quotes in string values is required * ``required`` defines whether using quotes in string values is required
(``true``, default) or not (``false``), or only allowed when really needed (``true``, default) or not (``false``), or only allowed when really needed
(``only-when-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. **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked.
@ -63,8 +68,44 @@ used.
:: ::
foo: 'bar' 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 import yaml
from yamllint.linter import LintProblem from yamllint.linter import LintProblem
@ -72,24 +113,55 @@ from yamllint.linter import LintProblem
ID = 'quoted-strings' ID = 'quoted-strings'
TYPE = 'token' TYPE = 'token'
CONF = {'quote-type': ('any', 'single', 'double'), 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', 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' 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 return ((quote_type == 'any') or
(quote_type == 'single' and token_style == "'") or (quote_type == 'single' and token_style == "'") or
(quote_type == 'double' and token_style == '"')) (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): def check(conf, token, prev, next, nextnext, context):
if not (isinstance(token, yaml.tokens.ScalarToken) and 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 return
# Ignore explicit types, e.g. !!str testtest or !!int 42 # Ignore explicit types, e.g. !!str testtest or !!int 42
@ -108,36 +180,48 @@ def check(conf, token, prev, next, nextnext, context):
return return
quote_type = conf['quote-type'] 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 msg = None
if required is True: if conf['required'] is True:
# Quotes are mandatory and need to match config # 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) 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 # 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) msg = "string value is not quoted with %s quotes" % quote_type
elif not token.plain: elif not token.style:
is_extra_required = any(re.search(r, token.value)
# Quotes are disallowed when not needed for r in conf['extra-required'])
if (tag == DEFAULT_SCALAR_TAG and token.value if is_extra_required:
and token.value[0] not in START_TOKENS): msg = "string value is not quoted"
msg = "string value is redundantly quoted with %s quotes" % (
quote_type) 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 # 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) 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: if msg is not None:
yield LintProblem( yield LintProblem(

@ -30,6 +30,9 @@ This can be useful to prevent surprises from YAML parsers transforming
``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``, ``'False'``, ``'false'``, ``'YES'``, ``'Yes'``, ``'yes'``, ``'NO'``,
``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'Off'``, ``'No'``, ``'no'``, ``'ON'``, ``'On'``, ``'on'``, ``'OFF'``, ``'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 .. rubric:: Examples
@ -92,6 +95,22 @@ This can be useful to prevent surprises from YAML parsers transforming
- false - false
- on - on
- off - 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 import yaml
@ -109,14 +128,18 @@ TRUTHY = ['YES', 'Yes', 'yes',
ID = 'truthy' ID = 'truthy'
TYPE = 'token' TYPE = 'token'
CONF = {'allowed-values': list(TRUTHY)} CONF = {'allowed-values': list(TRUTHY), 'check-keys': bool}
DEFAULT = {'allowed-values': ['true', 'false']} DEFAULT = {'allowed-values': ['true', 'false'], 'check-keys': True}
def check(conf, token, prev, next, nextnext, context): def check(conf, token, prev, next, nextnext, context):
if prev and isinstance(prev, yaml.tokens.TagToken): if prev and isinstance(prev, yaml.tokens.TagToken):
return 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 isinstance(token, yaml.tokens.ScalarToken):
if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and
token.style is None): token.style is None):

Loading…
Cancel
Save