Skip to content

Commit f65e10e

Browse files
committed
Capture python warnings and report them as matches
This change introduces a new way to generate warnings in linter, one that uses the warnings support in Python. The benefit is that this allows use to generate warnings from anywhere inside the code without having to pass them from function to function. For start we will be using this for internal rules, like ability to report outdated noqa comments.
1 parent 2cb73c6 commit f65e10e

File tree

8 files changed

+88
-15
lines changed

8 files changed

+88
-15
lines changed

playbook.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
- name: Example
33
hosts: localhost
44
tasks:
5-
- name: include extra tasks
5+
- name: include extra tasks # noqa: 102
66
ansible.builtin.include_tasks:
77
file: /dev/null

src/ansiblelint/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
from ansiblelint.file_utils import Lintable, normpath
1010

1111

12+
class LintWarning(Warning):
13+
"""Used by linter."""
14+
15+
1216
class StrictModeError(RuntimeError):
1317
"""Raise when we encounter a warning in strict mode."""
1418

src/ansiblelint/rules/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,10 @@ def matchlines(self, file: Lintable) -> list[MatchError]:
121121
if line.lstrip().startswith("#"):
122122
continue
123123

124-
rule_id_list = ansiblelint.skip_utils.get_rule_skips_from_line(line)
124+
rule_id_list = ansiblelint.skip_utils.get_rule_skips_from_line(
125+
line,
126+
lintable=file,
127+
)
125128
if self.id in rule_id_list:
126129
continue
127130

src/ansiblelint/rules/jinja.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ def matchyaml(self, file: Lintable) -> list[MatchError]:
200200
lines = file.content.splitlines()
201201
for match in raw_results:
202202
# lineno starts with 1, not zero
203-
skip_list = get_rule_skips_from_line(lines[match.lineno - 1])
203+
skip_list = get_rule_skips_from_line(
204+
line=lines[match.lineno - 1],
205+
lintable=file,
206+
)
204207
if match.rule.id not in skip_list and match.tag not in skip_list:
205208
results.append(match)
206209
else:

src/ansiblelint/rules/var_naming.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,10 @@ def matchplay(self, file: Lintable, data: dict[str, Any]) -> list[MatchError]:
9191
lines = file.content.splitlines()
9292
for match in raw_results:
9393
# lineno starts with 1, not zero
94-
skip_list = get_rule_skips_from_line(lines[match.lineno - 1])
94+
skip_list = get_rule_skips_from_line(
95+
line=lines[match.lineno - 1],
96+
lintable=file,
97+
)
9598
if match.rule.id not in skip_list and match.tag not in skip_list:
9699
results.append(match)
97100

@@ -171,7 +174,10 @@ def matchyaml(self, file: Lintable) -> list[MatchError]:
171174
lines = file.content.splitlines()
172175
for match in raw_results:
173176
# lineno starts with 1, not zero
174-
skip_list = get_rule_skips_from_line(lines[match.lineno - 1])
177+
skip_list = get_rule_skips_from_line(
178+
line=lines[match.lineno - 1],
179+
lintable=file,
180+
)
175181
if match.rule.id not in skip_list and match.tag not in skip_list:
176182
results.append(match)
177183
else:

src/ansiblelint/runner.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55
import multiprocessing
66
import multiprocessing.pool
77
import os
8+
import warnings
89
from collections.abc import Generator
910
from dataclasses import dataclass
1011
from fnmatch import fnmatch
1112
from typing import TYPE_CHECKING, Any
1213

1314
import ansiblelint.skip_utils
1415
import ansiblelint.utils
15-
from ansiblelint._internal.rules import LoadingFailureRule
16+
from ansiblelint._internal.rules import LoadingFailureRule, WarningRule
1617
from ansiblelint.config import Options
1718
from ansiblelint.constants import States
18-
from ansiblelint.errors import MatchError
19+
from ansiblelint.errors import LintWarning, MatchError
1920
from ansiblelint.file_utils import Lintable, expand_dirs_in_lintables
2021
from ansiblelint.rules.syntax_check import AnsibleSyntaxCheckRule
2122

@@ -33,6 +34,14 @@ class LintResult:
3334
files: set[Lintable]
3435

3536

37+
@dataclass
38+
class Occurrence:
39+
"""Class that tracks result of linting."""
40+
41+
file: Lintable
42+
lineno: MatchError
43+
44+
3645
class Runner:
3746
"""Runner class performs the linting process."""
3847

@@ -112,6 +121,38 @@ def is_excluded(self, lintable: Lintable) -> bool:
112121

