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

1"""Parser for svelte-check --output machine-verbose output.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import re 

7 

8from loguru import logger 

9 

10from lintro.parsers.base_parser import strip_ansi_codes 

11from lintro.parsers.svelte_check.svelte_check_issue import SvelteCheckIssue 

12 

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) 

24 

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) 

35 

36 

37def _normalize_severity(severity: str) -> str: 

38 """Normalize severity string to lowercase standard form. 

39 

40 Args: 

41 severity: Raw severity string from output. 

42 

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 

54 

55 

56def _parse_ndjson_line(line: str) -> SvelteCheckIssue | None: 

57 """Parse a machine-verbose NDJSON line. 

58 

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

62 

63 Args: 

64 line: A single line of svelte-check NDJSON output. 

65 

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 

75 

76 if not isinstance(data, dict): 

77 return None 

78 

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 

84 

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

94 

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 

110 

111 

112def _parse_legacy_machine_verbose_line(line: str) -> SvelteCheckIssue | None: 

113 """Parse a legacy plain-text machine-verbose format line. 

114 

115 Fallback for older svelte-check versions that emit plain-text instead of NDJSON. 

116 

117 Args: 

118 line: A single line of svelte-check output. 

119 

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 

126 

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() 

135 

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 

150 

151 

152def _parse_machine_line(line: str) -> SvelteCheckIssue | None: 

153 """Parse a machine format line (simpler format). 

154 

155 Args: 

156 line: A single line of svelte-check output. 

157 

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 

164 

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() 

171 

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 

182 

183 

184def _parse_line(line: str) -> SvelteCheckIssue | None: 

185 """Parse a single svelte-check output line into a SvelteCheckIssue. 

186 

187 Args: 

188 line: A single line of svelte-check output. 

189 

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 

196 

197 # Skip summary lines and noise 

198 if line.startswith(("=====", "svelte-check", "Loading", "Diagnostics")): 

199 return None 

200 

201 # Try NDJSON format first (modern svelte-check --output machine-verbose) 

202 issue = _parse_ndjson_line(line) 

203 if issue: 

204 return issue 

205 

206 # Try legacy plain-text machine-verbose format 

207 issue = _parse_legacy_machine_verbose_line(line) 

208 if issue: 

209 return issue 

210 

211 # Try machine format 

212 issue = _parse_machine_line(line) 

213 if issue: 

214 return issue 

215 

216 return None 

217 

218 

219def parse_svelte_check_output(output: str) -> list[SvelteCheckIssue]: 

220 """Parse svelte-check output into SvelteCheckIssue objects. 

221 

222 Args: 

223 output: Raw stdout emitted by svelte-check --output machine-verbose. 

224 

225 Returns: 

226 A list of SvelteCheckIssue instances parsed from the output. 

227 

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 [] 

241 

242 # Strip ANSI codes for consistent parsing 

243 output = strip_ansi_codes(output) 

244 

245 issues: list[SvelteCheckIssue] = [] 

246 for line in output.splitlines(): 

247 parsed = _parse_line(line) 

248 if parsed: 

249 issues.append(parsed) 

250 

251 return issues