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

1"""Parser for tsc (TypeScript Compiler) text output.""" 

2 

3from __future__ import annotations 

4 

5import re 

6 

7from loguru import logger 

8 

9from lintro.parsers.base_parser import strip_ansi_codes 

10from lintro.parsers.tsc.tsc_issue import TscIssue 

11 

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) 

23 

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) 

33 

34 

35def _parse_line(line: str) -> TscIssue | None: 

36 """Parse a single tsc output line into a TscIssue. 

37 

38 Args: 

39 line: A single line of tsc output. 

40 

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 

47 

48 match = TSC_ISSUE_PATTERN.match(line) 

49 if not match: 

50 return None 

51 

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

59 

60 # Normalize Windows paths to forward slashes 

61 file_path = file_path.replace("\\", "/") 

62 

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 

74 

75 

76def parse_tsc_output(output: str) -> list[TscIssue]: 

77 """Parse tsc text output into TscIssue objects. 

78 

79 Args: 

80 output: Raw stdout emitted by tsc with --pretty false. 

81 

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. 

85 

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

96 

97 # Strip ANSI codes for consistent parsing across environments 

98 output = strip_ansi_codes(output) 

99 

100 issues: list[TscIssue] = [] 

101 for line in output.splitlines(): 

102 parsed = _parse_line(line) 

103 if parsed: 

104 issues.append(parsed) 

105 

106 return issues 

107 

108 

109def categorize_tsc_issues( 

110 issues: list[TscIssue], 

111) -> tuple[list[TscIssue], list[TscIssue]]: 

112 """Categorize tsc issues into type errors and dependency errors. 

113 

114 Separates actual type errors from errors caused by missing dependencies 

115 (e.g., when node_modules is not installed). 

116 

117 Args: 

118 issues: List of TscIssue objects to categorize. 

119 

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 

124 

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

144 

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) 

150 

151 return type_errors, dependency_errors 

152 

153 

154def extract_missing_modules(dependency_errors: list[TscIssue]) -> list[str]: 

155 """Extract module names from dependency error messages. 

156 

157 Parses the error messages to extract the names of missing modules 

158 for clearer user feedback. 

159 

160 Args: 

161 dependency_errors: List of TscIssue objects with dependency errors. 

162 

163 Returns: 

164 List of unique module names that are missing. 

165 

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

178 

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 ['\"]([^'\"]+)['\"]") 

186 

187 for error in dependency_errors: 

188 message = error.message or "" 

189 

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 

195 

196 return sorted(modules)