Coverage for lintro / parsers / astro_check / astro_check_parser.py: 84%

55 statements  

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

1"""Parser for astro check output.""" 

2 

3from __future__ import annotations 

4 

5import re 

6 

7from loguru import logger 

8 

9from lintro.parsers.astro_check.astro_check_issue import AstroCheckIssue 

10from lintro.parsers.base_parser import strip_ansi_codes 

11 

12# Pattern for astro check output: 

13# file.astro:line:col - severity ts1234: message 

14# Also handles tsc-style output: file.astro(line,col): error TS1234: message 

15ASTRO_ISSUE_PATTERN = re.compile( 

16 r"^(?P<file>.+?)" 

17 r"(?:" 

18 r"\((?P<line_paren>\d+),(?P<col_paren>\d+)\)" # tsc style: file(line,col) 

19 r"|" 

20 r":(?P<line_colon>\d+):(?P<col_colon>\d+)" # astro style: file:line:col 

21 r")" 

22 r"(?:\s*[-:]\s*|\s+)" 

23 r"(?P<severity>error|warning|hint)?\s*" 

24 r"(?:(?P<code>ts\d+|TS\d+)[:\s]+)?" 

25 r"(?P<message>.+)$", 

26 re.IGNORECASE, 

27) 

28 

29# Alternative pattern for simpler format: 

30# src/pages/index.astro:5:3 Type 'X' is not assignable to type 'Y'. 

31ASTRO_SIMPLE_PATTERN = re.compile( 

32 r"^(?P<file>.+?):(?P<line>\d+):(?P<col>\d+)\s+(?P<message>.+)$", 

33) 

34 

35# Astro-check stderr timestamp prefix: HH:MM:SS 

36# e.g. "15:19:56 [content] Syncing content" 

37# These must be filtered before regex matching because the HH:MM:SS format 

38# is indistinguishable from a file:line:col triplet. 

39_TIMESTAMP_PREFIX = re.compile(r"^\d{1,2}:\d{2}:\d{2}\s") 

40 

41 

42def _parse_line(line: str) -> AstroCheckIssue | None: 

43 """Parse a single astro check output line into an AstroCheckIssue. 

44 

45 Args: 

46 line: A single line of astro check output. 

47 

48 Returns: 

49 An AstroCheckIssue instance or None if the line doesn't match. 

50 """ 

51 line = line.strip() 

52 if not line: 

53 return None 

54 

55 # Skip summary lines and noise 

56 if line.startswith(("Result", "Found", "Checking", "...")): 

57 return None 

58 

59 # Skip astro-check stderr timestamp lines (HH:MM:SS prefix). 

60 # These are informational log messages like: 

61 # 15:19:56 [WARN] Missing pages directory: src/pages 

62 # 15:19:56 [content] Syncing content 

63 # Without this filter the HH:MM:SS prefix is misinterpreted as 

64 # file:line:col by the fallback ASTRO_SIMPLE_PATTERN. 

65 if _TIMESTAMP_PREFIX.match(line): 

66 return None 

67 

68 match = ASTRO_ISSUE_PATTERN.match(line) 

69 if match: 

70 try: 

71 file_path = match.group("file") 

72 # Handle both tsc-style and astro-style line/col 

73 line_paren = match.group("line_paren") 

74 line_colon = match.group("line_colon") 

75 col_paren = match.group("col_paren") 

76 col_colon = match.group("col_colon") 

77 

78 # Warn if neither line format matched (unexpected regex state) 

79 if line_paren is None and line_colon is None: 

80 logger.warning( 

81 "[astro-check] Regex matched but no line number found: %s", 

82 line, 

83 ) 

84 return None 

85 

86 line_num = int(line_paren or line_colon) 

87 column = int(col_paren or col_colon or "1") # Default col to 1 if missing 

88 

89 severity = match.group("severity") 

90 code = match.group("code") 

91 message = match.group("message").strip() 

92 

93 # Normalize Windows paths to forward slashes 

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

95 

96 # Normalize code to uppercase 

97 if code: 

98 code = code.upper() 

99 

100 return AstroCheckIssue( 

101 file=file_path, 

102 line=line_num, 

103 column=column, 

104 code=code or "", 

105 message=message, 

106 severity=severity.lower() if severity else "error", 

107 ) 

108 except (ValueError, AttributeError) as e: 

109 logger.debug( 

110 "[astro-check] Failed to parse line with main pattern: {}", 

111 e, 

112 ) 

113 

114 # Try simpler pattern as fallback 

115 match = ASTRO_SIMPLE_PATTERN.match(line) 

116 if match: 

117 try: 

118 return AstroCheckIssue( 

119 file=match.group("file").replace("\\", "/"), 

120 line=int(match.group("line")), 

121 column=int(match.group("col")), 

122 message=match.group("message").strip(), 

123 severity="error", 

124 ) 

125 except (ValueError, AttributeError) as e: 

126 logger.debug( 

127 "[astro-check] Failed to parse line with simple pattern: {}", 

128 e, 

129 ) 

130 

131 return None 

132 

133 

134def parse_astro_check_output(output: str) -> list[AstroCheckIssue]: 

135 """Parse astro check output into AstroCheckIssue objects. 

136 

137 Args: 

138 output: Raw stdout emitted by astro check. 

139 

140 Returns: 

141 A list of AstroCheckIssue instances parsed from the output. 

142 

143 Examples: 

144 >>> output = "src/pages/index.astro:10:5 - error ts2322: Type error." 

145 >>> issues = parse_astro_check_output(output) 

146 >>> len(issues) 

147 1 

148 """ 

149 if not output or not output.strip(): 

150 return [] 

151 

152 # Strip ANSI codes for consistent parsing 

153 output = strip_ansi_codes(output) 

154 

155 issues: list[AstroCheckIssue] = [] 

156 for line in output.splitlines(): 

157 parsed = _parse_line(line) 

158 if parsed: 

159 issues.append(parsed) 

160 

161 return issues