Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b5b513
web-next: stop <Show> stale-read race via keyed-show lint rule
dahlia May 4, 2026
a66ef4d
web-next: scope keyed-show lint rule to Relay-backed values
dahlia May 4, 2026
b8e8ae1
web-next: Migrate Translating placeholder catalogs from {0} to {name}
dahlia May 5, 2026
46d492c
keyed-show: Propagate Relay binding through FunctionExpression too
dahlia May 5, 2026
7126126
keyed-show: Verify Relay primitives via solid-relay import origin
dahlia May 5, 2026
88516d9
notification: Tidy two stale references to fragment-accessor form
dahlia May 5, 2026
9977277
keyed-show: Recognise aliased solid-relay primitive imports
dahlia May 5, 2026
92dad28
keyed-show: Rewrite optional bare param?.() calls in autofix too
dahlia May 5, 2026
7b37e0f
i18n: Unify "contact support" wording in zh-CN and zh-TW
dahlia May 5, 2026
5750ca6
keyed-show: Suppress autofix entirely when body has same-name shadow
dahlia May 5, 2026
4731c76
keyed-show: Reject Relay primitive call when import is locally shadowed
dahlia May 5, 2026
42ebb81
keyed-show: Track shadow bindings so inner scopes hide outer Relay names
dahlia May 5, 2026
37f19a6
keyed-show: Match callback selection between JSXElement entry and exit
dahlia May 5, 2026
92bb406
keyed-show: Suppress autofix when body calls the param with arguments
dahlia May 5, 2026
a6c497d
keyed-show: Treat assignment to the param as a body rebinding
dahlia May 5, 2026
bca12e1
keyed-show: Push and pop scopes on BlockStatement and CatchClause
dahlia May 5, 2026
5193359
keyed-show: Tighten import-shadow check for nested-fn and self-binding
dahlia May 5, 2026
9c6d919
keyed-show: Wire OptionalCallExpression visitor as a defensive mirror
dahlia May 5, 2026
33593e1
keyed-show: Catch destructuring assignment and update-expression rebi…
dahlia May 5, 2026
4114223
keyed-show: Use scope stack for import-shadow check, not function-wid…
dahlia May 5, 2026
3c3a25b
keyed-show: Hoist detectRebinding's FIELDS map to module scope
dahlia May 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web-next/deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"#i18n": "./src/lib/i18n/server.ts"
},
"lint": {
"plugins": ["./lint-plugins/keyed-show.ts"],
"rules": {
"exclude": [
// SolidStart server functions must be async functions at all time
Expand Down
369 changes: 369 additions & 0 deletions web-next/lint-plugins/keyed-show.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,369 @@
import { assertEquals } from "@std/assert";
import plugin from "./keyed-show.ts";

const RULE = "hackerspub-solid/show-keyed-on-fn-child";

function lint(source: string) {
return Deno.lint.runPlugin(plugin, "test.tsx", source);
}

// Helper: most fixtures wrap their JSX in a function that declares a
// solid-relay primitive — the rule only fires when the gated value can be
// traced to one of those primitives.
const RELAY_PRELUDE = `
import { createPreloadedQuery, createFragment } from "solid-relay";
declare const env: unknown;
declare const Q: unknown;
declare function loadQuery(...args: unknown[]): unknown;
`;

Deno.test("flags non-keyed Show on Relay-backed value", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App(props: { $x: unknown }) {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data().actorByHandle}>
{(actor) => <div>{actor().name}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].id, RULE);
});

Deno.test("does NOT flag non-keyed Show on a plain Solid signal", () => {
const diagnostics = lint(`
import { createSignal } from "solid-js";
function App() {
const [value, _setValue] = createSignal();
return (
<Show when={value()}>
{(v) => <div>{v()}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});

Deno.test("does NOT flag non-keyed Show on a plain identifier", () => {
const diagnostics = lint(`
function App(props: { cond: () => unknown }) {
return (
<Show when={props.cond()}>
{(v) => <div>{v()}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});

Deno.test("does not flag already-keyed Relay-backed Show", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show keyed when={data().actorByHandle}>
{(actor) => <div>{actor.name}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});

Deno.test("does not flag Show whose child is not a function", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
<div>plain JSX child</div>
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});

Deno.test("does not flag Show with zero-arity function child", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
{() => <div>no params</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});

Deno.test("flags non-keyed Match with function child on Relay value", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Switch>
<Match when={data()}>
{(value) => <div>{value()}</div>}
</Match>
</Switch>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].id, RULE);
});

Deno.test("autofix adds keyed and rewrites bare param() calls", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data().actorByHandle}>
{(actor) => (
<div>
<a href={actor().url}>{actor().name}</a>
</div>
)}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix!.length, 3);
});

