Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";

import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";

/** Every specifier the static analysis recorded as a loader call. */
const requireSpecifiers = (source: string): readonly unknown[] =>
analyzeJavaScriptStaticSource(source)
.references.filter((reference) => reference.kind === "require")
.map((reference) => (reference as { specifier?: unknown }).specifier);

describe("static require loader calls", () => {
it("records a require call that no binding shadows", () => {
expect(requireSpecifiers('const loaded = require("./real.cjs");')).toEqual([
"./real.cjs",
]);
});

it("records a member loader call", () => {
expect(
requireSpecifiers('const loaded = module.require("./member.cjs");'),
).toEqual(["./member.cjs"]);
});

it("records a loader a createRequire binding provides", () => {
expect(
requireSpecifiers(`
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const loaded = require("./created.cjs");
`),
).toEqual(["./created.cjs"]);
});

it("follows an aliased createRequire import from node:module", () => {
expect(
requireSpecifiers(`
import { createRequire as makeRequire } from "node:module";
const require = makeRequire(import.meta.url);
const loaded = require("./aliased.cjs");
`),
).toEqual(["./aliased.cjs"]);
});

it("does not treat a local createRequire function as Node's factory", () => {
expect(
requireSpecifiers(`
function createRequire() { return () => "local"; }
const require = createRequire();
const loaded = require("./ghost.cjs");
`),
).toEqual([]);
});

it("keeps the CommonJS wrapper loader separate from globalThis.require", () => {
expect(
requireSpecifiers(`
globalThis.require = shim;
const loaded = require("./wrapper.cjs");
`),
).toEqual(["./wrapper.cjs"]);
});

it("keeps an uninitialized top-level var redeclaration of the wrapper loader", () => {
expect(requireSpecifiers('var require; require("./wrapper.cjs");')).toEqual(
["./wrapper.cjs"],
);
});

it("ignores a require call that a local binding shadows", () => {
expect(
requireSpecifiers(`
function run(require) {
return require("./ghost.cjs");
}
console.log(run(() => "local"));
`),
).toEqual([]);
});
});
96 changes: 94 additions & 2 deletions src/domain/javascript/javascriptStaticAnalysisCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
} from "./javascriptStaticAnalysisState.js";
import {
resolveSemanticBindingState,
semanticStableInitializer,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import {
Expand All @@ -57,7 +58,12 @@
const first = stringValue(node.arguments[0]);
const module = context.accumulator.moduleRangeIndex.find(node.start);
const moduleRequireName = module?.requireName ?? null;
const requireCall = requireCallSpecifier(node, name, moduleRequireName);
const requireCall = requireCallSpecifier(
node,
name,
moduleRequireName,
isRequireLoaderReference(node.callee, bindings, name),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate loader provenance checks to require calls

isRequireLoaderReference is evaluated eagerly for every call and new expression, although requireCallSpecifier reads the result only for the exact name require; ordinary local and global calls now traverse semantic scope chains, sometimes again through initializer import provenance. On large minified bundles where calls dominate nodes, this adds avoidable analysis CPU and time proportional to all call sites, so guard this lookup with name === "require" or make the argument lazy.

AGENTS.md reference: AGENTS.md:L49-L49

Useful? React with 👍 / 👎.

);
if (requireCall !== null)
addReference(context, {
node,
Expand Down Expand Up @@ -210,12 +216,98 @@
accumulator.mediaElementPreloads.add(property);
};

/**
* A `require` name is the loader unless a lexical binding replaces it. A binding
* proven to come from `createRequire(...)` is that loader; any other binding,
* such as a function parameter, is a different value that only shares the name.
*/
const isRequireLoaderReference = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string,
): boolean => {
const binding = resolveSemanticBindingState(state, callee, name);
if (binding === undefined) {
// Node's CommonJS wrapper supplies a lexical `require` parameter. Writes
// to globalThis.require do not replace it; retain that possibility for
// script/CommonJS source where wrapper mode is possible.
if (name === "require" && isScriptProgram(callee, state)) return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Replaced loaders invent dependencies

The new script shortcut ignores direct assignments to require. With require = () => "local"; require("./ghost.cjs");, no lexical binding is created, so this branch returns true even though the source replaced the loader. The previous isUnmodifiedGlobalReference check rejected this visible write. The analyzer now records a dependency for a call that loads nothing.

Keep the distinction between globalThis.require and the wrapper's require, but still reject direct writes to the latter.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/javascript/javascriptStaticAnalysisCalls.ts
Line: 234

Comment:
**Replaced loaders invent dependencies**

The new script shortcut ignores direct assignments to `require`. With `require = () => "local"; require("./ghost.cjs");`, no lexical binding is created, so this branch returns `true` even though the source replaced the loader. The previous `isUnmodifiedGlobalReference` check rejected this visible write. The analyzer now records a dependency for a call that loads nothing.

Keep the distinction between `globalThis.require` and the wrapper's `require`, but still reject direct writes to the latter.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

return isUnmodifiedGlobalReference(callee, state, name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Separate the CJS loader from globalThis.require

In a .cjs input such as globalThis.require = shim; require("./dep"), the assignment changes a global-object property but not Node's wrapper-local loader parameter. isUnmodifiedGlobalReference rejects the call because the ambient-write index covers ["global", "require"] (javascriptSemanticAmbientWrites.ts:280-293), so this change deletes a real static/application import edge even though no lexical require was shadowed; keep the CommonJS wrapper binding distinct from globalThis.require.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

}
if (
name === "require" &&
isTopLevelCommonJsVarRequire(binding, callee, state)
)
return true;
const initializer = semanticStableInitializer(binding, state)?.node;
Comment on lines +229 to +242

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve uninitialized top-level var require

For a .cjs input such as var require; require("./dep.cjs"), the semantic index resolves a program binding but it has no stable initializer, so this branch returns false and the static/application import edge is lost. An uninitialized top-level var only redeclares Node's wrapper parameter and does not replace its loader value; hasUninitializedTopLevelVarRequire in src/domain/referenceSourceRequireBindings.ts:39-40 and 91-111 already preserves this exact case, so keep that no-op wrapper redeclaration as a possible loader rather than treating it as absent.

AGENTS.md reference: AGENTS.md:L21-L21

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve loader calls before top-level var initializers

In a script/CommonJS file with require("./real.cjs"); var require = shim;, the top-level var shares Node's wrapper parameter and its initializer does not run until after the first call, so that call still uses the loader. This whole-binding initializer lookup sees shim for every reference and rejects the earlier call, removing the static reference and application import edge. Fresh evidence beyond the prior uninitialized-var case is execution ordering: a later initializer must not retroactively classify an earlier reference, so preserve the loader possibility until a write that can precede that call.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

const expression =
initializer !== undefined && t.isVariableDeclarator(initializer)
? initializer.init
: initializer;
return (
expression !== null &&
expression !== undefined &&
t.isCallExpression(expression) &&

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unwrap type-only createRequire initializers

When TypeScript writes a loader as const require = createRequire(import.meta.url) as NodeRequire, semanticStableInitializer returns the TSAsExpression, so this strict CallExpression check is false even though the assertion is erased at runtime. A subsequent require("./dep.cjs") is therefore omitted from references and from the application imports edge produced in JavaScriptArtifactGraphFindings.ts; this is fresh syntax evidence beyond the earlier bare-createRequire case, because that bare form now works while the parser's supported type-only wrappers still need unwrapJavaScriptExpression before classification.

AGENTS.md reference: AGENTS.md:L23-L23

Useful? React with 👍 / 👎.

isNodeCreateRequireFactory(expression.callee, state)
Comment on lines +242 to +251

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Loader properties become imports

isRequireLoaderReference ignores the initializer's projection. With import { createRequire } from 'node:module'; const { resolve: require } = createRequire(import.meta.url); require('./ghost.cjs');, the local require calls the loader's resolve method, not the loader. The new check still records ./ghost.cjs as a dependency even though the call never loads it.

Require an empty projection before treating the binding as the returned loader, and add a regression test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/javascript/javascriptStaticAnalysisCalls.ts
Line: 242-251

Comment:
**Loader properties become imports**

`isRequireLoaderReference` ignores the initializer's `projection`. With `import { createRequire } from 'node:module'; const { resolve: require } = createRequire(import.meta.url); require('./ghost.cjs');`, the local `require` calls the loader's `resolve` method, not the loader. The new check still records `./ghost.cjs` as a dependency even though the call never loads it.

Require an empty `projection` before treating the binding as the returned loader, and add a regression test.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment on lines +250 to +251

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Follow stable aliases of the CommonJS loader

In a CommonJS module using const loader = require; { const require = loader; require("./dep.cjs"); }, semanticStableInitializer returns the loader identifier, but this call-expression-only gate returns false, so the static reference and the imports edge created from it in src/application/javascript/JavaScriptArtifactGraphFindings.ts:82-162 disappear. Fresh evidence beyond the createRequire threads is that this binding is a direct alias of the ambient wrapper loader rather than another factory-import spelling; follow stable identifier aliases (or retain the candidate) before rejecting the binding.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment on lines +250 to +251

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve loader parameters in AMD factories

For RequireJS/AMD sources using the simplified CommonJS form define(function (require) { return require("./dep"); }), that parameter is the real module loader, not a shadowing local. It has no stable initializer, so this createRequire-only predicate now returns false; the moduleRequireName escape hatch is not populated because javascriptStaticAnalysisBundler.ts:42-49 only accepts webpack/rspack chunk-array registrations. The static reference and downstream imports edge that the previous name-based scanner emitted therefore disappear for every such factory, so recognize the define factory's loader parameter before rejecting resolved bindings.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

);
};

const isNodeCreateRequireFactory = (
callee: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean => {
if (!t.isIdentifier(callee)) return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Namespace-created loaders lose dependencies

The identifier-only check rejects real loaders created through a namespace import. For example, import * as mod from "node:module"; const require = mod.createRequire(import.meta.url); require("./real.cjs"); was recognized before this change. Now isNodeCreateRequireFactory rejects mod.createRequire, so ./real.cjs disappears from the static references.

Check the member's module origin rather than rejecting every member call, and add a regression test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/javascript/javascriptStaticAnalysisCalls.ts
Line: 259

Comment:
**Namespace-created loaders lose dependencies**

The identifier-only check rejects real loaders created through a namespace import. For example, `import * as mod from "node:module"; const require = mod.createRequire(import.meta.url); require("./real.cjs");` was recognized before this change. Now `isNodeCreateRequireFactory` rejects `mod.createRequire`, so `./real.cjs` disappears from the static `references`.

Check the member's module origin rather than rejecting every member call, and add a regression test.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Accept namespace-imported createRequire factories

When ESM uses import * as Module from "node:module"; const require = Module.createRequire(import.meta.url); require("./dep.cjs"), the factory callee is a MemberExpression, so this guard returns false even though the namespace binding's directOrigins proves the same Node export; the resulting static reference and application imports edge disappear. Fresh evidence beyond the earlier aliased named-import report is this final identifier-only guard, so resolve the root import plus the createRequire member path as the neighboring worker-import logic does.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

const binding = resolveSemanticBindingState(state, callee, callee.name);
const [origin] = binding?.directOrigins ?? [];
return (
binding?.runtimeImport === true &&
binding.definitions.length === 1 &&
binding.directOrigins.length === 1 &&
Comment on lines +263 to +265

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 CommonJS loader dependencies disappear

isNodeCreateRequireFactory rejects factories loaded through CommonJS. For example, const { createRequire } = require("node:module"); function run() { const require = createRequire(__filename); return require("./real.cjs"); } run(); creates a real loader, but the factory binding has runtimeImport: false and no directOrigins. The new check therefore drops ./real.cjs from references and the application graph.

Recover the factory's CommonJS origin instead of requiring an ESM import, and add a regression test.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/javascript/javascriptStaticAnalysisCalls.ts
Line: 263-265

Comment:
**CommonJS loader dependencies disappear**

`isNodeCreateRequireFactory` rejects factories loaded through CommonJS. For example, `const { createRequire } = require("node:module"); function run() { const require = createRequire(__filename); return require("./real.cjs"); } run();` creates a real loader, but the factory binding has `runtimeImport: false` and no `directOrigins`. The new check therefore drops `./real.cjs` from `references` and the application graph.

Recover the factory's CommonJS origin instead of requiring an ESM import, and add a regression test.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

origin !== undefined &&
(origin.specifier === "node:module" || origin.specifier === "module") &&
origin.importedPath.length === 1 &&
origin.importedPath[0] === "createRequire"
);
};

const isScriptProgram = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean => {
let parent = state.parentsByNode.get(node);
while (parent !== undefined && !t.isProgram(parent))
parent = state.parentsByNode.get(parent);
return (
parent !== undefined &&
(parent.sourceType === "script" || parent.sourceType === "commonjs")

Check failure on line 282 in src/domain/javascript/javascriptStaticAnalysisCalls.ts

View workflow job for this annotation

GitHub Actions / Docs Generation

This comparison appears to be unintentional because the types '"module"' and '"commonjs"' have no overlap.

Check failure on line 282 in src/domain/javascript/javascriptStaticAnalysisCalls.ts

View workflow job for this annotation

GitHub Actions / Build package

This comparison appears to be unintentional because the types '"module"' and '"commonjs"' have no overlap.

Check failure on line 282 in src/domain/javascript/javascriptStaticAnalysisCalls.ts

View workflow job for this annotation

GitHub Actions / Static checks

This comparison appears to be unintentional because the types '"module"' and '"commonjs"' have no overlap.
);
};

const isTopLevelCommonJsVarRequire = (
binding: NonNullable<ReturnType<typeof resolveSemanticBindingState>>,
node: t.Node,
state: JavaScriptSemanticAnalysisState,
): boolean =>
binding.name === "require" &&
state.scopesById.get(binding.scopeId)?.kind === "program" &&
binding.definitions.length > 0 &&
binding.definitions.every(({ kind }) => kind === "variable") &&
binding.initializers.length === 0 &&
Comment on lines +294 to +295

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Let bindings become loaders

isTopLevelCommonJsVarRequire also accepts let require;. Both var and let declarations get the semantic kind "variable", and a top-level declaration without a value has no initializer. Thus let require; function run() { return require("./ghost.cjs"); } passes this check even though let creates a separate binding, not a wrapper redeclaration. The analyzer records a false loader reference even when run is never called.

Check that the actual declaration uses var, and add a negative test for let.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/domain/javascript/javascriptStaticAnalysisCalls.ts
Line: 294-295

Comment:
**Let bindings become loaders**

`isTopLevelCommonJsVarRequire` also accepts `let require;`. Both `var` and `let` declarations get the semantic kind `"variable"`, and a top-level declaration without a value has no initializer. Thus `let require; function run() { return require("./ghost.cjs"); }` passes this check even though `let` creates a separate binding, not a wrapper redeclaration. The analyzer records a false loader reference even when `run` is never called.

Check that the actual declaration uses `var`, and add a negative test for `let`.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

isScriptProgram(node, state);

const requireCallSpecifier = (
node: JavaScriptCallLike,
name: string,
moduleRequireName: string | null,
isGlobalRequire: boolean,
): { readonly specifier: string | undefined } | null => {
if (name === "require" || name.endsWith(".require"))
// A name alone does not make a loader call: a local binding named `require`
// shadows the ambient loader, so only an unshadowed reference to it counts.
// The module's own factory require name is a real binding, so it keeps its
// own lane below.
if (name === "require" && isGlobalRequire)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore member-form require calls

For calls such as module.require("./dep.cjs"), calleeName returns "module.require", so this exact-name condition is false and neither later branch accepts the call. The previous name.endsWith(".require") lane was removed rather than preserved, causing these real loader calls to disappear from references and from downstream application import edges; keep a separate member-call branch while applying the shadow check only to bare require.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Comment thread
greptile-apps[bot] marked this conversation as resolved.
Resolved
return { specifier: argumentValue(node.arguments[0]) };
if (name.endsWith(".require"))
return { specifier: argumentValue(node.arguments[0]) };
if (name === "__webpack_require__" || name === moduleRequireName)
return { specifier: argumentValue(node.arguments[0]) };
Expand Down
Loading