fix(js): a CommonJS require and a module path import the file they name (#2263)

* fix(js): a CommonJS require and a module path import the file they name

A `require('./application')` was only a call, so a CommonJS file depended
on nothing at file level: Express's lib/express.js showed no imports and
impact missed the files it loads. Local require specifiers are now read
after extraction (both the wasm extractor and the kernel, including its
deferred-decode transport) as `imports` refs from the file, and a JS/TS
module path resolves to its file the way the runtime finds it —
extensions, index files, aliases — which also lands an extensionless
`import './polyfills'` on its file instead of its own import node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(js): read require imports inside each extractor, so kernel parity holds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-01 20:37:37 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 95f6f0f4a5
commit c4babfce6a
11 changed files with 225 additions and 4 deletions
+68
View File
@@ -0,0 +1,68 @@
/**
* CommonJS `require('…')` as an import.
*
* An ESM `import … from './application'` is an `import_statement` the
* extractor turns into an `imports` reference from the file, which the import
* resolver lands on the module's file. A `require('./application')` is just a
* call, so a CommonJS file depended on nothing at file level: Express's
* `lib/express.js` requires `./application`, `./request` and `./response` and
* the index said it imports nothing — an empty Imports rail in the viewer, no
* link on the Map, and no file dependency for impact. A binding used only as a
* value (`mixin(app, proto)`) never made a call edge to stand in for it either.
*
* Read from the source after extraction rather than in a walker, so the wasm
* extractor and the native kernel get it alike — including the kernel's
* deferred-decode transport, which carries these beside its buffers. Only the
* module's own file reference is emitted — the names a binding is called
* through already resolve through the require binding (import-resolver's
* CommonJS reader).
*/
import type { Language, UnresolvedReference } from '../types';
import { stripCommentsForRegex } from '../resolution/strip-comments';
const COMMONJS_LANGUAGES: ReadonlySet<Language> = new Set(['javascript', 'jsx', 'typescript', 'tsx']);
/**
* `require('x')` with a literal specifier, not a member's (`foo.require(…)`)
* or a property (`require.resolve(…)`). A template literal counts only when
* it has no `${…}` hole.
*/
const REQUIRE_CALL = /(?<![\w$.])require\s*\(\s*(?:'([^'\n]+)'|"([^"\n]+)"|`([^`$\n]+)`)\s*\)/g;
/**
* A module of this project: a relative or rooted path, or a project alias
* (`@/lib/x`, `~/x`, `#internal`). A package (`express`, `lodash/fp`) is
* outside the index — as a name it would only match some same-named symbol.
*/
const LOCAL_SPECIFIER = /^(?:\.{1,2}(?:\/|$)|\/|[@~]\/|#)/;
/** `import x = require('y')` is TypeScript's import-equals: an `import_statement` already. */
const IMPORT_EQUALS = /\bimport\s+(?:type\s+)?[\w$]+\s*=\s*$/;
/** The file node both extractors mint: `file:<path>`. */
export function commonJsRequireRefs(filePath: string, source: string, language: Language): UnresolvedReference[] {
if (!COMMONJS_LANGUAGES.has(language) || !source.includes('require')) return [];
const safe = stripCommentsForRegex(source, language === 'javascript' || language === 'jsx' ? 'javascript' : 'typescript');
const out: UnresolvedReference[] = [];
const seen = new Set<string>();
REQUIRE_CALL.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = REQUIRE_CALL.exec(safe)) !== null) {
const specifier = (m[1] ?? m[2] ?? m[3] ?? '').trim();
if (!LOCAL_SPECIFIER.test(specifier) || seen.has(specifier)) continue;
const lineStart = safe.lastIndexOf('\n', m.index - 1) + 1;
if (IMPORT_EQUALS.test(safe.slice(lineStart, m.index))) continue;
seen.add(specifier);
out.push({
fromNodeId: `file:${filePath}`,
referenceName: specifier,
referenceKind: 'imports',
line: safe.slice(0, m.index).split('\n').length,
column: m.index - lineStart,
filePath,
language,
});
}
return out;
}
+1
View File
@@ -2229,6 +2229,7 @@ export class ExtractionOrchestrator {
language,
buffers: result.kernelBuffers,
file: this.buildFileRecord(filePath, content, language, stats, nodeCount, result.errors),
...(result.unresolvedReferences.length > 0 ? { extraRefs: result.unresolvedReferences } : {}),
});
} else {
storeWriter.send(this.buildFreshStoreBundle(filePath, content, language, stats, result));
+5
View File
@@ -15,6 +15,7 @@
*/
import type { ExtractionResult, Language } from '../../types';
import { commonJsRequireRefs } from '../commonjs-requires';
import { EXTRACTORS } from '../languages';
import { getKernel, kernelSupports } from './loader';
import { decodeExtractBuffers } from './decode';
@@ -269,6 +270,8 @@ export function materializeKernelResult(
language
);
decoded.durationMs = result.durationMs;
// References read beside the tables (a CommonJS `require`) ride on the transport.
if (result.unresolvedReferences.length > 0) decoded.unresolvedReferences.push(...result.unresolvedReferences);
return decoded;
}
@@ -294,6 +297,8 @@ export function tryKernelExtract(
const buffers = kernel.extractFile(filePath, pre, language);
const result = decodeExtractBuffers(buffers, filePath, language);
POST_PASSES[language]?.(result, source);
// Read beside the tables, as TreeSitterExtractor does (a CommonJS `require`).
result.unresolvedReferences.push(...commonJsRequireRefs(filePath, source, language));
result.durationMs = Date.now() - t0;
return result;
} catch (err) {
+3 -1
View File
@@ -17,6 +17,7 @@ import { parentPort } from 'worker_threads';
import { extractFromSource } from './tree-sitter';
import { detectLanguage, loadGrammarsForLanguages, resetParser } from './grammars';
import { tryKernelExtractRaw } from './kernel';
import { commonJsRequireRefs } from './commonjs-requires';
import { getAllFrameworkResolvers, getApplicableFrameworks } from '../resolution/frameworks';
import type { Language, ExtractionResult } from '../types';
@@ -103,7 +104,8 @@ parentPort!.on('message', async (msg: { type: string; id?: number; filePath?: st
result = {
nodes: [],
edges: [],
unresolvedReferences: [],
// Read beside the kernel's tables (extractFromSource's own post-step).
unresolvedReferences: commonJsRequireRefs(filePath!, content!, language),
errors: raw.errors,
durationMs: 0,
kernelBuffers: raw.buffers,
+1
View File
@@ -70,6 +70,7 @@ function decodeKernelBundle(bundle: KernelStoreBundle): StoreBundle {
bundle.filePath,
bundle.language
);
if (bundle.extraRefs?.length) decoded.unresolvedReferences.push(...bundle.extraRefs);
return finalizeStoreBundle(decoded, bundle.filePath, bundle.language, bundle.file);
}
+2
View File
@@ -30,6 +30,8 @@ export interface KernelStoreBundle {
language: Language;
buffers: NonNullable<ExtractionResult['kernelBuffers']>;
file: FileRecord;
/** References read beside the kernel's tables (a CommonJS `require`). */
extraRefs?: ExtractionResult['unresolvedReferences'];
}
/**
+5
View File
@@ -32,6 +32,7 @@ import { VueExtractor } from './vue-extractor';
import { MyBatisExtractor } from './mybatis-extractor';
import { CfmlExtractor } from './cfml-extractor';
import { tryKernelExtract, takeDeferredPreParse } from './kernel';
import { commonJsRequireRefs } from './commonjs-requires';
import {
getAllFrameworkResolvers,
getApplicableFrameworks,
@@ -657,6 +658,10 @@ export class TreeSitterExtractor {
if (packageNodeId) this.nodeStack.pop();
this.nodeStack.pop();
// A CommonJS `require('./x')` is a file import, like ESM's `import`. The
// kernel reads the same (kernel/index.ts and the parse worker's transport).
this.unresolvedReferences.push(...commonJsRequireRefs(this.filePath, this.source, this.language));
// hasError is routine for several grammars; warn only when no symbols survived.
const symbolCount = this.nodes.filter((n) => n.kind !== 'file').length;
if (this.tree?.rootNode.hasError && symbolCount === 0) {