mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
6688ed6db7
Retain named operations in GraphQL Source bodies with locationOffset metadata. Use final property ownership and parsed lexical facts to suppress copies only when a recovered document, its containers and source aliases have proven private metadata ownership. Preserve independent observations after writes, exports, opaque uses, classic-script global exposure or direct evaluation. Share interpretation across application and captured-web analysis. Build optional reference indexes once per source and checkpoint examined records, including suppressed findings, to retain cooperative cancellation. Fixes #3311 Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
222 lines
7.5 KiB
TypeScript
222 lines
7.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import {
|
|
unwrapJavaScriptExpression,
|
|
semanticStaticPropertyKey,
|
|
semanticFinalObjectStringProperty,
|
|
} from "./javascriptAstValues.js";
|
|
import { calleeName } from "./javascriptStaticAnalysisHelpers.js";
|
|
import type { JavaScriptBindingIndex } from "./javascriptSemanticState.js";
|
|
import { createJavaScriptGraphqlScan } from "./javascriptGraphqlOperations.js";
|
|
|
|
/** One RPC operation or transport URL recovered from client-side syntax. */
|
|
export interface JavaScriptRpcEndpoint {
|
|
readonly value: string;
|
|
readonly mechanism: string;
|
|
}
|
|
|
|
/** Feed every AST node in pre-order, then drain finish() for Source findings. */
|
|
export interface JavaScriptRpcScan {
|
|
inspect(node: t.Node): JavaScriptRpcEndpoint | undefined;
|
|
finish(): Iterable<
|
|
(JavaScriptRpcEndpoint & { readonly node: t.Node }) | void
|
|
>;
|
|
}
|
|
|
|
// tRPC proxies preserve router and procedure names as property names, so they
|
|
// survive identifier minification. Method names follow @trpc/react-query and
|
|
// @trpc/tanstack-react-query 11.19 documentation; cache-key helpers such as
|
|
// queryKey are excluded because they never issue a request.
|
|
const TRPC_HOOK_METHODS: ReadonlySet<string> = new Set([
|
|
"useQuery",
|
|
"useSuspenseQuery",
|
|
"useInfiniteQuery",
|
|
"useSuspenseInfiniteQuery",
|
|
"usePrefetchQuery",
|
|
"usePrefetchInfiniteQuery",
|
|
"useMutation",
|
|
"useSubscription",
|
|
"queryOptions",
|
|
"infiniteQueryOptions",
|
|
"mutationOptions",
|
|
"subscriptionOptions",
|
|
]);
|
|
|
|
// @trpc/client 11.19 clientCallTypeMap methods; common outside tRPC (Apollo,
|
|
// database pools, stores), so they need corroborating evidence.
|
|
const TRPC_REQUEST_METHODS: ReadonlySet<string> = new Set(["query", "mutate"]);
|
|
// @trpc/client 11.19 TRPCSubscriptionObserver callbacks; RxJS observers use
|
|
// next/error/complete instead.
|
|
const TRPC_SUBSCRIPTION_CALLBACKS: ReadonlySet<string> = new Set([
|
|
"onStarted",
|
|
"onData",
|
|
"onError",
|
|
"onStopped",
|
|
"onComplete",
|
|
"onConnectionStateChange",
|
|
]);
|
|
// @trpc/client 11.19 client factories (createTRPCProxyClient is the
|
|
// deprecated alias of createTRPCClient).
|
|
const TRPC_CLIENT_FACTORIES: ReadonlySet<string> = new Set([
|
|
"createTRPCClient",
|
|
"createTRPCProxyClient",
|
|
]);
|
|
// Built-in HTTP links documented by tRPC 11.19 that carry a `url` option.
|
|
const TRPC_URL_LINKS: ReadonlySet<string> = new Set([
|
|
"httpLink",
|
|
"httpBatchLink",
|
|
"httpBatchStreamLink",
|
|
"httpSubscriptionLink",
|
|
]);
|
|
// Literals retained by @trpc/client 11.19 bundles (`this.name`, the default
|
|
// accept header key) or by the conventional transport path.
|
|
const TRPC_SOURCE_MARKERS = ["TRPCClientError", "trpc-accept", "/trpc"];
|
|
|
|
// Browser-owned roots whose methods share tRPC names, such as
|
|
// navigator.permissions.query.
|
|
const BROWSER_GLOBAL_NAMES: ReadonlySet<string> = new Set([
|
|
"window",
|
|
"self",
|
|
"globalThis",
|
|
"document",
|
|
"navigator",
|
|
"location",
|
|
"history",
|
|
"console",
|
|
"performance",
|
|
"screen",
|
|
"top",
|
|
"parent",
|
|
"frames",
|
|
]);
|
|
|
|
const PROCEDURE_SEGMENT = /^[A-Za-z_$][\w$]*$/u;
|
|
|
|
interface MemberPath {
|
|
readonly root: t.Node;
|
|
/** Non-computed property names from the root outward. */
|
|
readonly properties: readonly string[];
|
|
}
|
|
|
|
/** Read a static member path and the expression it starts from. */
|
|
const memberPath = (callee: t.Node): MemberPath | undefined => {
|
|
const reversed: string[] = [];
|
|
let current: t.Node = callee;
|
|
while (
|
|
t.isMemberExpression(current) ||
|
|
t.isOptionalMemberExpression(current)
|
|
) {
|
|
const property = semanticStaticPropertyKey(
|
|
current.property,
|
|
current.computed,
|
|
);
|
|
if (property === null || !PROCEDURE_SEGMENT.test(property))
|
|
return undefined;
|
|
reversed.push(property);
|
|
current = current.object;
|
|
}
|
|
return { root: current, properties: reversed.reverse() };
|
|
};
|
|
|
|
const objectKeys = (node: t.Node | undefined): readonly string[] =>
|
|
t.isObjectExpression(node)
|
|
? node.properties.flatMap((property) => {
|
|
if (!t.isObjectProperty(property) && !t.isObjectMethod(property))
|
|
return [];
|
|
const key = semanticStaticPropertyKey(property.key, property.computed);
|
|
return key === null ? [] : [key];
|
|
})
|
|
: [];
|
|
|
|
/** Bundlers use `(0, imported.factory)` to call an import without a receiver. */
|
|
const rpcFactoryName = (callee: t.Node): string => {
|
|
let current = unwrapJavaScriptExpression(callee).node;
|
|
while (t.isSequenceExpression(current)) {
|
|
const last = current.expressions.at(-1);
|
|
if (last === undefined) return "";
|
|
current = unwrapJavaScriptExpression(last).node;
|
|
}
|
|
return calleeName(current);
|
|
};
|
|
|
|
/** Whether an initializer has the syntax of a documented tRPC client factory. */
|
|
export const isJavaScriptRpcClientFactory = (node: t.Node): boolean => {
|
|
const call = unwrapJavaScriptExpression(node).node;
|
|
return (
|
|
(t.isCallExpression(call) || t.isOptionalCallExpression(call)) &&
|
|
TRPC_CLIENT_FACTORIES.has(
|
|
rpcFactoryName(call.callee).split(".").pop() ?? "",
|
|
)
|
|
);
|
|
};
|
|
|
|
/** Recover the literal `url` option passed to a documented tRPC HTTP link. */
|
|
const trpcLinkUrl = (
|
|
node: t.CallExpression | t.OptionalCallExpression,
|
|
): JavaScriptRpcEndpoint | undefined => {
|
|
const name = rpcFactoryName(node.callee);
|
|
const factory = name.slice(name.lastIndexOf(".") + 1);
|
|
if (!TRPC_URL_LINKS.has(factory)) return undefined;
|
|
const options = node.arguments[0];
|
|
if (!t.isObjectExpression(options)) return undefined;
|
|
const url = semanticFinalObjectStringProperty(options, "url");
|
|
return url === undefined
|
|
? undefined
|
|
: { value: url, mechanism: `trpc:link:${factory}` };
|
|
};
|
|
|
|
/**
|
|
* Create the per-source scan. Hook and option-factory methods are distinctive.
|
|
* `query`/`mutate` need a root bound to a tRPC client factory, or a tRPC source
|
|
* marker with a router path of at least two segments; `subscribe` needs the
|
|
* marker and a tRPC observer argument. `this` roots, browser globals and RTK
|
|
* Query `endpoints` chains are never procedures.
|
|
*/
|
|
export const createJavaScriptRpcScan = (
|
|
source: string,
|
|
clientRoots: ReadonlySet<t.Node>,
|
|
index: JavaScriptBindingIndex,
|
|
): JavaScriptRpcScan => {
|
|
let marker: boolean | undefined;
|
|
const hasMarker = (): boolean =>
|
|
(marker ??= TRPC_SOURCE_MARKERS.some((value) => source.includes(value)));
|
|
const graphql = createJavaScriptGraphqlScan(index);
|
|
|
|
const procedure = (
|
|
node: t.CallExpression | t.OptionalCallExpression,
|
|
): JavaScriptRpcEndpoint | undefined => {
|
|
const path = memberPath(node.callee);
|
|
if (path === undefined || t.isThisExpression(path.root)) return undefined;
|
|
if (t.isIdentifier(path.root) && BROWSER_GLOBAL_NAMES.has(path.root.name))
|
|
return undefined;
|
|
const method = path.properties[path.properties.length - 1];
|
|
const segments = path.properties.slice(0, -1);
|
|
const first = segments[0];
|
|
if (method === undefined || first === undefined) return undefined;
|
|
if (BROWSER_GLOBAL_NAMES.has(first) || segments.includes("endpoints"))
|
|
return undefined;
|
|
const bound = clientRoots.has(path.root);
|
|
const recognized = TRPC_HOOK_METHODS.has(method)
|
|
? true
|
|
: TRPC_REQUEST_METHODS.has(method)
|
|
? bound || (segments.length >= 2 && hasMarker())
|
|
: method === "subscribe" &&
|
|
(bound || hasMarker()) &&
|
|
objectKeys(node.arguments[1]).some((key) =>
|
|
TRPC_SUBSCRIPTION_CALLBACKS.has(key),
|
|
);
|
|
return recognized
|
|
? { value: segments.join("."), mechanism: `trpc:procedure:${method}` }
|
|
: undefined;
|
|
};
|
|
|
|
return {
|
|
inspect: (node) => {
|
|
if (t.isCallExpression(node) || t.isOptionalCallExpression(node))
|
|
return trpcLinkUrl(node) ?? procedure(node);
|
|
return graphql.inspect(node);
|
|
},
|
|
finish: graphql.finish,
|
|
};
|
|
};
|