diff --git a/.github/workflows/tox.yml b/.github/workflows/tox.yml index 31c6d0958a..38bf1f4b55 100644 --- a/.github/workflows/tox.yml +++ b/.github/workflows/tox.yml @@ -73,7 +73,7 @@ jobs: env: # Number of expected test passes, safety measure for accidental skip of # tests. Update value if you add/remove tests. - PYTEST_REQPASS: 856 + PYTEST_REQPASS: 857 steps: - uses: actions/checkout@v4 with: diff --git a/examples/playbooks/action_plugins/some_action.py b/examples/playbooks/action_plugins/some_action.py new file mode 100644 index 0000000000..1dc01aacf1 --- /dev/null +++ b/examples/playbooks/action_plugins/some_action.py @@ -0,0 +1,13 @@ +"""Sample action_plugin.""" + +from ansible.plugins.action import ActionBase + + +class ActionModule(ActionBase): # type: ignore[misc] + """Sample module.""" + + def run(self, tmp=None, task_vars=None): # type: ignore[no-untyped-def] + """.""" + super().run(tmp, task_vars) + ret = {"foo": "bar"} + return {"ansible_facts": ret} diff --git a/examples/playbooks/adj_action.yml b/examples/playbooks/adj_action.yml new file mode 100644 index 0000000000..4c78a2ba71 --- /dev/null +++ b/examples/playbooks/adj_action.yml @@ -0,0 +1,10 @@ +--- +- name: Fixture for testing adjacent plugins + hosts: localhost + tasks: + - name: Call adjacent action plugin + some_action: {} + + - name: Call adjacent filter plugin + ansible.builtin.debug: + msg: "{{ 'foo' | some_filter }}" diff --git a/examples/playbooks/example.yml b/examples/playbooks/example.yml index fa1a635226..14f7927758 100644 --- a/examples/playbooks/example.yml +++ b/examples/playbooks/example.yml @@ -36,8 +36,8 @@ - git # yamllint wrong indentation - bobbins - - name: Yum latest - ansible.builtin.yum: state=latest name=httpd + - name: Dnf latest + ansible.builtin.dnf: state=latest name=httpd - ansible.builtin.debug: msg="debug task without a name" diff --git a/examples/playbooks/filter_plugins/some_filter.py b/examples/playbooks/filter_plugins/some_filter.py new file mode 100644 index 0000000000..86ebda8a84 --- /dev/null +++ b/examples/playbooks/filter_plugins/some_filter.py @@ -0,0 +1,13 @@ +"""Sample adjacent filter plugin.""" + +from __future__ import annotations + + +class FilterModule: # pylint: disable=too-few-public-methods + """Ansible filters.""" + + def filters(self): # type: ignore[no-untyped-def] + """Return list of exposed filters.""" + return { + "some_filter": str, + } diff --git a/examples/playbooks/test_skip_inside_yaml.yml b/examples/playbooks/test_skip_inside_yaml.yml index 1f7295442a..88c396adfb 100644 --- a/examples/playbooks/test_skip_inside_yaml.yml +++ b/examples/playbooks/test_skip_inside_yaml.yml @@ -44,9 +44,9 @@ - name: Test no-free-form # <-- 3 no-free-form ansible.builtin.command: creates=B chmod 644 A # noqa: no-free-form - name: Test no-free-form # <-- 4 no-free-form - ansible.builtin.command: warn=yes creates=B chmod 644 A # noqa: no-free-form + ansible.builtin.command: creates=B chmod 644 A # noqa: no-free-form - name: Test no-free-form (skipped via no warn) - ansible.builtin.command: warn=no creates=B chmod 644 A # noqa: no-free-form + ansible.builtin.command: creates=B chmod 644 A # noqa: no-free-form - name: Test no-free-form (skipped via skip_ansible_lint) ansible.builtin.command: creates=B chmod 644 A # noqa: no-free-form tags: diff --git a/src/ansiblelint/schemas/__store__.json b/src/ansiblelint/schemas/__store__.json index 2bd7bb5e74..bbe922699c 100644 --- a/src/ansiblelint/schemas/__store__.json +++ b/src/ansiblelint/schemas/__store__.json @@ -44,7 +44,7 @@ "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/requirements.json" }, "role-arg-spec": { - "etag": "74fc5d429919813f2c977a2e3ed2afee1ca3dba242f6978bded8199895642db6", + "etag": "e41a42e1ca634a9eb2edbc4a180f404bdc71e17aafa464e6651387c08152bbc5", "url": "https://raw.githubusercontent.com/ansible/ansible-lint/main/src/ansiblelint/schemas/role-arg-spec.json" }, "rulebook": { diff --git a/src/ansiblelint/utils.py b/src/ansiblelint/utils.py index 35c9a933c2..2da21ca15e 100644 --- a/src/ansiblelint/utils.py +++ b/src/ansiblelint/utils.py @@ -44,7 +44,12 @@ from ansible.parsing.yaml.constructor import AnsibleConstructor, AnsibleMapping from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleSequence -from ansible.plugins.loader import PluginLoadContext, add_all_plugin_dirs, module_loader +from ansible.plugins.loader import ( + PluginLoadContext, + action_loader, + add_all_plugin_dirs, + module_loader, +) from ansible.template import Templar from ansible.utils.collection_loader import AnsibleCollectionConfig from yaml.composer import Composer @@ -1071,11 +1076,17 @@ def parse_examples_from_plugin(lintable: Lintable) -> tuple[int, str]: @lru_cache def load_plugin(name: str) -> PluginLoadContext: """Return loaded ansible plugin/module.""" - loaded_module = module_loader.find_plugin_with_context( + loaded_module = action_loader.find_plugin_with_context( name, ignore_deprecated=True, check_aliases=True, ) + if not loaded_module.resolved: + loaded_module = module_loader.find_plugin_with_context( + name, + ignore_deprecated=True, + check_aliases=True, + ) if not loaded_module.resolved and name.startswith("ansible.builtin."): # fallback to core behavior of using legacy loaded_module = module_loader.find_plugin_with_context( diff --git a/test/test_adjacent_plugins.py b/test/test_adjacent_plugins.py new file mode 100644 index 0000000000..3e642ceb24 --- /dev/null +++ b/test/test_adjacent_plugins.py @@ -0,0 +1,25 @@ +"""Test ability to recognize adjacent modules/plugins.""" + +import logging + +import pytest + +from ansiblelint.rules import RulesCollection +from ansiblelint.runner import Runner + + +def test_adj_action( + default_rules_collection: RulesCollection, + caplog: pytest.LogCaptureFixture, +) -> None: + """Assures local collections are found.""" + playbook_path = "examples/playbooks/adj_action.yml" + + with caplog.at_level(logging.DEBUG): + runner = Runner(playbook_path, rules=default_rules_collection, verbosity=1) + results = runner.run() + assert "Unable to load module" not in caplog.text + assert "Unable to resolve FQCN" not in caplog.text + + assert len(runner.lintables) == 1 + assert len(results) == 0 diff --git a/test/test_examples.py b/test/test_examples.py index bdaec51e77..1c38ccb03b 100644 --- a/test/test_examples.py +++ b/test/test_examples.py @@ -14,7 +14,7 @@ def test_example(default_rules_collection: RulesCollection) -> None: "examples/playbooks/example.yml", rules=default_rules_collection, ).run() - assert len(result) == 22 + assert len(result) == 21 @pytest.mark.parametrize( diff --git a/test/test_skip_inside_yaml.py b/test/test_skip_inside_yaml.py index fab03ea127..8050f13312 100644 --- a/test/test_skip_inside_yaml.py +++ b/test/test_skip_inside_yaml.py @@ -20,7 +20,7 @@ def test_role_tasks_with_block(default_rules_collection: RulesCollection) -> Non @pytest.mark.parametrize( ("lintable", "expected"), - (pytest.param("examples/playbooks/test_skip_inside_yaml.yml", 6, id="yaml"),), + (pytest.param("examples/playbooks/test_skip_inside_yaml.yml", 4, id="yaml"),), ) def test_inline_skips( default_rules_collection: RulesCollection,