Coverage for lintro / tools / definitions / tsc.py: 79%

228 statements  

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

1"""Tsc (TypeScript Compiler) tool definition. 

2 

3Tsc is the TypeScript compiler which performs static type checking on 

4TypeScript files. It helps catch type-related bugs before runtime by 

5analyzing type annotations and inferences. 

6 

7File Targeting Behavior: 

8 By default, lintro respects your file selection even when tsconfig.json exists. 

9 This is achieved by creating a temporary tsconfig that extends your project's 

10 config but overrides the `include` pattern to target only the specified files. 

11 

12 To use native tsconfig.json file selection instead, set `use_project_files=True`. 

13 

14Example: 

15 # Check only specific files (default behavior) 

16 lintro check src/utils.ts --tools tsc 

17 

18 # Check all files defined in tsconfig.json 

19 lintro check . --tools tsc --tool-options "tsc:use_project_files=True" 

20""" 

21 

22from __future__ import annotations 

23 

24import json 

25import shutil 

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

27import tempfile 

28from dataclasses import dataclass 

29from pathlib import Path 

30from typing import Any, NoReturn 

31 

32from loguru import logger 

33 

34from lintro._tool_versions import get_min_version 

35from lintro.enums.doc_url_template import DocUrlTemplate 

36from lintro.enums.tool_name import ToolName 

37from lintro.enums.tool_type import ToolType 

38from lintro.models.core.tool_result import ToolResult 

39from lintro.parsers.base_parser import strip_ansi_codes 

40from lintro.parsers.tsc.tsc_parser import ( 

41 categorize_tsc_issues, 

42 extract_missing_modules, 

43 parse_tsc_output, 

44) 

45from lintro.plugins.base import BaseToolPlugin 

46from lintro.plugins.protocol import ToolDefinition 

47from lintro.plugins.registry import register_tool 

48from lintro.tools.core.timeout_utils import create_timeout_result 

49from lintro.utils.jsonc import extract_type_roots, load_jsonc 

50 

51# Constants for Tsc configuration 

52TSC_DEFAULT_TIMEOUT: int = 60 

53TSC_DEFAULT_PRIORITY: int = 82 # Same as mypy (type checkers) 

54TSC_FILE_PATTERNS: list[str] = ["*.ts", "*.tsx", "*.mts", "*.cts"] 

55 

56# Framework config files that indicate tsc should defer to framework-specific checker 

57# Note: vite.config.ts is NOT included for Vue because it's used by many 

58# non-Vue projects (e.g., React, vanilla TS, Svelte without svelte.config) 

59FRAMEWORK_CONFIGS: dict[str, tuple[str, list[str]]] = { 

60 "Astro": ( 

61 "astro-check", 

62 ["astro.config.mjs", "astro.config.ts", "astro.config.js"], 

63 ), 

64 "Vue": ( 

65 "vue-tsc", 

66 ["vue.config.js", "vue.config.ts"], 

67 ), 

68 "Svelte": ( 

69 "svelte-check", 

70 ["svelte.config.js", "svelte.config.ts"], 

71 ), 

72} 

73 

74 

75@register_tool 

76@dataclass 

77class TscPlugin(BaseToolPlugin): 

78 """TypeScript Compiler (tsc) type checking plugin. 

79 

80 This plugin integrates the TypeScript compiler with Lintro for static 

81 type checking of TypeScript files. 

82 """ 

83 

84 @property 

85 def definition(self) -> ToolDefinition: 

86 """Return the tool definition. 

87 

88 Returns: 

89 ToolDefinition containing tool metadata. 

90 """ 

91 return ToolDefinition( 

92 name="tsc", 

93 description="TypeScript compiler for static type checking", 

94 can_fix=False, 

95 tool_type=ToolType.LINTER | ToolType.TYPE_CHECKER, 

96 file_patterns=TSC_FILE_PATTERNS, 

97 priority=TSC_DEFAULT_PRIORITY, 

98 conflicts_with=[], 

99 native_configs=["tsconfig.json"], 

100 version_command=["tsc", "--version"], 

101 min_version=get_min_version(ToolName.TSC), 

102 default_options={ 

103 "timeout": TSC_DEFAULT_TIMEOUT, 

104 "project": None, 

105 "strict": None, 

106 "skip_lib_check": True, 

107 "use_project_files": False, 

108 }, 

109 default_timeout=TSC_DEFAULT_TIMEOUT, 

110 ) 

