Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5b01ef756 | ||
|
|
3ebab7ab45 |
No files matched your search
@@ -1,6 +1,13 @@
|
|||||||
|
import * as t from "@babel/types";
|
||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createJavaScriptRpcScan } from "./javascriptRpcEndpoints.js";
|
||||||
|
import { indexJavaScriptBindingsWithReceiversSteps } from "./javascriptSemanticAnalysis.js";
|
||||||
|
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
|
||||||
|
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||||
|
import { finishPartialNetworkEndpointsSteps } from "./javascriptStaticAnalysisCalls.js";
|
||||||
|
import { createJavaScriptAnalysisAccumulator } from "./javascriptStaticAnalysisState.js";
|
||||||
|
|
||||||
const endpoints = (source: string) =>
|
const endpoints = (source: string) =>
|
||||||
analyzeJavaScriptStaticSource(source).endpoints.map((endpoint) => ({
|
analyzeJavaScriptStaticSource(source).endpoints.map((endpoint) => ({
|
||||||
@@ -10,6 +17,51 @@ const endpoints = (source: string) =>
|
|||||||
resolution: endpoint.resolution,
|
resolution: endpoint.resolution,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
/** Finish every `fetch` call in source order and count member-index builds. */
|
||||||
|
const finishFetchCalls = (source: string) => {
|
||||||
|
const file = parseJavaScriptSource(source);
|
||||||
|
if (file === null) throw new TypeError("Fixture must parse");
|
||||||
|
const index = completeSemanticSteps(
|
||||||
|
indexJavaScriptBindingsWithReceiversSteps(file.program),
|
||||||
|
);
|
||||||
|
completeSemanticSteps(index.indexAmbientWritesSteps());
|
||||||
|
let memberIndexBuilds = 0;
|
||||||
|
const observed = {
|
||||||
|
...index,
|
||||||
|
*indexMemberMutationFactsSteps() {
|
||||||
|
memberIndexBuilds += 1;
|
||||||
|
yield* index.indexMemberMutationFactsSteps();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const context = {
|
||||||
|
source,
|
||||||
|
accumulator: createJavaScriptAnalysisAccumulator(source.length),
|
||||||
|
vitePreloadDependencyTable: null,
|
||||||
|
rpcScan: createJavaScriptRpcScan(
|
||||||
|
source,
|
||||||
|
index.receivers.rpcClientRoots,
|
||||||
|
observed,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
t.traverseFast(file.program, (node) => {
|
||||||
|
if (
|
||||||
|
t.isCallExpression(node) &&
|
||||||
|
t.isIdentifier(node.callee, { name: "fetch" })
|
||||||
|
)
|
||||||
|
context.accumulator.pendingNetworkCalls.push({ node, name: "fetch" });
|
||||||
|
});
|
||||||
|
completeSemanticSteps(
|
||||||
|
finishPartialNetworkEndpointsSteps(context, index.bindings, observed),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
memberIndexBuilds,
|
||||||
|
endpoints: context.accumulator.endpoints.map(({ value }) => ({
|
||||||
|
value: value.value,
|
||||||
|
resolution: value.resolution,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
it("resolves unchanged lexical strings and object properties", () => {
|
it("resolves unchanged lexical strings and object properties", () => {
|
||||||
expect(
|
expect(
|
||||||
endpoints(`
|
endpoints(`
|
||||||
@@ -215,6 +267,67 @@ it("keeps an escaped URL object partial", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("checks member effects for stable targets beside parameter targets", () => {
|
||||||
|
expect(
|
||||||
|
endpoints(`
|
||||||
|
const load = (url) => fetch(url);
|
||||||
|
const target = "/items";
|
||||||
|
const socket = new URL("wss://old.example.test/socket");
|
||||||
|
configure(socket);
|
||||||
|
fetch(target);
|
||||||
|
fetch(socket);
|
||||||
|
`),
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
kind: "network",
|
||||||
|
value: "url",
|
||||||
|
mechanism: "call:fetch",
|
||||||
|
resolution: "partial",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "network",
|
||||||
|
value: "/items",
|
||||||
|
mechanism: "call:fetch",
|
||||||
|
resolution: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "network",
|
||||||
|
value: "socket",
|
||||||
|
mechanism: "call:fetch",
|
||||||
|
resolution: "partial",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("builds member effects only when a pending target can resolve", () => {
|
||||||
|
expect(
|
||||||
|
finishFetchCalls(`
|
||||||
|
const load = (url) => fetch(url);
|
||||||
|
fetch(later);
|
||||||
|
const later = "/after";
|
||||||
|
`),
|
||||||
|
).toEqual({
|
||||||
|
memberIndexBuilds: 0,
|
||||||
|
endpoints: [
|
||||||
|
{ value: "url", resolution: "partial" },
|
||||||
|
{ value: "later", resolution: "partial" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
finishFetchCalls(`
|
||||||
|
const load = (url) => fetch(url);
|
||||||
|
const target = "/items";
|
||||||
|
fetch(target);
|
||||||
|
`),
|
||||||
|
).toEqual({
|
||||||
|
memberIndexBuilds: 1,
|
||||||
|
endpoints: [
|
||||||
|
{ value: "url", resolution: "partial" },
|
||||||
|
{ value: "/items", resolution: undefined },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("retains dynamic targets on known global fetch receivers", () => {
|
it("retains dynamic targets on known global fetch receivers", () => {
|
||||||
expect(
|
expect(
|
||||||
endpoints(`
|
endpoints(`
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
addReference,
|
addReference,
|
||||||
addSourceMapDirectives,
|
addSourceMapDirectives,
|
||||||
finishPartialNetworkEndpoints,
|
finishPartialNetworkEndpointsSteps,
|
||||||
inspectCall,
|
inspectCall,
|
||||||
inspectRouteProperty,
|
inspectRouteProperty,
|
||||||
inspectRoleProperty,
|
inspectRoleProperty,
|
||||||
@@ -99,10 +99,8 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
|||||||
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
|
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
|
||||||
};
|
};
|
||||||
yield* traverseStaticSourceSteps(file, findings, bindings);
|
yield* traverseStaticSourceSteps(file, findings, bindings);
|
||||||
if (accumulator.pendingNetworkCalls.length > 0) {
|
if (accumulator.pendingNetworkCalls.length > 0)
|
||||||
yield* index.indexMemberMutationFactsSteps();
|
yield* finishPartialNetworkEndpointsSteps(findings, bindings, index);
|
||||||
finishPartialNetworkEndpoints(findings, bindings, index);
|
|
||||||
}
|
|
||||||
accumulator.electronCallPaths = collectElectronWrappedCallPaths(
|
accumulator.electronCallPaths = collectElectronWrappedCallPaths(
|
||||||
file.program,
|
file.program,
|
||||||
bindings,
|
bindings,
|
||||||
|
|||||||
@@ -522,15 +522,44 @@ const isKnownNetworkRequest = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Finish queued targets after shared member-mutation facts are indexed. */
|
/** Finish queued targets after shared member-mutation facts are indexed. */
|
||||||
export const finishPartialNetworkEndpoints = (
|
/**
|
||||||
|
* Only a target with a stable earlier initializer can resolve, and that proof
|
||||||
|
* reads definition facts alone. Build the whole-source mutation index only when
|
||||||
|
* such a target needs its effects; every other request stays partial.
|
||||||
|
*/
|
||||||
|
export function* finishPartialNetworkEndpointsSteps(
|
||||||
context: FindingContext,
|
context: FindingContext,
|
||||||
bindings: JavaScriptSemanticAnalysisState,
|
bindings: JavaScriptSemanticAnalysisState,
|
||||||
index: JavaScriptBindingIndex,
|
index: JavaScriptBindingIndex,
|
||||||
): void => {
|
): Generator<void, void> {
|
||||||
for (const { node, name } of context.accumulator.pendingNetworkCalls) {
|
const pending = context.accumulator.pendingNetworkCalls.flatMap(
|
||||||
const target = argumentNode(node.arguments[0]);
|
({ node, name }) => {
|
||||||
if (target === undefined) continue;
|
const target = argumentNode(node.arguments[0]);
|
||||||
const resolved = resolvedRequestTarget(target, node, bindings, index);
|
if (target === undefined) return [];
|
||||||
|
const capture = requestTargetCapture(target, node);
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
node,
|
||||||
|
name,
|
||||||
|
target,
|
||||||
|
capture,
|
||||||
|
resolvable: requestInitializerPrecedesSite(
|
||||||
|
capture.reference,
|
||||||
|
capture.site,
|
||||||
|
bindings,
|
||||||
|
index,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
context.accumulator.pendingNetworkCalls.length = 0;
|
||||||
|
if (pending.some(({ resolvable }) => resolvable))
|
||||||
|
yield* index.indexMemberMutationFactsSteps();
|
||||||
|
for (const { node, name, target, capture, resolvable } of pending) {
|
||||||
|
const resolved = resolvable
|
||||||
|
? resolvedRequestTarget(target, capture, bindings, index)
|
||||||
|
: undefined;
|
||||||
if (resolved !== undefined)
|
if (resolved !== undefined)
|
||||||
addEndpoint(context, {
|
addEndpoint(context, {
|
||||||
node,
|
node,
|
||||||
@@ -540,11 +569,10 @@ export const finishPartialNetworkEndpoints = (
|
|||||||
});
|
});
|
||||||
else addPartialNetworkEndpoint(context, node, name, target);
|
else addPartialNetworkEndpoint(context, node, name, target);
|
||||||
}
|
}
|
||||||
context.accumulator.pendingNetworkCalls.length = 0;
|
|
||||||
context.accumulator.endpoints.sort(
|
context.accumulator.endpoints.sort(
|
||||||
(left, right) => left.offset - right.offset,
|
(left, right) => left.offset - right.offset,
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
const addPartialNetworkEndpoint = (
|
const addPartialNetworkEndpoint = (
|
||||||
context: FindingContext,
|
context: FindingContext,
|
||||||
@@ -561,24 +589,34 @@ const addPartialNetworkEndpoint = (
|
|||||||
resolution: "partial",
|
resolution: "partial",
|
||||||
});
|
});
|
||||||
|
|
||||||
const resolvedRequestTarget = (
|
interface RequestTargetCapture {
|
||||||
|
readonly reference: t.Node;
|
||||||
|
readonly site: t.Node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestTargetCapture = (
|
||||||
node: t.Node,
|
node: t.Node,
|
||||||
site: t.Node,
|
site: t.Node,
|
||||||
bindings: JavaScriptSemanticAnalysisState,
|
): RequestTargetCapture => {
|
||||||
index: JavaScriptBindingIndex,
|
|
||||||
): string | undefined => {
|
|
||||||
const reference = requestReferenceNode(node);
|
const reference = requestReferenceNode(node);
|
||||||
// A toString result is selected before the enclosing request invocation.
|
// A toString result is selected before the enclosing request invocation.
|
||||||
const expression = unwrapJavaScriptExpression(node).node;
|
const expression = unwrapJavaScriptExpression(node).node;
|
||||||
const capture = reference === expression ? site : expression;
|
return { reference, site: reference === expression ? site : expression };
|
||||||
if (referenceMayHaveChangedAtSite(reference, capture, index))
|
};
|
||||||
return undefined;
|
|
||||||
if (!requestInitializerPrecedesSite(reference, capture, bindings, index))
|
/** Resolve a target whose stable earlier initializer the caller has proved. */
|
||||||
|
const resolvedRequestTarget = (
|
||||||
|
node: t.Node,
|
||||||
|
capture: RequestTargetCapture,
|
||||||
|
bindings: JavaScriptSemanticAnalysisState,
|
||||||
|
index: JavaScriptBindingIndex,
|
||||||
|
): string | undefined => {
|
||||||
|
if (referenceMayHaveChangedAtSite(capture.reference, capture.site, index))
|
||||||
return undefined;
|
return undefined;
|
||||||
const evaluated = evaluateSemanticExpression(node, bindings);
|
const evaluated = evaluateSemanticExpression(node, bindings);
|
||||||
if (evaluated.status === "literal" && typeof evaluated.value === "string")
|
if (evaluated.status === "literal" && typeof evaluated.value === "string")
|
||||||
return evaluated.value;
|
return evaluated.value;
|
||||||
return urlObjectRequestTarget(node, capture, bindings, index);
|
return urlObjectRequestTarget(node, capture.site, bindings, index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestInitializerPrecedesSite = (
|
const requestInitializerPrecedesSite = (
|
||||||
|
|||||||
Reference in new issue
Block a user