Coverage for lintro / tools / definitions / svelte_check.py: 74%

118 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2026-04-03 18:53 +0000

1"""Svelte-check tool definition. 

2 

3Svelte-check is the official type checker and linter for Svelte components. 

4It provides TypeScript type checking, unused CSS detection, and accessibility 

5hints for `.svelte` files. 

6 

7Example: 

8 # Check Svelte project 

9 lintro check src/ --tools svelte-check 

10 

11 # Check with specific threshold 

12 lintro check src/ --tools svelte-check \ 

13 --tool-options "svelte-check:threshold=warning" 

14""" 

15 

16from __future__ import annotations 

17 

18import shutil 

19import subprocess # nosec B404 - used safely with shell disabled 

20from dataclasses import dataclass 

21from pathlib import Path 

22from typing import Any, NoReturn 

23 

24from loguru import logger 

25 

26from lintro._tool_versions import get_min_version 

27from lintro.enums.tool_name import ToolName 

28from lintro.enums.tool_type import ToolType 

29from lintro.models.core.tool_result import ToolResult 

30from lintro.parsers.base_parser import strip_ansi_codes 

31from lintro.parsers.svelte_check.svelte_check_parser import parse_svelte_check_output 

32from lintro.plugins.base import BaseToolPlugin 

33from lintro.plugins.protocol import ToolDefinition 

34from lintro.plugins.registry import register_tool 

35from lintro.tools.core.timeout_utils import create_timeout_result 

36 

37# Constants for Svelte-check configuration 

38SVELTE_CHECK_DEFAULT_TIMEOUT: int = 120 

39SVELTE_CHECK_DEFAULT_PRIORITY: int = 83 # After tsc (82) 

40SVELTE_CHECK_FILE_PATTERNS: list[str] = ["*.svelte"] 

41 

42 

43@register_tool 

44@dataclass 

45class SvelteCheckPlugin(BaseToolPlugin): 

46 """Svelte-check type checking plugin. 

47 

48 This plugin integrates svelte-check with Lintro for static type checking 

49 and linting of Svelte components. 

50 """ 

51 

52 @property 

53 def definition(self) -> ToolDefinition: 

54 """Return the tool definition. 

55 

56 Returns: 

57 ToolDefinition containing tool metadata. 

58 """ 

59 return ToolDefinition( 

60 name="svelte-check", 

61 description="Svelte type checker and linter for Svelte components", 

62 can_fix=False, 

63 tool_type=ToolType.LINTER | ToolType.TYPE_CHECKER, 

64 file_patterns=SVELTE_CHECK_FILE_PATTERNS, 

65 priority=SVELTE_CHECK_DEFAULT_PRIORITY, 

66 conflicts_with=[], 

67 native_configs=[ 

68 "svelte.config.js", 

69 "svelte.config.ts", 

70 "svelte.config.mjs", 

71 ], 

72 version_command=self._get_svelte_check_command() + ["--version"], 

73 min_version=get_min_version(ToolName.SVELTE_CHECK), 

74 default_options={ 

75 "timeout": SVELTE_CHECK_DEFAULT_TIMEOUT, 

76 "threshold": "error", # error, warning, or hint 

77 "tsconfig": None, 

78 }, 

79 default_timeout=SVELTE_CHECK_DEFAULT_TIMEOUT, 

80 ) 

81 

82 def set_options( 

83 self, 

84 threshold: str | None = None, 

85 tsconfig: str | None = None, 

86 **kwargs: Any, 

87 ) -> None: 

88 """Set svelte-check-specific options. 

89 

90 Args: 

91 threshold: Minimum severity to report ("error", "warning", "hint"). 

92 tsconfig: Path to tsconfig.json file. 

93 **kwargs: Other tool options. 

94 

95 Raises: 

96 ValueError: If any provided option is of an unexpected type. 

97 """ 

98 if threshold is not None: 

99 if not isinstance(threshold, str): 

100 raise ValueError("threshold must be a string") 

101 if threshold not in ("error", "warning", "hint"): 

102 raise ValueError("threshold must be 'error', 'warning', or 'hint'") 

103 if tsconfig is not None and not isinstance(tsconfig, str): 

104 raise ValueError("tsconfig must be a string path") 

105 

106 options: dict[str, object] = { 

107 "threshold": threshold, 

108 "tsconfig": tsconfig, 

109 } 

110 options = {k: v for k, v in options.items() if v is not None} 

111 super().set_options(**options, **kwargs) 

112 

113 def _get_svelte_check_command(self) -> list[str]: 

