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

1"""Parser for actionlint CLI output. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10import re 

11from collections.abc import Iterable 

12 

13from lintro.parsers.actionlint.actionlint_issue import ActionlintIssue 

14from lintro.parsers.base_parser import strip_ansi_codes 

15 

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) 

19 

20 

21def parse_actionlint_output(output: str | None) -> list[ActionlintIssue]: 

22 """Parse raw actionlint output into structured issues. 

23 

24 Args: 

25 output: Raw stdout/stderr combined output from actionlint. 

26 

27 Returns: 

28 list[ActionlintIssue]: Parsed issues from the tool output. 

29 """ 

30 if not output: 

31 return [] 

32 

33 # Strip ANSI codes for consistent parsing across environments 

34 output = strip_ansi_codes(output) 

35 

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 

58 

59 

60def _iter_nonempty_lines(text: str) -> Iterable[str]: 

61 """Iterate non-empty lines from a text block. 

62 

63 Args: 

64 text: Input text to split into lines. 

65 

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