From f874b6607c6f31d461af65c02552857e6e9fb38b 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 + }