114 """Get the command to run svelte-check. 

115 

116 Prefers direct svelte-check executable, falls back to bunx/npx. 

117 

118 Returns: 

119 Command arguments for svelte-check. 

120 """ 

121 # Prefer direct executable if available 

122 if shutil.which("svelte-check"): 

123 return ["svelte-check"] 

124 # Try bunx (bun) 

125 if shutil.which("bunx"): 

126 return ["bunx", "svelte-check"] 

127 # Try npx (npm) 

128 if shutil.which("npx"): 

129 return ["npx", "svelte-check"] 

130 # Last resort 

131 return ["svelte-check"] 

132 

133 def _find_svelte_config(self, cwd: Path) -> Path | None: 

134 """Find svelte config file in the working directory. 

135 

136 Args: 

137 cwd: Working directory to search for config. 

138 

139 Returns: 

140 Path to svelte config if found, None otherwise. 

141 """ 

142 config_names = ["svelte.config.js", "svelte.config.ts", "svelte.config.mjs"] 

143 for config_name in config_names: 

144 config_path = cwd / config_name 

145 if config_path.exists(): 

146 return config_path 

147 return None 

148 

149 def _build_command( 

150 self, 

151 options: dict[str, object] | None = None, 

152 ) -> list[str]: 

153 """Build the svelte-check invocation command. 

154 

155 Args: 

156 options: Options dict to use for flags. Defaults to self.options. 

157 

158 Returns: 

159 A list of command arguments ready to be executed. 

160 """ 

161 if options is None: 

162 options = self.options 

163 

164 cmd: list[str] = self._get_svelte_check_command() 

165 

166 # Use machine-verbose output for parseable format 

167 cmd.extend(["--output", "machine-verbose"]) 

168 

169 # Threshold option 

170 threshold = options.get("threshold", "error") 

171 if threshold: 

172 cmd.extend(["--threshold", str(threshold)]) 

173 

174 # Tsconfig option 

175 tsconfig = options.get("tsconfig") 

176 if tsconfig: 

177 cmd.extend(["--tsconfig", str(tsconfig)]) 

178 

179 return cmd 

180 

181 def check(self, paths: list[str], options: dict[str, object]) -> ToolResult: 

182 """Check files with svelte-check. 

183 

184 Svelte-check runs on the entire project and uses the project's 

185 svelte.config and tsconfig.json for configuration. 

186 

187 Args: 

188 paths: List of file or directory paths to check. 

189 options: Runtime options that override defaults. 

190 

191 Returns: 

192 ToolResult with check results. 

193 """ 

194 # Merge runtime options 

195 merged_options = dict(self.options) 

196 merged_options.update(options) 

197 

198 # Use shared preparation for version check, path validation, file discovery 

199 ctx = self._prepare_execution( 

200 paths, 

201 merged_options, 

202 no_files_message="No Svelte files to check.", 

203 ) 

204 

205 if ctx.should_skip and ctx.early_result is not None: 

206 return ctx.early_result 

207 

208 # Safety check: if should_skip but no early_result, create one 

209 if ctx.should_skip: 

210 return ToolResult( 

211 name=self.definition.name, 

212 success=True, 

213 output="No Svelte files to check.", 

214 issues_count=0, 

215 ) 

216 

217 logger.debug("[svelte-check] Discovered {} Svelte file(s)", len(ctx.files)) 

218 

219 cwd_path = Path(ctx.cwd) if ctx.cwd else Path.cwd() 

220 

221 # Warn if no svelte config found, but still proceed with defaults 

222 svelte_config = self._find_svelte_config(cwd_path) 

223 if not svelte_config: 

224 logger.warning( 

225 "[svelte-check] No svelte.config.* found — proceeding with defaults", 

226 ) 

227 

228 # Check if dependencies need installing 

229 from lintro.utils.node_deps import install_node_deps, should_install_deps 

230 

231 try: 

232 needs_install = should_install_deps(cwd_path) 

233 except PermissionError as e: 

234 logger.warning("[svelte-check] {}", e) 

235 return ToolResult( 

236 name=self.definition.name, 

237 success=True, 

238 output=f"Skipping svelte-check: {e}", 

239 issues_count=0, 

240 skipped=True, 

241 skip_reason="directory not writable", 

242 ) 

243 

244 if needs_install: 

245 auto_install = merged_options.get("auto_install", False) 

246 if auto_install: 

247 logger.info("[svelte-check] Auto-installing Node.js dependencies...") 

248 install_ok, install_output = install_node_deps(cwd_path) 

249 if install_ok: 

