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
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Svelte-check tool definition.
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.
7Example:
8 # Check Svelte project
9 lintro check src/ --tools svelte-check
11 # Check with specific threshold
12 lintro check src/ --tools svelte-check \
13 --tool-options "svelte-check:threshold=warning"
14"""
16from __future__ import annotations
18import shutil
19import subprocess # nosec B404 - used safely with shell disabled
20from dataclasses import dataclass
21from pathlib import Path
22from typing import Any, NoReturn
24from loguru import logger
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
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"]
43@register_tool
44@dataclass
45class SvelteCheckPlugin(BaseToolPlugin):
46 """Svelte-check type checking plugin.
48 This plugin integrates svelte-check with Lintro for static type checking
49 and linting of Svelte components.
50 """
52 @property
53 def definition(self) -> ToolDefinition:
54 """Return the tool definition.
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 )
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.
90 Args:
91 threshold: Minimum severity to report ("error", "warning", "hint").
92 tsconfig: Path to tsconfig.json file.
93 **kwargs: Other tool options.
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")
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)
113 def _get_svelte_check_command(self) -> list[str]:
114 """Get the command to run svelte-check.
116 Prefers direct svelte-check executable, falls back to bunx/npx.
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"]
133 def _find_svelte_config(self, cwd: Path) -> Path | None:
134 """Find svelte config file in the working directory.
136 Args:
137 cwd: Working directory to search for config.
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
149 def _build_command(
150 self,
151 options: dict[str, object] | None = None,
152 ) -> list[str]:
153 """Build the svelte-check invocation command.
155 Args:
156 options: Options dict to use for flags. Defaults to self.options.
158 Returns:
159 A list of command arguments ready to be executed.
160 """
161 if options is None:
162 options = self.options
164 cmd: list[str] = self._get_svelte_check_command()
166 # Use machine-verbose output for parseable format
167 cmd.extend(["--output", "machine-verbose"])
169 # Threshold option
170 threshold = options.get("threshold", "error")
171 if threshold:
172 cmd.extend(["--threshold", str(threshold)])
174 # Tsconfig option
175 tsconfig = options.get("tsconfig")
176 if tsconfig:
177 cmd.extend(["--tsconfig", str(tsconfig)])
179 return cmd
181 def check(self, paths: list[str], options: dict[str, object]) -> ToolResult:
182 """Check files with svelte-check.
184 Svelte-check runs on the entire project and uses the project's
185 svelte.config and tsconfig.json for configuration.
187 Args:
188 paths: List of file or directory paths to check.
189 options: Runtime options that override defaults.
191 Returns:
192 ToolResult with check results.
193 """
194 # Merge runtime options
195 merged_options = dict(self.options)
196 merged_options.update(options)
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 )
205 if ctx.should_skip and ctx.early_result is not None:
206 return ctx.early_result
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 )
217 logger.debug("[svelte-check] Discovered {} Svelte file(s)", len(ctx.files))
219 cwd_path = Path(ctx.cwd) if ctx.cwd else Path.cwd()
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 )
228 # Check if dependencies need installing
229 from lintro.utils.node_deps import install_node_deps, should_install_deps
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 )
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 )
281 # Build command
282 cmd = self._build_command(options=merged_options)
283 logger.debug("[svelte-check] Running with cwd={} and cmd={}", ctx.cwd, cmd)
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 )
323 # Parse output
324 all_issues = parse_svelte_check_output(output=output or "")
325 issues_count = len(all_issues)
327 # Normalize output for fallback analysis
328 normalized_output = strip_ansi_codes(output) if output else ""
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 )
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 )
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 )
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 )
374 def fix(self, paths: list[str], options: dict[str, object]) -> NoReturn:
375 """Svelte-check does not support auto-fixing.
377 Args:
378 paths: Paths or files passed for completeness.
379 options: Runtime options (unused).
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 )