diff --git a/docs/rules.rst b/docs/rules.rst
index c030c3d..eb3bc82 100644
--- a/docs/rules.rst
+++ b/docs/rules.rst
@@ -14,6 +14,11 @@ This page describes the rules and their options.
:local:
:depth: 1
+anchors
+-------
+
+.. automodule:: yamllint.rules.anchors
+
braces
------
diff --git a/tests/rules/test_anchors.py b/tests/rules/test_anchors.py
new file mode 100644
index 0000000..da1c523
--- /dev/null
+++ b/tests/rules/test_anchors.py
@@ -0,0 +1,209 @@
+# Copyright (C) 2023 Adrien Vergé
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from tests.common import RuleTestCase
+
+
+class AnchorsTestCase(RuleTestCase):
+ rule_id = 'anchors'
+
+ def test_disabled(self):
+ conf = 'anchors: disable'
+ 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'
+ '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)
+
+ def test_forbid_undeclared_aliases(self):
+ conf = ('anchors:\n'
+ ' forbid-undeclared-aliases: true\n'
+ ' forbid-duplicated-anchors: false\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'
+ '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=(9, 3),
+ problem2=(10, 3),
+ problem3=(11, 3),
+ problem4=(12, 3),
+ problem5=(13, 3),
+ problem6=(24, 7),
+ problem7=(27, 36))
+
+ def test_forbid_duplicated_anchors(self):
+ conf = ('anchors:\n'
+ ' forbid-undeclared-aliases: false\n'
+ ' forbid-duplicated-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'
+ '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=(5, 3),
+ problem2=(6, 3),
+ problem3=(21, 18),
+ problem4=(27, 20))
diff --git a/yamllint/conf/default.yaml b/yamllint/conf/default.yaml
index 0dea0aa..b082e22 100644
--- a/yamllint/conf/default.yaml
+++ b/yamllint/conf/default.yaml
@@ -6,6 +6,7 @@ yaml-files:
- '.yamllint'
rules:
+ anchors: enable
braces: enable
brackets: enable
colons: enable
diff --git a/yamllint/rules/__init__.py b/yamllint/rules/__init__.py
index 5a4fe81..6b5e446 100644
--- a/yamllint/rules/__init__.py
+++ b/yamllint/rules/__init__.py
@@ -14,6 +14,7 @@
# along with this program. If not, see .
from yamllint.rules import (
+ anchors,
braces,
brackets,
colons,
@@ -39,6 +40,7 @@ from yamllint.rules import (
)
_RULES = {
+ anchors.ID: anchors,
braces.ID: braces,
brackets.ID: brackets,
colons.ID: colons,
diff --git a/yamllint/rules/anchors.py b/yamllint/rules/anchors.py
new file mode 100644
index 0000000..6d0b1e6
--- /dev/null
+++ b/yamllint/rules/anchors.py
@@ -0,0 +1,118 @@
+# Copyright (C) 2023 Adrien Vergé
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Use this rule to report duplicated anchors and aliases referencing undeclared
+anchors.
+
+.. rubric:: Options
+
+* Set ``forbid-undeclared-aliases`` to ``true`` to avoid aliases that reference
+ an anchor that hasn't been declared (either not declared at all, or declared
+ later in the document).
+* Set ``forbid-duplicated-anchors`` to ``true`` to avoid duplications of a same
+ anchor.
+
+.. rubric:: Default values (when enabled)
+
+.. code-block:: yaml
+
+ rules:
+ anchors:
+ forbid-undeclared-aliases: true
+ forbid-duplicated-anchors: false
+
+.. rubric:: Examples
+
+#. With ``anchors: {forbid-undeclared-aliases: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - *anchor
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - *unknown
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor
+ foo: bar
+ - <<: *unknown
+ extra: value
+
+#. With ``anchors: {forbid-duplicated-anchors: true}``
+
+ the following code snippet would **PASS**:
+ ::
+
+ ---
+ - &anchor1 Foo Bar
+ - &anchor2 [item 1, item 2]
+
+ the following code snippet would **FAIL**:
+ ::
+
+ ---
+ - &anchor Foo Bar
+ - &anchor [item 1, item 2]
+"""
+
+
+import yaml
+
+from yamllint.linter import LintProblem
+
+
+ID = 'anchors'
+TYPE = 'token'
+CONF = {'forbid-undeclared-aliases': bool,
+ 'forbid-duplicated-anchors': bool}
+DEFAULT = {'forbid-undeclared-aliases': True,
+ 'forbid-duplicated-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'] and
+ isinstance(token, yaml.AliasToken) and
+ token.value not in context['anchors']):
+ yield LintProblem(
+ token.start_mark.line + 1, token.start_mark.column + 1,
+ f'found undeclared alias "{token.value}"')
+
+ if (conf['forbid-duplicated-anchors'] and
+ isinstance(token, yaml.AnchorToken) and
+ token.value in context['anchors']):
+ yield LintProblem(
+ 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 isinstance(token, yaml.AnchorToken):
+ context['anchors'].add(token.value)