Coverage for lintro / parsers / tsc / tsc_parser.py: 93%
57 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 tsc (TypeScript Compiler) text output."""
3from __future__ import annotations
5import re
7from loguru import logger
9from lintro.parsers.base_parser import strip_ansi_codes
10from lintro.parsers.tsc.tsc_issue import TscIssue
12# Error codes that indicate missing dependencies rather than actual type errors
13# These occur when node_modules is missing or dependencies aren't installed
14# Note: TS2792 is intentionally excluded from DEPENDENCY_ERROR_CODES because it
15# indicates module resolution/configuration problems rather than missing deps
16DEPENDENCY_ERROR_CODES: frozenset[str] = frozenset(
17 {
18 "TS2307", # Cannot find module 'X' or its corresponding type declarations
19 "TS2688", # Cannot find type definition file for 'X'
20 "TS7016", # Could not find a declaration file for module 'X'
21 },
22)
24# Pattern for tsc output with --pretty false:
25# file.ts(line,col): error TS1234: message
26# file.ts(line,col): warning TS1234: message
27# Also handles Windows paths with backslashes
28TSC_ISSUE_PATTERN = re.compile(
29 r"^(?P<file>.+?)\((?P<line>\d+),(?P<column>\d+)\):\s*"
30 r"(?P<severity>error|warning)\s+(?P<code>TS\d+):\s*"
31 r"(?P<message>.+)$",
32)
35def _parse_line(line: str) -> TscIssue | None:
36 """Parse a single tsc output line into a TscIssue.
38 Args:
39 line: A single line of tsc output.
41 Returns:
42 A TscIssue instance or None if the line doesn't match the expected format.
43 """
44 line = line.strip()
45 if not line:
46 return None
48 match = TSC_ISSUE_PATTERN.match(line)
49 if not match:
50 return None
52 try:
53 file_path = match.group("file")
54 line_num = int(match.group("line"))
55 column = int(match.group("column"))
56 severity = match.group("severity")
57 code = match.group("code")
58 message = match.group("message").strip()
60 # Normalize Windows paths to forward slashes
61 file_path = file_path.replace("\\", "/")
63 return TscIssue(
64 file=file_path,
65 line=line_num,
66 column=column,
67 code=code,
68 message=message,
69 severity=severity,
70 )
71 except (ValueError, AttributeError) as e:
72 logger.debug(f"Failed to parse tsc line: {e}")
73 return None
76def parse_tsc_output(output: str) -> list[TscIssue]:
77 """Parse tsc text output into TscIssue objects.
79 Args:
80 output: Raw stdout emitted by tsc with --pretty false.
82 Returns:
83 A list of TscIssue instances parsed from the output. Returns an
84 empty list when no issues are present or the output cannot be decoded.
86 Examples:
87 >>> output = "src/main.ts(10,5): error TS2322: Type error."
88 >>> issues = parse_tsc_output(output)
89 >>> len(issues)
90 1
91 >>> issues[0].code
92 'TS2322'
93 """
94 if not output or not output.strip():
95 return []
97 # Strip ANSI codes for consistent parsing across environments
98 output = strip_ansi_codes(output)
100 issues: list[TscIssue] = []
101 for line in output.splitlines():
102 parsed = _parse_line(line)
103 if parsed:
104 issues.append(parsed)
106 return issues
109def categorize_tsc_issues(
110 issues: list[TscIssue],
111) -> tuple[list[TscIssue], list[TscIssue]]:
112 """Categorize tsc issues into type errors and dependency errors.
114 Separates actual type errors from errors caused by missing dependencies
115 (e.g., when node_modules is not installed).
117 Args:
118 issues: List of TscIssue objects to categorize.
120 Returns:
121 A tuple of (type_errors, dependency_errors) where:
122 - type_errors: Issues that are actual type errors in the code
123 - dependency_errors: Issues caused by missing modules/dependencies
125 Examples:
126 >>> issues = [
127 ... TscIssue(
128 ... file="a.ts", line=1, column=1,
129 ... code="TS2322", message="Type error",
130 ... ),
131 ... TscIssue(
132 ... file="b.ts", line=1, column=1,
133 ... code="TS2307", message="Cannot find module",
134 ... ),
135 ... ]
136 >>> type_errors, dep_errors = categorize_tsc_issues(issues)
137 >>> len(type_errors)
138 1
139 >>> len(dep_errors)
140 1
141 """
142 type_errors: list[TscIssue] = []
143 dependency_errors: list[TscIssue] = []
145 for issue in issues:
146 if issue.code and issue.code in DEPENDENCY_ERROR_CODES:
147 dependency_errors.append(issue)
148 else:
149 type_errors.append(issue)
151 return type_errors, dependency_errors
154def extract_missing_modules(dependency_errors: list[TscIssue]) -> list[str]:
155 """Extract module names from dependency error messages.
157 Parses the error messages to extract the names of missing modules
158 for clearer user feedback.
160 Args:
161 dependency_errors: List of TscIssue objects with dependency errors.
163 Returns:
164 List of unique module names that are missing.
166 Examples:
167 >>> from lintro.parsers.tsc.tsc_issue import TscIssue
168 >>> errors = [
169 ... TscIssue(
170 ... file="a.ts", line=1, column=1, code="TS2307",
171 ... message="Cannot find module 'react'.",
172 ... ),
173 ... ]
174 >>> extract_missing_modules(errors)
175 ['react']
176 """
177 modules: set[str] = set()
179 # Pattern to extract module name from common tsc error messages
180 # Matches: "Cannot find module 'X'" or "Cannot find module \"X\""
181 module_pattern = re.compile(r"Cannot find module ['\"]([^'\"]+)['\"]")
182 # Matches: "type definition file for 'X'"
183 typedef_pattern = re.compile(r"type definition file for ['\"]([^'\"]+)['\"]")
184 # Matches: "declaration file for module 'X'"
185 decl_pattern = re.compile(r"declaration file for module ['\"]([^'\"]+)['\"]")
187 for error in dependency_errors:
188 message = error.message or ""
190 for pattern in (module_pattern, typedef_pattern, decl_pattern):
191 match = pattern.search(message)
192 if match:
193 modules.add(match.group(1))
194 break
196 return sorted(modules)