From 4095b8e2e6c6986f33e8d9eafbadfc79827c3bac Mon Sep 17 00:00:00 2001 From: amimas Date: Sat, 1 Apr 2023 19:22:10 -0400 Subject: [PATCH] anchors: Add new option to detect unused anchors According to the YAML specification [^1]: - > An anchored node need not be referenced by any alias nodes This means that it's OK to declare anchors but don't have any alias referencing them. However users could want to avoid this, so a new option (e.g. `forbid-unused-anchors`) is implemented in this change. It is disabled by default. [^1]: https://yaml.org/spec/1.2.2/#692-node-anchors --- tests/rules/test_anchors.py | 84 ++++++++++++++++++++++++++++++++++--- yamllint/rules/anchors.py | 70 +++++++++++++++++++++++++++---- 2 files changed, 141 insertions(+), 13 deletions(-) diff --git a/tests/rules/test_anchors.py b/tests/rules/test_anchors.py index d8fa5bd..7d7cbb7 100644 --- a/tests/rules/test_anchors.py +++ b/tests/rules/test_anchors.py @@ -80,7 +80,8 @@ class AnchorsTestCase(RuleTestCase): def test_forbid_undeclared_aliases(self): conf = ('anchors:\n' ' forbid-undeclared-aliases: true\n' - ' forbid-duplicated-anchors: false\n') + ' forbid-duplicated-anchors: false\n' + ' forbid-unused-anchors: false\n') self.check('---\n' '- &b true\n' '- &i 42\n' @@ -122,6 +123,7 @@ class AnchorsTestCase(RuleTestCase): '- *f_m\n' '- *f_s\n' # declared after '- &f_s [1, 2]\n' + '...\n' '---\n' 'block mapping: &b_m\n' ' key: value\n' @@ -141,13 +143,14 @@ class AnchorsTestCase(RuleTestCase): problem3=(11, 3), problem4=(12, 3), problem5=(13, 3), - problem6=(24, 7), - problem7=(27, 37)) + problem6=(25, 7), + problem7=(28, 37)) def test_forbid_duplicated_anchors(self): conf = ('anchors:\n' ' forbid-undeclared-aliases: false\n' - ' forbid-duplicated-anchors: true\n') + ' forbid-duplicated-anchors: true\n' + ' forbid-unused-anchors: false\n') self.check('---\n' '- &b true\n' '- &i 42\n' @@ -189,6 +192,7 @@ class AnchorsTestCase(RuleTestCase): '- *f_m\n' '- *f_s\n' # declared after '- &f_s [1, 2]\n' + '...\n' '---\n' 'block mapping: &b_m\n' ' key: value\n' @@ -205,5 +209,73 @@ class AnchorsTestCase(RuleTestCase): '...\n', conf, problem1=(5, 3), problem2=(6, 3), - problem3=(21, 18), - problem4=(27, 20)) + problem3=(22, 18), + problem4=(28, 20)) + + def test_forbid_unused_anchors(self): + conf = ('anchors:\n' + ' forbid-undeclared-aliases: false\n' + ' forbid-duplicated-anchors: false\n' + ' forbid-unused-anchors: true\n') + + self.check('---\n' + '- &b true\n' + '- &i 42\n' + '- &s hello\n' + '- &f_m {k: v}\n' + '- &f_s [1, 2]\n' + '- *b\n' + '- *i\n' + '- *s\n' + '- *f_m\n' + '- *f_s\n' + '---\n' # redeclare anchors in a new document + '- &b true\n' + '- &i 42\n' + '- &s hello\n' + '- *b\n' + '- *i\n' + '- *s\n' + '---\n' + 'block mapping: &b_m\n' + ' key: value\n' + 'extended:\n' + ' <<: *b_m\n' + ' foo: bar\n' + '---\n' + '{a: 1, &x b: 2, c: &y 3, *x : 4, e: *y}\n' + '...\n', conf) + self.check('---\n' + '- &i 42\n' + '---\n' + '- &b true\n' + '- &b true\n' + '- &b true\n' + '- &s hello\n' + '- *b\n' + '- *i\n' # declared in a previous document + '- *f_m\n' # never declared + '- *f_m\n' + '- *f_m\n' + '- *f_s\n' # declared after + '- &f_s [1, 2]\n' + '...\n' + '---\n' + 'block mapping: &b_m\n' + ' key: value\n' + '---\n' + 'block mapping 1: &b_m_bis\n' + ' key: value\n' + 'block mapping 2: &b_m_bis\n' + ' key: value\n' + 'extended:\n' + ' <<: *b_m\n' + ' foo: bar\n' + '---\n' + '{a: 1, &x b: 2, c: &x 3, *x : 4, e: *y}\n' + '...\n', conf, + problem1=(2, 3), + problem2=(7, 3), + problem3=(14, 3), + problem4=(17, 16), + problem5=(22, 18)) diff --git a/yamllint/rules/anchors.py b/yamllint/rules/anchors.py index 6d0b1e6..befb8b4 100644 --- a/yamllint/rules/anchors.py +++ b/yamllint/rules/anchors.py @@ -24,6 +24,8 @@ anchors. later in the document). * Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same anchor. +* Set ``forbid-unused-anchors`` to ``true`` to avoid anchors being declared but + not used anywhere in the YAML document via alias. .. rubric:: Default values (when enabled) @@ -33,6 +35,7 @@ anchors. anchors: forbid-undeclared-aliases: true forbid-duplicated-anchors: false + forbid-unused-anchors: false .. rubric:: Examples @@ -78,6 +81,26 @@ anchors. --- - &anchor Foo Bar - &anchor [item 1, item 2] + +#. With ``anchors: {forbid-unused-anchors: true}`` + + the following code snippet would **PASS**: + :: + + --- + - &anchor + foo: bar + - *anchor + + the following code snippet would **FAIL**: + :: + + --- + - &anchor + foo: bar + - items: + - item1 + - item2 """ @@ -89,15 +112,22 @@ from yamllint.linter import LintProblem ID = 'anchors' TYPE = 'token' CONF = {'forbid-undeclared-aliases': bool, - 'forbid-duplicated-anchors': bool} + 'forbid-duplicated-anchors': bool, + 'forbid-unused-anchors': bool} DEFAULT = {'forbid-undeclared-aliases': True, - 'forbid-duplicated-anchors': False} + 'forbid-duplicated-anchors': False, + 'forbid-unused-anchors': False} def check(conf, token, prev, next, nextnext, context): - if conf['forbid-undeclared-aliases'] or conf['forbid-duplicated-anchors']: - if isinstance(token, (yaml.StreamStartToken, yaml.DocumentStartToken)): - context['anchors'] = set() + if (conf['forbid-undeclared-aliases'] or + conf['forbid-duplicated-anchors'] or + conf['forbid-unused-anchors']): + if isinstance(token, ( + yaml.StreamStartToken, + yaml.DocumentStartToken, + yaml.DocumentEndToken)): + context['anchors'] = {} if (conf['forbid-undeclared-aliases'] and isinstance(token, yaml.AliasToken) and @@ -113,6 +143,32 @@ def check(conf, token, prev, next, nextnext, context): token.start_mark.line + 1, token.start_mark.column + 1, f'found duplicated anchor "{token.value}"') - if conf['forbid-undeclared-aliases'] or conf['forbid-duplicated-anchors']: + if conf['forbid-unused-anchors']: + # Unused anchors can only be detected at the end of Document. + # End of document can be either + # - end of stream + # - end of document sign '...' + # - start of a new document sign '---' + # If next token indicates end of document, + # check if the anchors have been used or not. + # If they haven't been used, report problem on those anchors. + if isinstance(next, (yaml.StreamEndToken, + yaml.DocumentStartToken, + yaml.DocumentEndToken)): + for anchor, info in context['anchors'].items(): + if not info['used']: + yield LintProblem(info['line'] + 1, + info['column'] + 1, + f"found unused anchor {anchor}") + elif isinstance(token, yaml.AliasToken): + context['anchors'].get(token.value, {})['used'] = True + + if (conf['forbid-undeclared-aliases'] or + conf['forbid-duplicated-anchors'] or + conf['forbid-unused-anchors']): if isinstance(token, yaml.AnchorToken): - context['anchors'].add(token.value) + context['anchors'][token.value] = { + "line": token.start_mark.line, + "column": token.start_mark.column, + "used": False + }