test(gates): the complexity ratchet holds each function by name, and its scope is registered
The ceilings were a per-file ranked list, so one function could grow by what another in the same file lost: Home.tsx's [34, 32] passed with Home at 32 and Hero at 34. Both twins now key every offender as module::function (an unnamed one is (anonymous); a repeated name gets 2, 3 in source order) and re-baseline at today's measured values. Swapping two ceilings is red on both sides (Home 34 > 32; feedquery filtered_query 28 > 14). The ratchet measures shipped code only. docs/gates.md said C901 covered backend + scripts; it covers backend/app. The exclusion of tests, test kits, e2e and scripts/ is now a row with its measured hits, and the stale counts are replaced (9 BE modules over 800, not 11).
This commit is contained in:
@@ -12,8 +12,10 @@ import {
|
||||
// CEILING:
|
||||
// • a file over 800 lines — the listed ones may not grow past their line count; any other
|
||||
// file may not cross the limit;
|
||||
// • a function over complexity 20 (eslint `complexity`, cyclomatic) — per file, the
|
||||
// offenders' values highest-first may not rise, and no new offender may appear.
|
||||
// • a function over complexity 20 (eslint `complexity`, cyclomatic) — a listed one, by its
|
||||
// NAME, may not rise; no other function may cross the limit. (Ranked per file until the E32
|
||||
// review: one function could grow by what another in the same file lost.) A rename reads as a
|
||||
// new offender plus a stale entry: move its row.
|
||||
// Shrinking is free; a file or function that drops UNDER the limit must be taken off its list (red
|
||||
// until it is), so the lists only get shorter. Raising a ceiling is a decision for the owner, with
|
||||
// the reason in docs/gates.md. JSX branches count toward complexity, so a component scores high by
|
||||
@@ -35,61 +37,71 @@ const BIG_FILES: Record<string, number> = {
|
||||
"lib/plexPlayerHooks.ts": 922,
|
||||
};
|
||||
|
||||
const COMPLEX: Record<string, number[]> = {
|
||||
"App.tsx": [28],
|
||||
"components/AccountPanel.tsx": [29],
|
||||
"components/AdminLogs.tsx": [35],
|
||||
"components/auditColumns.tsx": [24],
|
||||
"components/ChannelPage.tsx": [91],
|
||||
"components/Channels.tsx": [53],
|
||||
"components/ConfigPanel.tsx": [28, 24],
|
||||
"components/DownloadCenter.tsx": [36],
|
||||
"components/downloadRow.tsx": [25],
|
||||
"components/Feed.tsx": [64],
|
||||
"components/Home.tsx": [34, 32, 21],
|
||||
"components/IssueComposer.tsx": [34],
|
||||
"components/KeyGate.tsx": [22],
|
||||
"components/NotificationsPanel.tsx": [28],
|
||||
"components/OnboardingWizard.tsx": [23],
|
||||
"components/PersonalViewsModal.tsx": [24],
|
||||
"components/PlayerModal.tsx": [99],
|
||||
"components/Playlists.tsx": [66],
|
||||
"components/PlexBrowse.tsx": [45],
|
||||
"components/plexCards.tsx": [58],
|
||||
"components/PlexCollectionView.tsx": [41],
|
||||
"components/PlexDiscover.tsx": [21],
|
||||
"components/PlexInfo.tsx": [65],
|
||||
"components/PlexPlayer.tsx": [103, 22],
|
||||
"components/PlexPlaylistView.tsx": [22],
|
||||
"components/PlexSidebar.tsx": [32],
|
||||
"components/PlexStatsOverlay.tsx": [29],
|
||||
"components/plexViews.tsx": [50, 30, 21],
|
||||
"components/Scheduler.tsx": [21, 21],
|
||||
"components/SetupWizard.tsx": [21],
|
||||
"components/SyncStatus.tsx": [22],
|
||||
"components/ui/AnchoredPopover.tsx": [22],
|
||||
"components/ui/anchoredPopoverHooks.ts": [30],
|
||||
"components/ui/Combobox.tsx": [22],
|
||||
"components/ui/DataTable.tsx": [42],
|
||||
"components/ui/PanelGroup.tsx": [23],
|
||||
"components/ui/SidePanel.tsx": [45],
|
||||
"components/ui/SortControl.tsx": [23],
|
||||
"components/usePlaylistActions.tsx": [25],
|
||||
"components/usePlexActiveFilters.ts": [29],
|
||||
"components/useVideoActions.tsx": [25],
|
||||
"components/VideoCard.tsx": [39],
|
||||
"components/VideoEditor.tsx": [50],
|
||||
"components/WatchPage.tsx": [30],
|
||||
"components/Welcome.tsx": [39, 24],
|
||||
"lib/api/core.ts": [48, 22],
|
||||
"lib/api/plex.ts": [26],
|
||||
"lib/diagnostics.ts": [22],
|
||||
"lib/keymap.ts": [28],
|
||||
"lib/logHooks.ts": [21],
|
||||
"lib/plexDetail.ts": [24],
|
||||
"lib/plexPlayerHooks.ts": [30],
|
||||
"lib/urlState.ts": [26, 24],
|
||||
"lib/useActiveFilters.ts": [28],
|
||||
const COMPLEX: Record<string, number> = {
|
||||
"App.tsx::App": 28,
|
||||
"components/AccountPanel.tsx::AccountPanel": 29,
|
||||
"components/AdminLogs.tsx::AdminLogs": 35,
|
||||
"components/ChannelPage.tsx::ChannelPage": 91,
|
||||
"components/Channels.tsx::Channels": 53,
|
||||
"components/ConfigPanel.tsx::ConfigPanel": 24,
|
||||
"components/ConfigPanel.tsx::Field": 28,
|
||||
"components/DownloadCenter.tsx::DownloadCenter": 36,
|
||||
"components/Feed.tsx::Feed": 64,
|
||||
"components/Home.tsx::Hero": 32,
|
||||
"components/Home.tsx::Home": 34,
|
||||
"components/Home.tsx::useHomeCardMenu": 21,
|
||||
"components/IssueComposer.tsx::ComposerBody": 34,
|
||||
"components/KeyGate.tsx::KeyGate": 22,
|
||||
"components/NotificationsPanel.tsx::NotificationRow": 28,
|
||||
"components/OnboardingWizard.tsx::OnboardingWizard": 21,
|
||||
"components/PersonalViewsModal.tsx::PersonalViewsModal": 22,
|
||||
"components/PlayerModal.tsx::PlayerModal": 98,
|
||||
"components/Playlists.tsx::Playlists": 66,
|
||||
"components/PlexBrowse.tsx::PlexBrowse": 45,
|
||||
"components/PlexCollectionView.tsx::PlexCollectionView": 41,
|
||||
"components/PlexDiscover.tsx::PlexDiscover": 21,
|
||||
"components/PlexInfo.tsx::PlexInfo": 65,
|
||||
"components/PlexPlayer.tsx::(anonymous)": 22,
|
||||
"components/PlexPlayer.tsx::PlexPlayer": 103,
|
||||
"components/PlexPlaylistView.tsx::PlexPlaylistView": 22,
|
||||
"components/PlexSidebar.tsx::PlexSidebar": 32,
|
||||
"components/PlexStatsOverlay.tsx::(anonymous)": 25,
|
||||
"components/Scheduler.tsx::JobRow": 21,
|
||||
"components/Scheduler.tsx::Scheduler": 21,
|
||||
"components/SetupWizard.tsx::SetupWizard": 21,
|
||||
"components/SyncStatus.tsx::SyncStatus": 22,
|
||||
"components/VideoCard.tsx::VideoCard": 39,
|
||||
"components/VideoEditor.tsx::VideoEditor": 49,
|
||||
"components/WatchPage.tsx::WatchPage": 30,
|
||||
"components/Welcome.tsx::AuthCard": 39,
|
||||
"components/Welcome.tsx::onSubmit": 24,
|
||||
"components/auditColumns.tsx::localDetails": 24,
|
||||
"components/downloadRow.tsx::DownloadRow": 25,
|
||||
"components/plexCards.tsx::PlexPosterCard": 58,
|
||||
"components/plexViews.tsx::PlexPersonView": 30,
|
||||
"components/plexViews.tsx::PlexSeasonView": 21,
|
||||
"components/plexViews.tsx::PlexShowView": 50,
|
||||
"components/ui/AnchoredPopover.tsx::PopoverSurfaceInner": 22,
|
||||
"components/ui/Combobox.tsx::Combobox": 22,
|
||||
"components/ui/DataTable.tsx::DataTable": 42,
|
||||
"components/ui/PanelGroup.tsx::PanelGroup": 23,
|
||||
"components/ui/SidePanel.tsx::SidePanel": 45,
|
||||
"components/ui/SortControl.tsx::SortControl": 23,
|
||||
"components/ui/anchoredPopoverHooks.ts::useAnchoredPopover": 30,
|
||||
"components/usePlaylistActions.tsx::pushSteps": 23,
|
||||
"components/usePlexActiveFilters.ts::buildPlexActiveFilters": 25,
|
||||
"components/useVideoActions.tsx::(anonymous)": 25,
|
||||
"lib/api/core.ts::localizeDetail": 48,
|
||||
"lib/api/core.ts::req": 22,
|
||||
"lib/api/plex.ts::appendPlexFilters": 26,
|
||||
"lib/diagnostics.ts::applyDiagnostics": 22,
|
||||
"lib/keymap.ts::dispatch": 28,
|
||||
"lib/logHooks.ts::controlAttrs": 21,
|
||||
"lib/plexDetail.ts::useDetailAside": 24,
|
||||
"lib/plexPlayerHooks.ts::usePlexTracks": 30,
|
||||
"lib/urlState.ts::filtersToParams": 24,
|
||||
"lib/urlState.ts::paramsToFilters": 26,
|
||||
"lib/useActiveFilters.ts::useActiveFilters": 25,
|
||||
};
|
||||
|
||||
describe("file size", () => {
|
||||
@@ -112,23 +124,13 @@ describe("function complexity", async () => {
|
||||
const actual = await complexityOffenders();
|
||||
|
||||
it(`no function crosses ${MAX_COMPLEXITY}, and a listed one does not rise`, () => {
|
||||
const worse: string[] = [];
|
||||
for (const [file, values] of Object.entries(actual)) {
|
||||
const ceiling = COMPLEX[file] ?? [];
|
||||
values.forEach((v, i) => {
|
||||
if (v > (ceiling[i] ?? MAX_COMPLEXITY))
|
||||
worse.push(`${file}: #${i + 1} is ${v} (ceiling ${ceiling[i] ?? MAX_COMPLEXITY})`);
|
||||
});
|
||||
}
|
||||
const worse = Object.entries(actual)
|
||||
.filter(([fn, v]) => v > (COMPLEX[fn] ?? MAX_COMPLEXITY))
|
||||
.map(([fn, v]) => `${fn}: ${v} (ceiling ${COMPLEX[fn] ?? MAX_COMPLEXITY})`);
|
||||
expect(worse).toEqual([]);
|
||||
});
|
||||
|
||||
it("lists no offender that is back under the limit (shorten the entry)", () => {
|
||||
const stale = Object.entries(COMPLEX)
|
||||
.filter(([file, values]) => (actual[file]?.length ?? 0) < values.length)
|
||||
.map(
|
||||
([file, values]) => `${file}: listed ${values.length}, now ${actual[file]?.length ?? 0}`,
|
||||
);
|
||||
expect(stale).toEqual([]);
|
||||
it("lists no function that is back under the limit, or gone (delete its row)", () => {
|
||||
expect(Object.keys(COMPLEX).filter((fn) => !(fn in actual))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,9 +27,12 @@ export function lineCounts(files: string[]): Record<string, number> {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** file → the complexities over MAX_COMPLEXITY, highest first. A bare rule run (no type info, no
|
||||
* other rules), so it costs a couple of seconds, not the full lint. */
|
||||
export async function complexityOffenders(): Promise<Record<string, number[]>> {
|
||||
/** `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
|
||||
* much (E32 review). A function without a name is `(anonymous)`; 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. */
|
||||
export async function complexityOffenders(): Promise<Record<string, number>> {
|
||||
const eslint = new ESLint({
|
||||
cwd: fileURLToPath(new URL("../../", import.meta.url)),
|
||||
overrideConfigFile: true,
|
||||
@@ -45,19 +48,19 @@ export async function complexityOffenders(): Promise<Record<string, number[]>> {
|
||||
},
|
||||
],
|
||||
});
|
||||
const out: Record<string, number[]> = {};
|
||||
const out: Record<string, number> = {};
|
||||
for (const r of await eslint.lintFiles(["src/**/*.{ts,tsx}"])) {
|
||||
const values = r.messages
|
||||
.filter((m) => m.ruleId === "complexity")
|
||||
.map((m) => Number(/complexity of (\d+)/.exec(m.message)?.[1]))
|
||||
.sort((a, b) => b - a);
|
||||
if (values.length)
|
||||
out[
|
||||
r.filePath
|
||||
.split("\\")
|
||||
.join("/")
|
||||
.replace(/^.*\/frontend\/src\//, "")
|
||||
] = values;
|
||||
const file = r.filePath
|
||||
.split("\\")
|
||||
.join("/")
|
||||
.replace(/^.*\/frontend\/src\//, "");
|
||||
for (const m of r.messages) {
|
||||
if (m.ruleId !== "complexity") continue;
|
||||
const name = /'([^']+)'/.exec(m.message)?.[1] ?? "(anonymous)";
|
||||
let key = `${file}::${name}`;
|
||||
for (let n = 2; key in out; n++) key = `${file}::${name} ${n}`;
|
||||
out[key] = Number(/complexity of (\d+)/.exec(m.message)?.[1]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user