Coverage for lintro / parsers / actionlint / actionlint_parser.py: 100%
27 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Parser for actionlint CLI output.
3This module parses the default text output produced by the ``actionlint``
4binary into structured ``ActionlintIssue`` objects so that Lintro can render
5uniform tables and reports across styles.
6"""
8from __future__ import annotations
10import re
11from collections.abc import Iterable
13from lintro.parsers.actionlint.actionlint_issue import ActionlintIssue
14from lintro.parsers.base_parser import strip_ansi_codes
16_LINE_RE: re.Pattern[str] = re.compile(
17 r"^(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+):\s*(?:(?P<level>error|warning):\s*)?(?P<msg>.*?)(?:\s*\[(?P<code>[A-Za-z0-9_\-\.]+)\])?$",
18)
21def parse_actionlint_output(output: str | None) -> list[ActionlintIssue]:
22 """Parse raw actionlint output into structured issues.
24 Args:
25 output: Raw stdout/stderr combined output from actionlint.
27 Returns:
28 list[ActionlintIssue]: Parsed issues from the tool output.
29 """
30 if not output:
31 return []
33 # Strip ANSI codes for consistent parsing across environments
34 output = strip_ansi_codes(output)
36 issues: list[ActionlintIssue] = []
37 for line in _iter_nonempty_lines(output):
38 m = _LINE_RE.match(line.strip())
39 if not m:
40 continue
41 file_path = m.group("file")
42 line_no = int(m.group("line"))
43 col_no = int(m.group("col"))
44 level = m.group("level") or "error"
45 msg = m.group("msg").strip()
46 code = m.group("code")
47 issues.append(
48 ActionlintIssue(
49 file=file_path,
50 line=line_no,
51 column=col_no,
52 level=level,
53 code=code or "",
54 message=msg,
55 ),
56 )
57 return issues
60def _iter_nonempty_lines(text: str) -> Iterable[str]:
61 """Iterate non-empty lines from a text block.
63 Args:
64 text: Input text to split into lines.
66 Yields:
67 str: Non-empty lines stripped of surrounding whitespace.
68 """
69 for ln in text.splitlines():
70 if ln.strip():
71 yield ln