Skip to content

Commit

Permalink
truthy: Adapt forbidden values based on YAML version
Browse files Browse the repository at this point in the history
Specification of YAML ≤ 1.1 has 22 boolean values:

    y     | Y            | n     | N
    yes   | Yes   | YES  | no    | No    | NO
    true  | True  | TRUE | false | False | FALSE
    on    | On    | ON   | off   | Off   | OFF

Whereas YAML 1.2 spec recognizes only 6 [^1]:

    true  | True  | TRUE | false | False | FALSE

For documents that explicit state their YAML spec version at the top of
the document, let's adapt the list of forbidden values.

In the future, we should:
- implement a configuration option to declare the default YAML spec
  version, e.g. `default-yaml-spec-version: 1.2`,
- consider making 1.2 the default in a future release (this would be a
  slight breaking change, but yamllint always tried to be
  1.2-compatible).
- consider adapting yamllint to other 1.1 vs. 1.2 differences [^2].

Solves: #587

Related to: #559 #540 #430 #344 #247 #232 #158

[^1]: https://yaml.org/spec/1.2.2/#1032-tag-resolution
[^2]: https://yaml.org/spec/1.2.2/ext/changes/#changes-in-version-12-revision-120-2009-07-21
  • Loading branch information
adrienverge committed Feb 6, 2024
1 parent 9931ad6 commit 3b6a3df
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 18 deletions.
88 changes: 80 additions & 8 deletions tests/rules/test_truthy.py
Expand Up @@ -27,15 +27,17 @@ def test_disabled(self):
'True: 1\n', conf)

def test_enabled(self):
conf = 'truthy: enable\n'
conf = ('truthy: enable\n'
'document-start: disable\n')
self.check('---\n'
'1: True\n'
'True: 1\n',
conf, problem1=(2, 4), problem2=(3, 1))
self.check('---\n'
'1: "True"\n'
'"True": 1\n', conf)
self.check('---\n'
self.check('%YAML 1.1\n'
'---\n'
'[\n'
' true, false,\n'
' "false", "FALSE",\n'
Expand All @@ -44,9 +46,47 @@ def test_enabled(self):
' on, OFF,\n'
' NO, Yes\n'
']\n', conf,
problem1=(6, 3), problem2=(6, 9),
problem3=(7, 3), problem4=(7, 7),
problem5=(8, 3), problem6=(8, 7))
problem1=(7, 3), problem2=(7, 9),
problem3=(8, 3), problem4=(8, 7),
problem5=(9, 3), problem6=(9, 7))
self.check('y: 1\n'
'yes: 2\n'
'on: 3\n'
'true: 4\n'
'True: 5\n'
'...\n'
'%YAML 1.2\n'
'---\n'
'y: 1\n'
'yes: 2\n'
'on: 3\n'
'true: 4\n'
'True: 5\n'
'...\n'
'%YAML 1.1\n'
'---\n'
'y: 1\n'
'yes: 2\n'
'on: 3\n'
'true: 4\n'
'True: 5\n'
'---\n'
'y: 1\n'
'yes: 2\n'
'on: 3\n'
'true: 4\n'
'True: 5\n',
conf,
problem1=(2, 1),
problem2=(3, 1),
problem3=(5, 1),
problem4=(13, 1),
problem5=(18, 1),
problem6=(19, 1),
problem7=(21, 1),
problem8=(24, 1),
problem9=(25, 1),
problem10=(27, 1))

def test_different_allowed_values(self):
conf = ('truthy:\n'
Expand All @@ -56,15 +96,16 @@ def test_different_allowed_values(self):
'key2: yes\n'
'key3: bar\n'
'key4: no\n', conf)
self.check('---\n'
self.check('%YAML 1.1\n'
'---\n'
'key1: true\n'
'key2: Yes\n'
'key3: false\n'
'key4: no\n'
'key5: yes\n',
conf,
problem1=(2, 7), problem2=(3, 7),
problem3=(4, 7))
problem1=(3, 7), problem2=(4, 7),
problem3=(5, 7))

def test_combined_allowed_values(self):
conf = ('truthy:\n'
Expand All @@ -81,6 +122,22 @@ def test_combined_allowed_values(self):
'key4: no\n'
'key5: yes\n',
conf, problem1=(3, 7))
self.check('%YAML 1.1\n'
'---\n'
'key1: true\n'
'key2: Yes\n'
'key3: false\n'
'key4: no\n'
'key5: yes\n',
conf, problem1=(4, 7))
self.check('%YAML 1.2\n'
'---\n'
'key1: true\n'
'key2: Yes\n'
'key3: false\n'
'key4: no\n'
'key5: yes\n',
conf)

