Skip to content

Commit f874b66

Browse files
amimasadrienverge
authored andcommitted
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
1 parent 98f2281 commit f874b66

File tree

2 files changed

+141
-13
lines changed

2 files changed

+141
-13
lines changed

tests/rules/test_anchors.py

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def test_disabled(self):
8080
def test_forbid_undeclared_aliases(self):
8181
conf = ('anchors:\n'
8282
' forbid-undeclared-aliases: true\n'
83-
' forbid-duplicated-anchors: false\n')
83+
' forbid-duplicated-anchors: false\n'
84+
' forbid-unused-anchors: false\n')
8485
self.check('---\n'
8586
'- &b true\n'
8687
'- &i 42\n'
@@ -122,6 +123,7 @@ def test_forbid_undeclared_aliases(self):
122123
'- *f_m\n'
123124
'- *f_s\n' # declared after
124125
'- &f_s [1, 2]\n'
126+
'...\n'
125127
'---\n'
126128
'block mapping: &b_m\n'
127129
' key: value\n'
@@ -141,13 +143,14 @@ def test_forbid_undeclared_aliases(self):
141143
problem3=(11, 3),
142144
problem4=(12, 3),
143145
problem5=(13, 3),
144-
problem6=(24, 7),
145-
problem7=(27, 37))
146+
problem6=(25, 7),
147+
problem7=(28, 37))
146148

147149
def test_forbid_duplicated_anchors(self):
148150
conf = ('anchors:\n'
149151
' forbid-undeclared-aliases: false\n'
150-
' forbid-duplicated-anchors: true\n')
152+
' forbid-duplicated-anchors: true\n'
153+
' forbid-unused-anchors: false\n')
151154
self.check('---\n'
152155
'- &b true\n'
153156
'- &i 42\n'
@@ -189,6 +192,7 @@ def test_forbid_duplicated_anchors(self):
189192
'- *f_m\n'
190193
'- *f_s\n' # declared after
191194
'- &f_s [1, 2]\n'
195+
'...\n'
192196
'---\n'
193197
'block mapping: &b_m\n'
194198
' key: value\n'
@@ -205,5 +209,73 @@ def test_forbid_duplicated_anchors(self):
205209
'...\n', conf,
206210
problem1=(5, 3),
207211
problem2=(6, 3),
208-
problem3=(21, 18),
209-
problem4=(27, 20))
212+
problem3=(22, 18),
213+
problem4=(28, 20))
214+
215+
def test_forbid_unused_anchors(self):
216+
conf = ('anchors:\n'
217+
' forbid-undeclared-aliases: false\n'
218+
' forbid-duplicated-anchors: false\n'
219+
' forbid-unused-anchors: true\n')
220+
221+
self.check('---\n'
222+
'- &b true\n'
223+
'- &i 42\n'
224+
'- &s hello\n'
225+
'- &f_m {k: v}\n'
226+
'- &f_s [1, 2]\n'
227+
'- *b\n'
228+
'- *i\n'
229+
'- *s\n'
230+
'- *f_m\n'
231+
'- *f_s\n'
232+
'---\n' # redeclare anchors in a new document
233+
'- &b true\n'
234+
'- &i 42\n'
235+
'- &s hello\n'
236+
'- *b\n'
237+
'- *i\n'
238+
'- *s\n'
239+
'---\n'
240+
'block mapping: &b_m\n'
241+
' key: value\n'
242+
'extended:\n'
243+
' <<: *b_m\n'
244+
' foo: bar\n'
245+
'---\n'
246+
'{a: 1, &x b: 2, c: &y 3, *x : 4, e: *y}\n'
247+
'...\n', conf)
248+
self.check('---\n'
249+
'- &i 42\n'
250+
'---\n'
251+
'- &b true\n'
252+
'- &b true\n'
253+
'- &b true\n'
254+
'- &s hello\n'
255+
'- *b\n'
256+
'- *i\n' # declared in a previous document
257+
'- *f_m\n' # never declared
258+
'- *f_m\n'
259+
'- *f_m\n'
260+
'- *f_s\n' # declared after
261+
'- &f_s [1, 2]\n'
262+
'...\n'
263+
'---\n'
264+
'block mapping: &b_m\n'
265+
' key: value\n'
266+
'---\n'
267+
'block mapping 1: &b_m_bis\n'
268+
' key: value\n'
269+
'block mapping 2: &b_m_bis\n'
270+
' key: value\n'
271+
'extended:\n'
272+
' <<: *b_m\n'
273+
' foo: bar\n'
274+
'---\n'
275+
'{a: 1, &x b: 2, c: &x 3, *x : 4, e: *y}\n'
276+
'...\n', conf,
277+
problem1=(2, 3),
278+
problem2=(7, 3),
279+
problem3=(14, 3),
280+
problem4=(17, 16),
281+
problem5=(22, 18))

