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 { 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 { finishPartialNetworkEndpointsSteps } from "./javascriptStaticAnalysisCalls.js";
|
||||
import { createJavaScriptAnalysisAccumulator } from "./javascriptStaticAnalysisState.js";
|
||||
|
||||
const endpoints = (source: string) =>
|
||||
analyzeJavaScriptStaticSource(source).endpoints.map((endpoint) => ({
|
||||
@@ -10,6 +17,51 @@ const endpoints = (source: string) =>
|
||||
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", () => {
|
||||
expect(
|
||||
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", () => {
|
||||
expect(
|
||||
endpoints(`
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import {
|
||||
addReference,
|
||||
addSourceMapDirectives,
|
||||
finishPartialNetworkEndpoints,
|
||||
finishPartialNetworkEndpointsSteps,
|
||||
inspectCall,
|
||||
inspectRouteProperty,
|
||||
inspectRoleProperty,
|
||||
@@ -99,10 +99,8 @@ export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
rpcScan: createJavaScriptRpcScan(source, receivers.rpcClientRoots, index),
|
||||
};
|
||||
yield* traverseStaticSourceSteps(file, findings, bindings);
|
||||
if (accumulator.pendingNetworkCalls.length > 0) {
|
||||
yield* index.indexMemberMutationFactsSteps();
|
||||
finishPartialNetworkEndpoints(findings, bindings, index);
|
||||
}
|
||||
if (accumulator.pendingNetworkCalls.length > 0)
|
||||
yield* finishPartialNetworkEndpointsSteps(findings, bindings, index);
|
||||
accumulator.electronCallPaths = collectElectronWrappedCallPaths(
|
||||
file.program,
|
||||
bindings,
|
||||
|
||||
@@ -522,15 +522,44 @@ const isKnownNetworkRequest = (
|
||||
};
|
||||
|
||||
/** 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,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
index: JavaScriptBindingIndex,
|
||||
): void => {
|
||||
for (const { node, name } of context.accumulator.pendingNetworkCalls) {
|
||||
const target = argumentNode(node.arguments[0]);
|
||||
if (target === undefined) continue;
|
||||
const resolved = resolvedRequestTarget(target, node, bindings, index);
|
||||
): Generator<void, void> {
|
||||
const pending = context.accumulator.pendingNetworkCalls.flatMap(
|
||||
({ node, name }) => {
|
||||
const target = argumentNode(node.arguments[0]);
|
||||
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)
|
||||
addEndpoint(context, {
|
||||
node,
|
||||
@@ -540,11 +569,10 @@ export const finishPartialNetworkEndpoints = (
|
||||
});
|
||||
else addPartialNetworkEndpoint(context, node, name, target);
|
||||
}
|
||||
context.accumulator.pendingNetworkCalls.length = 0;
|
||||
context.accumulator.endpoints.sort(
|
||||
(left, right) => left.offset - right.offset,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const addPartialNetworkEndpoint = (
|
||||
context: FindingContext,
|
||||
@@ -561,24 +589,34 @@ const addPartialNetworkEndpoint = (
|
||||
resolution: "partial",
|
||||
});
|
||||
|
||||
const resolvedRequestTarget = (
|
||||
interface RequestTargetCapture {
|
||||
readonly reference: t.Node;
|
||||
readonly site: t.Node;
|
||||
}
|
||||
|
||||
const requestTargetCapture = (
|
||||
node: t.Node,
|
||||
site: t.Node,
|
||||
bindings: JavaScriptSemanticAnalysisState,
|
||||
index: JavaScriptBindingIndex,
|
||||
): string | undefined => {
|
||||
): RequestTargetCapture => {
|
||||
const reference = requestReferenceNode(node);
|
||||
// A toString result is selected before the enclosing request invocation.
|
||||
const expression = unwrapJavaScriptExpression(node).node;
|
||||
const capture = reference === expression ? site : expression;
|
||||
if (referenceMayHaveChangedAtSite(reference, capture, index))
|
||||
return undefined;
|
||||
if (!requestInitializerPrecedesSite(reference, capture, bindings, index))
|
||||
return { reference, site: reference === expression ? site : expression };
|
||||
};
|
||||
|
||||
/** 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;
|
||||
const evaluated = evaluateSemanticExpression(node, bindings);
|
||||
if (evaluated.status === "literal" && typeof evaluated.value === "string")
|
||||
return evaluated.value;
|
||||
return urlObjectRequestTarget(node, capture, bindings, index);
|
||||
return urlObjectRequestTarget(node, capture.site, bindings, index);
|
||||
};
|
||||
|
||||
const requestInitializerPrecedesSite = (
|
||||
|
||||
Reference in new issue
Block a user