Spaces:
Sleeping
Sleeping
File size: 3,230 Bytes
baa4989 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | from __future__ import annotations
import ast
from dataclasses import dataclass
@dataclass(frozen=True)
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
|