Skip to content

Issue sweep: precision fixes, why command, config, rev ranges, tap - #22

Merged
itaywol merged 9 commits into
mainfrom
issue-sweep
Jul 24, 2026
Merged

Issue sweep: precision fixes, why command, config, rev ranges, tap#22
itaywol merged 9 commits into
mainfrom
issue-sweep

Conversation

@itaywol

@itaywol itaywol commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #11 #12 #13 #14 #15 #16 #17 #20.

  • Precision: TS class-field refs attribute to the class; Go import aliases in known-names; Unknown-call widening scoped to import-reachable files (herdr worst-case 2876 to 27 of 3079); deleted files seed importers (Go package-aware) instead of run_all
  • Features: testless why <test-id> explains selection paths; testless.toml with always-run and ignore globs; --to <rev> rev-range analysis via temp worktrees
  • Ecosystem: Homebrew tap (itaywol/homebrew-testless), brew install in docs
  • Soundness holes caught in final review and fixed before merge: Go deletion under-selection, ignored-change run_all escalation

Test plan

176+ workspace tests green (all TDD, RED-verified regressions per issue), clippy/fmt clean, real-repo herdr numbers measured before/after.

itaywol added 9 commits July 25, 2026 00:54
class_declaration nodes had no scope_of entry, so refs inside field
initializers (e.g. `x = compute();`) fell through to the enclosing
current_def instead of the class def -- module-level code (from_def=0)
when the class was top-level. Register the class node itself in
scope_of; method bodies still resolve correctly since walk_refs
re-checks scope_of at every node and method_definition's own (more
specific) entry overrides the class-level one for nodes inside it.
build_known_names derived a package qualifier only from the raw
import path's last segment, so `import foo "example.com/x/bar"`
allow-listed "bar" instead of "foo" -- reads via the actual alias
qualifier were silently dropped. collect_imports now also collects
each import_spec's alias (its `name` field, when it's a genuine
package_identifier rather than a blank/dot import) and threads it
into build_known_names. ImportRef itself is left untouched since it's
a shared cross-language type.
A deleted source file no longer forces RunAll unconditionally. Deleted
files (indexed or not) are now routed through the same raw-import stem
scan used for unrecognized changed files: any surviving importer whose
raw import text still references the deleted path gets its ModuleInit
seeded, since it will fail to resolve/compile. A deleted file with no
remaining importer contributes zero seeds.
Unknown-by-short-name widening previously enqueued every Calls{Unknown(n)}
caller repo-wide, exploding selection on dense single-crate repos where a
common method name matches unrelated defs across the whole codebase.

Now an Unknown caller C only widens through to a visited def D when D's
file is in C's forward transitive import closure, or they share a file:
tier-1 resolution scope for a ref in C is C's file plus its direct
imports, so a def outside that reachable set could never have been C's
actual (unresolved) callee under our model.

Exposed two pre-existing indexer gaps the old repo-wide sweep was
papering over: Go same-package cross-file calls (no `import` needed
within a package) were never resolved to `Resolved` edges -- fixed by
extending a Go file's tier-1 scope to its package siblings. The Rust
fixture relying on a bare `crate::`-qualified call with no local `use`
was adjusted to use an explicit `use`, an equally idiomatic style, rather
than extending Rust's import model further.
Add `walk::impacted_tests_with_paths`, which records each def's
first-discovery predecessor during the impact BFS so the shortest
seed -> test hop path can be reconstructed for any selected test.
`impacted_tests` becomes a thin wrapper that discards the paths, so all
prior selection semantics and tests are unchanged.

Wire this up as `testless why <test-id>`: runs the same analyze+walk
pipeline as `select`, forgivingly substring-matches `test_id` against
selected tests' `<file> :: <name chain>`, and prints the hop path
(human text on a terminal, JSON when piped). Zero matches -> not
selected (exit 1); multiple matches -> lists candidates (exit 1);
run-all classification mirrors select/changes' exit-2 contract.
Adds an optional repo-root testless.toml escape hatch: `ignore` globs
(globset) drop matching files at discovery time so they're never
indexed; `always-run` globs are unioned into the selection post-walk,
so a smoke test always runs even when the walk itself seeds nothing.
A malformed testless.toml (bad TOML, invalid glob, wrong value type)
is a hard error (exit 1) in every command that loads it, never a
silent run_all degrade.

`why` explains an always-run-only selection with a dedicated
"selected by always-run glob '<glob>'" line/field instead of the
ordinary hop path, since the walk never reached that test at all.
Materializes --to in a temporary git worktree (git worktree add --detach,
removed on Drop) and runs the whole analysis pipeline rooted there, so
--from/--to can name any two revisions instead of only --from vs. the
live worktree. changed_files/show_file still run against the main repo
(it has the objects); a bad/unfetched --to rev degrades to the existing
run_all/exit-2 fallback, same as a bad --from.
- classify: a deleted Go file now seeds its surviving package siblings'
  ModuleInit directly and folds the package directory into the importer
  stem scan, since Go imports name directories, never file stems, and
  same-package files never reference each other by name (was under-
  selecting to empty).
- analyze: filter `changed` by testless.toml's ignore globset before
  classify, so an ignored file's change contributes zero seeds instead of
  hitting classify's "missing from graph" path and forcing run_all.
- gitio: TempWorktree dir gets a per-process pid suffix (fixes a real
  collision between concurrent runs against the same --to rev) and its
  doc comment's incorrect "collision-safe" claim is corrected.
- classify: scan_importers requires a >=3 char stem for its substring
  match; a deleted+indexed file left with no usable stem escalates to
  RunAll (under-select forbidden) rather than a non-indexed file's short
  stem, which just yields no extra seeds (accepted, lower-stakes case).
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.

TS: attribute class-field-initializer calls to the class, not module_init

1 participant