diff --git a/src/ansiblelint/rules/args.py b/src/ansiblelint/rules/args.py index 29d4dd0ebf..6e2a48ea53 100644 --- a/src/ansiblelint/rules/args.py +++ b/src/ansiblelint/rules/args.py @@ -26,11 +26,12 @@ from ansiblelint.yaml_utils import clean_json if TYPE_CHECKING: + from ansible.plugins.loader import PluginLoadContext + from ansiblelint.errors import MatchError from ansiblelint.file_utils import Lintable from ansiblelint.utils import Task - _logger = logging.getLogger(__name__) ignored_re = re.compile( @@ -106,7 +107,7 @@ def matchtask( if module_name in self.module_aliases: return [] - loaded_module = load_plugin(module_name) + loaded_module: PluginLoadContext = load_plugin(module_name) # https://github.com/ansible/ansible-lint/issues/3200 # since "ps1" modules cannot be executed on POSIX platforms, we will @@ -144,6 +145,14 @@ def matchtask( "AnsibleModule", CustomAnsibleModule, ): + if not loaded_module.plugin_resolved_name: + _logger.warning( + "Unable to load module %s at %s:%s for options validation", + module_name, + file.filename if file else None, + task[LINE_NUMBER_KEY], + ) + return [] spec = importlib.util.spec_from_file_location( name=loaded_module.plugin_resolved_name, location=loaded_module.plugin_resolved_path, diff --git a/src/ansiblelint/rules/jinja.py b/src/ansiblelint/rules/jinja.py index 324fa07b64..44cbc3c992 100644 --- a/src/ansiblelint/rules/jinja.py +++ b/src/ansiblelint/rules/jinja.py @@ -8,7 +8,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, NamedTuple +from typing import TYPE_CHECKING, NamedTuple import black import jinja2 @@ -16,7 +16,6 @@ from ansible.parsing.yaml.objects import AnsibleUnicode from jinja2.exceptions import TemplateSyntaxError -from ansiblelint.constants import LINE_NUMBER_KEY from ansiblelint.errors import RuleMatchTransformMeta from ansiblelint.file_utils import Lintable from ansiblelint.rules import AnsibleLintRule, TransformMixin @@ -195,7 +194,7 @@ def matchtask( result.append( self.create_matcherror( message=str(exc), - lineno=_get_error_line(task, path), + lineno=task.get_error_line(path), filename=file, tag=f"{self.id}[invalid]", ), @@ -214,7 +213,7 @@ def matchtask( value=v, reformatted=reformatted, ), - lineno=_get_error_line(task, path), + lineno=task.get_error_line(path), details=details, filename=file, tag=f"{self.id}[{tag}]", @@ -233,12 +232,13 @@ def matchtask( def matchyaml(self, file: Lintable) -> list[MatchError]: """Return matches for variables defined in vars files.""" - data: dict[str, Any] = {} raw_results: list[MatchError] = [] results: list[MatchError] = [] if str(file.kind) == "vars": data = parse_yaml_from_file(str(file.path)) + if not isinstance(data, dict): + return results for key, v, _path in nested_items_path(data): if isinstance(v, AnsibleUnicode): reformatted, details, tag = self.check_whitespace( @@ -406,7 +406,7 @@ def uncook(value: str, *, implicit: bool = False) -> str: except jinja2.exceptions.TemplateSyntaxError as exc: return "", str(exc.message), "invalid" # pylint: disable=c-extension-no-member - except (NotImplementedError, black.parsing.InvalidInput) as exc: + except (NotImplementedError, ValueError) as exc: # black is not able to recognize all valid jinja2 templates, so we # just ignore InvalidInput errors. # NotImplementedError is raised internally for expressions with @@ -898,17 +898,3 @@ def _do_template(*args, **kwargs): # type: ignore[no-untyped-def] # Templar.do_ with mock.patch.object(Templar, "do_template", _do_template): results = Runner(lintable, rules=collection).run() assert len(results) == 0 - - -def _get_error_line(task: dict[str, Any], path: list[str | int]) -> int: - """Return error line number.""" - line = task[LINE_NUMBER_KEY] - ctx = task - for _ in path: - ctx = ctx[_] - if LINE_NUMBER_KEY in ctx: - line = ctx[LINE_NUMBER_KEY] - if not isinstance(line, int): - msg = "Line number is not an integer" - raise TypeError(msg) - return line diff --git a/src/ansiblelint/rules/role_name.py b/src/ansiblelint/rules/role_name.py index 7794f48ea0..5118c8cb1c 100644 --- a/src/ansiblelint/rules/role_name.py +++ b/src/ansiblelint/rules/role_name.py @@ -105,11 +105,17 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: msg = "Role dependency has unexpected type." raise TypeError(msg) if "/" in role_name: + lineno = 1 + if hasattr(role_name, "ansible_pos"): + lineno = role_name.ansible_pos[ # pyright: ignore[reportAttributeAccessIssue] + 1 + ] + result.append( self.create_matcherror( f"Avoid using paths when importing roles. ({role_name})", filename=file, - lineno=role_name.ansible_pos[1], + lineno=lineno, tag=f"{self.id}[path]", ), ) @@ -165,7 +171,7 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: def _infer_role_name(meta: Path, default: str) -> str: if meta.is_file(): meta_data = parse_yaml_from_file(str(meta)) - if meta_data: + if meta_data and isinstance(meta_data, dict): try: return str(meta_data["galaxy_info"]["role_name"]) except (KeyError, TypeError): diff --git a/src/ansiblelint/utils.py b/src/ansiblelint/utils.py index 83a70d4c0b..c240a726f1 100644 --- a/src/ansiblelint/utils.py +++ b/src/ansiblelint/utils.py @@ -24,6 +24,7 @@ from __future__ import annotations import ast +import collections.abc import contextlib import inspect import logging @@ -306,7 +307,7 @@ def include_children( v = v["file"] # we cannot really parse any jinja2 in includes, so we ignore them - if not v or "{{" in v: + if not v or not isinstance(v, str) or "{{" in v: return [] # handle include: filename.yml tags=blah @@ -877,6 +878,31 @@ def __iter__(self) -> Iterator[str]: """Provide support for 'key in task'.""" yield from (f for f in self.normalized_task) + def get_error_line(self, path: list[str | int]) -> int: + """Return error line number.""" + ctx = self.normalized_task + line = self.normalized_task[LINE_NUMBER_KEY] + for _ in path: + if ( + isinstance(ctx, collections.abc.Container) and _ in ctx + ): # isinstance(ctx, collections.abc.Container) and + value = ctx.get( # pyright: ignore[reportAttributeAccessIssue] + _ # pyright: ignore[reportArgumentType] + ) + if isinstance(value, dict): + ctx = value + if ( + isinstance(ctx, collections.abc.Container) + and LINE_NUMBER_KEY in ctx + ): + line = ctx[LINE_NUMBER_KEY] # pyright: ignore[reportIndexIssue] + # else: + # break + if not isinstance(line, int): + msg = "Line number is not an integer" + raise TypeError(msg) + return line + def task_in_list( data: AnsibleBaseYAMLObject, diff --git a/src/ansiblelint/yaml_utils.py b/src/ansiblelint/yaml_utils.py index f273357228..3b0f41f793 100644 --- a/src/ansiblelint/yaml_utils.py +++ b/src/ansiblelint/yaml_utils.py @@ -244,15 +244,15 @@ def _nested_items_path( """ # we have to cast each convert_to_tuples assignment or mypy complains # that both assignments (for dict and list) do not have the same type - convert_to_tuples_type = Callable[[], Iterator[tuple[str | int, Any]]] + # convert_to_tuples_type = Callable[[], Iterator[tuple[str | int, Any]]] if isinstance(data_collection, dict): convert_data_collection_to_tuples = cast( - convert_to_tuples_type, + Callable[[], Iterator[tuple[str | int, Any]]], functools.partial(data_collection.items), ) elif isinstance(data_collection, list): convert_data_collection_to_tuples = cast( - convert_to_tuples_type, + Callable[[], Iterator[tuple[str | int, Any]]], functools.partial(enumerate, data_collection), ) else: diff --git a/test/test_utils.py b/test/test_utils.py index bc0a115e24..a91d29b713 100644 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -45,10 +45,10 @@ from _pytest.capture import CaptureFixture from _pytest.logging import LogCaptureFixture from _pytest.monkeypatch import MonkeyPatch + from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject from ansiblelint.rules import RulesCollection - runtime = Runtime(require_module=True) @@ -237,7 +237,7 @@ def test_extract_from_list_recursive() -> None: block = { "block": [{"block": [{"name": "hello", "command": "whoami"}]}], } - blocks = [block] + blocks: AnsibleBaseYAMLObject = [block] test_list = utils.extract_from_list(blocks, ["block"]) assert list(block["block"]) == test_list diff --git a/test/test_yaml_utils.py b/test/test_yaml_utils.py index 36178d9f1e..ae225ee179 100644 --- a/test/test_yaml_utils.py +++ b/test/test_yaml_utils.py @@ -38,7 +38,7 @@ def test_tasks_in_list_empty_file(empty_lintable: Lintable) -> None: assert empty_lintable.path res = list( task_in_list( - data=empty_lintable, + data=empty_lintable.data, file=empty_lintable, kind=empty_lintable.kind, ),