diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 973660c..0fdba71 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -22,6 +22,7 @@ class QuotedTestCase(RuleTestCase): def test_disabled(self): conf = 'quoted-strings: disable' + self.check('---\n' 'foo: bar\n', conf) self.check('---\n' @@ -35,18 +36,18 @@ class QuotedTestCase(RuleTestCase): def test_quote_type_any(self): conf = 'quoted-strings: {quote-type: any}\n' + self.check('---\n' 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' # fails - 'string2: "true"\n' - 'string3: "123"\n' - 'string4: \'true\'\n' - 'string5: "foo"\n' - 'string6: \'bar\'\n' - 'string7: !!str genericstring\n' - 'string8: !!str 456\n' - 'string9: !!str "quotedgenericstring"\n' + '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' @@ -60,7 +61,7 @@ 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' @@ -69,6 +70,7 @@ class QuotedTestCase(RuleTestCase): def test_quote_type_single(self): conf = 'quoted-strings: {quote-type: single}\n' + self.check('---\n' 'boolean1: true\n' 'number1: 123\n' @@ -94,7 +96,7 @@ 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' @@ -103,13 +105,83 @@ class QuotedTestCase(RuleTestCase): def test_quote_type_double(self): conf = 'quoted-strings: {quote-type: double}\n' + self.check('---\n' 'boolean1: true\n' 'number1: 123\n' 'string1: foo\n' # fails 'string2: "foo"\n' - 'string3: \'true\'\n' # fails - 'string4: \'123\'\n' # fails + 'string3: "true"\n' + 'string4: "123"\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, problem1=(4, 10), problem2=(8, 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_disallow_redundant_quotes(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' + 'string4: "123"\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, problem1=(5, 10), problem2=(8, 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' + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' word 2"\n', + conf, problem1=(12, 3)) + + def test_disallow_redundant_single_quotes(self): + conf = 'quoted-strings: {quote-type: single, ' + \ + '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' # fails 'string6: !!str genericstring\n' 'string7: !!str 456\n' @@ -118,7 +190,7 @@ class QuotedTestCase(RuleTestCase): 'integer: !!int intstring\n' 'boolean2: !!bool boolstring\n' 'boolean3: !!bool "quotedboolstring"\n', - conf, problem1=(4, 10), problem2=(6, 10), + conf, problem1=(5, 10), problem2=(6, 10), problem3=(7, 10), problem4=(8, 10)) self.check('---\n' 'multiline string 1: |\n' @@ -131,6 +203,144 @@ class QuotedTestCase(RuleTestCase): ' word 1\n' ' word 2\n' 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' 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' + + self.check('---\n' + 'boolean1: true\n' + 'number1: 123\n' + 'string1: foo\n' + '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) + 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' + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' + ' word 2"\n', + conf) + + def test_single_quotes_relaxed(self): + conf = 'quoted-strings: {quote-type: single, 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' + '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)) + 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' + ' word 2\n' + 'multiline string 4:\n' + ' "word 1\\\n' # fails + ' 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)) diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 4fc5aa2..aaa635d 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -15,15 +15,23 @@ # along with this program. If not, see . """ -Use this rule to forbid any string values that are not quoted. -You can also enforce the type of the quote used using the ``quote-type`` option -(``single``, ``double`` or ``any``). +Use this rule to forbid any string values that are not quoted, or to prevent +quoted strings without needing it. You can also enforce the type of the quote +used. + +.. rubric:: Options + +* ``quote-type`` defines allowed quotes: ``single``, ``double`` or ``any`` + (default). +* ``required`` defines whether using quotes in string values is required + (``true``, default) or not (``false``), or only allowed when really needed + (``only-when-needed``). **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked. .. rubric:: Examples -#. With ``quoted-strings: {quote-type: any}`` +#. With ``quoted-strings: {quote-type: any, required: true}`` the following code snippet would **PASS**: :: @@ -37,6 +45,24 @@ You can also enforce the type of the quote used using the ``quote-type`` option :: foo: bar + +#. With ``quoted-strings: {quote-type: single, required: only-when-needed}`` + + the following code snippet would **PASS**: + :: + + foo: bar + bar: foo + not_number: '123' + not_boolean: 'true' + not_comment: '# comment' + not_list: '[1, 2, 3]' + not_map: '{a: 1, b: 2}' + + the following code snippet would **FAIL**: + :: + + foo: 'bar' """ import yaml @@ -45,13 +71,23 @@ from yamllint.linter import LintProblem ID = 'quoted-strings' TYPE = 'token' -CONF = {'quote-type': ('any', 'single', 'double')} -DEFAULT = {'quote-type': 'any'} +CONF = {'quote-type': ('any', 'single', 'double'), + 'required': (True, False, 'only-when-needed')} +DEFAULT = {'quote-type': 'any', + 'required': True} +DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' +START_TOKENS = {'#', '*', '!', '?', '@', '`', '&', + ',', '-', '{', '}', '[', ']', ':'} -def check(conf, token, prev, next, nextnext, context): - quote_type = conf['quote-type'] +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 check(conf, token, prev, next, nextnext, context): if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.ValueToken, yaml.TagToken))): return @@ -62,20 +98,49 @@ def check(conf, token, prev, next, nextnext, context): return # Ignore numbers, booleans, etc. - if token.plain: - resolver = yaml.resolver.Resolver() - if resolver.resolve(yaml.nodes.ScalarNode, token.value, - (True, False)) != 'tag:yaml.org,2002:str': - return + resolver = yaml.resolver.Resolver() + tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False)) + if token.plain and tag != DEFAULT_SCALAR_TAG: + return # Ignore multi-line strings if (not token.plain) and (token.style == "|" or token.style == ">"): return - if ((quote_type == 'single' and token.style != "'") or - (quote_type == 'double' and token.style != '"') or - (quote_type == 'any' and token.style is None)): + 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: + + # 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) + + 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): + 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) + + # 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) + + if msg is not None: yield LintProblem( token.start_mark.line + 1, token.start_mark.column + 1, - "string value is not quoted with %s quotes" % (quote_type)) + msg)