fix(javascript): retain configuration reads before member access (#2756)
Squash merge of https://github.com/morluto/rea/pull/2756. Original pull request submitted by @xu91102. Co-authored-by: hhang <169027046+xu91102@users.noreply.github.com>
This commit is contained in:
parent
d4f0c5e664
commit
72b9ab4af1
|
|
@ -275,12 +275,3 @@ export const dataEffectMemberCallee = (
|
|||
t.isMemberExpression(node.callee) || t.isOptionalMemberExpression(node.callee)
|
||||
? node.callee
|
||||
: null;
|
||||
|
||||
/** Read a parent member's object when present. */
|
||||
export const dataEffectMemberObject = (
|
||||
node: t.Node | null,
|
||||
): t.Expression | t.Super | null =>
|
||||
node !== null &&
|
||||
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
|
||||
? node.object
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import {
|
|||
dataEffectArgumentBindingId as argumentBindingId,
|
||||
dataEffectLiteralString as literalString,
|
||||
dataEffectMemberCallee as memberCallee,
|
||||
dataEffectMemberObject as memberObject,
|
||||
dataEffectPrimitive as primitiveValue,
|
||||
isSemanticProcessObject as isProcessObject,
|
||||
outerDataEffectBinding as outerAssignedBinding,
|
||||
|
|
@ -83,8 +82,8 @@ const collectConfigurations = (
|
|||
context: TraversalContext,
|
||||
): ConfigurationCandidate[] => {
|
||||
const output: ConfigurationCandidate[] = [];
|
||||
traverseWithContext(program, context, (node, parent) => {
|
||||
const details = configurationDetails(node, parent, context);
|
||||
traverseWithContext(program, context, (node) => {
|
||||
const details = configurationDetails(node, context);
|
||||
if (details === null) return;
|
||||
const operation: JavaScriptSemanticConfigurationOperation = {
|
||||
configId: `config:${details.kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
|
||||
|
|
@ -104,17 +103,13 @@ const collectConfigurations = (
|
|||
|
||||
const configurationDetails = (
|
||||
node: t.Node,
|
||||
parent: t.Node | null,
|
||||
context: TraversalContext,
|
||||
): {
|
||||
readonly kind: "environment" | "argv" | "file";
|
||||
readonly key: string | null;
|
||||
readonly resolution: "complete" | "partial";
|
||||
} | null => {
|
||||
if (
|
||||
(t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) &&
|
||||
!(memberObject(parent) === node)
|
||||
) {
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const processMember = processConfigurationMember(node, context.state);
|
||||
if (processMember !== null) return processMember;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
|
||||
import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js";
|
||||
|
||||
cliTest.for(["CLI", "stdio MCP"] as const)(
|
||||
"retains configuration sources consumed by member access through %s",
|
||||
async (surface, { cli, processes }) => {
|
||||
const root = await createTestTempDirectory("rea-config-reads-");
|
||||
await writeFixtureFiles(root, {
|
||||
"entry.mjs": [
|
||||
"const length = process.env.REA_ENDPOINT.length;",
|
||||
"const endpoint = process.env.REA_ENDPOINT;",
|
||||
"const argumentLength = process.argv[2].length;",
|
||||
"const mode = process.argv[3].trim();",
|
||||
"const optional = process.env.REA_MISSING?.length;",
|
||||
"function shadow(process) { return process.env.LOCAL.length + process.argv[2].length; }",
|
||||
'const local = { env: { FAKE: "fake" }, argv: [0, 1, "abc"] };',
|
||||
'const control = shadow({ env: { LOCAL: "local" }, argv: [0, 1, "abc"] });',
|
||||
"console.log(JSON.stringify([length, endpoint, argumentLength, mode, optional, control, local.env.FAKE.length]));",
|
||||
].join("\n"),
|
||||
});
|
||||
const control = await processes.run(
|
||||
process.execPath,
|
||||
[join(root, "entry.mjs"), "abc", " mode "],
|
||||
{ env: { REA_ENDPOINT: "endpoint" }, timeoutMs: 15_000 },
|
||||
);
|
||||
expect(control.exitCode, control.stderr).toBe(0);
|
||||
expect(JSON.parse(control.stdout)).toEqual([
|
||||
8,
|
||||
"endpoint",
|
||||
3,
|
||||
"mode",
|
||||
null,
|
||||
8,
|
||||
4,
|
||||
]);
|
||||
const graph = (await analyzeJavaScriptDirectory(surface, cli, root))
|
||||
.semantic_graph;
|
||||
const sources = graph.nodes.filter((node) => node.kind === "config-source");
|
||||
expect(
|
||||
sources
|
||||
.map((node) => [node.properties.source_kind, node.properties.key])
|
||||
.sort(),
|
||||
).toEqual([
|
||||
["argv", "2"],
|
||||
["argv", "3"],
|
||||
["environment", "REA_ENDPOINT"],
|
||||
["environment", "REA_ENDPOINT"],
|
||||
["environment", "REA_MISSING"],
|
||||
]);
|
||||
expect(sources.every((node) => node.properties.value === null)).toBe(true);
|
||||
const endpoint = graph.nodes.find(
|
||||
(node) => node.kind === "binding" && node.label === "endpoint",
|
||||
);
|
||||
expect(endpoint).toBeDefined();
|
||||
for (const source of sources) {
|
||||
const bindingTargets = graph.relations
|
||||
.filter((relation) => relation.source_node_id === source.node_id)
|
||||
.flatMap((relation) =>
|
||||
graph.nodes.filter(
|
||||
(node) =>
|
||||
node.node_id === relation.target_node_id &&
|
||||
node.kind === "binding",
|
||||
),
|
||||
);
|
||||
expect(bindingTargets.map((node) => node.node_id)).toEqual(
|
||||
source.identity.source_range?.start.line === 2
|
||||
? [endpoint?.node_id]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
Loading…
Reference in New Issue