diff options
Diffstat (limited to 'src/ansiblelint/rules/fqcn.py')
-rw-r--r-- | src/ansiblelint/rules/fqcn.py | 84 |
1 files changed, 50 insertions, 34 deletions
diff --git a/src/ansiblelint/rules/fqcn.py b/src/ansiblelint/rules/fqcn.py index 768fb9e..b571db3 100644 --- a/src/ansiblelint/rules/fqcn.py +++ b/src/ansiblelint/rules/fqcn.py @@ -1,17 +1,19 @@ """Rule definition for usage of fully qualified collection names for builtins.""" + from __future__ import annotations import logging import sys from typing import TYPE_CHECKING, Any -from ansible.plugins.loader import module_loader +from ruamel.yaml.comments import CommentedSeq from ansiblelint.constants import LINE_NUMBER_KEY from ansiblelint.rules import AnsibleLintRule, TransformMixin +from ansiblelint.utils import load_plugin if TYPE_CHECKING: - from ruamel.yaml.comments import CommentedMap, CommentedSeq + from ruamel.yaml.comments import CommentedMap from ansiblelint.errors import MatchError from ansiblelint.file_utils import Lintable @@ -114,11 +116,16 @@ class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin): task: Task, file: Lintable | None = None, ) -> list[MatchError]: - result = [] + result: list[MatchError] = [] + if file and file.failed(): + return result module = task["action"]["__ansible_module_original__"] + if not isinstance(module, str): + msg = "Invalid data for module." + raise TypeError(msg) if module not in self.module_aliases: - loaded_module = module_loader.find_plugin_with_context(module) + loaded_module = load_plugin(module) target = loaded_module.resolved_fqcn self.module_aliases[module] = target if target is None: @@ -137,40 +144,45 @@ class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin): 1, ) if module != legacy_module: + if module == "ansible.builtin.include": + message = f"Avoid deprecated module ({module})" + details = "Use `ansible.builtin.include_task` or `ansible.builtin.import_tasks` instead." + else: + message = f"Use FQCN for builtin module actions ({module})." + details = f"Use `{module_alias}` or `{legacy_module}` instead." result.append( self.create_matcherror( - message=f"Use FQCN for builtin module actions ({module}).", - details=f"Use `{module_alias}` or `{legacy_module}` instead.", + message=message, + details=details, filename=file, lineno=task["__line__"], tag="fqcn[action-core]", ), ) - else: - if module.count(".") < 2: - result.append( - self.create_matcherror( - message=f"Use FQCN for module actions, such `{self.module_aliases[module]}`.", - details=f"Action `{module}` is not FQCN.", - filename=file, - lineno=task["__line__"], - tag="fqcn[action]", - ), - ) - # TODO(ssbarnea): Remove the c.g. and c.n. exceptions from here once # noqa: FIX002 - # community team is flattening these. - # https://github.com/ansible-community/community-topics/issues/147 - elif not module.startswith("community.general.") or module.startswith( - "community.network.", - ): - result.append( - self.create_matcherror( - message=f"You should use canonical module name `{self.module_aliases[module]}` instead of `{module}`.", - filename=file, - lineno=task["__line__"], - tag="fqcn[canonical]", - ), - ) + elif module.count(".") < 2: + result.append( + self.create_matcherror( + message=f"Use FQCN for module actions, such `{self.module_aliases[module]}`.", + details=f"Action `{module}` is not FQCN.", + filename=file, + lineno=task["__line__"], + tag="fqcn[action]", + ), + ) + # TODO(ssbarnea): Remove the c.g. and c.n. exceptions from here once # noqa: FIX002 + # community team is flattening these. + # https://github.com/ansible-community/community-topics/issues/147 + elif not module.startswith("community.general.") or module.startswith( + "community.network.", + ): + result.append( + self.create_matcherror( + message=f"You should use canonical module name `{self.module_aliases[module]}` instead of `{module}`.", + filename=file, + lineno=task["__line__"], + tag="fqcn[canonical]", + ), + ) return result def matchyaml(self, file: Lintable) -> list[MatchError]: @@ -220,6 +232,8 @@ class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin): target_task = self.seek(match.yaml_path, data) # Unfortunately, a lot of data about Ansible content gets lost here, you only get a simple dict. # For now, just parse the error messages for the data about action names etc. and fix this later. + current_action = "" + new_action = "" if match.tag == "fqcn[action-core]": # split at the first bracket, cut off the last bracket and dot current_action = match.message.split("(")[1][:-2] @@ -233,6 +247,8 @@ class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin): current_action = match.message.split("`")[3] new_action = match.message.split("`")[1] for _ in range(len(target_task)): + if isinstance(target_task, CommentedSeq): + continue k, v = target_task.popitem(False) target_task[new_action if k == current_action else k] = v match.fixed = True @@ -241,7 +257,7 @@ class FQCNBuiltinsRule(AnsibleLintRule, TransformMixin): # testing code to be loaded only with pytest or when executed the rule file if "pytest" in sys.modules: from ansiblelint.rules import RulesCollection - from ansiblelint.runner import Runner # pylint: disable=ungrouped-imports + from ansiblelint.runner import Runner def test_fqcn_builtin_fail() -> None: """Test rule matches.""" @@ -269,7 +285,7 @@ if "pytest" in sys.modules: """Test rule matches.""" collection = RulesCollection() collection.register(FQCNBuiltinsRule()) - failure = "examples/collection/plugins/modules/deep/beta.py" + failure = "examples/.collection/plugins/modules/deep/beta.py" results = Runner(failure, rules=collection).run() assert len(results) == 1 assert results[0].tag == "fqcn[deep]" @@ -279,6 +295,6 @@ if "pytest" in sys.modules: """Test rule does not match.""" collection = RulesCollection() collection.register(FQCNBuiltinsRule()) - success = "examples/collection/plugins/modules/alpha.py" + success = "examples/.collection/plugins/modules/alpha.py" results = Runner(success, rules=collection).run() assert len(results) == 0 |