Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import ast | |
| from dataclasses import dataclass | |
| class SemanticIssue: | |
| line: int | |
| severity: str | |
| message: str | |
| stage: str | |
| def _build_undirected_graph(tree: ast.AST) -> dict[str, set[str]]: | |
| adjacency: dict[str, set[str]] = {} | |
| def ensure(node: str) -> None: | |
| adjacency.setdefault(node, set()) | |
| for node in ast.walk(tree): | |
| if not isinstance(node, ast.Assign) or len(node.targets) != 1: | |
| continue | |
| target = node.targets[0] | |
| if not isinstance(target, ast.Name): | |
| continue | |
| var_name = target.id | |
| value = node.value | |
| if not isinstance(value, ast.Call): | |
| continue | |
| if not isinstance(value.func, ast.Name) or value.func.id != "Node": | |
| continue | |
| ensure(var_name) | |
| if len(value.args) >= 3 and isinstance(value.args[2], ast.List): | |
| for item in value.args[2].elts: | |
| if isinstance(item, ast.Name): | |
| ensure(item.id) | |
| adjacency[var_name].add(item.id) | |
| adjacency[item.id].add(var_name) | |
| return adjacency | |
| def _connected(adjacency: dict[str, set[str]], left: str, right: str) -> bool: | |
| if left == right: | |
| return True | |
| if left not in adjacency or right not in adjacency: | |
| return False | |
| seen = {left} | |
| stack = [left] | |
| while stack: | |
| current = stack.pop() | |
| for neighbor in adjacency.get(current, set()): | |
| if neighbor in seen: | |
| continue | |
| if neighbor == right: | |
| return True | |
| seen.add(neighbor) | |
| stack.append(neighbor) | |
| return False | |
| def detect_semantic_issues(raw_code: str) -> list[SemanticIssue]: | |
| try: | |
| tree = ast.parse(raw_code) | |
| except SyntaxError: | |
| return [] | |
| lines = raw_code.splitlines() | |
| adjacency = _build_undirected_graph(tree) | |
| issues: list[SemanticIssue] = [] | |
| for node in ast.walk(tree): | |
| if not isinstance(node, ast.If): | |
| continue | |
| test = node.test | |
| if not isinstance(test, ast.Call): | |
| continue | |
| if not isinstance(test.func, ast.Name) or test.func.id != "breadth_first_search": | |
| continue | |
| if len(test.args) < 2: | |
| continue | |
| if not isinstance(test.args[0], ast.Name) or not isinstance(test.args[1], ast.Name): | |
| continue | |
| src = test.args[0].id | |
| dst = test.args[1].id | |
| line_no = int(getattr(node, "lineno", 1)) | |
| context_start = max(0, line_no - 4) | |
| context = "\n".join(lines[context_start:line_no]).lower() | |
| if "unconnected" in context and _connected(adjacency, src, dst): | |
| issues.append( | |
| SemanticIssue( | |
| line=line_no, | |
| severity="medium", | |
| stage="medium", | |
| message=( | |
| f"Comment claims unconnected nodes, but '{src}' and '{dst}' belong to the same undirected" | |
| " component. Test intent is misleading and can hide logical regressions." | |
| ), | |
| ) | |
| ) | |
| return issues | |