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(