111 

112 def set_options( 

113 self, 

114 project: str | None = None, 

115 strict: bool | None = None, 

116 skip_lib_check: bool | None = None, 

117 use_project_files: bool | None = None, 

118 **kwargs: Any, 

119 ) -> None: 

120 """Set tsc-specific options. 

121 

122 Args: 

123 project: Path to tsconfig.json file. 

124 strict: Enable strict type checking mode. 

125 skip_lib_check: Skip type checking of declaration files (default: True). 

126 use_project_files: When True, use tsconfig.json's include/files patterns 

127 instead of lintro's file targeting. Default is False, meaning lintro 

128 respects your file selection even when tsconfig.json exists. 

129 **kwargs: Other tool options. 

130 

131 Raises: 

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

133 """ 

134 if project is not None and not isinstance(project, str): 

135 raise ValueError("project must be a string path") 

136 if strict is not None and not isinstance(strict, bool): 

137 raise ValueError("strict must be a boolean") 

138 if skip_lib_check is not None and not isinstance(skip_lib_check, bool): 

139 raise ValueError("skip_lib_check must be a boolean") 

140 if use_project_files is not None and not isinstance(use_project_files, bool): 

141 raise ValueError("use_project_files must be a boolean") 

142 

143 options: dict[str, object] = { 

144 "project": project, 

145 "strict": strict, 

146 "skip_lib_check": skip_lib_check, 

147 "use_project_files": use_project_files, 

148 } 

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

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

151 

152 def _get_tsc_command(self) -> list[str]: 

153 """Get the command to run tsc. 

154 

155 Prefers direct tsc executable, falls back to bunx/npx. 

156 

157 Returns: 

158 Command arguments for tsc. 

159 """ 

160 # Prefer direct executable if available 

161 if shutil.which("tsc"): 

162 return ["tsc"] 

163 # Try bunx (bun) - note: bunx tsc works if typescript is installed 

164 if shutil.which("bunx"): 

165 return ["bunx", "tsc"] 

166 # Try npx (npm) 

167 if shutil.which("npx"): 

168 return ["npx", "tsc"] 

169 # Last resort - hope tsc is in PATH 

170 return ["tsc"] 

171 

172 def _find_tsconfig(self, cwd: Path) -> Path | None: 

173 """Find tsconfig.json in the working directory or via project option. 

174 

175 Args: 

176 cwd: Working directory to search for tsconfig.json. 

177 

178 Returns: 

179 Path to tsconfig.json if found, None otherwise. 

180 """ 

181 # Check explicit project option first 

182 project_opt = self.options.get("project") 

183 if project_opt and isinstance(project_opt, str): 

184 project_path = Path(project_opt) 

185 if project_path.is_absolute(): 

186 return project_path if project_path.exists() else None 

187 resolved = cwd / project_path 

188 return resolved if resolved.exists() else None 

189 

190 # Check for tsconfig.json in cwd 

191 tsconfig = cwd / "tsconfig.json" 

192 return tsconfig if tsconfig.exists() else None 

193 

194 def _detect_framework_project(self, cwd: Path) -> tuple[str, str] | None: 

195 """Detect if the project uses a framework with its own type checker. 

196 

197 Frameworks like Astro, Vue, and Svelte have their own type checkers 

198 that handle framework-specific syntax (e.g., .astro, .vue, .svelte files). 

199 When these frameworks are detected, tsc should skip and defer to the 

200 framework-specific tool. 

201 

202 Args: 

203 cwd: Working directory to search for framework config files. 

204 

205 Returns: 

206 Tuple of (framework_name, recommended_tool) if detected, None otherwise. 

207 """ 

208 for framework_name, (tool_name, config_files) in FRAMEWORK_CONFIGS.items(): 

209 for config_file in config_files: 

210 if (cwd / config_file).exists(): 

211 logger.debug( 

212 "[tsc] Detected {} project (found {})", 

213 framework_name, 

214 config_file, 

215 ) 

216 return (framework_name, tool_name) 

217 return None 

218 

219 def _create_temp_tsconfig( 

220 self, 

221 base_tsconfig: Path, 

222 files: list[str], 

223 cwd: Path, 

224 ) -> Path: 

