mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Two holes, both proven with a probe that produced zero errors. The rule matched the object properties `queryKey:` and `mutationKey:` and nothing else, so the positional form was never seen — `setQueryData(["bare"], 1)` seeds the same cache cell a `queryKey:` reads, and `getQueryData` / `getQueryState` / the four defaults setters read and write it the same way. Those seven methods are checked at their first argument now, whether the call is bare, a member, or a computed member. The enforced roots were components, hooks, query modules, and files carrying their own "use client", which left src/app out. That is where the server prefetch pages build the keys the client later reads, and they carry no directive, so a server key diverging from the client's was unguarded. src/app is a root now. No violation surfaces on the current tree. The rule header records what it still cannot see: a key assembled into a variable before the call site, and the method-name match, which neither follows a project helper nor knows an unrelated object with the same method name.
148 lines
5.4 KiB
JavaScript
148 lines
5.4 KiB
JavaScript
import { describe, it } from "vitest";
|
|
import { RuleTester } from "eslint";
|
|
import plugin from "../queryKey-factory.js";
|
|
|
|
RuleTester.describe = describe;
|
|
RuleTester.it = it;
|
|
RuleTester.itOnly = it.only;
|
|
|
|
const ruleTester = new RuleTester({
|
|
languageOptions: {
|
|
ecmaVersion: 2023,
|
|
sourceType: "module",
|
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
|
},
|
|
});
|
|
|
|
const rule = plugin.rules["queryKey-factory"];
|
|
|
|
ruleTester.run("queryKey-factory", rule, {
|
|
valid: [
|
|
{
|
|
code: "useQuery({ queryKey: queryKeys.example(id) });",
|
|
filename: "/repo/src/components/example.tsx",
|
|
},
|
|
{
|
|
code: "const key = queryKeys.example(id); useQuery({ queryKey: key });",
|
|
filename: "/repo/src/hooks/use-example.ts",
|
|
},
|
|
{
|
|
code: 'export const example = () => ["example"];',
|
|
filename: "/repo/src/lib/query-keys/example.ts",
|
|
},
|
|
{
|
|
code: 'useQuery({ queryKey: ["fixture"] });',
|
|
filename: "/repo/src/components/__tests__/example.test.tsx",
|
|
},
|
|
// A server module outside every guarded root stays unguarded — the roots
|
|
// are the scope decision, not an accident of this fixture.
|
|
{
|
|
code: 'useQuery({ queryKey: ["server-only-lib"] });',
|
|
filename: "/repo/src/lib/reports/build-report.ts",
|
|
},
|
|
// The positional cache API, used correctly: the key still comes from the
|
|
// factory, so the shape that poisons the cache never arises.
|
|
{
|
|
code: "queryClient.setQueryData(queryKeys.example(id), next);",
|
|
filename: "/repo/src/app/page.tsx",
|
|
},
|
|
{
|
|
code: "const key = queryKeys.example(id); queryClient.setQueryData(key, next);",
|
|
filename: "/repo/src/components/example.tsx",
|
|
},
|
|
// Tests seed caches with literal tuples on purpose; the factory directory
|
|
// constructs them by definition. Both stay exempt on the positional form
|
|
// exactly as they are on the object-property form.
|
|
{
|
|
code: 'client.setQueryData(["fixture"], 1);',
|
|
filename: "/repo/src/hooks/__tests__/use-example.test.ts",
|
|
},
|
|
{
|
|
code: 'client.setQueryData(["fixture"], 1);',
|
|
filename: "/repo/src/lib/query-keys/__fixtures__/seed.ts",
|
|
},
|
|
],
|
|
invalid: [
|
|
{
|
|
code: 'useQuery({ queryKey: ["unguarded-component"] });',
|
|
filename: "/repo/src/components/custom-metrics/example.tsx",
|
|
errors: [{ messageId: "bareArray" }],
|
|
},
|
|
{
|
|
code: 'useMutation({ mutationKey: ["unguarded-hook"] });',
|
|
filename: "/repo/src/hooks/use-example.ts",
|
|
errors: [{ messageId: "bareArray" }],
|
|
},
|
|
{
|
|
code: '"use client"; useQuery({ queryKey: ["client-app"] });',
|
|
filename: "/repo/src/app/previously-unguarded/page.tsx",
|
|
errors: [{ messageId: "bareArray" }],
|
|
},
|
|
{
|
|
code: '"use client"; useQuery({ queryKey: ["client-lib"] });',
|
|
filename: "/repo/src/lib/client-state.tsx",
|
|
errors: [{ messageId: "bareArray" }],
|
|
},
|
|
// src/app is where the server prefetch pages build the keys the client
|
|
// then reads. A key that differs from the client's is a cache miss at
|
|
// best and a poisoned cell at worst, and without `"use client"` on the
|
|
// page the rule used to skip the file entirely.
|
|
{
|
|
code: 'useQuery({ queryKey: ["server-prefetch"] });',
|
|
filename: "/repo/src/app/server-page.tsx",
|
|
errors: [{ messageId: "bareArray" }],
|
|
},
|
|
{
|
|
code: 'queryClient.prefetchQuery({ queryKey: ["dashboard"], queryFn: load });',
|
|
filename: "/repo/src/app/coach/page.tsx",
|
|
errors: [{ messageId: "bareArray" }],
|
|
},
|
|
// The positional form of the same mistake. `setQueryData` seeds a cache
|
|
// cell by key exactly as `queryKey:` reads one, so a bare array here is
|
|
// the same defect written differently.
|
|
{
|
|
code: 'queryClient.setQueryData(["bare", "positional"], 1);',
|
|
filename: "/repo/src/app/coach/page.tsx",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
{
|
|
code: 'const cached = queryClient.getQueryData(["bare"]);',
|
|
filename: "/repo/src/hooks/use-example.ts",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
{
|
|
code: 'queryClient.getQueryState(["bare"]);',
|
|
filename: "/repo/src/components/example.tsx",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
{
|
|
code: 'queryClient.setQueryDefaults(["bare"], { staleTime: 0 });',
|
|
filename: "/repo/src/lib/queries/use-example.ts",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
{
|
|
code: 'queryClient.setMutationDefaults(["bare"], { retry: 0 });',
|
|
filename: "/repo/src/components/example.tsx",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
// Destructured off the client, and reached through a computed member —
|
|
// both spell the same call and both must be seen.
|
|
{
|
|
code: 'const { setQueryData } = useQueryClient(); setQueryData(["bare"], 1);',
|
|
filename: "/repo/src/components/example.tsx",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
{
|
|
code: 'queryClient["setQueryData"](["bare"], 1);',
|
|
filename: "/repo/src/components/example.tsx",
|
|
errors: [{ messageId: "positionalArray" }],
|
|
},
|
|
// Both holes in one file, which is the probe that found them.
|
|
{
|
|
code: 'queryClient.setQueryData(["bare", "positional"], 1); useQuery({ queryKey: ["bare", "object-property"] });',
|
|
filename: "/repo/src/app/coach/page.tsx",
|
|
errors: [{ messageId: "positionalArray" }, { messageId: "bareArray" }],
|
|
},
|
|
],
|
|
});
|