Skip to content

Commit

Permalink
anchors: Add new option to detect unused anchors
Browse files Browse the repository at this point in the history
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
  • Loading branch information
amimas authored and adrienverge committed May 10, 2023
1 parent 98f2281 commit f874b66
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 13 deletions.
84 changes: 78 additions & 6 deletions tests/rules/test_anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ def test_disabled(self):
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'
Expand Down Expand Up @@ -122,6 +123,7 @@ def test_forbid_undeclared_aliases(self):
'- *f_m\n'
'- *f_s\n' # declared after
'- &f_s [1, 2]\n'
'...\n'
'---\n'
'block mapping: &b_m\n'
' key: value\n'
Expand All @@ -141,13 +143,14 @@ def test_forbid_undeclared_aliases(self):
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'
Expand Down Expand Up @@ -189,6 +192,7 @@ def test_forbid_duplicated_anchors(self):
'- *f_m\n'
'- *f_s\n' # declared after
'- &f_s [1, 2]\n'
'...\n'
'---\n'
'block mapping: &b_m\n'
' key: value\n'
Expand All @@ -205,5 +209,73 @@ def test_forbid_duplicated_anchors(self):
'...\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))
70 changes: 63 additions & 7 deletions yamllint/rules/anchors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
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)
Expand All @@ -33,6 +35,7 @@
anchors:
forbid-undeclared-aliases: true
forbid-duplicated-anchors: false
forbid-unused-anchors: false
.. rubric:: Examples
Expand Down Expand Up @@ -78,6 +81,26 @@
---
- &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
"""


Expand All @@ -89,15 +112,22 @@
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
Expand All @@ -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
}

0 comments on commit f874b66

Please sign in to comment.