diff --git a/playbook.yml b/playbook.yml index f55677edc3..bac516fdb7 100644 --- a/playbook.yml +++ b/playbook.yml @@ -2,6 +2,6 @@ - name: Example hosts: localhost tasks: - - name: include extra tasks + - name: include extra tasks # noqa: 102 ansible.builtin.include_tasks: file: /dev/null diff --git a/src/ansiblelint/errors.py b/src/ansiblelint/errors.py index c1f00125f2..23d18dceeb 100644 --- a/src/ansiblelint/errors.py +++ b/src/ansiblelint/errors.py @@ -9,6 +9,10 @@ from ansiblelint.file_utils import Lintable, normpath +class LintWarning(Warning): + """Used by linter.""" + + class StrictModeError(RuntimeError): """Raise when we encounter a warning in strict mode.""" diff --git a/src/ansiblelint/rules/__init__.py b/src/ansiblelint/rules/__init__.py index 59dcb71f34..f46c498efb 100644 --- a/src/ansiblelint/rules/__init__.py +++ b/src/ansiblelint/rules/__init__.py @@ -121,7 +121,10 @@ def matchlines(self, file: Lintable) -> list[MatchError]: if line.lstrip().startswith("#"): continue - rule_id_list = ansiblelint.skip_utils.get_rule_skips_from_line(line) + rule_id_list = ansiblelint.skip_utils.get_rule_skips_from_line( + line, + lintable=file, + ) if self.id in rule_id_list: continue diff --git a/src/ansiblelint/rules/jinja.py b/src/ansiblelint/rules/jinja.py index 83c2951d52..1bdf4a2d49 100644 --- a/src/ansiblelint/rules/jinja.py +++ b/src/ansiblelint/rules/jinja.py @@ -200,7 +200,10 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: lines = file.content.splitlines() for match in raw_results: # lineno starts with 1, not zero - skip_list = get_rule_skips_from_line(lines[match.lineno - 1]) + skip_list = get_rule_skips_from_line( + line=lines[match.lineno - 1], + lintable=file, + ) if match.rule.id not in skip_list and match.tag not in skip_list: results.append(match) else: diff --git a/src/ansiblelint/rules/var_naming.py b/src/ansiblelint/rules/var_naming.py index 0085e9277c..9451935729 100644 --- a/src/ansiblelint/rules/var_naming.py +++ b/src/ansiblelint/rules/var_naming.py @@ -91,7 +91,10 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]: lines = file.content.splitlines() for match in raw_results: # lineno starts with 1, not zero - skip_list = get_rule_skips_from_line(lines[match.lineno - 1]) + skip_list = get_rule_skips_from_line( + line=lines[match.lineno - 1], + lintable=file, + ) if match.rule.id not in skip_list and match.tag not in skip_list: results.append(match) @@ -171,7 +174,10 @@ def matchyaml(self, file: Lintable) -> list[MatchError]: lines = file.content.splitlines() for match in raw_results: # lineno starts with 1, not zero - skip_list = get_rule_skips_from_line(lines[match.lineno - 1]) + skip_list = get_rule_skips_from_line( + line=lines[match.lineno - 1], + lintable=file, + ) if match.rule.id not in skip_list and match.tag not in skip_list: results.append(match) else: diff --git a/src/ansiblelint/runner.py b/src/ansiblelint/runner.py index db154ecd32..662f2647fa 100644 --- a/src/ansiblelint/runner.py +++ b/src/ansiblelint/runner.py @@ -5,6 +5,7 @@ import multiprocessing import multiprocessing.pool import os +import warnings from collections.abc import Generator from dataclasses import dataclass from fnmatch import fnmatch @@ -12,10 +13,10 @@ import ansiblelint.skip_utils import ansiblelint.utils -from ansiblelint._internal.rules import LoadingFailureRule +from ansiblelint._internal.rules import LoadingFailureRule, WarningRule from ansiblelint.config import Options from ansiblelint.constants import States -from ansiblelint.errors import MatchError +from ansiblelint.errors import LintWarning, MatchError from ansiblelint.file_utils import Lintable, expand_dirs_in_lintables from ansiblelint.rules.syntax_check import AnsibleSyntaxCheckRule @@ -33,6 +34,14 @@ class LintResult: files: set[Lintable] +@dataclass +class Occurrence: + """Class that tracks result of linting.""" + + file: Lintable + lineno: MatchError + + class Runner: """Runner class performs the linting process.""" @@ -112,6 +121,38 @@ def is_excluded(self, lintable: Lintable) -> bool: def run(self) -> list[MatchError]: # noqa: C901 """Execute the linting process.""" + matches: list[MatchError] = [] + with warnings.catch_warnings(record=True) as captured_warnings: + warnings.simplefilter("always") + matches = self._run() + for warn in captured_warnings: + # For the moment we are ignoring deprecation warnings as Ansible + # modules outside current content can generate them and user + # might not be able to do anything about them. + if warn.category is DeprecationWarning: + continue + if warn.category is LintWarning: + filename: None | Lintable = None + if isinstance(warn.source, Lintable): + filename = warn.source + match = MatchError( + message=warn.message if isinstance(warn.message, str) else "?", + rule=WarningRule(), + filename=filename, + ) + matches.append(match) + continue + _logger.warning( + "%s:%s %s %s", + warn.filename, + warn.lineno or 1, + warn.category.__name__, + warn.message, + ) + return matches + + def _run(self) -> list[MatchError]: # noqa: C901 + """Internal implementation of run.""" files: list[Lintable] = [] matches: list[MatchError] = [] diff --git a/src/ansiblelint/skip_utils.py b/src/ansiblelint/skip_utils.py index 00c630d70b..4ebd512a4d 100644 --- a/src/ansiblelint/skip_utils.py +++ b/src/ansiblelint/skip_utils.py @@ -24,6 +24,7 @@ import collections.abc import logging import re +import warnings from collections.abc import Generator, Sequence from functools import cache from itertools import product @@ -42,6 +43,7 @@ RENAMED_TAGS, SKIPPED_RULES_KEY, ) +from ansiblelint.errors import LintWarning from ansiblelint.file_utils import Lintable if TYPE_CHECKING: @@ -57,7 +59,11 @@ # ansible.parsing.yaml.objects.AnsibleSequence -def get_rule_skips_from_line(line: str) -> list[str]: +def get_rule_skips_from_line( + line: str, + lintable: Lintable, + lineno: int = 1, +) -> list[str]: """Return list of rule ids skipped via comment on the line of yaml.""" _before_noqa, _noqa_marker, noqa_text = line.partition("# noqa") @@ -66,10 +72,16 @@ def get_rule_skips_from_line(line: str) -> list[str]: if v in RENAMED_TAGS: tag = RENAMED_TAGS[v] if v not in _found_deprecated_tags: - _logger.warning( - "Replaced outdated tag '%s' with '%s', replace it to avoid future regressions", - v, - tag, + msg = f"Replaced outdated tag '{v}' with '{tag}', replace it to avoid future regressions" + warnings.warn( + message=msg, + category=LintWarning, + source={ + "filename": lintable, + "lineno": lineno, + "tag": "warning[outdated-tag]", + }, + stacklevel=0, ) _found_deprecated_tags.add(v) v = tag @@ -253,7 +265,11 @@ def traverse_yaml(obj: Any) -> None: if _noqa_comment_re.match(comment_str): line = v.start_mark.line + 1 # ruamel line numbers start at 0 lintable.line_skips[line].update( - get_rule_skips_from_line(comment_str.strip()), + get_rule_skips_from_line( + comment_str.strip(), + lintable=lintable, + lineno=line, + ), ) yaml_comment_obj_strings.append(str(obj.ca.items)) if isinstance(obj, dict): @@ -273,7 +289,7 @@ def traverse_yaml(obj: Any) -> None: rule_id_list = [] for comment_obj_str in yaml_comment_obj_strings: for line in comment_obj_str.split(r"\n"): - rule_id_list.extend(get_rule_skips_from_line(line)) + rule_id_list.extend(get_rule_skips_from_line(line, lintable=lintable)) return [normalize_tag(tag) for tag in rule_id_list] diff --git a/test/test_skiputils.py b/test/test_skiputils.py index cf1de20f17..fd37b5a36a 100644 --- a/test/test_skiputils.py +++ b/test/test_skiputils.py @@ -42,7 +42,7 @@ ) def test_get_rule_skips_from_line(line: str, expected: str) -> None: """Validate get_rule_skips_from_line.""" - v = get_rule_skips_from_line(line) + v = get_rule_skips_from_line(line, lintable=Lintable("")) assert v == [expected]