mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 10:28:10 +08:00
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:
co-authored by
Claude Opus 5.5
parent
95f6f0f4a5
commit
c4babfce6a
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user