225 """Create a temporary tsconfig.json that extends the base config. 

226 

227 This allows lintro to respect user file selection while preserving 

228 all compiler options from the project's tsconfig.json. 

229 

230 Args: 

231 base_tsconfig: Path to the original tsconfig.json to extend. 

232 files: List of file paths to include (relative to cwd). 

233 cwd: Working directory for resolving paths. 

234 

235 Returns: 

236 Path to the temporary tsconfig.json file. 

237 

238 Raises: 

239 OSError: If the temporary file cannot be created or written. 

240 """ 

241 abs_base = base_tsconfig.resolve() 

242 

243 # Convert relative file paths to absolute paths since the temp tsconfig 

244 # may be in a different directory than cwd 

245 abs_files = [str((cwd / f).resolve()) for f in files] 

246 

247 compiler_options: dict[str, Any] = { 

248 # Ensure noEmit is set (type checking only) 

249 "noEmit": True, 

250 } 

251 

252 # Read typeRoots from the base tsconfig so they are preserved in the 

253 # temp config. TypeScript resolves typeRoots relative to the config 

254 # file, so we resolve them to absolute paths here because the temp 

255 # config lives in a different directory. 

256 try: 

257 base_content = load_jsonc(abs_base.read_text(encoding="utf-8")) 

258 resolved_roots = extract_type_roots(base_content, abs_base.parent) 

259 if resolved_roots is not None: 

260 compiler_options["typeRoots"] = resolved_roots 

261 except (json.JSONDecodeError, OSError) as exc: 

262 logger.debug("[tsc] Could not read typeRoots from {}: {}", abs_base, exc) 

263 

264 temp_config = { 

265 "extends": str(abs_base), 

266 "include": abs_files, 

267 "exclude": [], 

268 "compilerOptions": compiler_options, 

269 } 

270 

271 # Create temp file next to the base tsconfig so TypeScript can resolve 

272 # types/typeRoots by walking up from the temp file to node_modules. 

273 # Falls back to system temp dir with explicit typeRoots for read-only 

274 # filesystems (e.g. Docker volume mounts). 

275 try: 

276 fd, temp_path = tempfile.mkstemp( 

277 suffix=".json", 

278 prefix=".lintro-tsc-", 

279 dir=abs_base.parent, 

280 ) 

281 except OSError: 

282 fd, temp_path = tempfile.mkstemp( 

283 suffix=".json", 

284 prefix="lintro-tsc-", 

285 ) 

286 # Preserve existing typeRoots from the base tsconfig and add 

287 # the default node_modules/@types path so TypeScript can still 

288 # resolve type packages from the system temp dir. 

289 existing_type_roots: list[str] = [] 

290 type_roots_explicit = False 

291 try: 

292 base_content = load_jsonc( 

293 base_tsconfig.read_text(encoding="utf-8"), 

294 ) 

295 extracted = extract_type_roots(base_content, abs_base.parent) 

296 if extracted is not None: 

297 existing_type_roots = extracted 

298 type_roots_explicit = True 

299 except (json.JSONDecodeError, OSError): 

300 pass 

301 default_root = str(cwd / "node_modules" / "@types") 

302 # Add the default root when typeRoots was absent or had 

303 # entries (the temp file lives outside the project tree so 

304 # TypeScript cannot discover it by walking up). When the 

305 # user explicitly set typeRoots: [] to disable global types, 

306 # honour that intent and leave the list empty. 

307 if ( 

308 not type_roots_explicit or existing_type_roots 

309 ) and default_root not in existing_type_roots: 

310 existing_type_roots.append(default_root) 

311 compiler_options["typeRoots"] = existing_type_roots 

312 

313 try: 

314 with open(fd, "w", encoding="utf-8") as f: 

315 json.dump(temp_config, f, indent=2) 

316 except OSError: 

317 # Clean up on failure 

318 Path(temp_path).unlink(missing_ok=True) 

319 raise 

320 

321 logger.debug( 

322 "[tsc] Created temp tsconfig at {} extending {} with {} files", 

323 temp_path, 

324 abs_base, 

325 len(files), 

326 ) 

327 return Path(temp_path) 

328 

329 def _build_command( 

330 self, 

331 files: list[str], 

332 project_path: str | Path | None = None, 

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

334 ) -> list[str]: 

335 """Build the tsc invocation command. 

336 

337 Args: 

338 files: Relative file paths (used only when no project config). 

339 project_path: Path to tsconfig.json to use (temp or user-specified). 

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

341 

342 Returns: 

343 A list of command arguments ready to be executed. 

344 """ 

