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:
@@ -80,7 +80,8 @@ class AnchorsTestCase(RuleTestCase):
|
|||||||
def test_forbid_undeclared_aliases(self):
|
def test_forbid_undeclared_aliases(self):
|
||||||
conf = ('anchors:\n'
|
conf = ('anchors:\n'
|
||||||
' forbid-undeclared-aliases: true\n'
|
' forbid-undeclared-aliases: true\n'
|
||||||
' forbid-duplicated-anchors: false\n')
|
' forbid-duplicated-anchors: false\n'
|
||||||
|
' forbid-unused-anchors: false\n')
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
'- &b true\n'
|
'- &b true\n'
|
||||||
'- &i 42\n'
|
'- &i 42\n'
|
||||||
@@ -122,6 +123,7 @@ class AnchorsTestCase(RuleTestCase):
|
|||||||
'- *f_m\n'
|
'- *f_m\n'
|
||||||
'- *f_s\n' # declared after
|
'- *f_s\n' # declared after
|
||||||
'- &f_s [1, 2]\n'
|
'- &f_s [1, 2]\n'
|
||||||
|
'...\n'
|
||||||
'---\n'
|
'---\n'
|
||||||
'block mapping: &b_m\n'
|
'block mapping: &b_m\n'
|
||||||
' key: value\n'
|
' key: value\n'
|
||||||
@@ -141,13 +143,14 @@ class AnchorsTestCase(RuleTestCase):
|
|||||||
problem3=(11, 3),
|
problem3=(11, 3),
|
||||||
problem4=(12, 3),
|
problem4=(12, 3),
|
||||||
problem5=(13, 3),
|
problem5=(13, 3),
|
||||||
problem6=(24, 7),
|
problem6=(25, 7),
|
||||||
problem7=(27, 37))
|
problem7=(28, 37))
|
||||||
|
|
||||||
def test_forbid_duplicated_anchors(self):
|
def test_forbid_duplicated_anchors(self):
|
||||||
conf = ('anchors:\n'
|
conf = ('anchors:\n'
|
||||||
' forbid-undeclared-aliases: false\n'
|
' forbid-undeclared-aliases: false\n'
|
||||||
' forbid-duplicated-anchors: true\n')
|
' forbid-duplicated-anchors: true\n'
|
||||||
|
' forbid-unused-anchors: false\n')
|
||||||
self.check('---\n'
|
self.check('---\n'
|
||||||
'- &b true\n'
|
'- &b true\n'
|
||||||
'- &i 42\n'
|
'- &i 42\n'
|
||||||
@@ -189,6 +192,7 @@ class AnchorsTestCase(RuleTestCase):
|
|||||||
'- *f_m\n'
|
'- *f_m\n'
|
||||||
'- *f_s\n' # declared after
|
'- *f_s\n' # declared after
|
||||||
'- &f_s [1, 2]\n'
|
'- &f_s [1, 2]\n'
|
||||||
|
'...\n'
|
||||||
'---\n'
|
'---\n'
|
||||||
'block mapping: &b_m\n'
|
'block mapping: &b_m\n'
|
||||||
' key: value\n'
|
' key: value\n'
|
||||||
@@ -205,5 +209,73 @@ class AnchorsTestCase(RuleTestCase):
|
|||||||
'...\n', conf,
|
'...\n', conf,
|
||||||
problem1=(5, 3),
|
problem1=(5, 3),
|
||||||
problem2=(6, 3),
|
problem2=(6, 3),
|
||||||
problem3=(21, 18),
|
problem3=(22, 18),
|
||||||
problem4=(27, 20))
|
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))
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ anchors.
|
|||||||
later in the document).
|
later in the document).
|
||||||
* Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same
|
* Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same
|
||||||
anchor.
|
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)
|
.. rubric:: Default values (when enabled)
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ anchors.
|
|||||||
anchors:
|
anchors:
|
||||||
forbid-undeclared-aliases: true
|
forbid-undeclared-aliases: true
|
||||||
forbid-duplicated-anchors: false
|
forbid-duplicated-anchors: false
|
||||||
|
forbid-unused-anchors: false
|
||||||
|
|
||||||
.. rubric:: Examples
|
.. rubric:: Examples
|
||||||
|
|
||||||
@@ -78,6 +81,26 @@ anchors.
|
|||||||
---
|
---
|
||||||
- &anchor Foo Bar
|
- &anchor Foo Bar
|
||||||
- &anchor [item 1, item 2]
|
- &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'
|
ID = 'anchors'
|
||||||
TYPE = 'token'
|
TYPE = 'token'
|
||||||
CONF = {'forbid-undeclared-aliases': bool,
|
CONF = {'forbid-undeclared-aliases': bool,
|
||||||
'forbid-duplicated-anchors': bool}
|
'forbid-duplicated-anchors': bool,
|
||||||
|
'forbid-unused-anchors': bool}
|
||||||
DEFAULT = {'forbid-undeclared-aliases': True,
|
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):
|
def check(conf, token, prev, next, nextnext, context):
|
||||||
if conf['forbid-undeclared-aliases'] or conf['forbid-duplicated-anchors']:
|
if (conf['forbid-undeclared-aliases'] or
|
||||||
if isinstance(token, (yaml.StreamStartToken, yaml.DocumentStartToken)):
|
conf['forbid-duplicated-anchors'] or
|
||||||
context['anchors'] = set()
|
conf['forbid-unused-anchors']):
|
||||||
|
if isinstance(token, (
|
||||||
|
yaml.StreamStartToken,
|
||||||
|
yaml.DocumentStartToken,
|
||||||
|
yaml.DocumentEndToken)):
|
||||||
|
context['anchors'] = {}
|
||||||
|
|
||||||
if (conf['forbid-undeclared-aliases'] and
|
if (conf['forbid-undeclared-aliases'] and
|
||||||
isinstance(token, yaml.AliasToken) 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,
|
token.start_mark.line + 1, token.start_mark.column + 1,
|
||||||
f'found duplicated anchor "{token.value}"')
|
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):
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user