mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 10:25:42 +00:00
* fix(javascript): prove Electron module namespace conversion * refactor(javascript): share AST child field metadata Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): retain destructured intrinsic writes Share one pattern-target walker across lexical definitions, member mutation tracking and Electron identity writes. Keep overwritten globals unresolved without treating property keys or shadowed locals as global replacements. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): preserve intrinsic projections and fresh target identity Resolve known mutators through static binding projections and prove stable identifier chains to fresh literals before widening unknown mutation targets. Keep reassigned, uninitialized and member-projected targets conservative. Cover both review regressions through source tests and inert CLI/MCP fixtures. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): retain fresh function and class target identity Recognize stable local function, undecorated class and RegExp targets before widening intrinsic mutations. Keep decorated classes opaque because their binding value can be replaced, and preserve class-initialization writes. Cover fresh targets and replacement decorators in inert CLI/MCP fixtures. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): preserve intrinsic alias capture timing Validate each alias read after initialization and retain the original function capture across copies. Share declaration initializer facts with RPC and Electron analysis, including class declarations used for fresh-target identity. Accept single-statement control blocks while keeping lexical roles and added operations checked. Cover the capture regressions through inert source and public CLI/MCP workflows. * fix(javascript): respect hoisted fresh function declarations Function declarations provide fresh targets before their textual position. Retain initialization-order checks for variable and class values, including aliases. Cover the reviewed regression with source controls and the public CLI/MCP fixture. * fix(javascript): preserve proven interop result projections * fix(javascript): retain stable declarations and bound intrinsic writes Keep repeated variable declarations with one unconditional initializer while rejecting assignments, conditional initialization and multiple values. Normalize known intrinsic calls, aliases and bound argument prefixes at the semantic owner, retaining opaque targets without tainting unused bindings or fresh local objects. Cover both regressions through source and CLI/MCP tests. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> * fix(javascript): retain bound intrinsic mutation effects * fix(javascript): preserve bound receivers for intrinsic invocation * fix(javascript): prove generic binding and invocation effects * fix(javascript): retain Reflect set receiver effects --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
356 lines
11 KiB
TypeScript
356 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
|
|
|
const endpoints = (source: string) =>
|
|
analyzeJavaScriptStaticSource(source).endpoints.map(
|
|
({ kind, value, mechanism }) => ({ kind, value, mechanism }),
|
|
);
|
|
|
|
const rpcEndpoints = (source: string) =>
|
|
endpoints(source).filter(
|
|
({ mechanism }) =>
|
|
mechanism.startsWith("trpc:") || mechanism.startsWith("graphql:"),
|
|
);
|
|
|
|
describe("tRPC procedure recognition", () => {
|
|
it("recognizes React hook procedures on a renamed proxy root", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const a = n.post.byId.useQuery({ id: 1 });
|
|
const b = n.post.add.useMutation();
|
|
n.feed.useInfiniteQuery({ limit: 10 });
|
|
n.onEvent.useSubscription(undefined, { onData() {} });
|
|
n.invoice.list.useSuspenseQuery();
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "post.byId",
|
|
mechanism: "trpc:procedure:useQuery",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "post.add",
|
|
mechanism: "trpc:procedure:useMutation",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "feed",
|
|
mechanism: "trpc:procedure:useInfiniteQuery",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "onEvent",
|
|
mechanism: "trpc:procedure:useSubscription",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "invoice.list",
|
|
mechanism: "trpc:procedure:useSuspenseQuery",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("recognizes TanStack React Query integration option factories", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
useQuery(e.invoice.list.queryOptions({ page: 1 }));
|
|
useMutation(e.invoice.create.mutationOptions());
|
|
client.invalidateQueries({ queryKey: e.invoice.list.queryKey() });
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "invoice.list",
|
|
mechanism: "trpc:procedure:queryOptions",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "invoice.create",
|
|
mechanism: "trpc:procedure:mutationOptions",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("recognizes vanilla client calls and link URLs only in sources carrying a tRPC marker", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const client = createTRPCClient({
|
|
links: [httpBatchLink({ url: "/api/trpc" })],
|
|
});
|
|
const bilbo = await client.getUser.query("id_bilbo");
|
|
await client.post.add.mutate({ title: "x" });
|
|
client.onPost.subscribe(undefined, { onData() {} });
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "/api/trpc",
|
|
mechanism: "trpc:link:httpBatchLink",
|
|
},
|
|
{ kind: "network", value: "getUser", mechanism: "trpc:procedure:query" },
|
|
{
|
|
kind: "network",
|
|
value: "post.add",
|
|
mechanism: "trpc:procedure:mutate",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "onPost",
|
|
mechanism: "trpc:procedure:subscribe",
|
|
},
|
|
]);
|
|
expect(
|
|
rpcEndpoints(`
|
|
const rows = await pool.users.query("select 1");
|
|
emitter.changes.subscribe(listener);
|
|
store.cart.mutate(update);
|
|
`),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("requires corroboration for request methods shared with Apollo, RxJS and stores", () => {
|
|
// The source carries a tRPC marker, so only the call shape can decide.
|
|
expect(
|
|
rpcEndpoints(`
|
|
class TRPCClientError extends Error {}
|
|
this.client.query({ query: q });
|
|
e.queryManager.query(options);
|
|
this.untypedClient.query("path", input);
|
|
this.route.params.subscribe((params) => params);
|
|
t.current.subscribe(listener);
|
|
r.store.subscribe(() => {});
|
|
n.cache.mutate(update);
|
|
o.k.endpoints.getPosts.useQuery();
|
|
e.api.endpoints.list.useQuery();
|
|
this.trpc.invoice.list.useQuery();
|
|
`),
|
|
).toEqual([]);
|
|
expect(
|
|
rpcEndpoints(`
|
|
class TRPCClientError extends Error {}
|
|
e.invoice.list.query({ page: 1 });
|
|
e.invoice.onChange.subscribe(undefined, { onData() {} });
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "invoice.list",
|
|
mechanism: "trpc:procedure:query",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "invoice.onChange",
|
|
mechanism: "trpc:procedure:subscribe",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("never reports browser globals or RTK Query endpoints as procedures", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
class TRPCClientError extends Error {}
|
|
navigator.permissions.query({ name: "geolocation" });
|
|
window.navigator.permissions.query({ name: "camera" });
|
|
api.endpoints.getPosts.useQuery();
|
|
api.useGetPostsQuery();
|
|
useQuery({ queryKey: ["x"] });
|
|
`),
|
|
).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("tRPC binding identity and bundled syntax", () => {
|
|
it("keeps client bindings local to their lexical declaration", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
function first() { const c = createTRPCClient({}); c.users.query(); }
|
|
function unrelated(c) { c.users.query("select 1"); }
|
|
const c = createTRPCClient({});
|
|
function closure() { c.posts.query(); }
|
|
function shadowed() { c.users.query(); var c = pool; }
|
|
{ const c = pool; c.users.query(); }
|
|
try {} catch (c) { c.users.query(); }
|
|
`),
|
|
).toEqual([
|
|
{ kind: "network", value: "users", mechanism: "trpc:procedure:query" },
|
|
{ kind: "network", value: "posts", mechanism: "trpc:procedure:query" },
|
|
]);
|
|
});
|
|
|
|
it("does not corroborate overwritten or destructured client bindings", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
let c = createTRPCClient({}); c = pool; c.users.query();
|
|
const { client } = createTRPCClient({}); client.users.query();
|
|
`),
|
|
).toEqual([]);
|
|
expect(
|
|
rpcEndpoints(`
|
|
var c = createTRPCClient({}); c.users.query();
|
|
`),
|
|
).toEqual([
|
|
{ kind: "network", value: "users", mechanism: "trpc:procedure:query" },
|
|
]);
|
|
});
|
|
|
|
it("retains one client initializer across variable redeclarations", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
var c;
|
|
var c = createTRPCClient({});
|
|
var c;
|
|
c.users.query();
|
|
|
|
var replaced;
|
|
var replaced = createTRPCClient({});
|
|
replaced = pool;
|
|
replaced.posts.query();
|
|
|
|
var initializedTwice = createTRPCClient({});
|
|
var initializedTwice = pool;
|
|
initializedTwice.comments.query();
|
|
|
|
var conditional;
|
|
if (flag) { var conditional = createTRPCClient({}); }
|
|
conditional.groups.query();
|
|
`),
|
|
).toEqual([
|
|
{ kind: "network", value: "users", mechanism: "trpc:procedure:query" },
|
|
]);
|
|
});
|
|
|
|
it("recognizes surviving factory names inside bundled call wrappers", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const c = (0, t.createTRPCClient)({
|
|
links: [(0, t.httpBatchLink)({ url: "/api/rpc" })],
|
|
});
|
|
c.users.query();
|
|
const p = (t.createTRPCProxyClient as any)({});
|
|
p.posts.query();
|
|
(0, t.httpLink)({ url: "/api/other" });
|
|
`),
|
|
).toEqual([
|
|
{
|
|
kind: "network",
|
|
value: "/api/rpc",
|
|
mechanism: "trpc:link:httpBatchLink",
|
|
},
|
|
{ kind: "network", value: "users", mechanism: "trpc:procedure:query" },
|
|
{ kind: "network", value: "posts", mechanism: "trpc:procedure:query" },
|
|
{ kind: "network", value: "/api/other", mechanism: "trpc:link:httpLink" },
|
|
]);
|
|
});
|
|
|
|
it("preserves procedure paths beyond the former nesting cutoff", () => {
|
|
const path = Array.from({ length: 64 }, (_, i) => `router${i}`).join(".");
|
|
expect(rpcEndpoints(`root.${path}.useQuery();`)).toEqual([
|
|
{ kind: "network", value: path, mechanism: "trpc:procedure:useQuery" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe("GraphQL operation recognition", () => {
|
|
it("recognizes named operations in tagged templates and string literals", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const GET_USER = gql\`
|
|
# comment
|
|
query GetUser($id: ID!) { user(id: $id) { name } }
|
|
\`;
|
|
const UPDATE = "mutation UpdateInvoice($input: InvoiceInput!) { updateInvoice(input: $input) { id } }";
|
|
const ON = \`subscription OnInvoice { invoiceChanged { id } } \${fragment}\`;
|
|
`),
|
|
).toEqual([
|
|
{ kind: "network", value: "GetUser", mechanism: "graphql:query" },
|
|
{
|
|
kind: "network",
|
|
value: "UpdateInvoice",
|
|
mechanism: "graphql:mutation",
|
|
},
|
|
{
|
|
kind: "network",
|
|
value: "OnInvoice",
|
|
mechanism: "graphql:subscription",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("retains operations after long ignored prefixes and long headers", () => {
|
|
const name = "Operation" + "x".repeat(512);
|
|
const text =
|
|
"#" +
|
|
"comment".repeat(700) +
|
|
"\nquery " +
|
|
name +
|
|
" ".repeat(600) +
|
|
"{ users { id } }";
|
|
expect(rpcEndpoints(`const doc = ${JSON.stringify(text)};`)).toEqual([
|
|
{ kind: "network", value: name, mechanism: "graphql:query" },
|
|
]);
|
|
});
|
|
|
|
it("recognizes precompiled GraphQL document objects", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const doc = { kind: "Document", definitions: [{
|
|
kind: "OperationDefinition",
|
|
operation: "query",
|
|
name: { kind: "Name", value: "ListInvoices" },
|
|
selectionSet: { kind: "SelectionSet", selections: [] },
|
|
}] };
|
|
`),
|
|
).toEqual([
|
|
{ kind: "network", value: "ListInvoices", mechanism: "graphql:query" },
|
|
]);
|
|
});
|
|
|
|
it("ignores anonymous operations and ordinary prose", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const a = "query { viewer { id } }";
|
|
const b = "query string parameters";
|
|
const c = "mutation observer started";
|
|
const d = { kind: "OperationDefinition", operation: "query" };
|
|
const e = "mutation Observer {";
|
|
const f = "query Search(";
|
|
const g = "subscription Plan (monthly)";
|
|
`),
|
|
).toEqual([]);
|
|
});
|
|
|
|
it("reports a precompiled graphql-tag document once, not again from loc.source.body", () => {
|
|
expect(
|
|
rpcEndpoints(`
|
|
const doc = { kind: "Document", definitions: [{
|
|
kind: "OperationDefinition",
|
|
operation: "query",
|
|
name: { kind: "Name", value: "GetUser" },
|
|
selectionSet: { kind: "SelectionSet", selections: [] },
|
|
}], loc: { start: 0, end: 40, source: {
|
|
body: "query GetUser($id: ID!) { user(id: $id) { name } }",
|
|
name: "GraphQL request",
|
|
locationOffset: { line: 1, column: 1 },
|
|
} } };
|
|
`),
|
|
).toEqual([
|
|
{ kind: "network", value: "GetUser", mechanism: "graphql:query" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
it("keeps existing network endpoint recognition unchanged", () => {
|
|
expect(
|
|
endpoints(`fetch("/api/invoices"); n.invoice.list.useQuery();`),
|
|
).toEqual([
|
|
{ kind: "network", value: "/api/invoices", mechanism: "call:fetch" },
|
|
{
|
|
kind: "network",
|
|
value: "invoice.list",
|
|
mechanism: "trpc:procedure:useQuery",
|
|
},
|
|
]);
|
|
});
|