Skip to content

feat(lang): add TypeScript checker#3

Open
donnfelker wants to merge 9 commits intomainfrom
feat/lang-typescript
Open

feat(lang): add TypeScript checker#3
donnfelker wants to merge 9 commits intomainfrom
feat/lang-typescript

Conversation

@donnfelker
Copy link
Copy Markdown
Contributor

@donnfelker donnfelker commented Apr 21, 2026

Summary

Stacked on #2. Adds a TypeScript implementation of the lang.Language interface, registered via blank import. Self-contained: tsanalyzer unit tests + 10 evaldata fixtures run through the shared evalharness introduced in PR #2 .

Retarget to `main` once PR #2 merges.

What ships

  • internal/lang/tsanalyzer/ — 69 files: analyzer code, testdata, 10 evaldata fixtures (complexity/sizes/deps_cycle/mutation_kill/mutation_tsop in negative + positive variants).
  • go.mod / go.sum — add github.com/smacker/go-tree-sitter + transitive deps.
  • cmd/diffguard/main.go — second blank import (goanalyzer + tsanalyzer).
  • MakefileEVAL_ENV variable + eval-ts target + `.PHONY` update.
  • .github/workflows/ci.yml — Node 22 setup + npm cache + `Eval — TypeScript (EVAL-3)` step.

Verification

  • go build ./... && go vet ./... && go test ./... -count=1 — green.
  • make eval-ts passes locally (requires Node 22+).
  • CI runs eval-ts on every push (see the updated workflow).

Reviewer notes

donnfelker and others added 6 commits April 21, 2026 15:52
Adds the full tsanalyzer package — complexity, sizes, deps, mutation
(generate/apply/annotate), testrunner, eval harness fixtures — and
brings in go-tree-sitter via go.mod/go.sum as the parsing engine.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the EVAL_ENV variable and eval-ts target to the Makefile, and
wires up Node 22 setup + npm cache + the Eval TypeScript (EVAL-3) step
in ci.yml. Rust/cargo and eval-rust/eval-mixed steps land in PR #3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a blank import for tsanalyzer so its init() runs and registers
the TypeScript language implementation with the lang registry on
startup. rustanalyzer import lands in PR #3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TestRunner_Timeout was failing on Linux CI, taking the full 5s
sleep duration instead of honoring the 200ms timeout. On Linux,
/bin/sh forks sleep as a child rather than exec-optimizing to it,
so exec.CommandContext's default Cancel (SIGKILL to the leader)
kills only the shell. The orphaned sleep keeps the inherited
stdout/stderr pipes open, blocking cmd.Wait() on io.Copy until
sleep naturally exits.

Put the subprocess in its own process group via Setpgid and
signal the whole group on cancel. This also matters in production
where npx vitest / jest fork worker processes that would otherwise
be leaked on timeout. WaitDelay gives a cross-platform upper bound
on pipe-close wait as a safety net.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The diffguard dogfooding job on this PR flagged six complexity/size
violations against the new tsanalyzer package plus one surviving
math_operator mutant in replaceRange. All show up only because the
entire package is net-new in the diff, so every helper counts as
changed code subject to the thresholds.

Refactors applied:

  - complexity.go: split walkComplexity's 62-line switch into per-
    construct helpers (ifComplexity, loopComplexity, callComplexity,
    walkAllChildren, isFunctionLikeNode). Semantics unchanged.
  - mutation_generate.go: hoist binaryFlips / strictEqualityFlips to
    package scope, collapse MutantSite construction into newMutantSite,
    and split hasOptionalChainToken into a grammar fast path plus
    optionalChainTokenOffset (now shared with the applier).
  - mutation_apply.go: reuse optionalChainTokenOffset in
    applyOptionalChainRemoval; swap replaceRange's hand-rolled
    capacity math for slices.Concat, which removes the surviving
    math_operator mutant on the capacity expression (the `+`/`-`
    flips produced bitwise-identical output).
  - tsanalyzer.go: replace hasTSFile's 11-way `||` chain with a
    package-level detectionSkipDirs map.
  - testrunner.go: extract runCommand so RunTest stays under the
    50-line threshold after the process-group plumbing landed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@donnfelker donnfelker marked this pull request as ready for review April 24, 2026 17:03
@donnfelker donnfelker requested a review from cffls April 24, 2026 17:03
Copy link
Copy Markdown
Collaborator

@cffls cffls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Since the lang-foundation PR has been merged, the base branch could be changed to main.

Comment thread Makefile
Base automatically changed from feat/lang-foundation to main April 24, 2026 20:48
@donnfelker
Copy link
Copy Markdown
Contributor Author

@cffls

Thanks! Since the lang-foundation PR has been merged, the base branch could be changed to main.

Ahhh, this repo does not have "Automatically delete head branches after merge" turned on. I just deleted that branch from that closed PR. That forces GitHub to automatically update the stacked PR's. If you turn that on, it will do it automatically :)

donnfelker and others added 3 commits April 24, 2026 17:01
Addresses PR review feedback (@cffls): README was Go-only despite the
tsanalyzer being registered. Adds a Languages section covering
auto-detection, TS prerequisites (node/npm), test-file exclusions, and
runner precedence (vitest > jest > npm test), plus CLI examples for
running diffguard against a TypeScript repo and the extra GitHub Actions
steps needed so mutation runs can invoke npx vitest / npx jest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resolves README.md conflict by keeping both additions: origin/main's
--skip-generated mode description stays under the Modes subsection, and
this branch's Languages section sits immediately below it. No code-level
conflicts — go build, go vet, and go test ./... all clean on the merged
tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Temp-copy test runners (TypeScript) swap mutant bytes over the original
file during RunTest, which races with other workers' ApplyMutation reads
of the same path. When a worker's ApplyMutation ran while another
worker's RunTest had the file mutated on disk, it either mutated against
already-mutated source or failed to find its target node and returned
nil — silently classifying the mutant as SURVIVED.

This surfaced reliably on Ubuntu CI (slower I/O + Linux scheduler)
where TestEval_Mutation_TSOp_Positive saw 2 of 5 Tier-1 mutants
survive; on macOS the race window closed faster than any concurrent
read could land, hiding the bug.

Split runMutantsParallel into two phases: prepareMutants applies and
writes every mutant to its temp file upfront (file on disk still
pristine, parallel-safe), then runPreparedMutant fires RunTest calls in
parallel. Go's overlay-based runner is unaffected; the fix is only
load-bearing for temp-copy runners.

Added TestRunMutantsParallel_ApplyPrecedesTest as a regression guard —
fake applier timestamps reads, fake runner swaps the file briefly, and
the test asserts no ApplyMutation observed post-swap state. Confirmed
the test fails with the old single-phase orchestrator and passes with
the two-phase one.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants