nodeaudit-openenv / code-review-env /parser /semantic_checks.py
shreyas-joshi's picture
Add GraphReview report generation and semantic checks
baa4989
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