Coverage for integrations / coding_agent / aider_core / linter.py: 0.0%

178 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1import os 

2import re 

3import subprocess 

4import sys 

5import traceback 

6import warnings 

7from dataclasses import dataclass 

8from pathlib import Path 

9 

10try: 

11 import oslex 

12except ImportError: 

13 import shlex as oslex 

14from grep_ast import TreeContext, filename_to_lang 

15from grep_ast.tsl import get_parser # noqa: E402 

16 

17from .dump import dump # noqa: F401 

18from .run_cmd import run_cmd_subprocess # noqa: F401 

19 

20# tree_sitter is throwing a FutureWarning 

21warnings.simplefilter("ignore", category=FutureWarning) 

22 

23 

24class Linter: 

25 def __init__(self, encoding="utf-8", root=None): 

26 self.encoding = encoding 

27 self.root = root 

28 

29 self.languages = dict( 

30 python=self.py_lint, 

31 ) 

32 self.all_lint_cmd = None 

33 

34 def set_linter(self, lang, cmd): 

35 if lang: 

36 self.languages[lang] = cmd 

37 return 

38 

39 self.all_lint_cmd = cmd 

40 

41 def get_rel_fname(self, fname): 

42 if self.root: 

43 try: 

44 return os.path.relpath(fname, self.root) 

45 except ValueError: 

46 return fname 

47 else: 

48 return fname 

49 

50 def run_cmd(self, cmd, rel_fname, code): 

51 cmd += " " + oslex.quote(rel_fname) 

52 

53 returncode = 0 

54 stdout = "" 

55 try: 

56 returncode, stdout = run_cmd_subprocess( 

57 cmd, 

58 cwd=self.root, 

59 ) 

60 except OSError as err: 

61 print(f"Unable to execute lint command: {err}") 

62 return 

63 errors = stdout 

64 if returncode == 0: 

65 return # zero exit status 

66 

67 res = f"## Running: {cmd}\n\n" 

68 res += errors 

69 

70 return self.errors_to_lint_result(rel_fname, res) 

71 

72 def errors_to_lint_result(self, rel_fname, errors): 

73 if not errors: 

74 return 

75 

76 linenums = [] 

77 filenames_linenums = find_filenames_and_linenums(errors, [rel_fname]) 

78 if filenames_linenums: 

79 filename, linenums = next(iter(filenames_linenums.items())) 

80 linenums = [num - 1 for num in linenums] 

81 

82 return LintResult(text=errors, lines=linenums) 

83 

84 def lint(self, fname, cmd=None): 

85 rel_fname = self.get_rel_fname(fname) 

86 try: 

87 code = Path(fname).read_text(encoding=self.encoding, errors="replace") 

88 except OSError as err: 

89 print(f"Unable to read {fname}: {err}") 

90 return 

91 

92 if cmd: 

93 cmd = cmd.strip() 

94 if not cmd: 

95 lang = filename_to_lang(fname) 

96 if not lang: 

97 return 

98 if self.all_lint_cmd: 

99 cmd = self.all_lint_cmd 

100 else: 

101 cmd = self.languages.get(lang) 

102 

103 if callable(cmd): 

104 lintres = cmd(fname, rel_fname, code) 

105 elif cmd: 

106 lintres = self.run_cmd(cmd, rel_fname, code) 

107 else: 

108 lintres = basic_lint(rel_fname, code) 

109 

110 if not lintres: 

111 return 

112 

113 res = "# Fix any errors below, if possible.\n\n" 

114 res += lintres.text 

115 res += "\n" 

116 res += tree_context(rel_fname, code, lintres.lines) 

117 

118 return res 

119 

120 def py_lint(self, fname, rel_fname, code): 

121 basic_res = basic_lint(rel_fname, code) 

122 compile_res = lint_python_compile(fname, code) 

123 flake_res = self.flake8_lint(rel_fname) 

124 

125 text = "" 

126 lines = set() 

127 for res in [basic_res, compile_res, flake_res]: 

128 if not res: 

129 continue 

130 if text: 

131 text += "\n" 

132 text += res.text 

133 lines.update(res.lines) 

134 

135 if text or lines: 

136 return LintResult(text, lines) 

137 

138 def flake8_lint(self, rel_fname): 

139 fatal = "E9,F821,F823,F831,F406,F407,F701,F702,F704,F706" 

140 flake8_cmd = [ 

141 sys.executable, 

142 "-m", 

143 "flake8", 

144 f"--select={fatal}", 

145 "--show-source", 

146 "--isolated", 

147 rel_fname, 

148 ] 

149 

150 text = f"## Running: {' '.join(flake8_cmd)}\n\n" 

151 

152 try: 

153 try: 

