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
This commit is contained in:
amimas 2023-04-01 19:22:10 -04:00 committed by Adrien Vergé
parent 98f2281f56
commit f874b6607c
2 changed files with 141 additions and 13 deletions

View File

@ -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))

View File

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