This is the execution checklist for adding Rust and TypeScript analyzer support to diffguard, sized so a single diffguard run on a mixed-language repo reports both languages side by side.
For the deep technical decisions (interface shapes, tree-sitter vs. runtime parsers, mutation isolation strategy, per-language parser notes), see ../MULTI_LANGUAGE_SUPPORT.md. This checklist references that doc rather than duplicating it.
- In scope: Rust, TypeScript (including
.tsx). All five analyzers (complexity, sizes, deps, churn, mutation). Multi-language single-invocation support. - Out of scope: Java, Python, plain JavaScript-only (JS works incidentally under the TS grammar but the TS path is the supported one). A
--test-commandoverride flag (add only if a fixture needs it). - Left alone: Go keeps
go/ast. Only its packaging moves — the parser does not.
- [F] foundation work (blocks both languages)
- [O] orchestration (the "simultaneous" piece)
- [R] Rust analyzer
- [T] TypeScript analyzer
- [X] cross-cutting (docs, CI, evals)
- [EVAL] correctness-evidence work (proves diffguard catches real issues)
Parts R and T are disjoint and can be worked in parallel once F and O land.
Repo reorganization so Go becomes one of several registered languages. Every step leaves go test ./... green.
- Add
github.com/smacker/go-tree-sitter(and sub-packages forrust,typescript,tsx) togo.mod. - Create
internal/lang/lang.gowith the 9 sub-interfaces (FileFilter,FunctionExtractor,ComplexityCalculator,ComplexityScorer,ImportResolver,MutantGenerator,MutantApplier,AnnotationScanner,TestRunner) and the top-levelLanguageinterface — shapes fromMULTI_LANGUAGE_SUPPORT.md§Interface Definitions. - Create
internal/lang/registry.gowithRegister(Language),Get(name string), andAll(). - Create
internal/lang/detect.go. Detection rules fromMULTI_LANGUAGE_SUPPORT.md§Language detection. Return order must be deterministic (sorted by name) so downstream report ordering is stable. - Unit tests for registry (register/get/all, duplicate registration is an error) and detection (each manifest file → correct language, multi-language repos return multiple, empty repo returns empty).
- Create
internal/lang/goanalyzer/package. - Move the three duplicate
funcNamehelpers (sizes.go,complexity.go,churn.go) intointernal/lang/goanalyzer/parse.goas a single helper. - Implement each of the 9 interfaces in
goanalyzer/(one file per concern; filenames fromMULTI_LANGUAGE_SUPPORT.md§Resulting directory structure). -
goanalyzer/goanalyzer.goexposes aLanguagestruct and aninit()that callslang.Register(&Language{}). - Blank-import
_ "github.com/0xPolygon/diffguard/internal/lang/goanalyzer"incmd/diffguard/main.go.
- Replace
isAnalyzableGoFile(internal/diff/diff.go:175-177) with aFileFilterparameter. - Replace hardcoded
--'*.go'arg (internal/diff/diff.go:92) with globs fromFileFilter.DiffGlobs. - Replace the
+++handler's.go/_test.gocheck (internal/diff/diff.go:201-208) withFileFilter.IsTestFile+ extension check. - Update
Parse()andCollectPaths()signatures; callers incmd/diffguard/main.gopass the appropriate filter. - Keep
parseUnifiedDiffandparseHunkHeaderuntouched — they're already language-agnostic.
-
internal/complexity/complexity.go: take alang.ComplexityCalculatorparameter, delete the embedded AST walk, callcalc.AnalyzeFile(...)instead. -
internal/sizes/sizes.go: take alang.FunctionExtractor; delegate. -
internal/churn/churn.go: take alang.ComplexityScorer; delete the simplifiedcomputeComplexityduplicate; keepgit log --oneline --followcounting (language-agnostic). -
internal/deps/: split intograph.go(pure graph math — cycles, afferent/efferent coupling, instability, SDP) anddeps.go(orchestration takinglang.ImportResolver). -
internal/mutation/: routeAnalyzethroughMutantGenerator,MutantApplier,AnnotationScanner,TestRunner.tiers.gostays put;operatorTiergets new entries for Rust/TS operators (TBD in R/T phases).
-
go test ./...green. -
diffguardbinary on a self-diff of this repo produces byte-identical output before and after the reorg (record the baseline first). - Wall-clock regression <5% on the self-diff.
The "simultaneous" requirement. Lands after A, before R and T.
- Add
--languageflag tocmd/diffguard/main.go. Default empty → auto-detect. Accepts comma-separated values (--language rust,typescript). - Error messages cite the detected manifest files to help users debug "why did you pick that language".
- In
run()(currentlymain.go:79-102), resolve the language set:- If
--languageempty: calllang.Detect(repoPath). - Else: split the flag and call
lang.Get()for each; unknown names are a hard error. - Empty language set is a hard error with a clear message ("no supported language detected; pass --language to override").
- If
- For each resolved language, call
diff.Parse(repoPath, baseBranch, language.FileFilter())→ per-languagediff.Result. - For each
(language, Result)with non-emptyFiles, run the full analyzer pipeline using the language's interfaces. - Merge sections from all languages into the single
report.Report. No concurrency at this layer — analyzers already parallelize where it matters.
- Section names are suffixed
[<lang>](e.g.,Complexity [rust],Mutation [typescript]).report.Section.Nameis alreadystring, so no struct change. - Text output groups by language first, then metric, so mixed reports stay readable.
- JSON output is stable: sections ordered
(language, metric)lexicographically.
- If a detected language has no changed files in the diff, it produces no sections (no empty PASS rows). This matches existing Go behavior (
No Go files found.early return generalizes to "No <lang> files found." per language, collapsing to the existing message when only one language is present).
-
checkExitCodeunchanged: it already takes a mergedReportand returns the worst severity. Add a test that a FAIL in any language escalates the whole run.
-
cmd/diffguard/main_test.gogains a test using a temp git repo with a Go file and stub Rust/TS files: runmain()and assert all three language sections appear. (The Rust/TS analyzer impls are stubs at this point — they register, they return empty results. The point of this test is orchestration, not analysis.)
internal/lang/rustanalyzer/. See MULTI_LANGUAGE_SUPPORT.md §Rust for parser, complexity, import, and mutation notes.
- Confirm
github.com/smacker/go-tree-sitter/rustgrammar versions support the Rust edition(s) we care about. - Decide: integration-test crates under
tests/treated as test files? Inline#[cfg(test)] mod tests { ... }treated as live code? (Design doc recommends:tests/= test files, inline modules = live code ignored during analysis.)
-
.rsextension.IsTestFile: any path segment equal totests. -
DiffGlobs:*.rs. - Tests: fixtures include
src/lib.rs,tests/integration.rs,src/foo/bar.rs; assert expected inclusions/exclusions.
- Tree-sitter query for
function_item,impl_item→function_item(methods),trait_item→ default methods. - Name extraction: standalone
fn foo→foo;impl Type { fn bar }→Type::bar;impl Trait for Type { fn baz }→Type::baz. - Line range: node start/end lines. File line count from byte count.
- Filter to functions overlapping
FileChange.Regions. - Tests: each function form, filtering, nested functions (treated as separate).
- Base +1 on:
if_expression,while_expression,for_expression,loop_expression,match_expression,if_let_expression,while_let_expression. - +1 per arm of
match_expressionwith a guard (theifinpattern if cond =>). - +1 per logical-op token sequence change inside a binary_expression chain (
&&/||). - +1 per nesting level for each scope-introducing ancestor.
- Do not count:
?operator,unsafeblocks. -
ComplexityScorerreusesComplexityCalculator(fast enough). - Tests: empty fn (0),
matchwith N guarded arms (N), nestedif letinsidefor, logical chains.
-
DetectModulePath: parseCargo.toml[package] name. -
ScanPackageImports: finduse_declarationnodes. Internal iff the path starts withcrate::,self::, orsuper::. Also treatmod foo;declarations as an edge to the child module. - Map discovered paths back to package directories so the graph uses directory-level nodes consistent with Go's behavior.
- Tests: crate root detection, relative-path resolution (
super::foo), external imports filtered out.
- Scan
line_commenttokens formutator-disable-next-lineandmutator-disable-func. - Function ranges sourced from C2 so
mutator-disable-funccan expand to every line in the fn. - Tests: next-line, func-wide, unrelated comments ignored, disabled-line map is complete.
- Canonical operators (names from
MULTI_LANGUAGE_SUPPORT.md§MutantGenerator):-
conditional_boundary:>/>=/</<=swaps. -
negate_conditional:==/!=swap; relational flips. -
math_operator:+/-,*//swaps. -
return_value: replace return withDefault::default()/Nonewhen the return type is anOption/ unit. -
boolean_substitution:true/falseswap. -
branch_removal: emptyifbody. -
statement_deletion: remove bare expression statements.
-
- Skip
incdec(Rust has no++/--). - Rust-specific additions:
-
unwrap_removal(Tier 1 viaoperatorTieroverride): strip.unwrap()/.expect(...). Register ininternal/mutation/tiers.go. -
some_to_none(Tier 1):Some(x)→None. -
question_mark_removal(Tier 2): strip trailing?. Register in tiers.
-
- Filter mutants to changed regions; exclude disabled lines.
- Tests: each operator produces the expected mutant, out-of-range skipped, disabled lines honored.
- Text-based application using node byte ranges from the CST. Tree-sitter gives us exact byte offsets; simpler than re-rendering the tree.
- After application, re-parse with tree-sitter and assert no syntax errors; return
nilif the mutated source doesn't parse (silently skip corrupt mutants rather than running broken tests). - Tests: each mutation type applied, re-parse check catches malformed output.
- Temp-copy isolation strategy (from
MULTI_LANGUAGE_SUPPORT.md§Mutation isolation). - Per-file
sync.Mutexmap so concurrent mutations on the same file serialize but different files run in parallel. - Test command:
cargo testwithCARGO_INCREMENTAL=0. HonorTestRunConfig.TestPattern(pass as positional filter). - Kill original file from a backup on restore; panic-safe via
defer. - Honor
TestRunConfig.Timeoutviaexec.CommandContext. - Tests: killed mutant (test fails → killed), survived (test passes → survived), timeout, crash-during-run leaves source restored (simulate via deliberate panic in a helper test).
-
rustanalyzer/rustanalyzer.go:Languagestruct,Name() string { return "rust" },init()callinglang.Register. - Blank import in
cmd/diffguard/main.go.
internal/lang/tsanalyzer/. See MULTI_LANGUAGE_SUPPORT.md §TypeScript for parser and operator notes.
-
github.com/smacker/go-tree-sitter/typescript/typescriptfor.ts,.../typescript/tsxfor.tsx. Use the grammar matching the file extension. - Test runner detection: parse
package.jsondevDependencies — prefervitest, thenjest, then fall back tonpm test.
- Extensions:
.ts,.tsx. Deliberately exclude.js,.jsx,.mjs,.cjsfor now (JS-only repos out of scope). -
IsTestFile: suffixes.test.ts,.test.tsx,.spec.ts,.spec.tsx; any path segment__tests__or__mocks__. -
DiffGlobs:*.ts,*.tsx. - Tests: glob matches, test-file exclusion,
utils.test-helper.tsis NOT a test file (edge case).
- Tree-sitter queries for:
function_declaration,method_definition,arrow_functionassigned tovariable_declarator,functionexpressions assigned similarly,generator_function. - Name extraction:
ClassName.method,functionName, arrow assigned toconst x = () =>→x. - Line ranges, filtering, file LOC.
- Tests: each form, class methods (including static + private), nested arrow functions, exported vs. local.
- Base +1 on:
if_statement,for_statement,for_in_statement,for_of_statement,while_statement,switch_statement,try_statement,ternary_expression. - +1 per
catch_clause; +1 perelsebranch; +1 percasewith content (empty fall-through cases don't count). - +1 per
.catch(promise-chain method call (string-match on identifier to avoid CST depth). - +1 per
&&/||run change. - Do not count: optional chaining
?., nullish coalescing??,awaitalone,asynckeyword, stream method calls. - Tests: ternary nest,
try/catch/finally, logical chains, optional chaining ignored.
-
DetectModulePath: parsepackage.jsonnamefield. -
ScanPackageImports:importandrequire(...). Internal iff the specifier starts with.or a registered project alias (@/,~/). Resolve relative paths against the source file's directory, fold to dir-level for the graph. - Tests: internal vs. external classification, relative resolution, barrel re-exports count as one edge.
-
// mutator-disable-next-lineand// mutator-disable-funccomments. - Function ranges from D2 for func-scope disables.
- Tests: same shape as Rust's C5.
- Canonical operators:
conditional_boundary,negate_conditional(include===/!==),math_operator,return_value(usenull/undefinedappropriately),boolean_substitution,incdec(JS/TS has++/--),branch_removal,statement_deletion. - TS-specific additions — register in
internal/mutation/tiers.go:-
strict_equality(Tier 1): flip===↔==and!==↔!=. -
nullish_to_logical_or(Tier 2):??→||. -
optional_chain_removal(Tier 2):foo?.bar→foo.bar.
-
- Filter to changed regions, skip disabled lines.
- Tests: each operator emits mutants; TS-specific operators exercised.
- Same text-based strategy as Rust's C7. Re-parse check after mutation.
- Tests: each mutation applied, re-parse catches corrupt output.
- Temp-copy + per-file lock, identical to Rust.
- Command selection by detected runner (vitest / jest / npm test). Compose with
--testPathPatternor-thonoringTestPattern. - Honor
TestRunConfig.Timeout. - Set
CI=trueto suppress interactive prompts. - Tests: killed, survived, timeout, restoration after crash.
-
tsanalyzer/tsanalyzer.go:LanguagewithName() string { return "typescript" },init()callslang.Register. - Blank import in
cmd/diffguard/main.go.
- Fixture at
cmd/diffguard/testdata/mixed-repo/containing a minimal Cargo crate, a minimal TS package, and (for completeness) a Go file. - End-to-end test invoking the built binary (
go buildthenexec) against the fixture. Assert each language's sections appear with correct suffixes. - Negative control: same fixture stripped of violations must produce
WorstSeverity() == PASS.
- Extend
.github/workflows/to install Rust (rustup) and Node (for test runners) before running the eval suites. - Add
make eval-rust,make eval-ts,make eval-mixedtargets wrapping the eval Go tests with the right env (e.g.,CARGO_INCREMENTAL=0,CI=true). - Cache Cargo and npm artifacts so CI stays fast.
- Update
README.mdtop section: tagline no longer says Go-only; list supported languages. - Add a per-language "Install" subsection (required toolchain: Rust + cargo, Node + npm).
- Add
--languageto the CLI reference. - Document annotation syntax per language.
- Cross-link from
README.mdto this checklist and toMULTI_LANGUAGE_SUPPORT.md.
Structural tests (Parts A–E) prove the plumbing works. This section proves the analyzers produce correct verdicts on real, seeded problems. Every case is a positive / negative control pair: the positive must be flagged with the right severity, the negative must pass. Negative controls are the firewall against rubber-stamping.
-
internal/lang/<lang>analyzer/evaldata/holds fixtures. -
eval_test.goin each analyzer package runs the full pipeline (built binary, full CLI path) against each fixture and diff-compares emitted findings toexpected.json. - Comparison is semantic (file + function + severity), not byte-for-byte, so cosmetic line shifts don't break the eval.
- Eval runs are deterministic:
--mutation-sample-rate 100, fixed--mutation-workers, a stable seed for any randomized orderings. - Each fixture directory has a
README.mddocumenting the seeded issue and the expected verdict.
- complexity:
- Positive
complex_positive.rs: nestedmatch+if let+ guarded arms, cognitive ≥11 → section FAIL with finding on that fn. - Negative
complex_negative.rs: same behavior split into helpers, each <10 → section PASS, zero findings.
- Positive
- sizes (function):
- Positive: single
fn>50 lines → FAIL. - Negative: same behavior factored across fns, each <50 → PASS.
- Positive: single
- sizes (file):
- Positive:
large_file.rs>500 LOC → FAIL. - Negative: <500 LOC → PASS.
- Positive:
- deps (cycle):
- Positive:
a.rs↔b.rs→ FAIL with cycle finding. - Negative: same modules with a shared
types.rsbreaking the cycle → PASS.
- Positive:
- deps (SDP):
- Positive: unstable concrete module imported by stable abstract one → WARN/FAIL per current SDP severity.
- Negative: reversed dependency direction → PASS.
- churn:
- Positive
hot_complex.rswith a baked.gitdir showing 8+ commits on a complex fn → finding present. - Negative
hot_simple.rssame commit count, trivial fn → no finding.
- Positive
- mutation (kill):
- Positive
well_tested.rs: arithmetic fn + tests covering boundary and sign → Tier-1 ≥90% → PASS. - Negative
untested.rs: same fn, test covers only one branch → Tier-1 <90% → FAIL.
- Positive
- mutation (Rust-specific operator):
- Positive:
unwrap_removal/some_to_noneon a tested fn is killed; on an untested fn survives. - Proof that the operator adds signal, not noise.
- Positive:
- mutation (annotation respect):
- Positive
# mutator-disable-funcsuppresses all mutants in that fn. - Negative (same file, annotation removed) regenerates them.
- Positive
- complexity:
- Positive
complex_positive.ts: nested ternaries + try/catch +&&/||chains ≥11 → FAIL. - Negative
complex_negative.ts: refactored into named helpers → PASS.
- Positive
- sizes (function):
- Positive: arrow fn assigned to
const>50 LOC → FAIL. - Negative: same logic across named exports → PASS.
- Positive: arrow fn assigned to
- sizes (file):
- Positive
large_file.ts>500 LOC → FAIL. - Negative: split across files → PASS.
- Positive
- deps (cycle):
- Positive
a.ts↔b.ts→ FAIL. - Negative: shared
types.tsbreaking cycle → PASS.
- Positive
- deps (internal vs external):
- Positive:
./fooappears in internal graph;import 'lodash'does NOT. - Assert directly on the graph shape, not just pass/fail.
- Positive:
- churn:
- Positive
hot_complex.tswith seeded history → finding. - Negative
hot_simple.tssame history → no finding.
- Positive
- mutation (kill, with configured runner):
- Positive:
arithmetic.ts+ tests covering boundary + sign → Tier-1 ≥90% → PASS. - Negative: same fn, test covers one branch → Tier-1 <90% → FAIL.
- Positive:
- mutation (TS-specific operators):
- Positive:
strict_equalityflip killed by tests that rely on strict equality;nullish_to_logical_orkilled by tests that distinguishnullfromundefined. - Negative: same operators survive when the test only asserts non-distinguishing inputs. Confirms the operators generate meaningful mutants, not noise.
- Positive:
- mutation (annotation respect):
- Positive
// mutator-disable-next-linesuppresses the next-line mutant. - Negative: annotation removed, mutant regenerated.
- Positive
- Mixed-repo severity propagation:
- Rust FAIL + TS PASS → overall FAIL; TS section independently reports PASS.
- Flip: Rust PASS + TS FAIL → overall FAIL; Rust section independently reports PASS.
- Proves language sections don't contaminate each other.
- Mutation concurrency safety:
- Fixture with 3+ Rust and 3+ TS files, each with multiple mutants. Run
--mutation-workers 4. - Assert
git status --porcelainis empty after the run (no temp-copy corruption). - Assert repeated runs produce identical reports.
- Sweep
--mutation-workers1, 2, 4, 8 and assert report stability.
- Fixture with 3+ Rust and 3+ TS files, each with multiple mutants. Run
- Disabled-line respect under concurrency:
- A file with
mutator-disable-funcon one fn and live code on another,--mutation-workers 4. - Assert zero mutants generated for the disabled fn; live fn's mutants execute.
- A file with
- False-positive ceiling:
- Known-clean fixture (well-tested small Rust crate + well-tested small TS module) →
WorstSeverity() == PASS, zero FAIL findings across all analyzers. - This is the "does it cry wolf" gate.
- Known-clean fixture (well-tested small Rust crate + well-tested small TS module) →
- Rust: run the built diffguard against two open-source crates (one small, one mid-sized). Triage every FAIL and WARN. If >20% are noise, iterate on thresholds/detection before declaring Rust support shipped.
- TypeScript: repeat with one app and one library project.
- Record triage findings in this document under a "Baseline noise rate" appendix so future changes know what "good" looks like.
A (foundation) ──► B (orchestration) ──┬──► C (Rust) ──┬──► E (integration + CI)
└──► D (TypeScript) ──┘
│
└──► EVAL runs alongside C/D, per analyzer
Parts C and D are disjoint packages and can be implemented in parallel by separate agents / PRs, rebased onto the B branch. Part E holds the merge point and the final evaluation gate.
Before calling this done:
- All checklist items above checked.
-
go test ./...green. -
make eval-rust,make eval-ts,make eval-mixedall green in CI. - Pre-flight calibration triage documented with <20% noise rate per language.
- README reflects multi-language support with install instructions for each toolchain.
-
diffguardrun on this repo's own HEAD produces identical output before and after the reorg (the Go path must be byte-stable).