Skip to content

fix: repair snapshot IDs, three inert detectors and dead_code's cubic scan - #39

Open
fixcik wants to merge 5 commits into
mainfrom
worktree-fix-detector-bugs
Open

fix: repair snapshot IDs, three inert detectors and dead_code's cubic scan#39
fixcik wants to merge 5 commits into
mainfrom
worktree-fix-detector-bugs

Conversation

@fixcik

@fixcik fixcik commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Fixes a set of detector and snapshot bugs found while auditing all 30 detectors. The
test suite was green before this work, so none of these were caught by existing tests —
several were actively hidden by a gap between the test harness and the production path.

Every bug below was reproduced on the real CLI before the fix and re-checked after.

1. archlint diff was unusable for three smell types

package_cycles broke diff outright. PackageCycle carries no files, so its ID
fell through to the generic hash of an empty file list — every package cycle in a project
got the same packagecycle:e3b0c442. Two cycles were enough to make diff fail with
Duplicate smell ID, even when comparing a snapshot against itself:

$ archlint diff s.json s.json
error: Snapshot error: Duplicate smell ID: packagecycle:e3b0c442

layer_violation hid fixes. Keyed by source file and target layer only, so N illegal
imports from one file into one layer collapsed onto one ID. Fixing 2 of 3 violations
produced no improvement in the diff. Now disambiguated by import line, as side-effect
imports already were. SdpViolation had the same collision and gets the same treatment.

feature_envy produced phantom regressions. The envied module was picked with
max_by_key over a HashMap, so ties were resolved by iteration order. That module is
part of the smell ID, so on unchanged code diff reported a regression plus a fix.
20 runs of the same project split 9/11 between two modules; now 25/25 identical.

Finally, write_snapshot now validates before writing — until now archlint snapshot
happily produced files that archlint diff then refused to read.

2. Three detectors could never fire

  • circular_type_deps resolved type-only imports by walking the dependency graph.
    The graph only holds files with runtime code, so a cycle between pure type
    declarations — the case the detector exists for — had no node on either end. Its own
    fixture type_cycles/basic reports nothing in production; adding one
    export const x = 1 to both files makes it fire.

  • shotgun_surgery keyed co-change stats by git's repository-relative paths, then
    looked them up in file_symbols, which is keyed by absolute path. Nothing ever
    matched — a repo with 8 commits × 4 files returned zero even with both thresholds
    lowered to 1. GitHistoryCache::get_churn_map already had the right convention.

  • cyclomatic_complexity ignored both exclude and path overrides. The legacy
    complexity alias was consulted exactly when the modern rule resolved to None — that
    is, when the user had switched the rule off or excluded the file — and resurrected it
    with default settings. Identical override, same file:

    before after
    cognitive_complexity 0 0
    cyclomatic_complexity 1 0

    This also repairs the alias itself: configuring complexity previously had no effect
    either, because the modern id resolved to a permissive default first.

3. dead_code: cubic runtime and a large false-negative class

find_dead_files iterated every file; for each, three helpers scanned every other file,
and check_direct_reexports ran a nested scan inside that.

files before after
200 0.36 s 0.03 s
400 3.2 s 0.06 s
800 24.8 s 0.11 s
1600 >12 min (killed) 0.19 s
3200 0.41 s

The cost came from matches_source, a fuzzy comparison that cannot be indexed. Replaced
with a SourceIndex built once per run, keyed on the resolved absolute path; specifiers
that fail to resolve keep their raw form and are indexed by trailing segment, so projects
with unresolvable aliases don't light up with false reports (guarded by a test).

Because that comparison matched on basename, an unused file was considered alive whenever
any file with the same basename was imported anywhere — in a codebase full of
index.ts, types.ts and utils.ts that suppressed a large share of real findings.

Also here: is_entry_point parsed patterns by hand and its strip_prefix('*') branch
reduced any star-leading pattern to a literal ends_with. entry_points: ["**/bin/**"]
was silently ignored, as were the built-in **/test/**, **/tests/**,
**/__fixtures__/** and **/*.mock.ts. Now compiled as real globs.