Deno.test(
"autofix skips param() calls inside nested fn that rebinds the name",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
{(actor) => (
<For each={items()}>
{(actor) => <span>{actor()}</span>}
</For>
)}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix!.length, 1);
},
);

Deno.test("autofix preserves param() calls passing arguments", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
{(value) => <div>{value(1)}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix!.length, 1);
});

Deno.test(
"flags but does not rewrite calls when param is destructured",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
{({ name }) => <div>{name}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix!.length, 1);
},
);

Deno.test(
"flags but does not rewrite when body has a const shadowing",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
{(value) => {
const value = compute();
return <div>{value()}</div>;
}}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix!.length, 1);
},
);

Deno.test(
"flags but does not rewrite when class static block shadows the param",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show when={data()}>
{(value) => {
class C { static { const value = 1; console.log(value); } }
return <div>{value()}</div>;
}}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix!.length, 1);
},
);

Deno.test(
"flags Show with keyed={false} but does not autofix",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show keyed={false} when={data()}>
{(value) => <div>{value()}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix, []);
},
);

Deno.test(
"flags Show with keyed={someVar} but does not autofix",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show keyed={isKeyed} when={data()}>
{(value) => <div>{value()}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].fix, []);
},
);

Deno.test("does not flag Show with keyed={true}", () => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show keyed={true} when={data()}>
{(value) => <div>{value.name}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});

Deno.test(
"propagates Relay-backed-ness through outer keyed Show callback param",
() => {
const diagnostics = lint(`${RELAY_PRELUDE}
function App() {
const data = createPreloadedQuery(env, () => loadQuery());
return (
<Show keyed when={data()}>
{(d) => (
<Show when={d.actorByHandle}>
{(actor) => <div>{actor().name}</div>}
</Show>
)}
</Show>
);
}
`);
// Inner Show should be flagged: \`d\` is Relay-backed by propagation.
assertEquals(diagnostics.length, 1);
assertEquals(diagnostics[0].id, RULE);
},
);

Deno.test(
"does not propagate when outer Show is not Relay-backed",
() => {
const diagnostics = lint(`
import { createSignal } from "solid-js";
function App() {
const [outer, _set] = createSignal();
return (
<Show keyed when={outer()}>
{(d) => (
<Show when={d.something}>
{(thing) => <div>{thing()}</div>}
</Show>
)}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
},
);

Deno.test("recognises namespace imports of Relay primitives", () => {
const diagnostics = lint(`
import * as relay from "solid-relay";
declare const env: unknown;
declare const Q: unknown;
declare function loadQuery(...args: unknown[]): unknown;
function App() {
const data = relay.createFragment(Q, () => null);
return (
<Show when={data()}>
{(value) => <div>{value()}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 1);
});

Deno.test("scopes Relay binding to its declaring function", () => {
// \`data\` declared inside FunctionA must NOT be considered Relay-backed
// inside FunctionB (separate scope).
const diagnostics = lint(`${RELAY_PRELUDE}
function FunctionA() {
const data = createPreloadedQuery(env, () => loadQuery());
return null;
}
function FunctionB() {
// No relay primitive bound here.
return (
<Show when={data()}>
{(value) => <div>{value()}</div>}
</Show>
);
}
`);
assertEquals(diagnostics.length, 0);
});
Loading
Loading