fix(ingestion): let type-annotated params shadow Nuxt auto-imports

hasLocalBindingInScopeChain only consulted scope.bindings, but type-annotated
function parameters live in scope.typeBindings (the TS scope query records them
as `@type-binding.parameter`, not `@declaration`). A parameter named like a
composable therefore failed to suppress the auto-import, leaking a spurious
CALLS edge.

Also check scope.typeBindings for the name (same-file scopes only). typeBindings
holds value-space binders' type facts (parameter annotations, `self`, variable
annotations) and never a pure type that belongs to callable space, so this
cannot over-suppress a real auto-import.

Documents the residual: function-typed params (`p: () => void`), untyped params,
destructured locals, and catch-clause vars are captured by neither map and still
leak — closing that needs shared scope-query changes beyond this feature, left
as a follow-up. Also adds a no-vacuous-pass guard to the shadowing/noise test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4W25WLfYD1JNy8icxeLPU
This commit is contained in:
Gergo Magyar 2026-06-24 08:46:47 +00:00
parent 8571d9e090
commit 7d8743327a
3 changed files with 36 additions and 0 deletions

View file

@ -322,6 +322,26 @@ function hasLocalBindingInScopeChain(
return true;
}
// Type-annotated function parameters — and other value-space type facts
// such as `self` and variable annotations — live in `scope.typeBindings`,
// not `scope.bindings`. A parameter named like a composable genuinely
// shadows the auto-import, and typeBindings never holds a pure type that
// belongs to callable space, so a same-file presence check here cannot
// over-suppress a legitimate auto-import.
//
// Residual (known limitation): this catches parameters whose annotation the
// TS scope query records as a type-binding (`p: Named`, generics, unions,
// predefined, arrays). Function-typed params (`p: () => void`), untyped
// params, destructured locals (`const { x } = …`), and catch-clause vars
// are captured by NEITHER map — the scope query emits no `@declaration` /
// `@type-binding` for them — so those shadow forms still leak an edge.
// Closing that needs shared TS scope-query/extractor changes that alter call
// resolution beyond Nuxt, so it is deferred to a follow-up rather than fixed
// here.
if (scope.filePath === filePath && scope.typeBindings.has(name)) {
return true;
}
cursor = scope.parent;
}

View file

@ -0,0 +1,3 @@
export function renderTyped(validate: ValidateFn) {
validate();
}

View file

@ -103,11 +103,24 @@ describe('Nuxt/Nitro auto-import scope resolution', () => {
it('does not emit auto-import edges for local shadowing or lexical noise', () => {
const calls = nuxtCalls();
// Guard against a vacuous pass: the feature must have emitted edges elsewhere.
expect(calls.length).toBeGreaterThan(0);
expect(calls.filter((edge) => edge.sourceFilePath.endsWith('pages/local.ts'))).toHaveLength(0);
expect(calls.filter((edge) => edge.sourceFilePath.endsWith('pages/noise.ts'))).toHaveLength(0);
});
it('does not emit an auto-import edge when a typed parameter shadows the name', () => {
const calls = nuxtCalls();
expect(calls.length).toBeGreaterThan(0);
// pages/param-typed.ts has `function renderTyped(validate: ValidateFn)` and
// calls validate() — the type-annotated parameter (in scope.typeBindings)
// shadows the composable, so no nuxt edge is emitted.
expect(
calls.filter((edge) => edge.sourceFilePath.endsWith('pages/param-typed.ts')),
).toHaveLength(0);
});
it('allows type-only local declarations to coexist with value auto-import calls', () => {
const calls = nuxtCalls();