Add support for redundant quotes in quoted-strings rule

Co-Authored-By: Adrien Vergé
pull/244/head
Rui Pinge 5 years ago committed by Adrien Vergé
parent 15aea73fbe
commit 3a6a09b7b6

@ -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,83 @@ 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
'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 'string5: \'bar\'\n' # fails
'string6: !!str genericstring\n' 'string6: !!str genericstring\n'
'string7: !!str 456\n' 'string7: !!str 456\n'
@ -118,7 +190,7 @@ 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=(5, 10), problem2=(6, 10),
problem3=(7, 10), problem4=(8, 10)) problem3=(7, 10), problem4=(8, 10))
self.check('---\n' self.check('---\n'
'multiline string 1: |\n' 'multiline string 1: |\n'
@ -131,6 +203,144 @@ 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 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 1\\\n'
' word 2"\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 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 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)

Loading…
Cancel
Save