Heads-up: snapshot IDs change

The ID formulas for PackageCycle, LayerViolation and SdpViolation changed, so
existing baseline snapshots will report those smells once as new. I did not add a
BREAKING CHANGE: footer, since on 0.x that would push semantic-release to 1.0.0 — that
release call is yours.

Testing

  • 288 tests pass (269 before), clippy --all-features and --no-default-features clean.
  • New crates/archlint/tests/cli_detectors.rs drives the actual binary. The shared
    analyze_fixture harness puts every scanned file into the graph while the production
    runner does not, which is why this whole class of bug could stay green.
  • Output of the old and new binary is byte-identical on this repo's own TS/JS
    (docs/, packages/, scripts/).

Known and left alone

Non-deterministic ordering of report/snapshot output (builder.rs iterates a HashSet,
cycles.rs sorts by a HashMap value), Rust Debug output leaking into the JSON type
field (report/json.rs:65), cluster severity taken from an arbitrary scc.first(),
order-insensitive matches_glob_pattern in layer_violation, and repeated file reads in
large_file / update_cache.

fixcik added 3 commits August 3, 2026 02:39
Three smell types produced colliding or unstable snapshot IDs, which broke
`archlint diff`:

- PackageCycle carries no files, so its ID fell through to the generic
  hash of an empty file list: every package cycle in a project got the
  same `packagecycle:e3b0c442`. Two cycles were enough to make `diff`
  fail outright with "Duplicate smell ID" — even when comparing a
  snapshot against itself. It is now keyed by the sorted package names.

- LayerViolation was keyed by source file and target layer only, so N
  illegal imports from one file into one layer collapsed onto one ID.
  Fixing 2 of 3 violations produced no improvement in the diff. The
  import line now disambiguates them, as it already does for
  side-effect imports.

- SdpViolation had the same collision (keyed by the source file alone)
  and gets the same treatment.

FeatureEnvy picked the envied module with `max_by_key` over a HashMap,
so a tie was resolved by iteration order and the winner changed between
runs. That module is part of the ID, so `diff` reported a phantom
regression plus a phantom fix on unchanged code — 20 runs of the same
project split 9/11 between two modules. Ties are now broken by source
path.

Finally, `write_snapshot` validates before writing. Until now `archlint
snapshot` happily produced files that `archlint diff` then refused to
read, which made all of the above much harder to diagnose.

Note: the ID formulas for PackageCycle, LayerViolation and SdpViolation
have changed, so existing baseline snapshots will report those smells
once as new.
Three detectors could never report anything in a real run.

circular_type_deps resolved every type-only import by walking the
dependency graph. The graph only holds files with runtime code, so a
cycle between pure type declarations — the very case the detector exists
for — had no node on either end and was invisible. It now resolves
through the already-resolved import specifier and `file_symbols`, which
also retires a hand-rolled path-segment matching heuristic.

shotgun_surgery keyed its co-change statistics by the paths git reports,
which are relative to the repository root, then looked them up in
`file_symbols`, which is keyed by absolute path. Nothing ever matched;
even with both thresholds lowered to 1 the detector returned nothing.
Paths are now joined onto the repository workdir, the same convention
`GitHistoryCache::get_churn_map` already uses.

cyclomatic_complexity ignored both `exclude` and path `overrides`. The
legacy `complexity` alias was consulted exactly when the modern rule
resolved to None — that is, when the user had switched the rule off or
excluded the file — and resurrected it with default settings. The alias
is now consulted only when `cyclomatic_complexity` is configured
nowhere, which also repairs the alias itself: configuring `complexity`
previously had no effect either, because the modern id resolved to a
permissive default first and the fallback was never reached.

The new tests drive the CLI rather than the shared `analyze_fixture`
harness. That harness puts every scanned file into the graph while the
production runner does not, which is why this whole class of bug could
stay green.
Whole-project dead-code analysis grew cubically. `find_dead_files`
iterated every file; for each one `check_default_imports`,
`check_reexports` and `check_local_usages` scanned every other file,
and `check_direct_reexports` ran a nested scan inside that. On a
release build: 200 files 0.36s, 400 files 3.2s, 800 files 24.8s — a
factor of eight per doubling. 1600 files had not finished after twelve
minutes.