def test_no_allowed_values(self):
conf = ('truthy:\n'
Expand All @@ -95,6 +152,21 @@ def test_no_allowed_values(self):
'key4: no\n', conf,
problem1=(2, 7), problem2=(3, 7),
problem3=(4, 7), problem4=(5, 7))
self.check('%YAML 1.1\n'
'---\n'
'key1: true\n'
'key2: yes\n'
'key3: false\n'
'key4: no\n', conf,
problem1=(3, 7), problem2=(4, 7),
problem3=(5, 7), problem4=(6, 7))
self.check('%YAML 1.2\n'
'---\n'
'key1: true\n'
'key2: yes\n'
'key3: false\n'
'key4: no\n', conf,
problem1=(3, 7), problem2=(5, 7))

def test_explicit_types(self):
conf = 'truthy: enable\n'
Expand Down
57 changes: 47 additions & 10 deletions yamllint/rules/truthy.py
Expand Up @@ -21,6 +21,13 @@
``[yes, FALSE, Off]`` into ``[true, false, false]`` or
``{y: 1, yes: 2, on: 3, true: 4, True: 5}`` into ``{y: 1, true: 5}``.
Depending on the YAML specification version used by the YAML document, the list
of truthy values can differ. In YAML 1.2, only capitalized / uppercased
combinations of ``true`` and ``false`` are considered truthy, whereas in YAML
1.1 combinations of ``yes``, ``no``, ``on`` and ``off`` are too. To make the
YAML specification version explicit in a YAML document, a ``%YAML 1.2``
directive can be used (see example below).
.. rubric:: Options
* ``allowed-values`` defines the list of truthy values which will be ignored
Expand Down Expand Up @@ -80,10 +87,21 @@
the following code snippet would **FAIL**:
::
%YAML 1.1
---
yes: 1
on: 2
True: 3
the following code snippet would **PASS**:
::
%YAML 1.2
---
yes: 1
on: 2
true: 3
#. With ``truthy: {allowed-values: ["yes", "no"]}``
the following code snippet would **PASS**:
Expand Down Expand Up @@ -125,31 +143,50 @@

from yamllint.linter import LintProblem

TRUTHY = ['YES', 'Yes', 'yes',
'NO', 'No', 'no',
'TRUE', 'True', 'true',
'FALSE', 'False', 'false',
'ON', 'On', 'on',
'OFF', 'Off', 'off']
TRUTHY_1_1 = ['YES', 'Yes', 'yes',
'NO', 'No', 'no',
'TRUE', 'True', 'true',
'FALSE', 'False', 'false',
'ON', 'On', 'on',
'OFF', 'Off', 'off']
TRUTHY_1_2 = ['TRUE', 'True', 'true',
'FALSE', 'False', 'false']


ID = 'truthy'
TYPE = 'token'
CONF = {'allowed-values': TRUTHY.copy(), 'check-keys': bool}
CONF = {'allowed-values': TRUTHY_1_1.copy(), 'check-keys': bool}
DEFAULT = {'allowed-values': ['true', 'false'], 'check-keys': True}


def yaml_spec_version_for_document(context):
if 'yaml_spec_version' in context:
return context['yaml_spec_version']
return (1, 1)


def check(conf, token, prev, next, nextnext, context):
if isinstance(token, yaml.tokens.DirectiveToken) and token.name == 'YAML':
context['yaml_spec_version'] = token.value
elif isinstance(token, yaml.tokens.DocumentEndToken):
context.pop('yaml_spec_version', None)
context.pop('bad_truthy_values', None)

if prev and isinstance(prev, yaml.tokens.TagToken):
return

if (not conf['check-keys'] and isinstance(prev, yaml.tokens.KeyToken) and
isinstance(token, yaml.tokens.ScalarToken)):
return

if isinstance(token, yaml.tokens.ScalarToken):
if (token.value in (set(TRUTHY) - set(conf['allowed-values'])) and
token.style is None):
if isinstance(token, yaml.tokens.ScalarToken) and token.style is None:
if 'bad_truthy_values' not in context:
context['bad_truthy_values'] = set(
TRUTHY_1_2 if yaml_spec_version_for_document(context) == (1, 2)
else TRUTHY_1_1)
context['bad_truthy_values'] -= set(conf['allowed-values'])

if token.value in context['bad_truthy_values']:
yield LintProblem(token.start_mark.line + 1,
token.start_mark.column + 1,
"truthy value should be one of [" +
Expand Down

0 comments on commit 3b6a3df

Please sign in to comment.