- parsers/ package: rust / python / go / typescript / generic
- parser registry with language+recipe -> fallback resolution
- fingerprint hash (kind+file+line+code) for cross-run dedup
- runner.py post-exec hook: parse log, persist findings, count on job row
(extraction runs before mark_job_finished so callers polling on terminal
status see findings_count populated atomically)
- db.insert_finding / list_findings / increment_findings_count DAOs already
shipped in wave 1; wired here
- GET /jobs/{id}/findings now returns real data (server route already
shipped; was returning empty list because nothing populated the table)
- tests/test_parsers/: 6 modules + 11 fixtures (rust/python/go/typescript)
- tests/test_runner_findings.py: 3 integration tests
- README: tick steps 2-6, add Findings section
Suite: 108 passing (62 wave-1 + 46 new).
Spec: memory/spec-crafting-table.md
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
"""Per-language parsers — turn tool output into structured Finding rows.
|
|
|
|
Wave 2A (steps 5+6 of the spec). The runner calls `find_parser(language,
|
|
recipe)` after a job's subprocess exits, hands the parser the captured log +
|
|
exit code + recipe, and gets back a list of Finding dataclasses to persist.
|
|
|
|
Resolution order in `find_parser`:
|
|
1. exact match `language:recipe` (e.g. ``rust:audit``)
|
|
2. language-only match (e.g. ``rust`` for any rust recipe)
|
|
3. `GenericParser` fallback — emits one ``recipe_fail`` finding when the
|
|
recipe exited non-zero, otherwise empty.
|
|
|
|
Each parser is best-effort: if its expected JSON shape doesn't parse it
|
|
should fall back to "no findings, don't crash" rather than raising. The
|
|
runner wraps the call in a try/except as a belt-and-braces second line.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from .base import Finding, Parser, fingerprint
|
|
from .generic import GenericParser
|
|
from .go import GoParser
|
|
from .python import PythonParser
|
|
from .rust import RustParser
|
|
from .typescript import TypeScriptParser
|
|
|
|
|
|
# Order matters only for the language-only fallback step inside
|
|
# find_parser — exact-match keys are fully qualified so collisions are
|
|
# impossible there.
|
|
PARSERS: dict[str, type[Parser]] = {
|
|
"rust": RustParser,
|
|
"python": PythonParser,
|
|
"go": GoParser,
|
|
"typescript": TypeScriptParser,
|
|
# JS uses the same eslint/tsc machinery as ts.
|
|
"javascript": TypeScriptParser,
|
|
}
|
|
|
|
|
|
def find_parser(language: str, recipe: str) -> type[Parser]:
|
|
"""Pick the most specific parser for (language, recipe).
|
|
|
|
The Parser protocol exposes a `matches` classmethod so a single parser
|
|
class can self-gate on (language, recipe) — useful when one class
|
|
handles multiple recipes (e.g. RustParser handles audit / lint / test).
|
|
The resolution loop here is a thin wrapper around that.
|
|
"""
|
|
language = (language or "").strip().lower()
|
|
recipe = (recipe or "").strip().lower()
|
|
|
|
# Step 1+2: ask each registered parser if it claims this combo.
|
|
candidate = PARSERS.get(language)
|
|
if candidate is not None and candidate.matches(language, recipe):
|
|
return candidate
|
|
|
|
# Step 3: generic fallback. Always claims everything.
|
|
return GenericParser
|
|
|
|
|
|
__all__ = [
|
|
"Finding",
|
|
"Parser",
|
|
"fingerprint",
|
|
"find_parser",
|
|
"PARSERS",
|
|
"GenericParser",
|
|
"RustParser",
|
|
"PythonParser",
|
|
"GoParser",
|
|
"TypeScriptParser",
|
|
]
|