The cost came from `matches_source`, a fuzzy comparison
(`source == path || source.ends_with(file_name) || ...`) that cannot be
indexed, so every question had to be answered by a scan. It is replaced
by `SourceIndex`, built once per run and keyed on the resolved absolute
path. Specifiers that failed to resolve — unconfigured aliases, virtual
modules — keep their raw form and are indexed by their trailing
segment, so projects whose aliases archlint cannot resolve do not light
up with false reports.

Same measurements after: 200 files 0.03s, 800 files 0.11s, 3200 files
0.41s — linear.

Because the fuzzy comparison matched on basename, an unused file was
considered alive whenever any other file with the same basename was
imported somewhere. In a codebase full of `index.ts`, `types.ts` and
`utils.ts` that suppressed a large share of real findings. Exact keys
fix that too.

`is_reexported` turned out to be unreachable: `has_used_exports` ran
first and already returned true on the weaker condition that something
re-exports the file, so the extra "and is the re-exporter itself used"
test never applied. Observable behaviour is kept, the dead branch is
gone.

Also in this file: `is_entry_point` parsed patterns by hand, and its
`strip_prefix('*')` branch reduced any pattern starting with a star to
a literal `ends_with`. `entry_points: ["**/bin/**"]` was silently
ignored, as were the built-in `**/test/**`, `**/tests/**`,
`**/__fixtures__/**` and `**/*.mock.ts`. Patterns are now compiled as
globs and matched against both the project-relative path and the file
name.
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fixcik, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9f06c5ca-06d2-4c2d-a2f9-2c6fa61eb0ee

📥 Commits

Reviewing files that changed from the base of the PR and between 9394aa2 and 3efe5bf.

📒 Files selected for processing (6)
  • context7.json
  • crates/archlint/src/detectors/code_clone/engine.rs
  • crates/archlint/src/engine/runner.rs
  • crates/archlint/src/snapshot/id.rs
  • crates/archlint/src/utils/package.rs
  • crates/archlint/tests/cli_detectors.rs
📝 Walkthrough

Summary

  • Made snapshot IDs deterministic and unique for package cycles, layer violations, SDP violations, and feature envy.
  • Added snapshot validation before writing to prevent duplicate-ID files.
  • Repaired circular_type_deps, shotgun_surgery, and cyclomatic_complexity.
  • Added support for exclusions, overrides, and the legacy complexity alias.
  • Reworked dead_code with indexed lookups, normalized paths, compiled globs, and improved basename handling.
  • Added CLI regression tests for detector behavior and path handling.

Testing

  • 288 tests pass.
  • Clippy passes with default and no-default features.

Compatibility

Existing snapshots report PackageCycle, LayerViolation, and SdpViolation findings as new because their ID formulas changed.

Documentation

The detector documentation exists in English, Russian, Spanish, Japanese, Portuguese, and Chinese. The new legacy alias, snapshot validation, deterministic IDs, and dead_code matching changes are not documented in these language versions.

Walkthrough

The PR updates detector path resolution, dead-code indexing, rule selection, smell ID generation, snapshot validation, and CLI integration coverage.

Changes

Detector correctness

Layer / File(s) Summary
Path-based detector resolution
crates/archlint/src/detectors/dependency/circular_type_deps.rs, crates/archlint/src/detectors/design/shotgun_surgery.rs
Type imports and Git-changed files now use absolute analysis paths.
Dead-code usage indexing
crates/archlint/src/detectors/hygiene/dead_code.rs
Dead-code analysis builds shared usage indexes and uses compiled entry-point and exclusion globs.
Detector selection and configuration
crates/archlint/src/detectors/design/feature_envy.rs, crates/archlint/src/detectors/metrics/mod.rs
Feature-envy ties use source-path ordering. Complexity rules use legacy fallback only when explicitly configured.
Snapshot identity and validation
crates/archlint/src/snapshot/id.rs, crates/archlint/src/snapshot/io.rs
Smell IDs include location or sorted package data. Snapshot writes reject duplicate IDs before creating files.
End-to-end detector coverage
crates/archlint/tests/cli_detectors.rs
CLI tests cover configuration, dead-code reachability, Git history, and type-only dependency detection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested labels: bug, core, detectors

