fix(semantics): recognize Node builtin default namespaces (#769)
* fix(semantics): recognize Node builtin default namespaces Signed-off-by: Rudy Celekli <rudy@gradiahq.com> * style(semantics): keep Babel imports before local helpers Signed-off-by: Rudy Celekli <rudy@gradiahq.com> --------- Signed-off-by: Rudy Celekli <rudy@gradiahq.com>
This commit is contained in:
parent
630bdafcac
commit
24a0bf9a61
|
|
@ -0,0 +1,39 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
|
||||
it.each(["namespace", "default", "named-default"])(
|
||||
"retains Node built-in effects from %s imports",
|
||||
(mode) => {
|
||||
const imports = ["fs", "child_process", "timers", "http"]
|
||||
.map((name, index) => {
|
||||
const declaration =
|
||||
mode === "namespace"
|
||||
? `* as m${String(index)}`
|
||||
: mode === "default"
|
||||
? `m${String(index)}`
|
||||
: `{default as m${String(index)}}`;
|
||||
return `import ${declaration} from 'node:${name}';`;
|
||||
})
|
||||
.join("\n");
|
||||
const ir = analyzeJavaScriptSemantics(`${imports}
|
||||
const text = m0.readFileSync('config.json','utf8');
|
||||
const stream = m0.createReadStream('config.json'); stream.destroy();
|
||||
const child = m1.spawn('/usr/bin/tool', ['--version']);
|
||||
const timer = m2.setTimeout(work,1000); m2.clearTimeout(timer);
|
||||
const request = m3.get('http://localhost/');
|
||||
`);
|
||||
expect(ir.configurationOperations).toHaveLength(1);
|
||||
expect(ir.resourceOperations).toHaveLength(2);
|
||||
expect(ir.childProcessSpawns).toHaveLength(1);
|
||||
expect(ir.timerOperations).toHaveLength(2);
|
||||
expect(ir.requestOperations).toHaveLength(1);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"import fs from './fs.js'; fs.createReadStream('config.json');",
|
||||
"import {constants as fs} from 'node:fs'; fs.createReadStream('config.json');",
|
||||
"function run(fs) { fs.createReadStream('config.json'); }",
|
||||
])("does not invent a built-in namespace for %s", (source) => {
|
||||
expect(analyzeJavaScriptSemantics(source).resourceOperations).toEqual([]);
|
||||
});
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticCallable,
|
||||
JavaScriptSemanticEventOperation,
|
||||
|
|
@ -338,7 +340,7 @@ const timerNamespaceBinding = (
|
|||
): boolean =>
|
||||
binding?.directOrigins.some(
|
||||
({ specifier, importedPath }) =>
|
||||
timerModule(specifier) && importedPath.length === 0,
|
||||
timerModule(specifier) && semanticBuiltinNamespacePath(importedPath),
|
||||
) ?? false;
|
||||
|
||||
const timerModule = (specifier: string): boolean =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticCallable,
|
||||
JavaScriptSemanticChildProcessInteraction,
|
||||
|
|
@ -305,7 +307,8 @@ const childProcessNamespace = (
|
|||
): boolean =>
|
||||
binding?.directOrigins.some(
|
||||
({ specifier, importedPath }) =>
|
||||
childProcessModule(specifier) && importedPath.length === 0,
|
||||
childProcessModule(specifier) &&
|
||||
semanticBuiltinNamespacePath(importedPath),
|
||||
) ?? false;
|
||||
|
||||
const childProcessModule = (specifier: string): boolean =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
||||
|
||||
import { semanticCallableIdForNode } from "./javascriptSemanticProjection.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
|
|
@ -157,7 +159,8 @@ export const builtinDataEffectMethod = (
|
|||
);
|
||||
const namespace = binding?.directOrigins.some(
|
||||
({ specifier, importedPath }) =>
|
||||
specifiers.includes(specifier) && importedPath.length === 0,
|
||||
specifiers.includes(specifier) &&
|
||||
semanticBuiltinNamespacePath(importedPath),
|
||||
);
|
||||
return namespace
|
||||
? semanticStaticPropertyName(callee.property, callee.computed)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticBoundaryOperation,
|
||||
JavaScriptSemanticCallable,
|
||||
|
|
@ -377,7 +379,7 @@ const requestMethod = (
|
|||
return binding?.directOrigins.some(
|
||||
({ specifier, importedPath }) =>
|
||||
["http", "https", "node:http", "node:https"].includes(specifier) &&
|
||||
importedPath.length === 0,
|
||||
semanticBuiltinNamespacePath(importedPath),
|
||||
)
|
||||
? method
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -55,3 +55,10 @@ export const semanticPrimitiveKey = (
|
|||
value: JavaScriptSemanticPrimitive,
|
||||
): string =>
|
||||
`${value === null ? "null" : typeof value}:${JSON.stringify(value)}`;
|
||||
|
||||
/** Match namespace paths only after the caller has identified a Node built-in. */
|
||||
export const semanticBuiltinNamespacePath = (
|
||||
importedPath: readonly string[],
|
||||
): boolean =>
|
||||
importedPath.length === 0 ||
|
||||
(importedPath.length === 1 && importedPath[0] === "default");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import * as t from "@babel/types";
|
||||
|
||||
import { semanticBuiltinNamespacePath } from "./javascriptSemanticProvenance.js";
|
||||
|
||||
import type {
|
||||
JavaScriptSemanticCallable,
|
||||
JavaScriptSemanticResourceOperation,
|
||||
|
|
@ -173,7 +175,7 @@ const resourceNamespace = (
|
|||
): boolean =>
|
||||
binding?.directOrigins.some(
|
||||
({ specifier, importedPath }) =>
|
||||
resourceModule(specifier) && importedPath.length === 0,
|
||||
resourceModule(specifier) && semanticBuiltinNamespacePath(importedPath),
|
||||
) ?? false;
|
||||
|
||||
const resourceModule = (specifier: string): boolean =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascriptApplicationAnalysis.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
it.each([
|
||||
"import fs from 'node:fs';",
|
||||
"import {default as fs} from 'node:fs';",
|
||||
])(
|
||||
"retains filesystem effects in the public CLI with %s",
|
||||
async (declaration) => {
|
||||
const root = await createTestTempDirectory("rea-builtin-default-cli-");
|
||||
await writeFile(
|
||||
join(root, "app.js"),
|
||||
`${declaration} const text=fs.readFileSync('config.json','utf8'); const stream=fs.createReadStream('config.json'); stream.destroy();`,
|
||||
);
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
["scripts/rea.mjs", "analyze-javascript-application", root, "--json"],
|
||||
{ cwd: process.cwd(), maxBuffer: 16 * 1024 * 1024 },
|
||||
);
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse(
|
||||
parseEvidence(JSON.parse(stdout)).normalized_result,
|
||||
);
|
||||
expect(result.semantic_graph.nodes).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: "config-source",
|
||||
properties: expect.objectContaining({ key: "config.json" }),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "resource",
|
||||
properties: expect.objectContaining({ method: "createReadStream" }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(result.semantic_graph.relations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
relation: "releases",
|
||||
resolution: "resolved",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
Loading…
Reference in New Issue