|
3 | 3 | import ast |
4 | 4 | import time |
5 | 5 | from dataclasses import dataclass |
| 6 | +from importlib.util import find_spec |
6 | 7 | from itertools import chain |
7 | 8 | from pathlib import Path |
8 | 9 | from typing import TYPE_CHECKING, Optional, Union |
@@ -1172,6 +1173,198 @@ def get_fn_references_jedi( |
1172 | 1173 | return [] |
1173 | 1174 |
|
1174 | 1175 |
|
| 1176 | +has_numba = find_spec("numba") is not None |
| 1177 | + |
| 1178 | +NUMERICAL_MODULES = frozenset({"numpy", "torch", "numba", "jax", "tensorflow", "math", "scipy"}) |
| 1179 | +# Modules that require numba to be installed for optimization |
| 1180 | +NUMBA_REQUIRED_MODULES = frozenset({"numpy", "math", "scipy"}) |
| 1181 | + |
| 1182 | + |
| 1183 | +class NumericalUsageChecker(ast.NodeVisitor): |
| 1184 | + """AST visitor that checks if a function uses numerical computing libraries.""" |
| 1185 | + |
| 1186 | + def __init__(self, numerical_names: set[str]) -> None: |
| 1187 | + self.numerical_names = numerical_names |
| 1188 | + self.found_numerical = False |
| 1189 | + |
| 1190 | + def visit_Call(self, node: ast.Call) -> None: |
| 1191 | + """Check function calls for numerical library usage.""" |
| 1192 | + if self.found_numerical: |
| 1193 | + return |
| 1194 | + call_name = self._get_root_name(node.func) |
| 1195 | + if call_name and call_name in self.numerical_names: |
| 1196 | + self.found_numerical = True |
| 1197 | + return |
| 1198 | + self.generic_visit(node) |
| 1199 | + |
| 1200 | + def visit_Attribute(self, node: ast.Attribute) -> None: |
| 1201 | + """Check attribute access for numerical library usage.""" |
| 1202 | + if self.found_numerical: |
| 1203 | + return |
| 1204 | + root_name = self._get_root_name(node) |
| 1205 | + if root_name and root_name in self.numerical_names: |
| 1206 | + self.found_numerical = True |
| 1207 | + return |
| 1208 | + self.generic_visit(node) |
| 1209 | + |
| 1210 | + def visit_Name(self, node: ast.Name) -> None: |
| 1211 | + """Check name references for numerical library usage.""" |
| 1212 | + if self.found_numerical: |
| 1213 | + return |
| 1214 | + if node.id in self.numerical_names: |
| 1215 | + self.found_numerical = True |
| 1216 | + |
| 1217 | + def _get_root_name(self, node: ast.expr) -> str | None: |
| 1218 | + """Get the root name from an expression (e.g., 'np' from 'np.array').""" |
| 1219 | + if isinstance(node, ast.Name): |
| 1220 | + return node.id |
| 1221 | + if isinstance(node, ast.Attribute): |
| 1222 | + return self._get_root_name(node.value) |
| 1223 | + return None |
| 1224 | + |
| 1225 | + |
| 1226 | +def _collect_numerical_imports(tree: ast.Module) -> tuple[set[str], set[str]]: |
| 1227 | + """Collect names that reference numerical computing libraries from imports. |
| 1228 | +
|
| 1229 | + Returns: |
| 1230 | + A tuple of (numerical_names, modules_used) where: |
| 1231 | + - numerical_names: set of names/aliases that reference numerical libraries |
| 1232 | + - modules_used: set of actual module names (e.g., "numpy", "math") being imported |
| 1233 | +
|
| 1234 | + """ |
| 1235 | + numerical_names: set[str] = set() |
| 1236 | + modules_used: set[str] = set() |
| 1237 | + |
| 1238 | + for node in ast.walk(tree): |
| 1239 | + if isinstance(node, ast.Import): |
| 1240 | + for alias in node.names: |
| 1241 | + # import numpy or import numpy as np |
| 1242 | + module_root = alias.name.split(".")[0] |
| 1243 | + if module_root in NUMERICAL_MODULES: |
| 1244 | + # Use the alias if present, otherwise the module name |
| 1245 | + name = alias.asname if alias.asname else alias.name.split(".")[0] |
| 1246 | + numerical_names.add(name) |
| 1247 | + modules_used.add(module_root) |
| 1248 | + elif isinstance(node, ast.ImportFrom) and node.module: |
| 1249 | + module_root = node.module.split(".")[0] |
| 1250 | + if module_root in NUMERICAL_MODULES: |
| 1251 | + # from numpy import array, zeros as z |
| 1252 | + for alias in node.names: |
| 1253 | + if alias.name == "*": |
| 1254 | + # Can't track star imports, but mark the module as numerical |
| 1255 | + numerical_names.add(module_root) |
| 1256 | + else: |
| 1257 | + name = alias.asname if alias.asname else alias.name |
| 1258 | + numerical_names.add(name) |
| 1259 | + modules_used.add(module_root) |
| 1260 | + |
| 1261 | + return numerical_names, modules_used |
| 1262 | + |
| 1263 | + |
| 1264 | +def _find_function_node(tree: ast.Module, name_parts: list[str]) -> ast.FunctionDef | None: |
| 1265 | + """Find a function node in the AST given its qualified name parts. |
| 1266 | +
|
| 1267 | + Note: This function only finds regular (sync) functions, not async functions. |
| 1268 | +
|
| 1269 | + Args: |
| 1270 | + tree: The parsed AST module |
| 1271 | + name_parts: List of name parts, e.g., ["ClassName", "method_name"] or ["function_name"] |
| 1272 | +
|
| 1273 | + Returns: |
| 1274 | + The function node if found, None otherwise |
| 1275 | +
|
| 1276 | + """ |
| 1277 | + if not name_parts: |
| 1278 | + return None |
| 1279 | + |
| 1280 | + if len(name_parts) == 1: |
| 1281 | + # Top-level function |
| 1282 | + func_name = name_parts[0] |
| 1283 | + for node in tree.body: |
| 1284 | + if isinstance(node, ast.FunctionDef) and node.name == func_name: |
| 1285 | + return node |
| 1286 | + return None |
| 1287 | + |
| 1288 | + if len(name_parts) == 2: |
| 1289 | + # Class method: ClassName.method_name |
| 1290 | + class_name, method_name = name_parts |
| 1291 | + for node in tree.body: |
| 1292 | + if isinstance(node, ast.ClassDef) and node.name == class_name: |
| 1293 | + for class_node in node.body: |
| 1294 | + if isinstance(class_node, ast.FunctionDef) and class_node.name == method_name: |
| 1295 | + return class_node |
| 1296 | + return None |
| 1297 | + |
| 1298 | + return None |
| 1299 | + |
| 1300 | + |
| 1301 | +def is_numerical_code(code_string: str, function_name: str) -> bool: |
| 1302 | + """Check if a function uses numerical computing libraries. |
| 1303 | +
|
| 1304 | + Detects usage of numpy, torch, numba, jax, tensorflow, scipy, and math libraries |
| 1305 | + within the specified function. |
| 1306 | +
|
| 1307 | + Note: For math, numpy, and scipy usage, this function returns True only if numba |
| 1308 | + is installed in the environment, as numba is required to optimize such code. |
| 1309 | +
|
| 1310 | + Args: |
| 1311 | + code_string: The entire file's content as a string |
| 1312 | + function_name: The name of the function to check. Can be a simple name like "foo" |
| 1313 | + or a qualified name like "ClassName.method_name" for methods, |
| 1314 | + staticmethods, or classmethods. |
| 1315 | +
|
| 1316 | + Returns: |
| 1317 | + True if the function uses any numerical computing library functions, False otherwise. |
| 1318 | + Returns False for math/numpy/scipy usage if numba is not installed. |
| 1319 | +
|
| 1320 | + Examples: |
| 1321 | + >>> code = ''' |
| 1322 | + ... import numpy as np |
| 1323 | + ... def process_data(x): |
| 1324 | + ... return np.sum(x) |
| 1325 | + ... ''' |
| 1326 | + >>> is_numerical_code(code, "process_data") # Returns True only if numba is installed |
| 1327 | + True |
| 1328 | +
|
| 1329 | + >>> code = ''' |
| 1330 | + ... def simple_func(x): |
| 1331 | + ... return x + 1 |
| 1332 | + ... ''' |
| 1333 | + >>> is_numerical_code(code, "simple_func") |
| 1334 | + False |
| 1335 | +
|
| 1336 | + """ |
| 1337 | + try: |
| 1338 | + tree = ast.parse(code_string) |
| 1339 | + except SyntaxError: |
| 1340 | + return False |
| 1341 | + |
| 1342 | + # Split the function name to handle class methods |
| 1343 | + name_parts = function_name.split(".") |
| 1344 | + |
| 1345 | + # Find the target function node |
| 1346 | + target_function = _find_function_node(tree, name_parts) |
| 1347 | + if target_function is None: |
| 1348 | + return False |
| 1349 | + |
| 1350 | + # Collect names that reference numerical modules from imports |
| 1351 | + numerical_names, modules_used = _collect_numerical_imports(tree) |
| 1352 | + |
| 1353 | + # Check if the function body uses any numerical library |
| 1354 | + checker = NumericalUsageChecker(numerical_names) |
| 1355 | + checker.visit(target_function) |
| 1356 | + |
| 1357 | + if not checker.found_numerical: |
| 1358 | + return False |
| 1359 | + |
| 1360 | + # If numba is not installed and all modules used require numba for optimization, |
| 1361 | + # return False since we can't optimize this code |
| 1362 | + if not has_numba and modules_used.issubset(NUMBA_REQUIRED_MODULES): # noqa : SIM103 |
| 1363 | + return False |
| 1364 | + |
| 1365 | + return True |
| 1366 | + |
| 1367 | + |
1175 | 1368 | def get_opt_review_metrics( |
1176 | 1369 | source_code: str, file_path: Path, qualified_name: str, project_root: Path, tests_root: Path |
1177 | 1370 | ) -> str: |
|
0 commit comments