Poem

A rabbit checks each path with care,
And counts the smells in tidy rows.
Tied choices now resolve the same,
While snapshot doors reject duplicate names.
Tests hop through projects, commits, and code—
Then rest beneath a carrot load.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the snapshot ID fixes, detector repairs, and dead_code performance improvement.
Description check ✅ Passed The description directly explains the implemented fixes, performance changes, regression tests, and snapshot ID behavior.
Docstring Coverage ✅ Passed Docstring coverage is 98.48% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-fix-detector-bugs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added bug Something isn't working core detectors Has changes in detectors labels Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/archlint/src/detectors/hygiene/dead_code.rs`:
- Around line 264-266: The flat name-keyed usage index incorrectly lets a file’s
own identifiers and same-named identifiers from unrelated files keep exports
alive. In crates/archlint/src/detectors/hygiene/dead_code.rs lines 264-266,
update build_usage_index and check_local_usages to retain usages per file and
exclude the candidate path when checking an export name. In
crates/archlint/tests/cli_detectors.rs lines 191-208, rename the export so it
differs from the imported binding and exercises SourceIndex’s trailing-segment
fallback.
- Around line 61-66: Update the filename key generation in the dead-code
detector to derive the extension-less key from the first dot after any leading
dot, rather than using rsplit_once('.') in the target file-name handling.
Preserve the full filename key and ensure names such as widget.test.ts also
produce widget for unresolved specifier matching.

In `@crates/archlint/src/snapshot/id.rs`:
- Around line 184-188: Update id_for_package_cycle to encode each sorted package
name unambiguously before hashing, using length-prefixing or an equivalent
delimiter-safe representation instead of sorted.join("|"). Preserve
deterministic sorting and ensure distinct package lists cannot produce the same
encoded input.

In `@crates/archlint/tests/cli_detectors.rs`:
- Around line 80-126: Add a positive-control test alongside the complexity tests
using the same complex_function fixture with cyclomatic_complexity enabled and
no exclude or path override, and assert that has_smell reports
high_cyclomatic_complexity before the suppression cases. Keep the existing
exclude, override, and legacy-name tests unchanged, using the JSON smell
identifier high_cyclomatic_complexity rather than the snapshot type string.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 6e0dc7c3-8099-4a2c-b04f-b746ffb7e8c9

📥 Commits

Reviewing files that changed from the base of the PR and between 351f919 and 9394aa2.

📒 Files selected for processing (8)
  • crates/archlint/src/detectors/dependency/circular_type_deps.rs
  • crates/archlint/src/detectors/design/feature_envy.rs
  • crates/archlint/src/detectors/design/shotgun_surgery.rs
  • crates/archlint/src/detectors/hygiene/dead_code.rs
  • crates/archlint/src/detectors/metrics/mod.rs
  • crates/archlint/src/snapshot/id.rs
  • crates/archlint/src/snapshot/io.rs
  • crates/archlint/tests/cli_detectors.rs
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Test Rust
  • GitHub Check: Lint
🔇 Additional comments (13)
crates/archlint/src/detectors/design/feature_envy.rs (2)

77-84: LGTM!


141-182: LGTM!

crates/archlint/src/detectors/metrics/mod.rs (1)

59-76: 🎯 Functional Correctness

No scope change needed.

resolve_complexity_rule already passes path to both cyclomatic_complexity and complexity, and get_rule_for_file resolves overrides through matches_path.

			> Likely an incorrect or invalid review comment.
crates/archlint/src/detectors/dependency/circular_type_deps.rs (2)

6-6: LGTM!

Also applies to: 37-37


84-95: 🩺 Stability & Availability

No path-key mismatch remains.