345 if options is None: 

346 options = self.options 

347 

348 cmd: list[str] = self._get_tsc_command() 

349 

350 # Core flags for linting (no output, machine-readable format) 

351 cmd.extend(["--noEmit", "--pretty", "false"]) 

352 

353 # Project flag (uses tsconfig.json - either temp, explicit, or auto-discovered) 

354 if project_path: 

355 cmd.extend(["--project", str(project_path)]) 

356 

357 # Strict mode override (--strict is off by default, no flag needed for False) 

358 if options.get("strict") is True: 

359 cmd.append("--strict") 

360 

361 # Skip lib check (faster, avoids issues with node_modules types) 

362 if options.get("skip_lib_check", True): 

363 cmd.append("--skipLibCheck") 

364 

365 # Only pass files directly if no project config is being used 

366 if not project_path and files: 

367 cmd.extend(files) 

368 

369 return cmd 

370 

371 def doc_url(self, code: str) -> str | None: 

372 """Return TypeScript error documentation URL. 

373 

374 Uses typescript.tv, a third-party error reference, since the 

375 official TypeScript handbook does not provide per-error pages. 

376 

377 Args: 

378 code: TypeScript error code (e.g., "TS2307" or "2307"). 

379 

380 Returns: 

381 URL to the TypeScript error documentation, or None if invalid. 

382 """ 

383 if not code: 

384 return None 

385 # Strip "TS"/"ts" prefix if present to get the numeric portion 

386 upper = code.upper() 

387 num = code[2:] if upper.startswith("TS") else code 

388 if num.isdigit(): 

389 return DocUrlTemplate.TSC.format(code=num) 

390 return None 

391 

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

393 """Check files with tsc. 

394 

395 By default, lintro respects your file selection even when tsconfig.json exists. 

396 This is achieved by creating a temporary tsconfig that extends your project's 

397 config but targets only the specified files. 

398 

399 To use native tsconfig.json file selection instead, set use_project_files=True. 

400 

401 Note: For projects using Astro, Vue, or Svelte, tsc will skip and recommend 

402 using the framework-specific type checker (astro-check, vue-tsc, svelte-check) 

403 which handles framework-specific syntax. 

404 

405 Args: 

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

407 options: Runtime options that override defaults. 

408 

409 Returns: 

410 ToolResult with check results. 

411 """ 

412 # Merge runtime options 

413 merged_options = dict(self.options) 

414 merged_options.update(options) 

415 

416 # Determine working directory for framework detection 

417 # Verify the path exists before using it (paths[0] could be a glob or missing) 

418 cwd_for_detection = Path.cwd() 

419 if paths: 

420 candidate = Path(paths[0]).resolve() 

421 if candidate.exists(): 

422 if candidate.is_file(): 

423 cwd_for_detection = candidate.parent 

424 else: 

425 cwd_for_detection = candidate 

426 elif candidate.parent.exists(): 

427 cwd_for_detection = candidate.parent 

428 

429 # Check for framework-specific projects that have their own type checkers 

430 framework_info = self._detect_framework_project(cwd_for_detection) 

431 if framework_info: 

432 framework_name, recommended_tool = framework_info 

433 return ToolResult( 

434 name=self.definition.name, 

435 success=True, 

436 output=( 

437 f"SKIPPED: {framework_name} project detected.\n" 

438 f"{framework_name} has its own type checker that handles " 

439 f"framework-specific syntax.\n" 

440 f"Use '{recommended_tool}' instead: " 

441 f"lintro check . --tools {recommended_tool}" 

442 ), 

443 issues_count=0, 

444 skipped=True, 

445 skip_reason=f"deferred to {recommended_tool}", 

446 ) 

447 

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

449 ctx = self._prepare_execution( 

450 paths, 

451 merged_options, 

452 no_files_message="No TypeScript files to check.", 

453 ) 

454 

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

456 return ctx.early_result 

457 

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

459 if ctx.should_skip: 

460 return ToolResult( 

461 name=self.definition.name, 

462 success=True, 

463 output="No TypeScript files to check.", 

464 issues_count=0, 

465 ) 

466 

467 logger.debug("[tsc] Discovered {} TypeScript file(s)", len(ctx.files)) 

468 

469 # Determine project configuration strategy 

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

471 

472 # Check if dependencies need installing 

473 from lintro.utils.node_deps import install_node_deps, should_install_deps 

474 

475 try: 

