Coverage for lintro / parsers / svelte_check / svelte_check_parser.py: 87%
99 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 svelte-check --output machine-verbose output."""
3from __future__ import annotations
5import json
6import re
8from loguru import logger
10from lintro.parsers.base_parser import strip_ansi_codes
11from lintro.parsers.svelte_check.svelte_check_issue import SvelteCheckIssue
13# Legacy fallback pattern for plain-text machine-verbose output:
14# <file>:<startLine>:<startCol>:<endLine>:<endCol> <severity> <message>
15_LEGACY_MACHINE_VERBOSE_PATTERN = re.compile(
16 r"^(?P<file>.+?):"
17 r"(?P<start_line>\d+):"
18 r"(?P<start_col>\d+):"
19 r"(?P<end_line>\d+):"
20 r"(?P<end_col>\d+)\s+"
21 r"(?P<severity>Error|Warning|Hint)\s+"
22 r"(?P<message>.+)$",
23)
25# Alternative pattern for simpler format (machine output without verbose):
26# <severity> <file>:<line>:<col> <message>
27MACHINE_PATTERN = re.compile(
28 r"^(?P<severity>ERROR|WARN|HINT)\s+"
29 r"(?P<file>.+?):"
30 r"(?P<line>\d+):"
31 r"(?P<col>\d+)\s+"
32 r"(?P<message>.+)$",
33 re.IGNORECASE,
34)
37def _normalize_severity(severity: str) -> str:
38 """Normalize severity string to lowercase standard form.
40 Args:
41 severity: Raw severity string from output.
43 Returns:
44 Normalized severity ("error", "warning", or "hint").
45 """
46 severity_lower = severity.lower()
47 if severity_lower in ("error", "err"):
48 return "error"
49 if severity_lower in ("warning", "warn"):
50 return "warning"
51 if severity_lower == "hint":
52 return "hint"
53 return "error" # Default to error for unknown
56def _parse_ndjson_line(line: str) -> SvelteCheckIssue | None:
57 """Parse a machine-verbose NDJSON line.
59 Modern svelte-check --output machine-verbose emits NDJSON (one JSON object
60 per line). Each object contains "type" (severity), "fn" or "filename",
61 "start"/"end" position objects, and "message".
63 Args:
64 line: A single line of svelte-check NDJSON output.
66 Returns:
67 A SvelteCheckIssue instance or None if the line is not valid NDJSON.
68 """
69 # Strip leading millisecond timestamp prefix (e.g. "1590680326283 {...}")
70 stripped = re.sub(r"^\d+\s+", "", line)
71 try:
72 data = json.loads(stripped)
73 except (json.JSONDecodeError, ValueError):
74 return None
76 if not isinstance(data, dict):
77 return None
79 try:
80 severity = _normalize_severity(data.get("type", "error"))
81 file_path = (data.get("fn") or data.get("filename", "")).replace("\\", "/")
82 if not file_path:
83 return None
85 start = data.get("start", {})
86 end = data.get("end", {})
87 start_line = int(start.get("line", 0))
88 start_col = int(start.get("character", 0))
89 end_line = int(end.get("line", start_line))
90 end_col = int(end.get("character", start_col))
91 message = str(data.get("message", "")).strip()
92 raw_code = data.get("code")
93 code = str(raw_code) if raw_code is not None else ""
95 return SvelteCheckIssue(
96 file=file_path,
97 line=start_line,
98 column=start_col,
99 end_line=end_line if end_line != start_line else None,
100 end_column=(
101 end_col if (end_line != start_line or end_col != start_col) else None
102 ),
103 severity=severity,
104 message=message,
105 code=code,
106 )
107 except (ValueError, TypeError, AttributeError) as e:
108 logger.debug(f"Failed to parse svelte-check NDJSON line: {e}")
109 return None
112def _parse_legacy_machine_verbose_line(line: str) -> SvelteCheckIssue | None:
113 """Parse a legacy plain-text machine-verbose format line.
115 Fallback for older svelte-check versions that emit plain-text instead of NDJSON.
117 Args:
118 line: A single line of svelte-check output.
120 Returns:
121 A SvelteCheckIssue instance or None if the line doesn't match.
122 """
123 match = _LEGACY_MACHINE_VERBOSE_PATTERN.match(line)
124 if not match:
125 return None
127 try:
128 file_path = match.group("file").replace("\\", "/")
129 start_line = int(match.group("start_line"))
130 start_col = int(match.group("start_col"))
131 end_line = int(match.group("end_line"))
132 end_col = int(match.group("end_col"))
133 severity = _normalize_severity(match.group("severity"))
134 message = match.group("message").strip()
136 return SvelteCheckIssue(
137 file=file_path,
138 line=start_line,
139 column=start_col,
140 end_line=end_line if end_line != start_line else None,
141 end_column=(
142 end_col if (end_line != start_line or end_col != start_col) else None
143 ),
144 severity=severity,
145 message=message,
146 )
147 except (ValueError, AttributeError) as e:
148 logger.debug(f"Failed to parse svelte-check legacy machine-verbose line: {e}")
149 return None
152def _parse_machine_line(line: str) -> SvelteCheckIssue | None:
153 """Parse a machine format line (simpler format).
155 Args:
156 line: A single line of svelte-check output.
158 Returns:
159 A SvelteCheckIssue instance or None if the line doesn't match.
160 """
161 match = MACHINE_PATTERN.match(line)
162 if not match:
163 return None
165 try:
166 file_path = match.group("file").replace("\\", "/")
167 line_num = int(match.group("line"))
168 column = int(match.group("col"))
169 severity = _normalize_severity(match.group("severity"))
170 message = match.group("message").strip()
172 return SvelteCheckIssue(
173 file=file_path,
174 line=line_num,
175 column=column,
176 severity=severity,
177 message=message,
178 )
179 except (ValueError, AttributeError) as e:
180 logger.debug(f"Failed to parse svelte-check machine line: {e}")
181 return None
184def _parse_line(line: str) -> SvelteCheckIssue | None:
185 """Parse a single svelte-check output line into a SvelteCheckIssue.
187 Args:
188 line: A single line of svelte-check output.
190 Returns:
191 A SvelteCheckIssue instance or None if the line doesn't match.
192 """
193 line = line.strip()
194 if not line:
195 return None
197 # Skip summary lines and noise
198 if line.startswith(("=====", "svelte-check", "Loading", "Diagnostics")):
199 return None
201 # Try NDJSON format first (modern svelte-check --output machine-verbose)
202 issue = _parse_ndjson_line(line)
203 if issue:
204 return issue
206 # Try legacy plain-text machine-verbose format
207 issue = _parse_legacy_machine_verbose_line(line)
208 if issue:
209 return issue
211 # Try machine format
212 issue = _parse_machine_line(line)
213 if issue:
214 return issue
216 return None
219def parse_svelte_check_output(output: str) -> list[SvelteCheckIssue]:
220 """Parse svelte-check output into SvelteCheckIssue objects.
222 Args:
223 output: Raw stdout emitted by svelte-check --output machine-verbose.
225 Returns:
226 A list of SvelteCheckIssue instances parsed from the output.
228 Examples:
229 >>> import json
230 >>> data = {"type": "ERROR", "fn": "src/lib/B.svelte",
231 ... "start": {"line": 15, "character": 5},
232 ... "end": {"line": 15, "character": 10},
233 ... "message": "Type error"}
234 >>> line = "1590680326283 " + json.dumps(data)
235 >>> issues = parse_svelte_check_output(line)
236 >>> len(issues)
237 1
238 """
239 if not output or not output.strip():
240 return []
242 # Strip ANSI codes for consistent parsing
243 output = strip_ansi_codes(output)
245 issues: list[SvelteCheckIssue] = []
246 for line in output.splitlines():
247 parsed = _parse_line(line)
248 if parsed:
249 issues.append(parsed)
251 return issues