fix(checks): detect clones that differ only in literal values (CD-331) - #216
Merged
Conversation
`Refactor.DuplicateBlock` missed structurally identical blocks whose literals differ. The ticket blamed unmodified identifiers; that was wrong — identifiers were already canonicalised to window-relative positional indices, verified with a fixture that renames every one and still fires. Literals were hashed by value, so three worker files differing in three strings produced nothing. Literals are now normalised to positional placeholders too, behind `normalize_literals` (default on). Two hashes are computed per window in one pass so a group can be told apart as verbatim or merely near. Validating against five real repos surfaced two consequences: - Normalising module specifiers made every run of `import` statements match every other one (one repo went 8 -> 66 findings). Imports and re-exports are now excluded from statement windows outright. - The remaining rise was dominated by test setup blocks differing in their fixture values. Since `--fail-on` defaults to medium, reporting those under the existing check would turn CI red for duplication nobody chose to gate on. Severity is stamped per check id, not per issue, so near-clones move to a new `Refactor.NearDuplicateBlock` at low severity. Medium-and-above counts now sit at or below baseline on four of five repos. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019afugU2bTYCtXn6Vy9dn1v
Criterion benchmark comparison (PR head vs base branch) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug, and the correction to its diagnosis
Refactor.DuplicateBlockmissed structurally identical blocks whose literals differ — the three 23-line worker files in the report produced zero findings.The ticket's diagnosis was wrong. It blamed unmodified identifiers. Identifiers were already canonicalised to window-relative positional indices; I verified this with a three-file fixture in which every identifier differs and the check fires correctly. Change only the string literals and it goes silent. Literals were the gap:
HashOp::Str/HashOp::Numwere hashed by value.hash_opsnow computes two hashes per window in a single pass — normalised (literals as positional placeholders,Str#/Num#prefixes distinct from the identifier ones) and exact. Behindnormalize_literals, default on.Two things real-repo validation caught
Validated against all five local repos that use cofferdam rather than fixtures alone.
Import blocks became mutual duplicates. Normalising module specifiers made every run of
importstatements hash equal — projektor went from 8 findings to 66, almost all on line 1. An import list cannot be extracted into a shared helper, so the finding is never actionable. Imports,export * fromandexport { x } fromare now excluded from statement windows outright — not merely as window starts, so a window cannot straddle into real code. A plainexport { x }orexport functionstill participates.The remainder would have turned CI red. After that fix the estate still rose several-fold, dominated by test setup blocks differing in fixture values. Real duplication, but
--fail-ondefaults tomediumand this is a medium check. Emitting near-clones at a lower severity is not possible — the engine stamps severity per check id in a post-pass, so a check cannot vary it per issue. Hence a second id:Refactor.DuplicateBlock— unchanged id, priority,mediumseverity and message; now reports verbatim clones only.Refactor.NearDuplicateBlock— new,low, reports the literal-drift groups. Prints without failing a build; raise its severity in config to gate on it.Both read one corpus slot (only
DuplicateBlock::runwrites) and share a single grouping-and-overlap-claim pass, so the two never report the same region twice.Measured outcome
The missed clone class is now caught and the gate-tripping count fell on four of five repos.
projektor's
+1is explained and accepted:apps/api/src/routes/task-statuses.ts:13is a genuine duplicate whose author suppressed it with a comment anchored to a preceding import line. Excluding imports moved the primary span out from under that anchor. Detection was right both times; only the anchor moved. Documented as an upgrade note in the CHANGELOG and on the check's page — move the comment to the first statement of the block. The underlying fragility is filed as CD-337 rather than fixed here, since it is engine-level suppression machinery.Full-run wall time on projektor is unchanged (0.78s vs 0.79s warm), so the two checks sharing the grouping pass costs nothing measurable.
Verification
cargo build --workspace,cargo test --workspace(1437 passed),cargo clippy --workspace --all-targets -D warnings,cargo fmt --check,gen-docs --checkvia pre-commit. Plus the original reproducer, which now reports underRefactor.NearDuplicateBlockatlow.🤖 Generated with Claude Code
https://claude.ai/code/session_019afugU2bTYCtXn6Vy9dn1v