Coverage for lintro / parsers / astro_check / astro_check_parser.py: 84%
55 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 astro check output."""
3from __future__ import annotations
5import re
7from loguru import logger
9from lintro.parsers.astro_check.astro_check_issue import AstroCheckIssue
10from lintro.parsers.base_parser import strip_ansi_codes
12# Pattern for astro check output:
13# file.astro:line:col - severity ts1234: message
14# Also handles tsc-style output: file.astro(line,col): error TS1234: message
15ASTRO_ISSUE_PATTERN = re.compile(
16 r"^(?P<file>.+?)"
17 r"(?:"
18 r"\((?P<line_paren>\d+),(?P<col_paren>\d+)\)" # tsc style: file(line,col)
19 r"|"
20 r":(?P<line_colon>\d+):(?P<col_colon>\d+)" # astro style: file:line:col
21 r")"
22 r"(?:\s*[-:]\s*|\s+)"
23 r"(?P<severity>error|warning|hint)?\s*"
24 r"(?:(?P<code>ts\d+|TS\d+)[:\s]+)?"
25 r"(?P<message>.+)$",
26 re.IGNORECASE,
27)
29# Alternative pattern for simpler format:
30# src/pages/index.astro:5:3 Type 'X' is not assignable to type 'Y'.
31ASTRO_SIMPLE_PATTERN = re.compile(
32 r"^(?P<file>.+?):(?P<line>\d+):(?P<col>\d+)\s+(?P<message>.+)$",
33)
35# Astro-check stderr timestamp prefix: HH:MM:SS
36# e.g. "15:19:56 [content] Syncing content"
37# These must be filtered before regex matching because the HH:MM:SS format
38# is indistinguishable from a file:line:col triplet.
39_TIMESTAMP_PREFIX = re.compile(r"^\d{1,2}:\d{2}:\d{2}\s")
42def _parse_line(line: str) -> AstroCheckIssue | None:
43 """Parse a single astro check output line into an AstroCheckIssue.
45 Args:
46 line: A single line of astro check output.
48 Returns:
49 An AstroCheckIssue instance or None if the line doesn't match.
50 """
51 line = line.strip()
52 if not line:
53 return None
55 # Skip summary lines and noise
56 if line.startswith(("Result", "Found", "Checking", "...")):
57 return None
59 # Skip astro-check stderr timestamp lines (HH:MM:SS prefix).
60 # These are informational log messages like:
61 # 15:19:56 [WARN] Missing pages directory: src/pages
62 # 15:19:56 [content] Syncing content
63 # Without this filter the HH:MM:SS prefix is misinterpreted as
64 # file:line:col by the fallback ASTRO_SIMPLE_PATTERN.
65 if _TIMESTAMP_PREFIX.match(line):
66 return None
68 match = ASTRO_ISSUE_PATTERN.match(line)
69 if match:
70 try:
71 file_path = match.group("file")
72 # Handle both tsc-style and astro-style line/col
73 line_paren = match.group("line_paren")
74 line_colon = match.group("line_colon")
75 col_paren = match.group("col_paren")
76 col_colon = match.group("col_colon")
78 # Warn if neither line format matched (unexpected regex state)
79 if line_paren is None and line_colon is None:
80 logger.warning(
81 "[astro-check] Regex matched but no line number found: %s",
82 line,
83 )
84 return None
86 line_num = int(line_paren or line_colon)
87 column = int(col_paren or col_colon or "1") # Default col to 1 if missing
89 severity = match.group("severity")
90 code = match.group("code")
91 message = match.group("message").strip()
93 # Normalize Windows paths to forward slashes
94 file_path = file_path.replace("\\", "/")
96 # Normalize code to uppercase
97 if code:
98 code = code.upper()
100 return AstroCheckIssue(
101 file=file_path,
102 line=line_num,
103 column=column,
104 code=code or "",
105 message=message,
106 severity=severity.lower() if severity else "error",
107 )
108 except (ValueError, AttributeError) as e:
109 logger.debug(
110 "[astro-check] Failed to parse line with main pattern: {}",
111 e,
112 )
114 # Try simpler pattern as fallback
115 match = ASTRO_SIMPLE_PATTERN.match(line)
116 if match:
117 try:
118 return AstroCheckIssue(
119 file=match.group("file").replace("\\", "/"),
120 line=int(match.group("line")),
121 column=int(match.group("col")),
122 message=match.group("message").strip(),
123 severity="error",
124 )
125 except (ValueError, AttributeError) as e:
126 logger.debug(
127 "[astro-check] Failed to parse line with simple pattern: {}",
128 e,
129 )
131 return None
134def parse_astro_check_output(output: str) -> list[AstroCheckIssue]:
135 """Parse astro check output into AstroCheckIssue objects.
137 Args:
138 output: Raw stdout emitted by astro check.
140 Returns:
141 A list of AstroCheckIssue instances parsed from the output.
143 Examples:
144 >>> output = "src/pages/index.astro:10:5 - error ts2322: Type error."
145 >>> issues = parse_astro_check_output(output)
146 >>> len(issues)
147 1
148 """
149 if not output or not output.strip():
150 return []
152 # Strip ANSI codes for consistent parsing
153 output = strip_ansi_codes(output)
155 issues: list[AstroCheckIssue] = []
156 for line in output.splitlines():
157 parsed = _parse_line(line)
158 if parsed:
159 issues.append(parsed)
161 return issues