476 needs_install = should_install_deps(cwd_path) 

477 except PermissionError as e: 

478 logger.warning("[tsc] {}", e) 

479 return ToolResult( 

480 name=self.definition.name, 

481 success=True, 

482 output=f"Skipping tsc: {e}", 

483 issues_count=0, 

484 skipped=True, 

485 skip_reason="directory not writable", 

486 ) 

487 

488 if needs_install: 

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

490 if auto_install: 

491 logger.info("[tsc] Auto-installing Node.js dependencies...") 

492 install_ok, install_output = install_node_deps(cwd_path) 

493 if install_ok: 

494 logger.info("[tsc] Dependencies installed successfully") 

495 else: 

496 logger.warning( 

497 "[tsc] Auto-install failed, skipping: {}", 

498 install_output, 

499 ) 

500 return ToolResult( 

501 name=self.definition.name, 

502 success=True, 

503 output=( 

504 f"Skipping tsc: auto-install failed.\n" f"{install_output}" 

505 ), 

506 issues_count=0, 

507 skipped=True, 

508 skip_reason="auto-install failed", 

509 ) 

510 else: 

511 return ToolResult( 

512 name=self.definition.name, 

513 success=True, 

514 output=( 

515 "node_modules not found. " 

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

517 ), 

518 issues_count=0, 

519 skipped=True, 

520 skip_reason="node_modules not found", 

521 ) 

522 

523 use_project_files = merged_options.get("use_project_files", False) 

524 explicit_project_opt = merged_options.get("project") 

525 explicit_project = str(explicit_project_opt) if explicit_project_opt else None 

526 temp_tsconfig: Path | None = None 

527 project_path: str | None = None 

528 

529 try: 

530 # Find existing tsconfig.json 

531 base_tsconfig = self._find_tsconfig(cwd_path) 

532 

533 if use_project_files or explicit_project: 

534 # Native mode: use tsconfig.json as-is for file selection 

535 # or explicit project path was provided 

536 project_path = explicit_project or ( 

537 str(base_tsconfig) if base_tsconfig else None 

538 ) 

539 logger.debug( 

540 "[tsc] Using native tsconfig file selection: {}", 

541 project_path, 

542 ) 

543 elif base_tsconfig: 

544 # Lintro mode: create temp tsconfig to respect file targeting 

545 # while preserving compiler options from the project's config 

546 temp_tsconfig = self._create_temp_tsconfig( 

547 base_tsconfig=base_tsconfig, 

548 files=ctx.rel_files, 

549 cwd=cwd_path, 

550 ) 

551 project_path = str(temp_tsconfig) 

552 logger.debug( 

553 "[tsc] Using temp tsconfig for file targeting: {}", 

554 project_path, 

555 ) 

556 else: 

557 # No tsconfig.json found - pass files directly 

558 project_path = None 

559 logger.debug("[tsc] No tsconfig.json found, passing files directly") 

560 

561 # Build command 

562 cmd = self._build_command( 

563 files=ctx.rel_files if not project_path else [], 

564 project_path=project_path, 

565 options=merged_options, 

566 ) 

567 logger.debug("[tsc] Running with cwd={} and cmd={}", ctx.cwd, cmd) 

568 

569 try: 

570 success, output = self._run_subprocess( 

571 cmd=cmd, 

572 timeout=ctx.timeout, 

573 cwd=ctx.cwd, 

574 ) 

575 except subprocess.TimeoutExpired: 

576 timeout_result = create_timeout_result( 

577 tool=self, 

578 timeout=ctx.timeout, 

579 cmd=cmd, 

580 ) 

581 return ToolResult( 

582 name=self.definition.name, 

583 success=timeout_result.success, 

584 output=timeout_result.output, 

585 issues_count=timeout_result.issues_count, 

586 issues=timeout_result.issues, 

587 ) 

588 except FileNotFoundError as e: 

589 return ToolResult( 

590 name=self.definition.name, 

591 success=False, 

592 output=f"TypeScript compiler not found: {e}\n\n" 

593 "Please ensure tsc is installed:\n" 

594 " - Run 'npm install -g typescript' or 'bun add -g typescript'\n" 

595 " - Or install locally: 'npm install typescript'", 

596 issues_count=0, 

597 ) 

598 except OSError as e: 

599 logger.error("[tsc] Failed to run tsc: {}", e) 

