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
+31
View File
@@ -1529,10 +1529,41 @@ export function resolvePhpImportedStaticCall(
return { original: ref, targetNodeId: methods[0]!.id, confidence: 0.95, resolvedBy: 'import' };
}
const JS_MODULE_LANGUAGES: ReadonlySet<string> = new Set(['javascript', 'jsx', 'typescript', 'tsx', 'arkts']);
/**
* A module specifier written as a path — relative (`./x`, `../x`), rooted, or
* an alias with a folder in it (`@/lib/x`, `~/x`, `$lib/x`) — rather than an
* imported binding's name or a bare package (`react`, `lodash`).
*/
function isJsPathSpecifier(name: string): boolean {
return name.startsWith('./') || name.startsWith('../') || name === '.' || name === '..' ||
(name.includes('/') && !/\s/.test(name) && !/^@[\w.-]+\/[\w.-]+$/.test(name));
}
/**
* A JS/TS `imports` reference that names a module by path. It names a FILE, not
* a symbol, so it skips the resolver's name-exists pre-filter — a CommonJS
* `require('./x')` has no import node of that name to pass it.
*/
export function isJsPathImportRef(ref: UnresolvedRef): boolean {
return ref.referenceKind === 'imports' && JS_MODULE_LANGUAGES.has(ref.language) && isJsPathSpecifier(ref.referenceName);
}
export function resolveViaImport(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// A JS/TS module specifier — `import './polyfills'`, the module of `import x
// from '../lib/a'`, a CommonJS `require('./application')` — names a FILE,
// found the way the runtime finds it: extensions, `index` files, path
// aliases. Matching the basename instead missed every extensionless one
// (the common spelling) and could land on a same-named file elsewhere.
if (isJsPathImportRef(ref)) {
const file = resolveImportPath(ref.referenceName, ref.filePath, ref.language, context);
const fileNode = file && file !== ref.filePath ? context.getNodesInFile(file).find((n) => n.kind === 'file') : undefined;
if (fileNode) return { original: ref, targetNodeId: fileNode.id, confidence: 0.9, resolvedBy: 'import' };
}
// C/C++ #include references — resolve directly to the included file
// (file→file edge), bypassing symbol lookup. The extractor emits these
// with `referenceKind: 'imports'` and `referenceName: <include path>`
+5 -3
View File
@@ -26,7 +26,7 @@ import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibili
import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor';
import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility';
import { gateTypeParameter, clearTypeParameterMemos } from './type-parameters';
import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver';
import { resolveViaImport, resolvePhpImportedStaticCall, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, isJsPathImportRef, isBoundToOutOfRepoImport, clearImportResolverMemos, resolveImportPath, isExternalImport } from './import-resolver';
import { ResolverPool, minRefsForPool } from './resolver-pool';
import { resolveAliasBinding } from './alias-binding';
import { detectFrameworks } from './frameworks';
@@ -1100,8 +1100,9 @@ export class ReferenceResolver {
// ArkTS chained-attribute refs carry a leading dot (`.titleStyle`) that
// routes them to the decorator-gated matcher; the symbol itself is
// indexed under the bare name, so the existence check strips the dot.
// Nix static path imports (`import ./x.nix`) name a FILE, not a symbol —
// they bypass the symbol-existence check and resolve via resolveViaImport.
// Nix static path imports (`import ./x.nix`) and JS/TS module paths
// (`require('./x')`) name a FILE, not a symbol — they bypass the
// symbol-existence check and resolve via resolveViaImport.
let existenceName =
ref.language === 'arkts' && ref.referenceName.startsWith('.')
? ref.referenceName.slice(1)
@@ -1112,6 +1113,7 @@ export class ReferenceResolver {
const tPre = this.profileStages ? process.hrtime.bigint() : 0n;
const preFilterPass =
isNixPathImportRef(ref) ||
isJsPathImportRef(ref) ||
this.hasAnyPossibleMatch(existenceName) ||
// PHP, Pascal, CFML, COBOL and VB.NET names ignore case: `formatprice()`
// calls `FormatPrice`, which the exact-name set never lists.