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..f0e6581 --- /dev/null +++ b/tests/rules/test_anchors.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2021 Sergei Mikhailov +# +# 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 + + +HIT_ANCHOR_POINTER = '''--- +key: &keyanchor a +otherkey: *keyanchor +''' +MISS_ANCHOR_POINTER = '''--- +key: &keyanchor a +otherkey: *missedkeyanchor +''' + +HIT_ANCHOR_MERGE = '''--- +block: &keyanchor + key: a +otherkey: + <<: *keyanchor + otherkey: b +''' +MISS_ANCHOR_MERGE = '''--- +block: &keyanchor + key: a +otherkey: + <<: *missedkeyanchor + otherkey: b +''' + +MULTI_HIT_ANCHOR_POINTER = '''--- +key: &keyanchor a +otherkey: *keyanchor +otherotherkey: *keyanchor +''' +MULTI_MISS_ANCHOR_POINTER = '''--- +key: &keyanchor a +otherkey: *missedkeyanchor +otherotherkey: *missedkeyanchor +''' + +MULTI_DOC_HIT_ANCHOR_POINTER = '''--- +key: &keyanchor a +otherkey: *keyanchor +--- +key: &otherkeyanchor a +otherkey: *otherkeyanchor +''' +MULTI_DOC_MISS_ANCHOR_POINTER = '''--- +key: &keyanchor a +otherkey: *missedkeyanchor +--- +key: &otherkeyanchor a +otherkey: *othermissedkeyanchor +''' + +DUPLICATE_ANCHORS = '''--- +first_block: &keyanchor + key: a + +second_block: &keyanchor + key: b + +target_block: *keyanchor +''' + + +class AnchorsTestCase(RuleTestCase): + rule_id = 'anchors' + + def test_disabled(self): + conf = 'anchors: disable' + + self.check(HIT_ANCHOR_POINTER, conf) + self.check(HIT_ANCHOR_MERGE, conf) + self.check(MULTI_HIT_ANCHOR_POINTER, conf) + self.check(MULTI_DOC_HIT_ANCHOR_POINTER, conf) + + self.check(MISS_ANCHOR_POINTER, conf) + self.check(MISS_ANCHOR_MERGE, conf) + self.check(MULTI_MISS_ANCHOR_POINTER, conf) + self.check(MULTI_DOC_MISS_ANCHOR_POINTER, conf) + self.check(DUPLICATE_ANCHORS, conf) + + def test_enabled(self): + conf = 'anchors: enable' + + self.check(HIT_ANCHOR_POINTER, conf) + self.check(HIT_ANCHOR_MERGE, conf) + self.check(MULTI_HIT_ANCHOR_POINTER, conf) + self.check(MULTI_DOC_HIT_ANCHOR_POINTER, conf) + + self.check(MISS_ANCHOR_POINTER, conf, + problem=(3, 11)) + self.check(MISS_ANCHOR_MERGE, conf, + problem_first=(5, 7), + problem_second=(5, 7)) + self.check(MULTI_MISS_ANCHOR_POINTER, conf, + problem_first=(3, 11), + problem_second=(4, 16)) + self.check(MULTI_DOC_MISS_ANCHOR_POINTER, conf, + problem_first=(3, 11), problem_second=(6, 11)) + self.check(DUPLICATE_ANCHORS, conf, + problem=(5, 15)) 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..23f48fd --- /dev/null +++ b/yamllint/rules/anchors.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2021 Sergei Mikhailov +# +# 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 prevent aliases pointing to pointers that don't exist. + +.. rubric:: Examples + +#. With ``anchors: {}`` + + the following code snippet would **PASS**: + :: + + address: &address | + Williams St. 13 + target_address: *address + + the following code snippet would **FAIL**: + :: + + address: &address | + Williams St. 13 + target_address: *wrong_address + +#. This appends to merges as well: + + the following code snippet would **PASS**: + :: + + default_address: &address + state: South Carolina + city: Barnwell + target_address: + <<: *address + city: Barnaul + + the following code snippet would **FAIL**: + :: + + default_address: &address + state: South Carolina + city: Barnwell + target_address: + <<: *wrong_address + city: Barnaul + +#. Duplicate anchors are not allowed. + + the following code snippet would **PASS**: + :: + + clients: + jack: &jack_client + billing_id: 1234 + bill: &bill_client + billing_id: 5678 + target_client: *jack_client + + the following code snippet would **FAIL**: + :: + + clients: + jack: &jack_client + billing_id: 1234 + bill: &jack_client + billing_id: 5678 + target_client: *jack_client +""" + +from yaml import DocumentStartToken, AnchorToken, AliasToken, BlockEndToken + +from yamllint.linter import LintProblem + + +ID = 'anchors' +TYPE = 'token' + + +def check(__conf, token, __prev, __next, __nextnext, context): + if isinstance(token, (DocumentStartToken)): + context['anchors'] = set() + context['aliases'] = {} + elif ('anchors' in context and 'aliases' in context): + if isinstance(token, (AnchorToken)): + if token.value in context['anchors']: + yield LintProblem( + token.start_mark.line + 1, token.start_mark.column + 1, + f"anchor '{token.value}' duplicates in document") + context['anchors'].add(token.value) + elif isinstance(token, (AliasToken)): + if token.value not in context['aliases']: + context['aliases'][token.value] = [] + alias_obj = {"line": token.start_mark.line, + "column": token.start_mark.column} + context['aliases'][token.value].append(alias_obj) + elif isinstance(token, (BlockEndToken)): + missing_anchors = [alias for alias in list(context['aliases']) + if alias not in context['anchors']] + if len(missing_anchors) > 0: + for miss_anchor in missing_anchors: + for location in context['aliases'][miss_anchor]: + yield LintProblem( + location['line'] + 1, location['column'] + 1, + f"anchor '{miss_anchor}' is not found in document")