113122
def run(self) -> list[MatchError]: # noqa: C901
114123
"""Execute the linting process."""
124+
matches: list[MatchError] = []
125+
with warnings.catch_warnings(record=True) as captured_warnings:
126+
warnings.simplefilter("always")
127+
matches = self._run()
128+
for warn in captured_warnings:
129+
# For the moment we are ignoring deprecation warnings as Ansible
130+
# modules outside current content can generate them and user
131+
# might not be able to do anything about them.
132+
if warn.category is DeprecationWarning:
133+
continue
134+
if warn.category is LintWarning:
135+
filename: None | Lintable = None
136+
if isinstance(warn.source, Lintable):
137+
filename = warn.source
138+
match = MatchError(
139+
message=warn.message if isinstance(warn.message, str) else "?",
140+
rule=WarningRule(),
141+
filename=filename,
142+
)
143+
matches.append(match)
144+
continue
145+
_logger.warning(
146+
"%s:%s %s %s",
147+
warn.filename,
148+
warn.lineno or 1,
149+
warn.category.__name__,
150+
warn.message,
151+
)
152+
return matches
153+
154+
def _run(self) -> list[MatchError]: # noqa: C901
155+
"""Internal implementation of run."""
115156
files: list[Lintable] = []
116157
matches: list[MatchError] = []
117158

src/ansiblelint/skip_utils.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import collections.abc
2525
import logging
2626
import re
27+
import warnings
2728
from collections.abc import Generator, Sequence
2829
from functools import cache
2930
from itertools import product
@@ -42,6 +43,7 @@
4243
RENAMED_TAGS,
4344
SKIPPED_RULES_KEY,
4445
)
46+
from ansiblelint.errors import LintWarning
4547
from ansiblelint.file_utils import Lintable
4648

4749
if TYPE_CHECKING:
@@ -57,7 +59,11 @@
5759
# ansible.parsing.yaml.objects.AnsibleSequence
5860

5961

60-
def get_rule_skips_from_line(line: str) -> list[str]:
62+
def get_rule_skips_from_line(
63+
line: str,
64+
lintable: Lintable,
65+
lineno: int = 1,
66+
) -> list[str]:
6167
"""Return list of rule ids skipped via comment on the line of yaml."""
6268
_before_noqa, _noqa_marker, noqa_text = line.partition("# noqa")
6369

@@ -66,10 +72,16 @@ def get_rule_skips_from_line(line: str) -> list[str]:
6672
if v in RENAMED_TAGS:
6773
tag = RENAMED_TAGS[v]
6874
if v not in _found_deprecated_tags:
69-
_logger.warning(
70-
"Replaced outdated tag '%s' with '%s', replace it to avoid future regressions",
71-
v,
72-
tag,
75+
msg = f"Replaced outdated tag '{v}' with '{tag}', replace it to avoid future regressions"
76+
warnings.warn(
77+
message=msg,
78+
category=LintWarning,
79+
source={
80+
"filename": lintable,
81+
"lineno": lineno,
82+
"tag": "warning[outdated-tag]",
83+
},
84+
stacklevel=0,
7385
)
7486
_found_deprecated_tags.add(v)
7587
v = tag
@@ -253,7 +265,11 @@ def traverse_yaml(obj: Any) -> None:
253265
if _noqa_comment_re.match(comment_str):
254266
line = v.start_mark.line + 1 # ruamel line numbers start at 0
255267
lintable.line_skips[line].update(
256-
get_rule_skips_from_line(comment_str.strip()),
268+
get_rule_skips_from_line(
269+
comment_str.strip(),
270+
lintable=lintable,
271+
lineno=line,
272+
),
257273
)
258274
yaml_comment_obj_strings.append(str(obj.ca.items))
259275
if isinstance(obj, dict):
@@ -273,7 +289,7 @@ def traverse_yaml(obj: Any) -> None:
273289
rule_id_list = []
274290
for comment_obj_str in yaml_comment_obj_strings:
275291
for line in comment_obj_str.split(r"\n"):
276-
rule_id_list.extend(get_rule_skips_from_line(line))
292+
rule_id_list.extend(get_rule_skips_from_line(line, lintable=lintable))
277293

278294
return [normalize_tag(tag) for tag in rule_id_list]
279295

test/test_skiputils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
)
4343
def test_get_rule_skips_from_line(line: str, expected: str) -> None:
4444
"""Validate get_rule_skips_from_line."""
45-
v = get_rule_skips_from_line(line)
45+
v = get_rule_skips_from_line(line, lintable=Lintable(""))
4646
assert v == [expected]
4747

4848

0 commit comments

Comments
 (0)