fix(reference): distinguish computed require member names
This commit is contained in:
parent
132a3a46be
commit
df6e476d67
|
|
@ -188,7 +188,8 @@ const isRequireCallee = (callee: Node | null | undefined): boolean => {
|
|||
isMemberExpression(callee) &&
|
||||
isIdentifier(callee.object) &&
|
||||
callee.object.name === "require" &&
|
||||
(isIdentifier(callee.property) || isStringLiteral(callee.property))
|
||||
((!callee.computed && isIdentifier(callee.property)) ||
|
||||
isStringLiteral(callee.property))
|
||||
) {
|
||||
const propertyName = isIdentifier(callee.property)
|
||||
? callee.property.name
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseReferenceSourceImports } from "./referenceSourceImportParsing.js";
|
||||
|
||||
const parseRequire = (source: string) =>
|
||||
parseReferenceSourceImports(
|
||||
"main.cjs",
|
||||
new TextEncoder().encode(source),
|
||||
"JavaScript",
|
||||
);
|
||||
|
||||
const knownCallees = [
|
||||
"require",
|
||||
"require.resolve",
|
||||
'require["resolve"]',
|
||||
"require.main",
|
||||
'require["main"]',
|
||||
];
|
||||
|
||||
describe("historical source require member names", () => {
|
||||
it.each([
|
||||
...knownCallees,
|
||||
'require[("resolve")]',
|
||||
'require["res\\u006flve"]',
|
||||
])(
|
||||
"retains a literal dependency for %s beside computed members",
|
||||
(callee) => {
|
||||
expect(
|
||||
parseRequire(
|
||||
`require[resolve]("./ignored.js"); ${callee}("./dep.js");`,
|
||||
),
|
||||
).toEqual({
|
||||
relationships: [
|
||||
{
|
||||
from_path: "main.cjs",
|
||||
to: "./dep.js",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
],
|
||||
parse_failures: [],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'require[resolve]("./dep.js");',
|
||||
'require[main]("./dep.js");',
|
||||
'const resolve = "toString"; require[resolve]("./dep.js");',
|
||||
'const main = "toString"; require[main]("./dep.js");',
|
||||
'const resolve = "resolve"; require[resolve]("./dep.js");',
|
||||
'const main = "main"; require[main]("./dep.js");',
|
||||
'const method = "resolve"; require[method]("./dep.js");',
|
||||
'require[`resolve`]("./dep.js");',
|
||||
'require["re" + "solve"]("./dep.js");',
|
||||
])("does not infer computed member values in %s", (source) => {
|
||||
expect(parseRequire(source)).toEqual({
|
||||
relationships: [],
|
||||
parse_failures: [],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"require?.",
|
||||
"require.resolve?.",
|
||||
'require["resolve"]?.',
|
||||
"require.main?.",
|
||||
'require["main"]?.',
|
||||
"require?.resolve",
|
||||
'require?.["resolve"]',
|
||||
"require?.[resolve]",
|
||||
"require?.main",
|
||||
'require?.["main"]',
|
||||
"require?.[main]",
|
||||
"(require?.resolve)",
|
||||
"require.resolve.call",
|
||||
"require.main.require",
|
||||
"module.require",
|
||||
"other.resolve",
|
||||
"other.require",
|
||||
"require.toString",
|
||||
'require["toString"]',
|
||||
])("leaves the unrecognized callee %s unchanged", (callee) => {
|
||||
expect(parseRequire(`${callee}("./dep.js");`)).toEqual({
|
||||
relationships: [],
|
||||
parse_failures: [],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(knownCallees)("does not guess unknown arguments to %s", (callee) => {
|
||||
expect(
|
||||
parseRequire(
|
||||
[
|
||||
'const target = "./dep.js";',
|
||||
`${callee}(target);`,
|
||||
`${callee}(\`./dep.js\`);`,
|
||||
`${callee}(getTarget());`,
|
||||
`${callee}();`,
|
||||
].join("\n"),
|
||||
),
|
||||
).toEqual({ relationships: [], parse_failures: [] });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { importReferenceSource } from "../../../src/application/ReferenceSourceImport.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
it("distinguishes computed require member values from literal members in source evidence", async () => {
|
||||
const root = await createTestTempDirectory("rea-reference-require-members-");
|
||||
const sources = {
|
||||
"computed.cjs": [
|
||||
'const resolve = "toString";',
|
||||
'const main = "toString";',
|
||||
'const value = require[resolve]("./dep.js");',
|
||||
'require[main]("./dep.js");',
|
||||
"console.log(typeof value);",
|
||||
].join("\n"),
|
||||
"literal.cjs": 'console.log(require["resolve"]("./dep.js"));\n',
|
||||
"dep.js": 'throw new Error("The dependency must not execute");\n',
|
||||
"unknown.cjs": [
|
||||
"function load(target) {",
|
||||
" require(target);",
|
||||
" require.resolve(target);",
|
||||
' require["resolve"](target);',
|
||||
" return import(target);",
|
||||
"}",
|
||||
].join("\n"),
|
||||
};
|
||||
await Promise.all(
|
||||
Object.entries(sources).map(([path, source]) =>
|
||||
writeFile(join(root, path), source),
|
||||
),
|
||||
);
|
||||
|
||||
// These owned producers demonstrate the member's runtime meaning without
|
||||
// loading the throwing dependency. The importer itself never executes them.
|
||||
const runNode = promisify(execFile);
|
||||
for (const [path, stdout] of [
|
||||
["computed.cjs", "string\n"],
|
||||
["literal.cjs", `${join(root, "dep.js")}\n`],
|
||||
] as const) {
|
||||
const executed = await runNode(process.execPath, [join(root, path)], {
|
||||
cwd: root,
|
||||
timeout: 5_000,
|
||||
});
|
||||
expect(executed.stdout).toBe(stdout);
|
||||
expect(executed.stderr).toBe("");
|
||||
}
|
||||
|
||||
const result = await importReferenceSource({
|
||||
root,
|
||||
caller: "reference-require-members-test",
|
||||
policy: { secretPatterns: [] },
|
||||
});
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
const graph = result.value;
|
||||
expect(graph.parse_failures).toEqual([]);
|
||||
expect(graph.relationships).toEqual([
|
||||
{
|
||||
from_path: "literal.cjs",
|
||||
to: "dep.js",
|
||||
kind: "requires",
|
||||
resolution: "internal",
|
||||
parse_state: "parsed",
|
||||
},
|
||||
{
|
||||
from_path: "unknown.cjs",
|
||||
to: "<dynamic-import>",
|
||||
kind: "imports",
|
||||
resolution: "unknown",
|
||||
parse_state: "partial",
|
||||
},
|
||||
]);
|
||||
expect(graph.entries).toHaveLength(Object.keys(sources).length);
|
||||
for (const [path, source] of Object.entries(sources)) {
|
||||
expect(graph.entries).toContainEqual({
|
||||
path,
|
||||
kind: "file",
|
||||
sha256: createHash("sha256").update(source).digest("hex"),
|
||||
size: Buffer.byteLength(source),
|
||||
language: "JavaScript",
|
||||
classifications: ["source"],
|
||||
content_state: "hashed",
|
||||
limitations: [],
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Reference in New Issue