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
« 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
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
17from .dump import dump # noqa: F401
18from .run_cmd import run_cmd_subprocess # noqa: F401
20# tree_sitter is throwing a FutureWarning
21warnings.simplefilter("ignore", category=FutureWarning)
24class Linter:
25 def __init__(self, encoding="utf-8", root=None):
26 self.encoding = encoding
27 self.root = root
29 self.languages = dict(
30 python=self.py_lint,
31 )
32 self.all_lint_cmd = None
34 def set_linter(self, lang, cmd):
35 if lang:
36 self.languages[lang] = cmd
37 return
39 self.all_lint_cmd = cmd
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
50 def run_cmd(self, cmd, rel_fname, code):
51 cmd += " " + oslex.quote(rel_fname)
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
67 res = f"## Running: {cmd}\n\n"
68 res += errors
70 return self.errors_to_lint_result(rel_fname, res)
72 def errors_to_lint_result(self, rel_fname, errors):
73 if not errors:
74 return
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]
82 return LintResult(text=errors, lines=linenums)
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
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)
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)
110 if not lintres:
111 return
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)
118 return res
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)
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)
135 if text or lines:
136 return LintResult(text, lines)
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 ]
150 text = f"## Running: {' '.join(flake8_cmd)}\n\n"
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)}"
172 if not errors:
173 return
175 text += errors
176 return self.errors_to_lint_result(rel_fname, text)
179@dataclass
180class LintResult:
181 text: str
182 lines: list
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))
193 tb_lines = traceback.format_exception(type(err), err, err.__traceback__)
194 last_file_i = 0
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
203 tb_lines = tb_lines[:1] + tb_lines[last_file_i + 1 :]
205 res = "".join(tb_lines)
206 return LintResult(text=res, lines=line_numbers)
209def basic_lint(fname, code):
210 """
211 Use tree-sitter to look for syntax errors, display them with tree context.
212 """
214 lang = filename_to_lang(fname)
215 if not lang:
216 return
218 # Tree-sitter linter is not capable of working with typescript #1132
219 if lang == "typescript":
220 return
222 try:
223 parser = get_parser(lang)
224 except Exception as err:
225 print(f"Unable to load parser: {err}")
226 return
228 tree = parser.parse(bytes(code, "utf-8"))
230 try:
231 errors = traverse_tree(tree.root_node)
232 except RecursionError:
233 print(f"Unable to lint {fname} due to RecursionError")
234 return
236 if not errors:
237 return
239 return LintResult(text="", lines=errors)
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()
264 return output
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)
274 for child in node.children:
275 errors += traverse_tree(child)
277 return errors
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
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)
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)
311if __name__ == "__main__":
312 main()