600 return ToolResult( 

601 name=self.definition.name, 

602 success=False, 

603 output="tsc execution failed: " + str(e), 

604 issues_count=0, 

605 ) 

606 

607 # Parse output (parser handles ANSI stripping internally) 

608 all_issues = parse_tsc_output(output=output or "") 

609 issues_count = len(all_issues) 

610 

611 # Normalize output for fallback substring matching below 

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

613 

614 # Categorize issues into type errors vs dependency errors 

615 type_errors, dependency_errors = categorize_tsc_issues(all_issues) 

616 

617 # If we have dependency errors, provide helpful guidance 

618 if dependency_errors: 

619 missing_modules = extract_missing_modules(dependency_errors) 

620 dep_output_lines = [ 

621 "Missing dependencies detected:", 

622 f" {len(dependency_errors)} dependency error(s)", 

623 ] 

624 if missing_modules: 

625 modules_str = ", ".join(missing_modules[:10]) 

626 if len(missing_modules) > 10: 

627 modules_str += f", ... (+{len(missing_modules) - 10} more)" 

628 dep_output_lines.append(f" Missing: {modules_str}") 

629 

630 dep_output_lines.extend( 

631 [ 

632 "", 

633 "Suggestions:", 

634 " - Run 'bun install' or 'npm install' in your project", 

635 " - Use '--auto-install' flag to auto-install dependencies", 

636 " - If using Docker, ensure node_modules is available", 

637 ], 

638 ) 

639 

640 # If there are also type errors, show both 

641 if type_errors: 

642 dep_output_lines.insert( 

643 0, 

644 f"Type errors: {len(type_errors)}", 

645 ) 

646 dep_output_lines.insert(1, "") 

647 

648 # Return all issues but with helpful output 

649 return ToolResult( 

650 name=self.definition.name, 

651 success=False, 

652 output="\n".join(dep_output_lines), 

653 issues_count=issues_count, 

654 issues=all_issues, 

655 ) 

656 

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

658 # Execution failed but no structured issues were parsed. 

659 # This can happen with malformed output or non-standard error formats. 

660 # Detect common dependency/configuration errors via substring matching 

661 # as a fallback when the parser couldn't extract structured issues. 

662 

663 # Type definition errors (usually means node_modules not installed) 

664 if ( 

665 "Cannot find type definition file" in normalized_output 

666 or "Cannot find module" in normalized_output 

667 ): 

668 helpful_output = ( 

669 f"TypeScript configuration error:\n{normalized_output}\n\n" 

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

671 "Suggestions:\n" 

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

673 " - Use '--auto-install' flag to auto-install dependencies\n" 

674 " - If using Docker, ensure node_modules is available\n" 

675 " - Use --tool-options 'tsc:skip_lib_check=true' to skip " 

676 "type checking of declaration files" 

677 ) 

678 return ToolResult( 

679 name=self.definition.name, 

680 success=False, 

681 output=helpful_output, 

682 issues_count=0, 

683 ) 

684 

685 # Generic failure 

686 return ToolResult( 

687 name=self.definition.name, 

688 success=False, 

689 output=normalized_output or "tsc execution failed.", 

690 issues_count=0, 

691 ) 

692 

693 if not success and issues_count == 0: 

694 # No output - generic failure 

695 return ToolResult( 

696 name=self.definition.name, 

697 success=False, 

698 output="tsc execution failed.", 

699 issues_count=0, 

700 ) 

701 

702 return ToolResult( 

703 name=self.definition.name, 

704 success=issues_count == 0, 

705 output=None, 

706 issues_count=issues_count, 

707 issues=all_issues, 

708 ) 

709 finally: 

710 # Clean up temp tsconfig 

711 if temp_tsconfig and temp_tsconfig.exists(): 

712 try: 

713 temp_tsconfig.unlink() 

714 logger.debug("[tsc] Cleaned up temp tsconfig: {}", temp_tsconfig) 

715 except OSError as e: 

716 logger.warning("[tsc] Failed to clean up temp tsconfig: {}", e) 

717 

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

719 """Tsc does not support auto-fixing. 

720 

721 Args: 

722 paths: Paths or files passed for completeness. 

723 options: Runtime options (unused). 

724 

725 Raises: 

726 NotImplementedError: Always, because tsc cannot fix issues. 

727 """ 

728 raise NotImplementedError( 

729 "Tsc cannot automatically fix issues. Type errors require " 

730 "manual code changes.", 

731 )