250 logger.info( 

251 "[svelte-check] Dependencies installed successfully", 

252 ) 

253 else: 

254 logger.warning( 

255 "[svelte-check] Auto-install failed, skipping: {}", 

256 install_output, 

257 ) 

258 return ToolResult( 

259 name=self.definition.name, 

260 success=True, 

261 output=( 

262 f"Skipping svelte-check: auto-install failed.\n" 

263 f"{install_output}" 

264 ), 

265 issues_count=0, 

266 skipped=True, 

267 skip_reason="auto-install failed", 

268 ) 

269 else: 

270 return ToolResult( 

271 name=self.definition.name, 

272 output=( 

273 "node_modules not found. " 

274 "Use --auto-install to install dependencies." 

275 ), 

276 issues_count=0, 

277 skipped=True, 

278 skip_reason="node_modules not found", 

279 ) 

280 

281 # Build command 

282 cmd = self._build_command(options=merged_options) 

283 logger.debug("[svelte-check] Running with cwd={} and cmd={}", ctx.cwd, cmd) 

284 

285 try: 

286 success, output = self._run_subprocess( 

287 cmd=cmd, 

288 timeout=ctx.timeout, 

289 cwd=ctx.cwd, 

290 ) 

291 except subprocess.TimeoutExpired: 

292 timeout_result = create_timeout_result( 

293 tool=self, 

294 timeout=ctx.timeout, 

295 cmd=cmd, 

296 ) 

297 return ToolResult( 

298 name=self.definition.name, 

299 success=timeout_result.success, 

300 output=timeout_result.output, 

301 issues_count=timeout_result.issues_count, 

302 issues=timeout_result.issues, 

303 ) 

304 except FileNotFoundError as e: 

305 return ToolResult( 

306 name=self.definition.name, 

307 success=False, 

308 output=f"svelte-check not found: {e}\n\n" 

309 "Please ensure svelte-check is installed:\n" 

310 " - Run 'bun add -D svelte-check' or 'npm install -D svelte-check'\n" 

311 " - Or install globally: 'bun add -g svelte-check'", 

312 issues_count=0, 

313 ) 

314 except OSError as e: 

315 logger.error("[svelte-check] Failed to run svelte-check: {}", e) 

316 return ToolResult( 

317 name=self.definition.name, 

318 success=False, 

319 output="svelte-check execution failed: " + str(e), 

320 issues_count=0, 

321 ) 

322 

323 # Parse output 

324 all_issues = parse_svelte_check_output(output=output or "") 

325 issues_count = len(all_issues) 

326 

327 # Normalize output for fallback analysis 

328 normalized_output = strip_ansi_codes(output) if output else "" 

329 

330 # Handle dependency errors 

331 if not success and issues_count == 0 and normalized_output: 

332 if ( 

333 "Cannot find module" in normalized_output 

334 or "Cannot find type definition" in normalized_output 

335 ): 

336 helpful_output = ( 

337 f"svelte-check configuration error:\n{normalized_output}\n\n" 

338 "This usually means dependencies aren't installed.\n" 

339 "Suggestions:\n" 

340 " - Run 'bun install' or 'npm install' in your project\n" 

341 " - Use '--auto-install' flag to auto-install dependencies" 

342 ) 

343 return ToolResult( 

344 name=self.definition.name, 

345 success=False, 

346 output=helpful_output, 

347 issues_count=0, 

348 ) 

349 

350 # Generic failure 

351 return ToolResult( 

352 name=self.definition.name, 

353 success=False, 

354 output=normalized_output or "svelte-check execution failed.", 

355 issues_count=0, 

356 ) 

357 

358 if not success and issues_count == 0: 

359 return ToolResult( 

360 name=self.definition.name, 

361 success=False, 

362 output="svelte-check execution failed.", 

363 issues_count=0, 

364 ) 

365 

366 return ToolResult( 

367 name=self.definition.name, 

368 success=success and issues_count == 0, 

369 output=None, 

370 issues_count=issues_count, 

371 issues=all_issues, 

372 ) 

373 

374 def fix(self, paths: list[str], options: dict[str, object]) -> NoReturn: 

375 """Svelte-check does not support auto-fixing. 

376 

377 Args: 

378 paths: Paths or files passed for completeness. 

379 options: Runtime options (unused). 

380 

381 Raises: 

382 NotImplementedError: Always, because svelte-check cannot fix issues. 

383 """ 

384 raise NotImplementedError( 

385 "svelte-check cannot automatically fix issues. Type errors and " 

386 "linting issues require manual code changes.", 

387 )