Add support for redundant quotes in quoted-strings rule
Co-Authored-By: Adrien Vergé
This commit is contained in:
@@ -22,6 +22,7 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
|
|
||||||
def test_disabled(self):
|
def test_disabled(self):
|
||||||
conf = 'quoted-strings: disable'
|
conf = 'quoted-strings: disable'
|
||||||
|
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
'foo: bar\n', conf)
|
'foo: bar\n', conf)
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
@@ -35,18 +36,18 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
|
|
||||||
def test_quote_type_any(self):
|
def test_quote_type_any(self):
|
||||||
conf = 'quoted-strings: {quote-type: any}\n'
|
conf = 'quoted-strings: {quote-type: any}\n'
|
||||||
|
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
'boolean1: true\n'
|
'boolean1: true\n'
|
||||||
'number1: 123\n'
|
'number1: 123\n'
|
||||||
'string1: foo\n' # fails
|
'string1: foo\n' # fails
|
||||||
'string2: "true"\n'
|
'string2: "foo"\n'
|
||||||
'string3: "123"\n'
|
'string3: "true"\n'
|
||||||
'string4: \'true\'\n'
|
'string4: "123"\n'
|
||||||
'string5: "foo"\n'
|
'string5: \'bar\'\n'
|
||||||
'string6: \'bar\'\n'
|
'string6: !!str genericstring\n'
|
||||||
'string7: !!str genericstring\n'
|
'string7: !!str 456\n'
|
||||||
'string8: !!str 456\n'
|
'string8: !!str "quotedgenericstring"\n'
|
||||||
'string9: !!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'
|
||||||
@@ -60,7 +61,7 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
' word 1\n'
|
' word 1\n'
|
||||||
' word 2\n'
|
' word 2\n'
|
||||||
'multiline string 3:\n'
|
'multiline string 3:\n'
|
||||||
' word 1\n'
|
' word 1\n' # fails
|
||||||
' word 2\n'
|
' word 2\n'
|
||||||
'multiline string 4:\n'
|
'multiline string 4:\n'
|
||||||
' "word 1\\\n'
|
' "word 1\\\n'
|
||||||
@@ -69,6 +70,7 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
|
|
||||||
def test_quote_type_single(self):
|
def test_quote_type_single(self):
|
||||||
conf = 'quoted-strings: {quote-type: single}\n'
|
conf = 'quoted-strings: {quote-type: single}\n'
|
||||||
|
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
'boolean1: true\n'
|
'boolean1: true\n'
|
||||||
'number1: 123\n'
|
'number1: 123\n'
|
||||||
@@ -94,7 +96,7 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
' word 1\n'
|
' word 1\n'
|
||||||
' word 2\n'
|
' word 2\n'
|
||||||
'multiline string 3:\n'
|
'multiline string 3:\n'
|
||||||
' word 1\n'
|
' word 1\n' # fails
|
||||||
' word 2\n'
|
' word 2\n'
|
||||||
'multiline string 4:\n'
|
'multiline string 4:\n'
|
||||||
' "word 1\\\n'
|
' "word 1\\\n'
|
||||||
@@ -103,13 +105,14 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
|
|
||||||
def test_quote_type_double(self):
|
def test_quote_type_double(self):
|
||||||
conf = 'quoted-strings: {quote-type: double}\n'
|
conf = 'quoted-strings: {quote-type: double}\n'
|
||||||
|
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
'boolean1: true\n'
|
'boolean1: true\n'
|
||||||
'number1: 123\n'
|
'number1: 123\n'
|
||||||
'string1: foo\n' # fails
|
'string1: foo\n' # fails
|
||||||
'string2: "foo"\n'
|
'string2: "foo"\n'
|
||||||
'string3: \'true\'\n' # fails
|
'string3: "true"\n'
|
||||||
'string4: \'123\'\n' # fails
|
'string4: "123"\n'
|
||||||
'string5: \'bar\'\n' # fails
|
'string5: \'bar\'\n' # fails
|
||||||
'string6: !!str genericstring\n'
|
'string6: !!str genericstring\n'
|
||||||
'string7: !!str 456\n'
|
'string7: !!str 456\n'
|
||||||
@@ -118,8 +121,146 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
'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=(6, 10),
|
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'
|
||||||
|
'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))
|
problem3=(7, 10), problem4=(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_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'
|
self.check('---\n'
|
||||||
'multiline string 1: |\n'
|
'multiline string 1: |\n'
|
||||||
' line 1\n'
|
' line 1\n'
|
||||||
@@ -133,4 +274,73 @@ class QuotedTestCase(RuleTestCase):
|
|||||||
'multiline string 4:\n'
|
'multiline string 4:\n'
|
||||||
' "word 1\\\n'
|
' "word 1\\\n'
|
||||||
' word 2"\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))
|
conf, problem1=(9, 3))
|
||||||
|
|||||||
@@ -15,15 +15,23 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Use this rule to forbid any string values that are not quoted.
|
Use this rule to forbid any string values that are not quoted, or to prevent
|
||||||
You can also enforce the type of the quote used using the ``quote-type`` option
|
quoted strings without needing it. You can also enforce the type of the quote
|
||||||
(``single``, ``double`` or ``any``).
|
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.
|
**Note**: Multi-line strings (with ``|`` or ``>``) will not be checked.
|
||||||
|
|
||||||
.. rubric:: Examples
|
.. rubric:: Examples
|
||||||
|
|
||||||
#. With ``quoted-strings: {quote-type: any}``
|
#. With ``quoted-strings: {quote-type: any, required: true}``
|
||||||
|
|
||||||
the following code snippet would **PASS**:
|
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
|
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
|
import yaml
|
||||||
@@ -45,13 +71,23 @@ 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'),
|
||||||
DEFAULT = {'quote-type': 'any'}
|
'required': (True, False, 'only-when-needed')}
|
||||||
|
DEFAULT = {'quote-type': 'any',
|
||||||
|
'required': True}
|
||||||
|
|
||||||
|
DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str'
|
||||||
|
START_TOKENS = {'#', '*', '!', '?', '@', '`', '&',
|
||||||
|
',', '-', '{', '}', '[', ']', ':'}
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def check(conf, token, prev, next, nextnext, context):
|
||||||
quote_type = conf['quote-type']
|
|
||||||
|
|
||||||
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.ValueToken, yaml.TagToken))):
|
||||||
return
|
return
|
||||||
@@ -62,20 +98,49 @@ def check(conf, token, prev, next, nextnext, context):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Ignore numbers, booleans, etc.
|
# Ignore numbers, booleans, etc.
|
||||||
if token.plain:
|
resolver = yaml.resolver.Resolver()
|
||||||
resolver = yaml.resolver.Resolver()
|
tag = resolver.resolve(yaml.nodes.ScalarNode, token.value, (True, False))
|
||||||
if resolver.resolve(yaml.nodes.ScalarNode, token.value,
|
if token.plain and tag != DEFAULT_SCALAR_TAG:
|
||||||
(True, False)) != 'tag:yaml.org,2002:str':
|
return
|
||||||
return
|
|
||||||
|
|
||||||
# Ignore multi-line strings
|
# Ignore multi-line strings
|
||||||
if (not token.plain) and (token.style == "|" or token.style == ">"):
|
if (not token.plain) and (token.style == "|" or token.style == ">"):
|
||||||
return
|
return
|
||||||
|
|
||||||
if ((quote_type == 'single' and token.style != "'") or
|
quote_type = conf['quote-type']
|
||||||
(quote_type == 'double' and token.style != '"') or
|
required = conf['required']
|
||||||
(quote_type == 'any' and token.style is None)):
|
|
||||||
|
# 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(
|
yield LintProblem(
|
||||||
token.start_mark.line + 1,
|
token.start_mark.line + 1,
|
||||||
token.start_mark.column + 1,
|
token.start_mark.column + 1,
|
||||||
"string value is not quoted with %s quotes" % (quote_type))
|
msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user