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