From 01df5bf038ed4df3170477aab5ec8949d9bc8260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Verg=C3=A9?= Date: Wed, 31 Jan 2024 16:23:04 +0100 Subject: [PATCH] truthy: Adapt forbidden values based on YAML version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: https://github.com/adrienverge/yamllint/issues/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 --- tests/rules/test_truthy.py | 88 ++++++++++++++++++++++++++++++++++---- yamllint/rules/truthy.py | 57 +++++++++++++++++++----- 2 files changed, 127 insertions(+), 18 deletions(-) diff --git a/tests/rules/test_truthy.py b/tests/rules/test_truthy.py index c8c8b7a1..e485d07e 100644 --- a/tests/rules/test_truthy.py +++ b/tests/rules/test_truthy.py @@ -27,7 +27,8 @@ 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', @@ -35,7 +36,8 @@ def test_enabled(self): 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' @@ -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' @@ -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' @@ -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' @@ -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' diff --git a/yamllint/rules/truthy.py b/yamllint/rules/truthy.py index 4b7179ab..ff47a835 100644 --- a/yamllint/rules/truthy.py +++ b/yamllint/rules/truthy.py @@ -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 @@ -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**: @@ -125,21 +143,35 @@ 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 @@ -147,9 +179,14 @@ def check(conf, token, prev, next, nextnext, context): 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 [" +