circular_type_deps now resolves type imports through the runtime graph and the normal resolver path, so the removed ctx.file_symbols.contains_key(import.source) lookup no longer applies.

crates/archlint/src/detectors/design/shotgun_surgery.rs (1)

26-35: LGTM!

Also applies to: 49-49, 85-97, 107-110

crates/archlint/src/detectors/hygiene/dead_code.rs (1)

13-19: LGTM!

Also applies to: 21-59, 71-109, 197-199, 272-307, 362-394, 396-410, 476-484

crates/archlint/tests/cli_detectors.rs (1)

1-78: LGTM!

Also applies to: 128-189, 210-257, 259-329

crates/archlint/src/snapshot/id.rs (3)

21-30: LGTM on the line-based disambiguation approach.

The LayerViolation and SdpViolation ID formulas now incorporate the location line via with_line_hash_fallback, and the accompanying tests confirm distinct offending imports produce distinct IDs. This resolves the reported collision.


118-134: LGTM!

Also applies to: 166-178


234-319: LGTM!

crates/archlint/src/snapshot/io.rs (2)

6-14: LGTM!


34-89: LGTM!

Comment on lines +61 to +66
if let Some(file_name) = target.file_name().and_then(|name| name.to_str()) {
keys.push(file_name.to_string());
if let Some((base, _)) = file_name.rsplit_once('.') {
keys.push(base.to_string());
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Extension-less lookup key misses multi-dot filenames.

rsplit_once('.') splits at the last dot. For widget.test.ts the extension-less key becomes widget.test. An unresolved specifier ./widget produces the key widget, so the two never match and the file is reported as dead. Split at the first dot after the leading dot to cover compound extensions.

♻️ Proposed change
             keys.push(file_name.to_string());
-            if let Some((base, _)) = file_name.rsplit_once('.') {
-                keys.push(base.to_string());
-            }
+            if let Some((base, _)) = file_name.rsplit_once('.') {
+                keys.push(base.to_string());
+            }
+            // Compound extensions (`widget.test.ts`) also answer to the leading segment.
+            if let Some((stem, _)) = file_name.trim_start_matches('.').split_once('.') {
+                keys.push(stem.to_string());
+            }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(file_name) = target.file_name().and_then(|name| name.to_str()) {
keys.push(file_name.to_string());
if let Some((base, _)) = file_name.rsplit_once('.') {
keys.push(base.to_string());
}
}
if let Some(file_name) = target.file_name().and_then(|name| name.to_str()) {
keys.push(file_name.to_string());
if let Some((base, _)) = file_name.rsplit_once('.') {
keys.push(base.to_string());
}
// Compound extensions (`widget.test.ts`) also answer to the leading segment.
if let Some((stem, _)) = file_name.trim_start_matches('.').split_once('.') {
keys.push(stem.to_string());
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/archlint/src/detectors/hygiene/dead_code.rs` around lines 61 - 66,
Update the filename key generation in the dead-code detector to derive the
extension-less key from the first dot after any leading dot, rather than using
rsplit_once('.') in the target file-name handling. Preserve the full filename
key and ensure names such as widget.test.ts also produce widget for unresolved
specifier matching.

Comment on lines +264 to +266
index
.local_usages
.extend(symbols.local_usages.iter().map(ToString::to_string));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Flat, name-keyed local_usages weakens dead-code detection and masks the test it should cover. build_usage_index merges every non-excluded file's identifiers into one set keyed by bare name, with no record of which file used which identifier. A file's own identifiers therefore keep its exports alive, and identical names in unrelated files do the same.

  • crates/archlint/src/detectors/hygiene/dead_code.rs#L264-L266: index local usages per file, then exclude the candidate path when check_local_usages tests an export name.
  • crates/archlint/tests/cli_detectors.rs#L191-L208: rename the export so it differs from the imported binding, so the test exercises the SourceIndex trailing-segment fallback instead of the identifier-name match.
📍 Affects 2 files
  • crates/archlint/src/detectors/hygiene/dead_code.rs#L264-L266 (this comment)
  • crates/archlint/tests/cli_detectors.rs#L191-L208
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/archlint/src/detectors/hygiene/dead_code.rs` around lines 264 - 266,
The flat name-keyed usage index incorrectly lets a file’s own identifiers and
same-named identifiers from unrelated files keep exports alive. In
crates/archlint/src/detectors/hygiene/dead_code.rs lines 264-266, update
build_usage_index and check_local_usages to retain usages per file and exclude
the candidate path when checking an export name. In
crates/archlint/tests/cli_detectors.rs lines 191-208, rename the export so it
differs from the imported binding and exercises SourceIndex’s trailing-segment
fallback.

Comment thread crates/archlint/src/snapshot/id.rs Outdated
Comment thread crates/archlint/tests/cli_detectors.rs
The Lint job runs `dtolnay/rust-toolchain@stable`, which has moved ahead
of the toolchain these were written against and now rejects the build:

  code_clone/engine.rs:324  consider using `sort_by_key`
  engine/runner.rs:347      called `map(<f>).unwrap_or(false)` on a Result
  utils/package.rs:26       called `map(<f>).unwrap_or(false)` on a Result

`context7.json` is missing its trailing newline, which prettier rejects.
It arrived in 351f919, pushed straight to main — the Lint job only runs
on pull requests and tags, so nothing checked it on the way in.

None of these files are otherwise touched by this branch; all four
predate it and would block any PR opened today. Mechanical fixes, no
behaviour change.
@fixcik
fixcik force-pushed the worktree-fix-detector-bugs branch from 9f81287 to eb659e8 Compare August 2, 2026 23:54
Two of the new tests did not test what they claimed.

`test_unresolved_import_still_keeps_a_file_alive` used an export whose
name matched the imported binding, so `check_local_usages` kept the file
alive on an identifier-name match and the unresolved-specifier fallback
was never exercised — the test passed with or without it. It now uses a
default export and a differently named local binding, so the trailing
segment of the specifier is the only thing left to match on. Verified by
disabling the fallback: the test fails, and passes again once restored.

The cyclomatic_complexity tests all assert an absence, so they would
have kept passing if the detector stopped reporting entirely. Added the
same fixture with nothing silencing it as a positive control.

Also hardened `id_for_package_cycle`: joining names with "|" let
["a|b", "c"] and ["a", "b|c"] hash to the same ID. NUL is the one byte a
path component cannot contain.

Reported by CodeRabbit on #39.
@fixcik

fixcik commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — two of these were real, and one of them caught a test of mine that was passing for the wrong reason. Addressed in 3efe5bf.

Applied

local_usages masks the test it should cover — correct, and the more important half of the finding. test_unresolved_import_still_keeps_a_file_alive exported widget and imported the binding widget, so check_local_usages kept the file alive on an identifier-name match; the SourceIndex fallback was never reached and the test passed with or without it. It now uses a default export (no export name to match on) and a differently named local binding. Verified it is no longer vacuous by stubbing out key_for_source's trailing-segment branch: the test fails, and passes again once restored.

Positive control for the complexity tests — right, all three asserted an absence and would have survived the detector going silent. Added the same fixture with nothing silencing it.

Ambiguous separator in id_for_package_cycle — fair, given the PR is specifically about ID collisions. Switched to NUL, the one byte a path component cannot contain, with a test for ["a|b", "c"] vs ["a", "b|c"].

Not applied

Index local_usages per file and exclude the candidate path — this is pre-existing behaviour, not something the PR introduces. The previous check_local_usages iterated every file including the candidate itself, so a file's own identifiers kept its exports alive there too. I agree it is too loose, but tightening it turns previously-alive files into new findings, which belongs in its own change rather than riding along with a perf fix.

Extension-less key from the first dot — declining, for two reasons. It matches the old matches_source, which used file_name.rfind('.'), i.e. the same last-dot split, so this is not a regression. And the motivating case cannot arise: foo.d.ts has no runtime code, so it never enters the dependency graph, and find_dead_files only considers graph nodes — a declaration file cannot be reported as dead code in the first place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working core detectors Has changes in detectors

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant