mirror of
https://github.com/morluto/rea.git
synced 2026-10-18 20:10:13 -05:00
fix(javascript): retain CommonJS exports defined with Object.defineProperty (#4725)
* fix(javascript): retain CommonJS exports defined with Object.defineProperty
tsc, Babel and Rollup emit Object.defineProperty(exports, name, descriptor) for CommonJS re-exports. The semantic projection only recognised assignments, so these exports were missing from export_names and exposes edges. Recognise the call on exports or module.exports with the existing unmodified-global and receiver checks, track its receiver lifetime like an assignment, and link a data descriptor's value like the assigned value; an accessor's result stays unknown.
* fix(javascript): unwrap assignment chains in defined CommonJS export values
A descriptor value such as holder.parse = require("./parser").parse lost its require origin because only the assignment path took the rightmost assigned value. Do it once in the shared export builder.
This commit is contained in:
1 parent
6b0113a534
commit
bec88b9d77
3 files changed
+214
-11
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
isUnshadowedGlobal,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
||||
import { traverseJavaScriptAstSteps } from "./javascriptSemanticTraversal.js";
|
||||
import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js";
|
||||
|
||||
@@ -17,6 +18,9 @@ export type CommonJsExportReceiverEffect =
|
||||
| "detached"
|
||||
| "uncertain";
|
||||
|
||||
/** A CommonJS export assignment or `Object.defineProperty` export call. */
|
||||
export type CommonJsExportSite = t.AssignmentExpression | t.CallExpression;
|
||||
|
||||
const isCommonJsModuleExports = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
@@ -41,6 +45,51 @@ export const commonJsExportName = (
|
||||
return isCommonJsModuleExports(node, state) ? "default" : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* `Object.defineProperty(exports, key, descriptor)`, as tsc and Babel emit
|
||||
* for CommonJS re-exports, defines the same export as an assignment.
|
||||
*/
|
||||
export const commonJsDefinedExport = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
):
|
||||
| {
|
||||
readonly exportedName: string;
|
||||
readonly receiver: t.Node;
|
||||
readonly descriptor: t.Node;
|
||||
}
|
||||
| undefined => {
|
||||
if (!t.isCallExpression(node)) return undefined;
|
||||
const [target, key, descriptor] = node.arguments;
|
||||
if (
|
||||
target === undefined ||
|
||||
key === undefined ||
|
||||
descriptor === undefined ||
|
||||
t.isSpreadElement(target) ||
|
||||
t.isSpreadElement(key) ||
|
||||
t.isSpreadElement(descriptor) ||
|
||||
t.isArgumentPlaceholder(target) ||
|
||||
t.isArgumentPlaceholder(key) ||
|
||||
t.isArgumentPlaceholder(descriptor)
|
||||
)
|
||||
return undefined;
|
||||
const receiver = unwrapJavaScriptExpression(target).node;
|
||||
if (
|
||||
(!isUnshadowedGlobal(receiver, state, "exports") &&
|
||||
!isCommonJsModuleExports(receiver, state)) ||
|
||||
!isUnmodifiedGlobalReference(node.callee, state, [
|
||||
"Object",
|
||||
"defineProperty",
|
||||
])
|
||||
)
|
||||
return undefined;
|
||||
return {
|
||||
exportedName: semanticStaticPropertyKey(key, true) ?? "*",
|
||||
receiver,
|
||||
descriptor,
|
||||
};
|
||||
};
|
||||
|
||||
type Identity = t.Node | null;
|
||||
type Execution = "ordered" | "conditional" | "deferred";
|
||||
|
||||
@@ -65,7 +114,7 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): Generator<
|
||||
void,
|
||||
ReadonlyMap<t.AssignmentExpression, CommonJsExportReceiverEffect>
|
||||
ReadonlyMap<CommonJsExportSite, CommonJsExportReceiverEffect>
|
||||
> {
|
||||
const initial = state.program;
|
||||
let publicIdentity: Identity = initial;
|
||||
@@ -74,7 +123,7 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
let deferredReplacement = false;
|
||||
const fresh = new WeakSet<t.Node>([initial]);
|
||||
const values = new WeakMap<t.Node, Identity>();
|
||||
const captures = new Map<t.AssignmentExpression, Identity>();
|
||||
const captures = new Map<CommonJsExportSite, Identity>();
|
||||
const execution: Execution[] = [];
|
||||
const publicCallWrites = new WeakSet<t.Node>();
|
||||
// Consume the shared write owner's resolved aliases, intrinsic invocation
|
||||
@@ -168,6 +217,16 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
: publicIdentity,
|
||||
);
|
||||
}
|
||||
const defined = commonJsDefinedExport(node, state);
|
||||
if (defined !== undefined && t.isCallExpression(node))
|
||||
captures.set(
|
||||
node,
|
||||
mode !== "ordered"
|
||||
? null
|
||||
: isUnshadowedGlobal(defined.receiver, state, "exports")
|
||||
? exportsIdentity
|
||||
: publicIdentity,
|
||||
);
|
||||
},
|
||||
exit: (node) => {
|
||||
const mode = execution.pop() ?? "conditional";
|
||||
@@ -222,10 +281,7 @@ export function* indexCommonJsExportReceiversSteps(
|
||||
}
|
||||
},
|
||||
});
|
||||
const effects = new Map<
|
||||
t.AssignmentExpression,
|
||||
CommonJsExportReceiverEffect
|
||||
>();
|
||||
const effects = new Map<CommonJsExportSite, CommonJsExportReceiverEffect>();
|
||||
for (const [node, receiver] of captures) {
|
||||
effects.set(
|
||||
node,
|
||||
|
||||
@@ -559,6 +559,120 @@ describe("CommonJS receiver lifetimes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("CommonJS exports defined with Object.defineProperty", () => {
|
||||
const exportsOf = (source: string) =>
|
||||
analyzeJavaScriptSemantics(source).moduleLinks.filter(
|
||||
({ kind }) => kind === "commonjs-export",
|
||||
);
|
||||
const defineParse =
|
||||
'Object.defineProperty(exports, "parse", { value: () => 1 });';
|
||||
|
||||
it("retains tsc CommonJS re-exports and data descriptors", () => {
|
||||
const links = exportsOf(`
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.VERSION = void 0;
|
||||
exports.VERSION = "1.0";
|
||||
var client_1 = require("./client");
|
||||
Object.defineProperty(exports, "Client", { enumerable: true, get: function () { return client_1.Client; } });
|
||||
`);
|
||||
expect(links.map(({ exportedName }) => exportedName)).toEqual([
|
||||
"__esModule",
|
||||
"VERSION",
|
||||
"VERSION",
|
||||
"Client",
|
||||
]);
|
||||
// An accessor's result is not proven by the descriptor.
|
||||
expect(links.at(-1)).toMatchObject({
|
||||
localName: null,
|
||||
callableId: null,
|
||||
specifier: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["() => 1", "parse"],
|
||||
['require("./parser")', "parse"],
|
||||
['require("./parser").parse', "parse"],
|
||||
["parse", "parse"],
|
||||
['holder.parse = require("./parser").parse', "parse"],
|
||||
["first = second = () => 1", "parse"],
|
||||
])(
|
||||
"links a data descriptor value %s like the equivalent assignment",
|
||||
(value, name) => {
|
||||
const strip = (links: ReturnType<typeof exportsOf>) =>
|
||||
links.map(({ location: _location, callableId, ...link }) => ({
|
||||
...link,
|
||||
callable: callableId !== null,
|
||||
}));
|
||||
const assigned = exportsOf(`exports.${name} = ${value};`);
|
||||
const defined = exportsOf(
|
||||
`Object.defineProperty(exports, "${name}", { value: ${value} });`,
|
||||
);
|
||||
expect(strip(defined)).toEqual(strip(assigned));
|
||||
expect(defined).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
defineParse,
|
||||
`module.exports = {}; ${defineParse.replace("(exports", "(module.exports")}`,
|
||||
`module.exports = {}; exports = module.exports; ${defineParse}`,
|
||||
])("retains a connected receiver: %s", (source) => {
|
||||
expect(exportsOf(source)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
exportedName: "parse",
|
||||
callableId: expect.any(String),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
`module.exports = {}; ${defineParse}`,
|
||||
`exports = {}; ${defineParse}`,
|
||||
`${defineParse} module.exports = {};`,
|
||||
])("excludes a detached receiver: %s", (source) => {
|
||||
expect(
|
||||
exportsOf(source).filter(({ exportedName }) => exportedName === "parse"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
`if (flag) module.exports = {}; ${defineParse}`,
|
||||
`function replace() { exports = {}; } replace(); ${defineParse}`,
|
||||
`module.exports = {}; Object.defineProperty(module, "exports", { value: exports }); ${defineParse}`,
|
||||
`if (flag) { ${defineParse} }`,
|
||||
])("keeps an uncertain receiver unresolved: %s", (source) => {
|
||||
expect(exportsOf(source)).toContainEqual(
|
||||
expect.objectContaining({
|
||||
exportedName: "parse",
|
||||
callableId: null,
|
||||
localName: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'const Object = { defineProperty() {} }; Object.defineProperty(exports, "parse", { value: 1 });',
|
||||
'Object.defineProperty = patched; Object.defineProperty(exports, "parse", { value: 1 });',
|
||||
'Object.defineProperty(other, "parse", { value: 1 });',
|
||||
'Object.defineProperty(module, "parse", { value: 1 });',
|
||||
'function local(exports) { Object.defineProperty(exports, "parse", { value: 1 }); }',
|
||||
'Object.defineProperty(exports, "parse");',
|
||||
"Object.defineProperty(...args);",
|
||||
])("does not treat other defineProperty calls as exports: %s", (source) => {
|
||||
expect(
|
||||
exportsOf(source).filter(({ exportedName }) => exportedName === "parse"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps a dynamic defined key unknown", () => {
|
||||
expect(
|
||||
exportsOf("Object.defineProperty(exports, key, { value: 1 });"),
|
||||
).toEqual([expect.objectContaining({ exportedName: "*" })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic analysis: write references", () => {
|
||||
it("classifies destructuring and loop targets as writes, not reads", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
propertyName,
|
||||
semanticFinalObjectProperty,
|
||||
semanticStaticPropertyKey,
|
||||
semanticWrappedWriteSite,
|
||||
semanticWriteSiteReadsFirst,
|
||||
@@ -39,8 +40,10 @@ import {
|
||||
semanticImportEqualsSpecifier,
|
||||
} from "./javascriptModuleDeclaration.js";
|
||||
import {
|
||||
commonJsDefinedExport,
|
||||
commonJsExportName,
|
||||
type CommonJsExportReceiverEffect,
|
||||
type CommonJsExportSite,
|
||||
} from "./javascriptCommonJsExportReceivers.js";
|
||||
|
||||
interface CollectCallableInput {
|
||||
@@ -83,7 +86,7 @@ export const collectSemanticModuleLink = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
commonJsReceivers: ReadonlyMap<
|
||||
t.AssignmentExpression,
|
||||
CommonJsExportSite,
|
||||
CommonJsExportReceiverEffect
|
||||
>,
|
||||
): void => {
|
||||
@@ -120,7 +123,10 @@ export const collectSemanticModuleLink = (
|
||||
else if (t.isVariableDeclarator(node)) collectRequireLink(node, state);
|
||||
else if (t.isAssignmentExpression(node))
|
||||
collectCommonJsExport(node, state, commonJsReceivers.get(node));
|
||||
else if (t.isCallExpression(node)) collectUnboundRequireLink(node, state);
|
||||
else if (t.isCallExpression(node)) {
|
||||
collectDefinedCommonJsExport(node, state, commonJsReceivers.get(node));
|
||||
collectUnboundRequireLink(node, state);
|
||||
}
|
||||
};
|
||||
|
||||
const collectImportEquals = (
|
||||
@@ -473,10 +479,37 @@ const collectCommonJsExport = (
|
||||
const exportedName = commonJsExportName(node.left, state);
|
||||
if (exportedName === undefined) return;
|
||||
if (receiverEffect === "detached") return;
|
||||
// Simple assignment chains yield their rightmost assigned value. Compound
|
||||
// and logical writes also depend on the previous value, so remain unknown.
|
||||
let value =
|
||||
// Compound and logical writes also depend on the previous value, so remain
|
||||
// unknown.
|
||||
const value =
|
||||
node.operator === "=" && receiverEffect !== "uncertain" ? node.right : null;
|
||||
addCommonJsExport(node, state, exportedName, value);
|
||||
};
|
||||
|
||||
const collectDefinedCommonJsExport = (
|
||||
node: t.CallExpression,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
receiverEffect: CommonJsExportReceiverEffect | undefined,
|
||||
): void => {
|
||||
const defined = commonJsDefinedExport(node, state);
|
||||
if (defined === undefined || receiverEffect === "detached") return;
|
||||
// A data descriptor exports its value; an accessor's result stays unknown.
|
||||
const descriptor = unwrapJavaScriptExpression(defined.descriptor).node;
|
||||
const value =
|
||||
receiverEffect !== "uncertain" && t.isObjectExpression(descriptor)
|
||||
? (semanticFinalObjectProperty(descriptor, "value") ?? null)
|
||||
: null;
|
||||
addCommonJsExport(node, state, defined.exportedName, value);
|
||||
};
|
||||
|
||||
const addCommonJsExport = (
|
||||
node: CommonJsExportSite,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
exportedName: string,
|
||||
exported: t.Node | null,
|
||||
): void => {
|
||||
// Simple assignment chains yield their rightmost assigned value.
|
||||
let value = exported;
|
||||
while (t.isAssignmentExpression(value, { operator: "=" }))
|
||||
value = value.right;
|
||||
const required =
|
||||
|
||||
Reference in new issue
Block a user