SwarmAudit / app /agents /quality_agent.py
Pranoy Mukherjee
Add performance and quality audit agents
de6f25f
import re
from app.schemas import AgentOutput, CodeChunk, Finding, Severity
PYTHON_DEF = re.compile(r"^\s*(async\s+def|def|class)\s+([A-Za-z_][A-Za-z0-9_]*)")
PYTHON_BRANCH = re.compile(r"^\s*(if|elif|for|while|except|with)\b")
JS_FUNCTION = re.compile(r"^\s*(function\s+[A-Za-z_$][\w$]*|(?:const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(?:async\s*)?\()")
JS_BRANCH = re.compile(r"^\s*(if|else\s+if|for|while|switch|catch)\b")
TODO_COMMENT = re.compile(r"(?i)\b(TODO|FIXME|HACK)\b")
MAX_CHUNK_LINES = 300
MAX_FUNCTION_LINES = 80
MAX_BRANCHES_PER_CHUNK = 25
MIN_MEANINGFUL_NAME_LENGTH = 3
class QualityAgent:
name = "Quality Agent"
async def analyze(self, chunks: list[CodeChunk]) -> AgentOutput:
findings: list[Finding] = []
for chunk in chunks:
findings.extend(self._scan_chunk(chunk))
return AgentOutput(
agent_name=self.name,
findings=findings,
metadata={"chunks_scanned": len(chunks), "mode": "static-rules"},
)
def _scan_chunk(self, chunk: CodeChunk) -> list[Finding]:
findings: list[Finding] = []
lines = chunk.content.splitlines()
findings.extend(self._check_large_chunk(chunk, lines))
findings.extend(self._check_long_functions(chunk, lines))
findings.extend(self._check_branch_density(chunk, lines))
findings.extend(self._check_todo_comments(chunk, lines))
findings.extend(self._check_short_names(chunk, lines))
return findings
def _check_large_chunk(self, chunk: CodeChunk, lines: list[str]) -> list[Finding]:
if len(lines) <= MAX_CHUNK_LINES:
return []
return [
self._finding(
"Large source file section",
Severity.low,
chunk,
chunk.line_start,
chunk.line_end,
"This source section is large enough to make review, testing, and future changes harder.",
"Split unrelated responsibilities into smaller modules or focused helper functions.",
)
]
def _check_long_functions(self, chunk: CodeChunk, lines: list[str]) -> list[Finding]:
findings: list[Finding] = []
active_start: int | None = None
active_name: str | None = None
active_indent = 0
for index, line in enumerate(lines):
if not line.strip():
continue
match = self._definition_match(line)
indent = self._indent_width(line)
if active_start is not None and indent <= active_indent and match:
findings.extend(self._long_function_finding(chunk, active_name, active_start, chunk.line_start + index - 1))
active_start = None
active_name = None
if match:
active_start = chunk.line_start + index
active_name = match.group(2) if match.lastindex and match.lastindex >= 2 else "function"
active_indent = indent
if active_start is not None:
findings.extend(self._long_function_finding(chunk, active_name, active_start, chunk.line_end))
return findings
def _check_branch_density(self, chunk: CodeChunk, lines: list[str]) -> list[Finding]:
branch_count = sum(1 for line in lines if PYTHON_BRANCH.match(line) or JS_BRANCH.match(line))
if branch_count <= MAX_BRANCHES_PER_CHUNK:
return []
return [
self._finding(
"High branching complexity",
Severity.medium,
chunk,
chunk.line_start,
chunk.line_end,
f"This code section contains {branch_count} control-flow branches.",
"Extract decision-heavy logic into named helpers and add focused tests around each path.",
)
]
def _check_todo_comments(self, chunk: CodeChunk, lines: list[str]) -> list[Finding]:
findings: list[Finding] = []
for offset, line in enumerate(lines):
if TODO_COMMENT.search(line):
line_number = chunk.line_start + offset
findings.append(
self._finding(
"Unresolved maintenance comment",
Severity.low,
chunk,
line_number,
line_number,
"A TODO/FIXME/HACK marker indicates known unfinished or fragile code.",
"Convert the comment into a tracked issue or resolve it before shipping.",
)
)
return findings
def _check_short_names(self, chunk: CodeChunk, lines: list[str]) -> list[Finding]:
findings: list[Finding] = []
for offset, line in enumerate(lines):
match = self._definition_match(line)
if not match:
continue
name = match.group(2) if match.lastindex and match.lastindex >= 2 else ""
if len(name) < MIN_MEANINGFUL_NAME_LENGTH and name not in {"id"}:
line_number = chunk.line_start + offset
findings.append(
self._finding(
"Very short symbol name",
Severity.low,
chunk,
line_number,
line_number,
f"The symbol `{name}` is short enough to make intent unclear.",
"Use a descriptive function, class, or variable name that explains the role of this code.",
)
)
return findings
def _long_function_finding(
self,
chunk: CodeChunk,
name: str | None,
line_start: int,
line_end: int,
) -> list[Finding]:
if line_end - line_start + 1 <= MAX_FUNCTION_LINES:
return []
return [
self._finding(
"Long function or class body",
Severity.medium,
chunk,
line_start,
line_end,
f"`{name or 'This symbol'}` spans more than {MAX_FUNCTION_LINES} lines.",
"Extract cohesive helper functions and keep each function centered on one responsibility.",
)
]
def _definition_match(self, line: str) -> re.Match[str] | None:
return PYTHON_DEF.match(line) or JS_FUNCTION.match(line)
def _indent_width(self, line: str) -> int:
return len(line) - len(line.lstrip(" "))
def _finding(
self,
title: str,
severity: Severity,
chunk: CodeChunk,
line_start: int,
line_end: int,
description: str,
suggested_fix: str,
) -> Finding:
return Finding(
title=title,
severity=severity,
file_path=chunk.file_path,
line_start=line_start,
line_end=line_end,
description=description,
why_it_matters="Maintainable code is easier to review, test, debug, and safely extend during a hackathon demo.",
suggested_fix=suggested_fix,
agent_source=self.name,
)