diff --git a/tests/rules/test_quoted_strings.py b/tests/rules/test_quoted_strings.py index 9c81d828..543cc0d1 100644 --- a/tests/rules/test_quoted_strings.py +++ b/tests/rules/test_quoted_strings.py @@ -453,3 +453,106 @@ def test_octal_values(self): '- "0o800"\n', conf, problem1=(9, 3), problem2=(10, 3)) + + def test_allow_quoted_quotes(self): + conf = ('quoted-strings: {quote-type: single,\n' + ' required: false,\n' + ' allow-quoted-quotes: false}\n') + self.check('---\n' + 'foo1: "[barbaz]"\n' # fails + 'foo2: "[bar\'baz]"\n', # fails + conf, problem1=(2, 7), problem2=(3, 7)) + + conf = ('quoted-strings: {quote-type: single,\n' + ' required: false,\n' + ' allow-quoted-quotes: true}\n') + self.check('---\n' + 'foo1: "[barbaz]"\n' # fails + 'foo2: "[bar\'baz]"\n', + conf, problem1=(2, 7)) + + conf = ('quoted-strings: {quote-type: single,\n' + ' required: true,\n' + ' allow-quoted-quotes: false}\n') + self.check('---\n' + 'foo1: "[barbaz]"\n' # fails + 'foo2: "[bar\'baz]"\n', # fails + conf, problem1=(2, 7), problem2=(3, 7)) + + conf = ('quoted-strings: {quote-type: single,\n' + ' required: true,\n' + ' allow-quoted-quotes: true}\n') + self.check('---\n' + 'foo1: "[barbaz]"\n' # fails + 'foo2: "[bar\'baz]"\n', + conf, problem1=(2, 7)) + + conf = ('quoted-strings: {quote-type: single,\n' + ' required: only-when-needed,\n' + ' allow-quoted-quotes: false}\n') + self.check('---\n' + 'foo1: "[barbaz]"\n' # fails + 'foo2: "[bar\'baz]"\n', # fails + conf, problem1=(2, 7), problem2=(3, 7)) + + conf = ('quoted-strings: {quote-type: single,\n' + ' required: only-when-needed,\n' + ' allow-quoted-quotes: true}\n') + self.check('---\n' + 'foo1: "[barbaz]"\n' # fails + 'foo2: "[bar\'baz]"\n', + conf, problem1=(2, 7)) + + conf = ('quoted-strings: {quote-type: double,\n' + ' required: false,\n' + ' allow-quoted-quotes: false}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" # fails + "foo2: '[bar\"baz]'\n", # fails + conf, problem1=(2, 7), problem2=(3, 7)) + + conf = ('quoted-strings: {quote-type: double,\n' + ' required: false,\n' + ' allow-quoted-quotes: true}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" # fails + "foo2: '[bar\"baz]'\n", + conf, problem1=(2, 7)) + + conf = ('quoted-strings: {quote-type: double,\n' + ' required: true,\n' + ' allow-quoted-quotes: false}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" # fails + "foo2: '[bar\"baz]'\n", # fails + conf, problem1=(2, 7), problem2=(3, 7)) + + conf = ('quoted-strings: {quote-type: double,\n' + ' required: true,\n' + ' allow-quoted-quotes: true}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" # fails + "foo2: '[bar\"baz]'\n", + conf, problem1=(2, 7)) + + conf = ('quoted-strings: {quote-type: double,\n' + ' required: only-when-needed,\n' + ' allow-quoted-quotes: false}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" # fails + "foo2: '[bar\"baz]'\n", # fails + conf, problem1=(2, 7), problem2=(3, 7)) + + conf = ('quoted-strings: {quote-type: double,\n' + ' required: only-when-needed,\n' + ' allow-quoted-quotes: true}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" # fails + "foo2: '[bar\"baz]'\n", + conf, problem1=(2, 7)) + + conf = ('quoted-strings: {quote-type: any}\n') + self.check("---\n" + "foo1: '[barbaz]'\n" + "foo2: '[bar\"baz]'\n", + conf) diff --git a/yamllint/rules/quoted_strings.py b/yamllint/rules/quoted_strings.py index 7dfb613f..dbc2e463 100644 --- a/yamllint/rules/quoted_strings.py +++ b/yamllint/rules/quoted_strings.py @@ -30,6 +30,8 @@ ``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. +* ``allow-quoted-quotes`` allows (``true``) using disallowed quotes for strings + with allowed quotes inside. Default ``false``. **Note**: Multi-line strings (with ``|`` or ``>``) will not be checked. @@ -43,6 +45,7 @@ required: true extra-required: [] extra-allowed: [] + allow-quoted-quotes: false .. rubric:: Examples @@ -112,6 +115,26 @@ - "localhost" - this is a string that needs to be QUOTED + +#. With ``quoted-strings: {quote-type: double, allow-quoted-quotes: false}`` + + the following code snippet would **PASS**: + :: + + foo: "bar\"baz" + + the following code snippet would **FAIL**: + :: + + foo: 'bar"baz' + +#. With ``quoted-strings: {quote-type: double, allow-quoted-quotes: true}`` + + the following code snippet would **PASS**: + :: + + foo: 'bar"baz' + """ import re @@ -125,11 +148,13 @@ CONF = {'quote-type': ('any', 'single', 'double'), 'required': (True, False, 'only-when-needed'), 'extra-required': [str], - 'extra-allowed': [str]} + 'extra-allowed': [str], + 'allow-quoted-quotes': bool} DEFAULT = {'quote-type': 'any', 'required': True, 'extra-required': [], - 'extra-allowed': []} + 'extra-allowed': [], + 'allow-quoted-quotes': False} def VALIDATE(conf): @@ -177,6 +202,12 @@ def _quotes_are_needed(string): return True +def _has_quoted_quotes(token): + return ((not token.plain) and + ((token.style == "'" and '"' in token.value) or + (token.style == '"' and "'" in token.value))) + + def check(conf, token, prev, next, nextnext, context): if not (isinstance(token, yaml.tokens.ScalarToken) and isinstance(prev, (yaml.BlockEntryToken, yaml.FlowEntryToken, @@ -206,13 +237,18 @@ def check(conf, token, prev, next, nextnext, context): 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): + if (token.style is None or + not (_quote_match(quote_type, token.style) or + (conf['allow-quoted-quotes'] and _has_quoted_quotes(token)))): msg = "string value is not quoted with %s quotes" % quote_type 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): + if (token.style and + not _quote_match(quote_type, token.style) and + not (conf['allow-quoted-quotes'] and + _has_quoted_quotes(token))): msg = "string value is not quoted with %s quotes" % quote_type elif not token.style: @@ -235,7 +271,9 @@ def check(conf, token, prev, next, nextnext, context): 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) and + not (conf['allow-quoted-quotes'] and _has_quoted_quotes(token))): msg = "string value is not quoted with %s quotes" % quote_type elif not token.style: