"""
Bug checker — v2, using real static analysis instead of a heuristic.
Why the old approach was wrong:
The previous version fed code into a base (non-fine-tuned) CodeT5 model and
flagged "bug" whenever its generated output differed from the input. That's
not bug detection -- an untrained generative model will almost always
produce *some* different text, so it was flagging correct code constantly.
What this version does instead:
1. Syntax check via Python's own `ast.parse` -- catches things like missing
colons, unmatched brackets, bad indentation. 100% accurate, since it's
Python's real parser, not a guess.
2. Static analysis via `pyflakes` -- catches real bugs: undefined names,
unused imports/variables, redefined functions, unreachable code, etc.
This is the same engine that powers linters in VS Code / PyCharm.
3. (Optional) The fine-tuned seq2seq model from train.py, if you've trained
one, is used ONLY to suggest a fix once a real issue has already been
found by steps 1-2 -- not to decide whether a bug exists in the first
place.
Install: pip install pyflakes
"""
import ast
import io
from pyflakes.api import check as pyflakes_check
from pyflakes.reporter import Reporter
import pycodestyle
class _ListReport(pycodestyle.BaseReport):
"""Collects pycodestyle warnings into a list instead of printing them."""
def __init__(self, options):
super().__init__(options)
self.results = []
def error(self, line_number, offset, text, check):
code = super().error(line_number, offset, text, check)
if code:
self.results.append(f"line {line_number}, col {offset + 1}: {text}")
return code
def check_style(code: str):
"""
Runs pycodestyle (PEP8) checks.
Returns a list of style warning strings, e.g.:
["line 2, col 80: E501 line too long (85 > 79 characters)"]
Empty list means no style issues found.
This is SEPARATE from real bugs -- style issues don't break the code,
they just deviate from PEP8 conventions (naming, spacing, line length).
"""
lines = code.splitlines(keepends=True)
guide = pycodestyle.StyleGuide(reporter=_ListReport)
report = guide.init_report()
checker = pycodestyle.Checker(lines=lines, options=guide.options, report=report)
try:
checker.check_all()
except Exception:
return ["Style check could not process this code."]
return report.results
def check_syntax(code: str):
"""Returns (is_valid: bool, error_message: str or None)"""
try:
ast.parse(code)
return True, None
except SyntaxError as e:
return False, f"Syntax error at line {e.lineno}: {e.msg}"
def check_static_issues(code: str):
"""
Runs pyflakes static analysis.
Returns a list of issue strings, e.g.:
["line 3: undefined name 'x'", "line 1: 'os' imported but unused"]
Empty list means no issues found.
"""
out_stream = io.StringIO()
err_stream = io.StringIO()
reporter = Reporter(out_stream, err_stream)
try:
pyflakes_check(code, filename="", reporter=reporter)
except Exception:
# pyflakes itself can't handle the code (e.g. severe syntax issues) --
# the syntax check above should catch these first, so this is a safety net.
return ["Static analysis could not process this code."]
issues = []
for stream in (out_stream, err_stream):
text = stream.getvalue().strip()
if text:
issues.extend(line.replace(":", "line ") for line in text.split("\n"))
return issues
def detect_bugs(code: str):
"""
Main entry point. Returns:
{
"has_bug": bool,
"syntax_valid": bool,
"syntax_error": str or None,
"static_issues": list[str],
"summary": str -- human-readable summary
}
"""
syntax_valid, syntax_error = check_syntax(code)
if not syntax_valid:
return {
"has_bug": True,
"syntax_valid": False,
"syntax_error": syntax_error,
"static_issues": [],
"summary": f"Syntax error found: {syntax_error}",
}
static_issues = check_static_issues(code)
has_bug = len(static_issues) > 0
if has_bug:
summary = f"{len(static_issues)} issue(s) found via static analysis."
else:
summary = "No issues found by static analysis (syntax is valid, no undefined names, no unused imports/variables detected)."
return {
"has_bug": has_bug,
"syntax_valid": True,
"syntax_error": None,
"static_issues": static_issues,
"summary": summary,
}
if __name__ == "__main__":
# Test cases
examples = [
"def add(a, b)\n return a + b", # syntax error (missing colon)
"def add(a, b):\n return a + c", # undefined name 'c'
"import os\ndef add(a, b):\n return a + b", # unused import
"def add(a, b):\n return a + b", # actually fine
]
for code in examples:
print("CODE:\n", code)
print("RESULT:", detect_bugs(code))
print("-" * 60)