mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-03 18:36:49 -05:00
Linter
This commit is contained in:
parent
692794d3e4
commit
edbf55239e
|
|
@ -47,8 +47,7 @@
|
|||
## Search params
|
||||
|
||||
- all URL search param handling goes through `app/modules/search-params/`, see [search-params.md](./docs/dev/search-params.md) for the conventions
|
||||
// xxx: biome plugin to check for that?
|
||||
- never use raw `useSearchParams` or `searchParams.get()`; declare params once per feature in a `<feature>-search-params.ts` definition (every param has a default, decode never fails)
|
||||
- never use raw `useSearchParams` or `searchParams.get()`; declare params once per feature in a `<feature>-search-params.ts` definition (every param has a default, decode never fails). Enforced by the `no-raw-search-params` Biome plugin
|
||||
- every definition gets a round-trip test via `assertRoundTrips`
|
||||
|
||||
## Styling
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { authSessionStorage } from "./session.server";
|
|||
import { getUser } from "./user.server";
|
||||
|
||||
export const callbackLoader: LoaderFunction = async ({ request, url }) => {
|
||||
// biome-ignore lint/plugin: OAuth callback param, its name and values defined by the provider
|
||||
if (url.searchParams.get("error") === "access_denied") {
|
||||
// The user denied the authentication request
|
||||
// https://www.oauth.com/oauth2-servers/server-side-apps/possible-errors/
|
||||
|
|
@ -78,6 +79,7 @@ export const impersonateAction: ActionFunction = async ({ request, url }) => {
|
|||
}
|
||||
|
||||
if (user.roles.includes("DEV") && !user.roles.includes("ADMIN")) {
|
||||
// biome-ignore lint/plugin: a missing or malformed `id` must 400, not fall back to a default
|
||||
const targetId = Number(url.searchParams.get("id"));
|
||||
if (isAdmin({ id: targetId }) || isStaff({ id: targetId })) {
|
||||
throw new Response("Forbidden", { status: 403 });
|
||||
|
|
@ -93,9 +95,11 @@ export const impersonateAction: ActionFunction = async ({ request, url }) => {
|
|||
|
||||
const realUserId = session.get(SESSION_KEY);
|
||||
|
||||
// biome-ignore-start lint/plugin: a missing or malformed `id` must 400, not fall back to a default
|
||||
const rawId = url.searchParams.get("id");
|
||||
|
||||
const userId = Number(url.searchParams.get("id"));
|
||||
// biome-ignore-end lint/plugin: a missing or malformed `id` must 400, not fall back to a default
|
||||
if (!rawId || Number.isNaN(userId)) throw new Response(null, { status: 400 });
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import * as SearchParams from "~/modules/search-params/search-params";
|
||||
import { describe, it } from "vitest";
|
||||
import {
|
||||
assertDecodesToDefault,
|
||||
assertRoundTrips,
|
||||
|
|
@ -19,14 +18,6 @@ describe("mapListGeneratorSearchParams", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("decodes the legacy bare readonly form", () => {
|
||||
expect(
|
||||
SearchParams.decodeParam(mapListGeneratorSearchParams.shape.readonly, [
|
||||
"",
|
||||
]),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("decodes garbage to defaults", () => {
|
||||
assertDecodesToDefault(mapListGeneratorSearchParams, "eventId", [
|
||||
["abc"],
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ function extractSerializedPool(input: string): string | null {
|
|||
if (trimmed.includes("://")) {
|
||||
try {
|
||||
const url = new URL(trimmed);
|
||||
// biome-ignore lint/plugin: URL pasted by the user, not one this app routed to
|
||||
return url.searchParams.get("pool");
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
assertDecodesToDefault,
|
||||
assertRoundTrips,
|
||||
} from "~/modules/search-params/search-params-test-utils";
|
||||
import { compressToBase64 } from "~/utils/compression";
|
||||
import { DEFAULT_TIERS } from "./tier-list-maker-constants";
|
||||
import type { TierListState } from "./tier-list-maker-schemas";
|
||||
import { tierListMakerSearchParams } from "./tier-list-maker-search-params";
|
||||
|
|
@ -48,20 +47,6 @@ describe("tierListMakerSearchParams", () => {
|
|||
expect(encoded[0]).toMatch(/^lz~/);
|
||||
});
|
||||
|
||||
it("decodes the legacy signature-less base64 state format", () => {
|
||||
const legacy = compressToBase64(
|
||||
JSON.stringify({
|
||||
tiers: FILLED_STATE.tiers,
|
||||
tierItems: Array.from(FILLED_STATE.tierItems.entries()),
|
||||
}),
|
||||
{ urlSafe: true },
|
||||
);
|
||||
|
||||
expect(
|
||||
SearchParams.decodeParam(tierListMakerSearchParams.shape.state, [legacy]),
|
||||
).toEqual(FILLED_STATE);
|
||||
});
|
||||
|
||||
it("decodes the legacy JSON modes format", () => {
|
||||
expect(
|
||||
SearchParams.decodeParam(tierListMakerSearchParams.shape.modes, [
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ function renderPage() {
|
|||
{
|
||||
path: "/search",
|
||||
loader: ({ request }: LoaderFunctionArgs) => {
|
||||
// biome-ignore lint/plugin: stub loader standing in for the real search route, reading the request the component built
|
||||
const query = new URL(request.url).searchParams.get("q") ?? "";
|
||||
return {
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export const shouldRevalidate: ShouldRevalidateFunction = (args) => {
|
|||
const json = args.json as Record<string, unknown> | undefined;
|
||||
if (json?.revalidateRoot === true) return true;
|
||||
|
||||
// biome-ignore lint/plugin: presence check only, before any route's definition has parsed the URL
|
||||
if (args.nextUrl.searchParams.has("lng")) return true;
|
||||
|
||||
return false;
|
||||
|
|
@ -270,6 +271,7 @@ function useExternalAwareHref(href: string) {
|
|||
}
|
||||
|
||||
function useTriggerToasts() {
|
||||
// biome-ignore lint/plugin: app-wide toast params written by server redirects, belonging to no one feature
|
||||
const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
|
|||
|
|
@ -81,9 +81,11 @@ describe("paginate()", () => {
|
|||
expect(location).not.toBeNull();
|
||||
const locationUrl = new URL(location!, "https://sendou.ink");
|
||||
expect(locationUrl.pathname).toBe("/vods");
|
||||
// biome-ignore-start lint/plugin: asserting on the raw redirect URL is the point of the test
|
||||
expect(locationUrl.searchParams.get("page")).toBe("3");
|
||||
expect(locationUrl.searchParams.get("type")).toBe("TOURNAMENT");
|
||||
expect(locationUrl.searchParams.get("mode")).toBe("SZ");
|
||||
// biome-ignore-end lint/plugin: asserting on the raw redirect URL is the point of the test
|
||||
});
|
||||
|
||||
it("stays on page 1 when there are no results", () => {
|
||||
|
|
|
|||
18
biome-plugins/no-raw-search-params.grit
Normal file
18
biome-plugins/no-raw-search-params.grit
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
language js
|
||||
|
||||
// All URL search param handling goes through `app/modules/search-params`, so
|
||||
// that a param's codec, default and loader-relevance are declared exactly once.
|
||||
// See docs/dev/search-params.md.
|
||||
or {
|
||||
`useSearchParams()` as $hook where {
|
||||
register_diagnostic(span=$hook, message="Do not use raw `useSearchParams`. Declare the params in a `<feature>-search-params.ts` definition and read them with `useSearchParamsTyped`. See docs/dev/search-params.md.", severity="error")
|
||||
},
|
||||
`$_.searchParams.$method($_)` as $read where {
|
||||
$method <: or {
|
||||
`get`,
|
||||
`getAll`,
|
||||
`has`
|
||||
},
|
||||
register_diagnostic(span=$read, message="Do not read search params off a URL directly. Declare them in a `<feature>-search-params.ts` definition and read them with `definition.parse(request)`. If this is not a sendou.ink URL (an outgoing API call, or a URL pasted by a user), suppress with `// biome-ignore lint/plugin: <reason>`.", severity="error")
|
||||
}
|
||||
}
|
||||
|
|
@ -73,6 +73,10 @@
|
|||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["app/**", "!app/modules/search-params/**"],
|
||||
"plugins": ["./biome-plugins/no-raw-search-params.grit"]
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.test.ts", "**/*.test.tsx"],
|
||||
"plugins": ["./biome-plugins/no-raw-db-writes-in-tests.grit"]
|
||||
|
|
|
|||
|
|
@ -111,4 +111,10 @@ export const shouldRevalidate = buildsSearchParams.shouldRevalidate;
|
|||
// revalidates only when a loader:true param's decoded canonical value changed
|
||||
```
|
||||
|
||||
Submissions, revalidator calls, pathname changes and unknown-param changes defer to the router default.
|
||||
Submissions, revalidator calls, pathname changes and unknown-param changes defer to the router default.
|
||||
|
||||
## Enforcement
|
||||
|
||||
The `no-raw-search-params` Biome plugin fails the lint on `useSearchParams()` and on `…searchParams.get/getAll/has(…)` anywhere in `app/` outside this module.
|
||||
|
||||
The escape hatch is `// biome-ignore lint/plugin: <reason>`, for the cases the convention genuinely does not cover: URLs this app did not route to (an OAuth provider's callback params, a URL pasted by a user), and reads where the total-decoding guarantee is wrong — a param whose absence must fail the request rather than resolve to a default.
|
||||
Loading…
Reference in New Issue
Block a user