fix: 🐛 address Sourcery review -- gate Nitro scan on imports.d.ts and pre-index explicit imports

This commit is contained in:
slugb0t 2026-06-04 00:31:05 -07:00
parent 97cfbbec88
commit ebf73aa7b6
2 changed files with 35 additions and 24 deletions

View file

@ -62,18 +62,24 @@ export interface NuxtAutoImportConfig {
* Load the Nuxt auto-import map for `repoRoot`.
*
* Returns null when:
* - `.nuxt/imports.d.ts` does not exist (non-Nuxt project), or
* - the file exists but yields zero project-local entries.
* - `.nuxt/imports.d.ts` does not exist (non-Nuxt project or pre-build), or
* - the file exists but yields zero project-local entries and `server/utils`
* is absent or empty.
*
* The `server/utils` scan is only attempted when `.nuxt/imports.d.ts` was
* successfully read, confirming this is an initialized Nuxt project. This
* avoids partial results from repos that have a `server/utils` directory but
* are not Nuxt projects.
*/
export async function loadNuxtAutoImports(repoRoot: string): Promise<NuxtAutoImportConfig | null> {
const byLocalName = new Map<string, NuxtAutoImportEntry>();
await collectImportsDts(repoRoot, byLocalName);
const nuxtInitialized = await collectImportsDts(repoRoot, byLocalName);
// Only attempt the server/utils scan when we confirmed this is a Nuxt project
// (imports.d.ts was present and populated the map, or at least the file existed).
const nuxtDirExists = await dirExists(path.join(repoRoot, '.nuxt'));
if (nuxtDirExists) {
// Only scan server/utils when imports.d.ts was present, confirming this is
// an initialized Nuxt project. Without this gate, a non-Nuxt repo with a
// server/utils directory would get spurious Nitro auto-import edges.
if (nuxtInitialized) {
await collectNitroServerUtils(repoRoot, byLocalName);
}
@ -93,16 +99,17 @@ export async function loadNuxtAutoImports(repoRoot: string): Promise<NuxtAutoImp
* included. Nuxt runtime paths (`#app/...`) and third-party packages are
* intentionally skipped because they have no graph nodes in the repo.
*/
/** Returns true when `.nuxt/imports.d.ts` was successfully read. */
async function collectImportsDts(
repoRoot: string,
byLocalName: Map<string, NuxtAutoImportEntry>,
): Promise<void> {
): Promise<boolean> {
const importsPath = path.join(repoRoot, '.nuxt', 'imports.d.ts');
let content: string;
try {
content = await fs.readFile(importsPath, 'utf-8');
} catch {
return;
return false;
}
const nuxtDir = path.join(repoRoot, '.nuxt');
@ -136,6 +143,7 @@ async function collectImportsDts(
}
}
}
return true;
}
// ---- server/utils (Nitro auto-imports) --------------------------------------

View file

@ -187,6 +187,22 @@ const typescriptScopeResolver: ScopeResolver = {
// Pre-index: localName -> entry for fast lookup during content scan.
const { byLocalName } = autoImports;
// Pre-build a file -> explicit-import-targets index so the per-file
// lookup below is O(1) rather than scanning all import edges every iteration.
const explicitImportsByFile = new Map<string, Set<string>>();
for (const [scopeId, edges] of indexes.imports) {
const scope = indexes.scopeTree.getScope(scopeId);
if (!scope?.filePath) continue;
let targets = explicitImportsByFile.get(scope.filePath);
if (!targets) {
targets = new Set<string>();
explicitImportsByFile.set(scope.filePath, targets);
}
for (const edge of edges) {
if (edge.targetFile !== null) targets.add(edge.targetFile);
}
}
// Regex matches bare identifier call sites: word-boundary + name + "(".
// Excludes `new X(` (constructor calls are not free-function auto-imports).
const CALL_RE = /(?<![.\w])([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g;
@ -198,15 +214,7 @@ const typescriptScopeResolver: ScopeResolver = {
const content = ctx.fileContents.get(filePath);
if (!content) continue;
// Collect files already brought in by an explicit import in this file.
const explicitImports = new Set<string>();
for (const [scopeId, edges] of indexes.imports) {
const scope = indexes.scopeTree.getScope(scopeId);
if (scope?.filePath !== filePath) continue;
for (const edge of edges) {
if (edge.targetFile !== null) explicitImports.add(edge.targetFile);
}
}
const explicitImports = explicitImportsByFile.get(filePath) ?? new Set<string>();
const fileId = generateId('File', filePath);
// Track (sourceFile) pairs already handled for this caller to avoid
@ -214,11 +222,6 @@ const typescriptScopeResolver: ScopeResolver = {
const emittedImports = new Set<string>();
const emittedCalls = new Set<string>();
// Skip source files: a file cannot auto-import its own exports.
// Without this guard the content scanner would match each exported
// function's own name inside its definition body and emit a self-loop.
const selfPath = filePath;
CALL_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = CALL_RE.exec(content)) !== null) {
@ -230,7 +233,7 @@ const typescriptScopeResolver: ScopeResolver = {
// Skip when the file already has an explicit import from this source,
// or when the file IS the source (a file cannot auto-import itself).
if (explicitImports.has(sourceFile) || sourceFile === selfPath) continue;
if (explicitImports.has(sourceFile) || sourceFile === filePath) continue;
// Emit one IMPORTS edge per (caller, sourceFile) pair.
if (!emittedImports.has(sourceFile)) {