|
1 | 1 | """Rule definition for usage of fully qualified collection names for builtins.""" |
2 | 2 | from __future__ import annotations |
3 | 3 |
|
| 4 | +import logging |
4 | 5 | import sys |
5 | 6 | from typing import Any |
6 | 7 |
|
| 8 | +from ansible.plugins.loader import module_loader |
| 9 | + |
7 | 10 | from ansiblelint.errors import MatchError |
8 | 11 | from ansiblelint.file_utils import Lintable |
9 | 12 | from ansiblelint.rules import AnsibleLintRule |
10 | 13 |
|
| 14 | +_logger = logging.getLogger(__name__) |
| 15 | + |
11 | 16 | builtins = [ |
12 | 17 | # spell-checker:disable |
13 | 18 | "add_host", |
@@ -92,33 +97,62 @@ class FQCNBuiltinsRule(AnsibleLintRule): |
92 | 97 | ) |
93 | 98 | tags = ["formatting"] |
94 | 99 | version_added = "v6.8.0" |
| 100 | + module_aliases: dict[str, str] = {"block/always/rescue": "block/always/rescue"} |
95 | 101 |
|
96 | 102 | def matchtask( |
97 | 103 | self, task: dict[str, Any], file: Lintable | None = None |
98 | 104 | ) -> list[MatchError]: |
99 | 105 | result = [] |
100 | 106 | module = task["action"]["__ansible_module_original__"] |
101 | | - if module in builtins: |
102 | | - result.append( |
103 | | - self.create_matcherror( |
104 | | - message=f"Use FQCN for builtin module actions ({module}).", |
105 | | - details=f"Use `ansible.builtin.{module}` or `ansible.legacy.{module}` instead.", |
106 | | - filename=file, |
107 | | - linenumber=task["__line__"], |
108 | | - tag="fqcn[action-core]", |
109 | | - ) |
110 | | - ) |
111 | | - # Add here implementation for fqcn[action-redirect] |
112 | | - elif module != "block/always/rescue" and module.count(".") < 2: |
113 | | - result.append( |
114 | | - self.create_matcherror( |
115 | | - message=f"Use FQCN for module actions, such `<namespace>.<collection>.{module}`.", |
116 | | - details=f"Action `{module}` is not FQCN.", |
117 | | - filename=file, |
118 | | - linenumber=task["__line__"], |
119 | | - tag="fqcn[action]", |
| 107 | + |
| 108 | + if module not in self.module_aliases: |
| 109 | + loaded_module = module_loader.find_plugin_with_context(module) |
| 110 | + target = loaded_module.resolved_fqcn |
| 111 | + self.module_aliases[module] = target |
| 112 | + if target is None: |
| 113 | + _logger.warning("Unable to resolve FQCN for module %s", module) |
| 114 | + self.module_aliases[module] = module |
| 115 | + return [] |
| 116 | + if target not in self.module_aliases: |
| 117 | + self.module_aliases[target] = target |
| 118 | + |
| 119 | + if module != self.module_aliases[module]: |
| 120 | + module_alias = self.module_aliases.get(module, "") |
| 121 | + if module_alias.startswith("ansible.builtin"): |
| 122 | + result.append( |
| 123 | + self.create_matcherror( |
| 124 | + message=f"Use FQCN for builtin module actions ({module}).", |
| 125 | + details=f"Use `ansible.builtin.{module}` or `ansible.legacy.{module}` instead.", |
| 126 | + filename=file, |
| 127 | + linenumber=task["__line__"], |
| 128 | + tag="fqcn[action-core]", |
| 129 | + ) |
120 | 130 | ) |
121 | | - ) |
| 131 | + else: |
| 132 | + if module.count(".") < 2: |
| 133 | + result.append( |
| 134 | + self.create_matcherror( |
| 135 | + message=f"Use FQCN for module actions, such `{self.module_aliases[module]}`.", |
| 136 | + details=f"Action `{module}` is not FQCN.", |
| 137 | + filename=file, |
| 138 | + linenumber=task["__line__"], |
| 139 | + tag="fqcn[action]", |
| 140 | + ) |
| 141 | + ) |
| 142 | + # TODO(ssbarnea): Remove the c.g. and c.n. exceptions from here once |
| 143 | + # community team is flattening these. |
| 144 | + # See: https://github.com/ansible-community/community-topics/issues/147 |
| 145 | + elif not module.startswith("community.general.") or module.startswith( |
| 146 | + "community.network." |
| 147 | + ): |
| 148 | + result.append( |
| 149 | + self.create_matcherror( |
| 150 | + message=f"You should use canonical module name `{self.module_aliases[module]}` instead of `{module}`.", |
| 151 | + filename=file, |
| 152 | + linenumber=task["__line__"], |
| 153 | + tag="fqcn[canonical]", |
| 154 | + ) |
| 155 | + ) |
122 | 156 | return result |
123 | 157 |
|
124 | 158 |
|
@@ -160,10 +194,7 @@ def test_fqcn_builtin_fail(rule_runner: RunFromText) -> None: |
160 | 194 | assert results[0].tag == "fqcn[action-core]" |
161 | 195 | assert "Use FQCN for builtin module actions" in results[0].message |
162 | 196 | assert results[1].tag == "fqcn[action]" |
163 | | - assert ( |
164 | | - "Use FQCN for module actions, such `<namespace>.<collection>" |
165 | | - in results[1].message |
166 | | - ) |
| 197 | + assert "Use FQCN for module actions, such" in results[1].message |
167 | 198 |
|
168 | 199 | @pytest.mark.parametrize( |
169 | 200 | "rule_runner", (FQCNBuiltinsRule,), indirect=["rule_runner"] |
|
0 commit comments