154 from core.subprocess_safe import hidden_popen_kwargs 

155 _hide = hidden_popen_kwargs() 

156 except Exception: 

157 _hide = {} 

158 result = subprocess.run( 

159 flake8_cmd, 

160 capture_output=True, 

161 text=True, 

162 check=False, 

163 encoding=self.encoding, 

164 errors="replace", 

165 cwd=self.root, 

166 **_hide, 

167 ) 

168 errors = result.stdout + result.stderr 

169 except Exception as e: 

170 errors = f"Error running flake8: {str(e)}" 

171 

172 if not errors: 

173 return 

174 

175 text += errors 

176 return self.errors_to_lint_result(rel_fname, text) 

177 

178 

179@dataclass 

180class LintResult: 

181 text: str 

182 lines: list 

183 

184 

185def lint_python_compile(fname, code): 

186 try: 

187 compile(code, fname, "exec") # USE TRACEBACK BELOW HERE 

188 return 

189 except Exception as err: 

190 end_lineno = getattr(err, "end_lineno", err.lineno) 

191 line_numbers = list(range(err.lineno - 1, end_lineno)) 

192 

193 tb_lines = traceback.format_exception(type(err), err, err.__traceback__) 

194 last_file_i = 0 

195 

196 target = "# USE TRACEBACK" 

197 target += " BELOW HERE" 

198 for i in range(len(tb_lines)): 

199 if target in tb_lines[i]: 

200 last_file_i = i 

201 break 

202 

203 tb_lines = tb_lines[:1] + tb_lines[last_file_i + 1 :] 

204 

205 res = "".join(tb_lines) 

206 return LintResult(text=res, lines=line_numbers) 

207 

208 

209def basic_lint(fname, code): 

210 """ 

211 Use tree-sitter to look for syntax errors, display them with tree context. 

212 """ 

213 

214 lang = filename_to_lang(fname) 

215 if not lang: 

216 return 

217 

218 # Tree-sitter linter is not capable of working with typescript #1132 

219 if lang == "typescript": 

220 return 

221 

222 try: 

223 parser = get_parser(lang) 

224 except Exception as err: 

225 print(f"Unable to load parser: {err}") 

226 return 

227 

228 tree = parser.parse(bytes(code, "utf-8")) 

229 

230 try: 

231 errors = traverse_tree(tree.root_node) 

232 except RecursionError: 

233 print(f"Unable to lint {fname} due to RecursionError") 

234 return 

235 

236 if not errors: 

237 return 

238 

239 return LintResult(text="", lines=errors) 

240 

241 

242def tree_context(fname, code, line_nums): 

243 context = TreeContext( 

244 fname, 

245 code, 

246 color=False, 

247 line_number=True, 

248 child_context=False, 

249 last_line=False, 

250 margin=0, 

251 mark_lois=True, 

252 loi_pad=3, 

253 # header_max=30, 

254 show_top_of_file_parent_scope=False, 

255 ) 

256 line_nums = set(line_nums) 

257 context.add_lines_of_interest(line_nums) 

258 context.add_context() 

259 s = "s" if len(line_nums) > 1 else "" 

260 output = f"## See relevant line{s} below marked with █.\n\n" 

261 output += fname + ":\n" 

262 output += context.format() 

263 

264 return output 

265 

266 

267# Traverse the tree to find errors 

268def traverse_tree(node): 

269 errors = [] 

270 if node.type == "ERROR" or node.is_missing: 

271 line_no = node.start_point[0] 

272 errors.append(line_no) 

273 

274 for child in node.children: 

275 errors += traverse_tree(child) 

276 

277 return errors 

278 

279 

280def find_filenames_and_linenums(text, fnames): 

281 """ 

282 Search text for all occurrences of <filename>:\\d+ and make a list of them 

283 where <filename> is one of the filenames in the list `fnames`. 

284 """ 

285 pattern = re.compile(r"(\b(?:" + "|".join(re.escape(fname) for fname in fnames) + r"):\d+\b)") 

286 matches = pattern.findall(text) 

287 result = {} 

288 for match in matches: 

289 fname, linenum = match.rsplit(":", 1) 

290 if fname not in result: 

291 result[fname] = set() 

292 result[fname].add(int(linenum)) 

293 return result 

294 

295 

296def main(): 

297 """ 

298 Main function to parse files provided as command line arguments. 

299 """ 

300 if len(sys.argv) < 2: 

301 print("Usage: python linter.py <file1> <file2> ...") 

302 sys.exit(1) 

303 

304 linter = Linter(root=os.getcwd()) 

305 for file_path in sys.argv[1:]: 

306 errors = linter.lint(file_path) 

307 if errors: 

308 print(errors) 

309 

310 

311if __name__ == "__main__": 

312 main()