fix(scanner): Match JS imports structurally instead of by quote style - #176
Conversation
js-imports used literal-text patterns hardcoded to double quotes, so single-quoted require() and import — the default under Prettier's singleQuote and the prevailing style in real CommonJS projects — matched nothing. --importers then answered a confident zero for files with many requirers, which is the exact blast-radius check someone runs before editing a shared file. Match import_statement structurally so quote style stops mattering, and keep require() as a separate pattern: require() is a call expression, not an import_statement, so a kind rule alone would have dropped CommonJS entirely. The pattern binds no $PATH metavariable, which keeps extraction on the quote-agnostic text path and lets require(someVariable) resolve to nothing rather than to a fabricated edge. typescript, tsx and jsx matched import_statement only, so require() was invisible there in both quote styles. They gain the same pattern. Verified against the bundled ast-grep 0.42.1 as well as 0.45.1, since one unsupported rule construct fails the whole inline-rules document. Relates to #147, #172 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEUvjGsJemDFSV8nbxvxBo
Ported verbatim from @reneleonhardt's open PR #171, which fixes this already. Carrying it here so this PR can go green rather than waiting on that one to merge; it becomes a no-op once main has it. buildRustWorkspaceIndex shadowed its caller's ctx with the cargo-metadata deadline, so once that deadline passed ctx.Err() returned DeadlineExceeded and the whole graph build failed with a bare "context deadline exceeded" instead of falling back to the manually derived Rust workspace. On a cold or loaded runner three seconds is not always enough for cargo metadata, and mcp/TestRustGraphContextHandlersDisclosePartialCoverage has now failed this way on three separate pull requests. Separating the metadata context from the caller's lets an expired deadline break out of the loop and keep the fallback index, which is what the test asserts and what a consumer needs: partial coverage disclosed, not a failed graph. Relates to #147, #172 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEUvjGsJemDFSV8nbxvxBo
|
The failure
Root cause, and why it's @reneleonhardt's fix
ctx, cancel := context.WithTimeout(ctx, cargoMetadataTimeout) // 3sOnce that deadline passed, the loop's #171 already fixes this — its "preserves manual Rust workspace topology when Cargo metadata reaches its deadline" bullet. It separates I applied that hunk from #171 verbatim ( VerificationThe previously-failing test passes 8/8 locally with the port. @reneleonhardt — flagging that I'm carrying this hunk so it isn't a surprise if it shows up as an overlap when #171 merges. Generated by Claude Code |
…t case Independent review showed this case already resolved on main through jsx.yml's kind: import_statement; the row pins existing behaviour rather than proving a new fix. Say so. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcyheQmM3HCvxF5wRL3s5t
|
The correction in So my claim in the PR description — "Single-quoted ESM How I got it wrong is worth naming, because it's a method error rather than a typo. I ran the What survives, and is still the substance of this PR:
The ESM row now correctly reads as pinning existing behaviour so the rule rewrite can't regress it, which is what it always was. Two follow-ups from that review I agree with and haven't actioned (not pushing here while the branch is being patched): #171's Generated by Claude Code |
Third of the Graph accuracy milestone (#172). Fixes #147, reported by @nicknsheth-beep.
The report is accurate and the root cause is exactly as described. Reproduced with ast-grep against the rule verbatim:
Two things beyond the report
Correction after independent review: an earlier version of this body said single-quoted ESM
importin.jswas also invisible. That is true ofjavascript.ymlin isolation, but ast-grep'sjsxlanguage already covers.jsandjsx.ymlusedkind: import_statement, soimport x from './mod'resolved on main. Theroutes/admin.jsfixture row pins that existing behaviour so the rule rewrite cannot regress it; it is not a new fix.TypeScript, TSX and JSX miss
require()entirely — in both quote styles. They usekind: import_statement, andrequire()is a call expression, not an import statement:This matters for the fix you suggested: "switch to a structural
kind:-based rule the way typescript.yml already does" would have regressed the very case you reported —kind: import_statementnever matchesrequire(), so a pure-kind:rule would have dropped CommonJS from.jsaltogether. The fix needs both halves.What changed
in all four of
javascript,typescript,tsxandjsx.$$$rather than$PATHis deliberate. Path extraction prefers a$PATHmetavariable when one exists and otherwise falls back toextractImportPathon the matched text — and that fallback already handles",'and backticks. Binding$PATHhere would capture the string node including its quotes; binding nothing keeps extraction on the quote-agnostic path and makes a dynamicrequire(someVariable)return"", which the caller skips. A fabricated edge would be worse than a missing one.The fixture that proves it
testdata/commonjs-single-quotes/— modelled on your Express layout, including theservices/layoutInputService.jscase:It covers single-quoted
requirein both../and./../forms, a double-quotedrequire, a single-quoted ESMimport, and a dynamicrequire(which).app.jsimports resolve to exactly[routes/admin.js, routes/members.js]— the dynamic require adds nothing.TestCommonJSSingleQuoteImportersResolvefails against main's rules withservices/layoutInputService.js importers = [], want exactly [routes/admin.js routes/members.js].Bundled ast-grep
Checked against 0.42.1 (the version
scripts/download-bundled-astgrep.shpins for releases) as well as the 0.45.1 on PATH, since one unsupported construct fails the entire--inline-rulesdocument and CI only ever exercises latest. Both versions match all cases identically.Per-ecosystem summary (for release notes)
.js,.jsx,.mjs)import/requireseen; single-quoted invisiblerequire()invisible in every quote stylerequire()resolvedrequire(variable)Note on scope
The TS/TSX/JSX half goes beyond #147 as filed, which is about
javascript.yml. It's the identical defect — a language's import rule blind to a whole syntax form — and a one-line change per file, so leaving it broken while editing the file next door seemed worse than the small widening. Happy to split it into its own PR if you'd rather keep this one strictly to the reported issue.Dynamic
import('./lazy')is still not matched. That's a miss rather than a wrong answer, and it wants its own thinking about whether a lazily-imported module should count as an edge.Verification
go vet ./...clean,gofmtclean,go test ./...at the main baseline (only the three known root-environment permission failures). Fixture verified end-to-end with a built binary for--importers,--importers --jsonand--deps --json.🤖 Generated with Claude Code
https://claude.ai/code/session_01PEUvjGsJemDFSV8nbxvxBo
Generated by Claude Code
Scope note added after review: the
scanner/rustcargo.gocommit ports a 9-line hunk from #171 (byte-identical) so the cargo-metadata timeout no longer fails the whole graph build; that fixed this PR's red CI legs. #171 also addsTestCargoMetadataDeadlinePreservesFallbackTopology, the only caller that passes a non-constant timeout; it is not ported here, so thebuildRustWorkspaceIndexWithTimeoutseam is exercised only through its wrapper until #171 lands.