yamllint/rules/anchors.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
later in the document).
2525
* Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same
2626
anchor.
27+
* Set ``forbid-unused-anchors`` to ``true`` to avoid anchors being declared but
28+
not used anywhere in the YAML document via alias.
2729
2830
.. rubric:: Default values (when enabled)
2931
@@ -33,6 +35,7 @@
3335
anchors:
3436
forbid-undeclared-aliases: true
3537
forbid-duplicated-anchors: false
38+
forbid-unused-anchors: false
3639
3740
.. rubric:: Examples
3841
@@ -78,6 +81,26 @@
7881
---
7982
- &anchor Foo Bar
8083
- &anchor [item 1, item 2]
84+
85+
#. With ``anchors: {forbid-unused-anchors: true}``
86+
87+
the following code snippet would **PASS**:
88+
::
89+
90+
---
91+
- &anchor
92+
foo: bar
93+
- *anchor
94+
95+
the following code snippet would **FAIL**:
96+
::
97+
98+
---
99+
- &anchor
100+
foo: bar
101+
- items:
102+
- item1
103+
- item2
81104
"""
82105

83106

@@ -89,15 +112,22 @@
89112
ID = 'anchors'
90113
TYPE = 'token'
91114
CONF = {'forbid-undeclared-aliases': bool,
92-
'forbid-duplicated-anchors': bool}
115+
'forbid-duplicated-anchors': bool,
116+
'forbid-unused-anchors': bool}
93117
DEFAULT = {'forbid-undeclared-aliases': True,
94-
'forbid-duplicated-anchors': False}
118+
'forbid-duplicated-anchors': False,
119+
'forbid-unused-anchors': False}
95120

96121

97122
def check(conf, token, prev, next, nextnext, context):
98-
if conf['forbid-undeclared-aliases'] or conf['forbid-duplicated-anchors']:
99-
if isinstance(token, (yaml.StreamStartToken, yaml.DocumentStartToken)):
100-
context['anchors'] = set()
123+
if (conf['forbid-undeclared-aliases'] or
124+
conf['forbid-duplicated-anchors'] or
125+
conf['forbid-unused-anchors']):
126+
if isinstance(token, (
127+
yaml.StreamStartToken,
128+
yaml.DocumentStartToken,
129+
yaml.DocumentEndToken)):
130+
context['anchors'] = {}
101131

102132
if (conf['forbid-undeclared-aliases'] and
103133
isinstance(token, yaml.AliasToken) and
@@ -113,6 +143,32 @@ def check(conf, token, prev, next, nextnext, context):
113143
token.start_mark.line + 1, token.start_mark.column + 1,
114144
f'found duplicated anchor "{token.value}"')
115145

116-
if conf['forbid-undeclared-aliases'] or conf['forbid-duplicated-anchors']:
146+
if conf['forbid-unused-anchors']:
147+
# Unused anchors can only be detected at the end of Document.
148+
# End of document can be either
149+
# - end of stream
150+
# - end of document sign '...'
151+
# - start of a new document sign '---'
152+
# If next token indicates end of document,
153+
# check if the anchors have been used or not.
154+
# If they haven't been used, report problem on those anchors.
155+
if isinstance(next, (yaml.StreamEndToken,
156+
yaml.DocumentStartToken,
157+
yaml.DocumentEndToken)):
158+
for anchor, info in context['anchors'].items():
159+
if not info['used']:
160+
yield LintProblem(info['line'] + 1,
161+
info['column'] + 1,
162+
f"found unused anchor {anchor}")
163+
elif isinstance(token, yaml.AliasToken):
164+
context['anchors'].get(token.value, {})['used'] = True
165+
166+
if (conf['forbid-undeclared-aliases'] or
167+
conf['forbid-duplicated-anchors'] or
168+
conf['forbid-unused-anchors']):
117169
if isinstance(token, yaml.AnchorToken):
118-
context['anchors'].add(token.value)
170+
context['anchors'][token.value] = {
171+
"line": token.start_mark.line,
172+
"column": token.start_mark.column,
173+
"used": False
174+
}

0 commit comments

Comments
 (0)