test(gates): the complexity ratchet names a function by what it is bound to
ESLint leaves an arrow bound to a const or a property unnamed, so three ratchet keys were (anonymous) and ranked by position again. A tiny rule reports each unnamed function's binding (const, property, assignment, else <callee> callback) at the head complexity reports, and the keys line up by location.
This commit is contained in:
@@ -61,11 +61,11 @@ const COMPLEX: Record<string, number> = {
|
|||||||
"components/PlexCollectionView.tsx::PlexCollectionView": 41,
|
"components/PlexCollectionView.tsx::PlexCollectionView": 41,
|
||||||
"components/PlexDiscover.tsx::PlexDiscover": 21,
|
"components/PlexDiscover.tsx::PlexDiscover": 21,
|
||||||
"components/PlexInfo.tsx::PlexInfo": 65,
|
"components/PlexInfo.tsx::PlexInfo": 65,
|
||||||
"components/PlexPlayer.tsx::(anonymous)": 22,
|
"components/PlexPlayer.tsx::onKey": 22,
|
||||||
"components/PlexPlayer.tsx::PlexPlayer": 103,
|
"components/PlexPlayer.tsx::PlexPlayer": 103,
|
||||||
"components/PlexPlaylistView.tsx::PlexPlaylistView": 22,
|
"components/PlexPlaylistView.tsx::PlexPlaylistView": 22,
|
||||||
"components/PlexSidebar.tsx::PlexSidebar": 32,
|
"components/PlexSidebar.tsx::PlexSidebar": 32,
|
||||||
"components/PlexStatsOverlay.tsx::(anonymous)": 25,
|
"components/PlexStatsOverlay.tsx::sample": 25,
|
||||||
"components/Scheduler.tsx::JobRow": 21,
|
"components/Scheduler.tsx::JobRow": 21,
|
||||||
"components/Scheduler.tsx::Scheduler": 21,
|
"components/Scheduler.tsx::Scheduler": 21,
|
||||||
"components/SetupWizard.tsx::SetupWizard": 21,
|
"components/SetupWizard.tsx::SetupWizard": 21,
|
||||||
@@ -90,7 +90,7 @@ const COMPLEX: Record<string, number> = {
|
|||||||
"components/ui/anchoredPopoverHooks.ts::useAnchoredPopover": 30,
|
"components/ui/anchoredPopoverHooks.ts::useAnchoredPopover": 30,
|
||||||
"components/usePlaylistActions.tsx::pushSteps": 23,
|
"components/usePlaylistActions.tsx::pushSteps": 23,
|
||||||
"components/usePlexActiveFilters.ts::buildPlexActiveFilters": 25,
|
"components/usePlexActiveFilters.ts::buildPlexActiveFilters": 25,
|
||||||
"components/useVideoActions.tsx::(anonymous)": 25,
|
"components/useVideoActions.tsx::items": 25,
|
||||||
"lib/api/core.ts::localizeDetail": 48,
|
"lib/api/core.ts::localizeDetail": 48,
|
||||||
"lib/api/core.ts::req": 22,
|
"lib/api/core.ts::req": 22,
|
||||||
"lib/api/plex.ts::appendPlexFilters": 26,
|
"lib/api/plex.ts::appendPlexFilters": 26,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readdirSync, readFileSync } from "node:fs";
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { ESLint } from "eslint";
|
import { ESLint, type Rule } from "eslint";
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
// Measures the shipped frontend source for the size/complexity ratchet (E32): lines per file, and the
|
// Measures the shipped frontend source for the size/complexity ratchet (E32): lines per file, and the
|
||||||
@@ -29,9 +29,53 @@ export function lineCounts(files: string[]): Record<string, number> {
|
|||||||
|
|
||||||
/** `file::function` → its complexity, for every function over MAX_COMPLEXITY. Keyed by NAME, not by
|
/** `file::function` → its complexity, for every function over MAX_COMPLEXITY. Keyed by NAME, not by
|
||||||
* rank in the file: a ranked list let one function grow while another in the same file shrank by as
|
* rank in the file: a ranked list let one function grow while another in the same file shrank by as
|
||||||
* much (E32 review). A function without a name is `(anonymous)`; a name taken twice in a file gets
|
* much (E32 review). ESLint names declarations and methods; a function it leaves unnamed takes the
|
||||||
* ` 2`, ` 3` in source order. A bare rule run (no type info, no other rules), so it costs a couple of
|
* name it is bound to (`const onSave = () => …`, `{ onSave: () => … }`), else `<callee> callback`
|
||||||
|
* (`useEffect callback`), else `(anonymous)` — so a ranked key cannot creep back in through the
|
||||||
|
* arrows (round 2). A name taken twice in a file gets ` 2`, ` 3` in source order. A bare rule run (no type info, no other rules), so it costs a couple of
|
||||||
* seconds, not the full lint. */
|
* seconds, not the full lint. */
|
||||||
|
type Node = Rule.Node;
|
||||||
|
const ident = (n: { type: string; name?: string } | null | undefined): string | undefined =>
|
||||||
|
n?.type === "Identifier" ? n.name : undefined;
|
||||||
|
function boundName(p: Node): string | undefined {
|
||||||
|
switch (p.type) {
|
||||||
|
case "VariableDeclarator":
|
||||||
|
return ident(p.id);
|
||||||
|
case "Property":
|
||||||
|
case "PropertyDefinition":
|
||||||
|
return ident(p.key);
|
||||||
|
case "AssignmentExpression":
|
||||||
|
return ident(p.left.type === "MemberExpression" ? p.left.property : p.left);
|
||||||
|
case "CallExpression": {
|
||||||
|
const callee = ident(p.callee.type === "MemberExpression" ? p.callee.property : p.callee);
|
||||||
|
return callee && `${callee} callback`;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** The name each function is bound to, reported where `complexity` reports it (the function's HEAD:
|
||||||
|
* the `=>` of an arrow, the key of a method, else the function's start) so the two line up. */
|
||||||
|
const functionNames: Rule.RuleModule = {
|
||||||
|
create(ctx) {
|
||||||
|
const visit = (node: Node) => {
|
||||||
|
const p = node.parent as Node | null;
|
||||||
|
const name = p && boundName(p);
|
||||||
|
if (!p || !name) return;
|
||||||
|
const head =
|
||||||
|
node.type === "ArrowFunctionExpression"
|
||||||
|
? ctx.sourceCode.getTokenBefore(node.body, (t) => t.value === "=>")!.loc.start
|
||||||
|
: p.type === "Property" ||
|
||||||
|
p.type === "MethodDefinition" ||
|
||||||
|
p.type === "PropertyDefinition"
|
||||||
|
? p.loc!.start
|
||||||
|
: node.loc!.start;
|
||||||
|
ctx.report({ loc: head, message: name });
|
||||||
|
};
|
||||||
|
return { ArrowFunctionExpression: visit, FunctionExpression: visit };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export async function complexityOffenders(): Promise<Record<string, number>> {
|
export async function complexityOffenders(): Promise<Record<string, number>> {
|
||||||
const eslint = new ESLint({
|
const eslint = new ESLint({
|
||||||
cwd: fileURLToPath(new URL("../../", import.meta.url)),
|
cwd: fileURLToPath(new URL("../../", import.meta.url)),
|
||||||
@@ -44,7 +88,8 @@ export async function complexityOffenders(): Promise<Record<string, number>> {
|
|||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
parserOptions: { ecmaFeatures: { jsx: true } },
|
parserOptions: { ecmaFeatures: { jsx: true } },
|
||||||
},
|
},
|
||||||
rules: { complexity: ["error", MAX_COMPLEXITY] },
|
plugins: { size: { rules: { "function-names": functionNames } } },
|
||||||
|
rules: { complexity: ["error", MAX_COMPLEXITY], "size/function-names": "error" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -54,9 +99,13 @@ export async function complexityOffenders(): Promise<Record<string, number>> {
|
|||||||
.split("\\")
|
.split("\\")
|
||||||
.join("/")
|
.join("/")
|
||||||
.replace(/^.*\/frontend\/src\//, "");
|
.replace(/^.*\/frontend\/src\//, "");
|
||||||
|
const at = (m: { line: number; column: number }) => `${m.line}:${m.column}`;
|
||||||
|
const bound = new Map(
|
||||||
|
r.messages.filter((m) => m.ruleId === "size/function-names").map((m) => [at(m), m.message]),
|
||||||
|
);
|
||||||
for (const m of r.messages) {
|
for (const m of r.messages) {
|
||||||
if (m.ruleId !== "complexity") continue;
|
if (m.ruleId !== "complexity") continue;
|
||||||
const name = /'([^']+)'/.exec(m.message)?.[1] ?? "(anonymous)";
|
const name = /'([^']+)'/.exec(m.message)?.[1] ?? bound.get(at(m)) ?? "(anonymous)";
|
||||||
let key = `${file}::${name}`;
|
let key = `${file}::${name}`;
|
||||||
for (let n = 2; key in out; n++) key = `${file}::${name} ${n}`;
|
for (let n = 2; key in out; n++) key = `${file}::${name} ${n}`;
|
||||||
out[key] = Number(/complexity of (\d+)/.exec(m.message)?.[1]);
|
out[key] = Number(/complexity of (\d+)/.exec(m.message)?.[1]);
|
||||||
|
|||||||
Reference in New Issue
Block a user