diff --git a/app/components/CustomThemeSelector.tsx b/app/components/CustomThemeSelector.tsx index 8f459b9f9..a896e6162 100644 --- a/app/components/CustomThemeSelector.tsx +++ b/app/components/CustomThemeSelector.tsx @@ -3,10 +3,7 @@ import * as React from "react"; import { useTranslation } from "react-i18next"; import * as v from "valibot"; import type { CustomTheme } from "~/db/tables-json"; -import { - CUSTOM_THEME_VARS, - type CustomThemeVar, -} from "~/features/theme/theme-constants"; +import { CUSTOM_THEME_VARS } from "~/features/theme/theme-constants"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; import { ACCENT_CHROMA_MULTIPLIERS, @@ -308,9 +305,9 @@ export function CustomThemeSelector({ const handleReset = () => { setThemeInput(DEFAULT_THEME_INPUT); - CUSTOM_THEME_VARS.forEach((varDef: CustomThemeVar) => { + for (const varDef of CUSTOM_THEME_VARS) { document.documentElement.style.removeProperty(varDef); - }); + } onReset(); }; diff --git a/app/components/ObjectiveTimeline.tsx b/app/components/ObjectiveTimeline.tsx index d2a9c9c7a..c30cf858b 100644 --- a/app/components/ObjectiveTimeline.tsx +++ b/app/components/ObjectiveTimeline.tsx @@ -252,7 +252,7 @@ export function ObjectiveTimeline({ /** One tick every 25 up to the top of the data, none below zero so the control gutter stays clean. */ function countAxisTicks(max: number) { - const ticks = []; + const ticks: Array<{ value: number }> = []; for (let value = 0; value <= max; value += COUNT_TICK_STEP) { ticks.push({ value }); } diff --git a/app/db/json-columns.test.ts b/app/db/json-columns.test.ts index d37ac76b8..50acb72ea 100644 --- a/app/db/json-columns.test.ts +++ b/app/db/json-columns.test.ts @@ -4,7 +4,9 @@ import { JSON_COLUMNS } from "./json-columns"; describe("JSON_COLUMNS", () => { test("matches the JSONColumnType declarations in tables.ts", () => { - expect([...JSON_COLUMNS].sort()).toEqual(jsonColumnsFromTablesSource()); + expect([...JSON_COLUMNS].sort(alphabetically)).toEqual( + jsonColumnsFromTablesSource(), + ); }); }); @@ -17,7 +19,7 @@ function jsonColumnsFromTablesSource() { const [, interfaceName, body] = match; if (interfaceName === "DB") continue; - const columns = []; + const columns: string[] = []; for (const line of body.split("\n")) { const columnMatch = line.match( /^\s*(\w+)\??:\s*.*JSONColumnType(?:Nullable)? a.localeCompare(b); diff --git a/app/db/seed/dev/tournaments.ts b/app/db/seed/dev/tournaments.ts index 02112034e..d76bd28dc 100644 --- a/app/db/seed/dev/tournaments.ts +++ b/app/db/seed/dev/tournaments.ts @@ -467,7 +467,7 @@ async function registerTeams({ registeredAt?: Date; mapPool?: () => MapPool; }) { - const teams = []; + const teams: Awaited>[] = []; for (const [i, roster] of rosters.entries()) { teams.push( await TournamentTeamFactory.create( diff --git a/app/features/build-analyzer/components/PerInkTankGrid.tsx b/app/features/build-analyzer/components/PerInkTankGrid.tsx index 4747bd0f9..95204a78d 100644 --- a/app/features/build-analyzer/components/PerInkTankGrid.tsx +++ b/app/features/build-analyzer/components/PerInkTankGrid.tsx @@ -191,21 +191,9 @@ function calculateGrid({ subsUsed: number; }) { const result: ("N/A" | ShotCellData)[][] = []; - for ( - let issAPIndex = 0; - issAPIndex < AP_VALUES_TO_SHOW.length; - issAPIndex++ - ) { - const issAP = AP_VALUES_TO_SHOW[issAPIndex]; - + for (const issAP of AP_VALUES_TO_SHOW) { const row: ("N/A" | ShotCellData)[] = []; - for ( - let ismAPIndex = 0; - ismAPIndex < AP_VALUES_TO_SHOW.length; - ismAPIndex++ - ) { - const ismAP = AP_VALUES_TO_SHOW[ismAPIndex]; - + for (const ismAP of AP_VALUES_TO_SHOW) { if (!apsArePossible(issAP, ismAP)) { row.push("N/A" as const); continue; diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx index 324cab42e..32b73bc0f 100644 --- a/app/features/build-analyzer/routes/analyzer.tsx +++ b/app/features/build-analyzer/routes/analyzer.tsx @@ -1196,7 +1196,10 @@ function subDefenseGraphOptions({ .map((d) => damageToKey(d)), ); - const result = []; + const result: Array<{ + label: string; + data: Array<{ primary: number; secondary: number }>; + }> = []; for (const key of distanceKeys) { const distance = key.split(",")[0]; diff --git a/app/features/calendar/components/FiltersBar.tsx b/app/features/calendar/components/FiltersBar.tsx index 25496dcd7..012ac8a22 100644 --- a/app/features/calendar/components/FiltersBar.tsx +++ b/app/features/calendar/components/FiltersBar.tsx @@ -45,7 +45,7 @@ export function FiltersBar() { }; const modesFormatted = () => { - const parts = []; + const parts: string[] = []; if (filters.modes.length < modesShortWithSpecial.length) { parts.push(filters.modes.join(", ")); } @@ -57,7 +57,7 @@ export function FiltersBar() { }; const eventTypeFormatted = () => { - const parts = []; + const parts: string[] = []; if (filters.games.length < gamesShort.length) { parts.push(filters.games.join(", ")); } @@ -89,7 +89,7 @@ export function FiltersBar() { }; const tagsFormatted = () => { - const parts = []; + const parts: string[] = []; if (filters.tagsIncluded.length > 0) { parts.push(`+${filters.tagsIncluded.length}`); } @@ -101,7 +101,7 @@ export function FiltersBar() { }; const organizersFormatted = () => { - const parts = []; + const parts: string[] = []; if (filters.orgsIncluded.length > 0) { parts.push(`+${filters.orgsIncluded.length}`); } @@ -115,7 +115,7 @@ export function FiltersBar() { }; const timeAndSizeFormatted = () => { - const parts = []; + const parts: string[] = []; if (filters.preferredStartTime !== "ANY") { parts.push( t( diff --git a/app/features/changelog/core/entries.server.ts b/app/features/changelog/core/entries.server.ts index 7fb8c4c2c..c90a650bd 100644 --- a/app/features/changelog/core/entries.server.ts +++ b/app/features/changelog/core/entries.server.ts @@ -73,6 +73,7 @@ function parseEntryFile(fileName: string): ChangelogGraphicEntry { `Invalid frontmatter in changelog entry "${fileName}": ${ error instanceof v.ValiError ? error.message : String(error) }`, + { cause: error }, ); } diff --git a/app/features/chat/ChatRoomResolver.server.test.ts b/app/features/chat/ChatRoomResolver.server.test.ts index 523735103..2a5e55d07 100644 --- a/app/features/chat/ChatRoomResolver.server.test.ts +++ b/app/features/chat/ChatRoomResolver.server.test.ts @@ -112,7 +112,9 @@ describe("ChatRoomResolver.resolve", () => { const room = await resolveOrThrow(await groupChatRoomId(group.id)); expect(room.type).toBe("SQ_GROUP"); - expect(room.participantUserIds.sort()).toEqual(memberUserIds.sort()); + expect(room.participantUserIds.sort(byId)).toEqual( + memberUserIds.sort(byId), + ); expect(room.url).toBe("/q/looking"); // a group chat resolves no observers of its own, site staff aside expect(hasPermission(room, "OBSERVE", { id: outsiderId() })).toBe(false); @@ -136,8 +138,8 @@ describe("ChatRoomResolver.resolve", () => { const room = await resolveOrThrow(match.chatRoomId!); expect(room.type).toBe("SQ_MATCH"); - expect(room.participantUserIds.sort()).toEqual( - [...alphaUserIds, ...bravoUserIds].sort(), + expect(room.participantUserIds.sort(byId)).toEqual( + [...alphaUserIds, ...bravoUserIds].sort(byId), ); expect(room.titleParams).toEqual({ matchId: String(match.id) }); expect(room.url).toContain(String(match.id)); @@ -155,8 +157,8 @@ describe("ChatRoomResolver.resolve", () => { const room = await resolveOrThrow(chatRoomId); expect(room.type).toBe("TOURNAMENT_MATCH"); - expect(room.participantUserIds.sort()).toEqual( - [...teamAlphaUserIds, ...teamBravoUserIds].sort(), + expect(room.participantUserIds.sort(byId)).toEqual( + [...teamAlphaUserIds, ...teamBravoUserIds].sort(byId), ); expect(room.titleParams.matchId).toBe(String(matchId)); expect(room.titleParams.tournamentName).toEqual(expect.any(String)); @@ -187,7 +189,9 @@ describe("ChatRoomResolver.resolve", () => { const room = await resolveOrThrow(await teamChatRoomId(team.id)); expect(room.type).toBe("TOURNAMENT_TEAM"); - expect(room.participantUserIds.sort()).toEqual(memberUserIds.sort()); + expect(room.participantUserIds.sort(byId)).toEqual( + memberUserIds.sort(byId), + ); expect(room.titleParams.teamName).toEqual(expect.any(String)); expect(room.permissions.OBSERVE).toContain(authorId); }); @@ -199,8 +203,8 @@ describe("ChatRoomResolver.resolve", () => { const room = await resolveOrThrow(chatRoomId); expect(room.type).toBe("SCRIM"); - expect(room.participantUserIds.sort()).toEqual( - [...postUserIds, ...requestUserIds].sort(), + expect(room.participantUserIds.sort(byId)).toEqual( + [...postUserIds, ...requestUserIds].sort(byId), ); expect(room.titleParams.startsAt).toBe( String(dateToDatabaseTimestamp(startsAt)), @@ -223,11 +227,18 @@ describe("ChatRoomResolver.findAllByUserId", () => { const rooms = await ChatRoomResolver.findAllByUserId(alphaUserIds[0]); - expect(rooms.map((room) => [room.roomId, room.type]).sort()).toEqual( + expect( + rooms + .map((room) => ({ roomId: room.roomId, type: room.type })) + .sort(byRoomId), + ).toEqual( [ - [match.chatRoomId!, "SQ_MATCH"], - [await groupChatRoomId(match.alphaGroup.id), "SQ_GROUP"], - ].sort(), + { roomId: match.chatRoomId!, type: "SQ_MATCH" }, + { + roomId: await groupChatRoomId(match.alphaGroup.id), + type: "SQ_GROUP", + }, + ].sort(byRoomId), ); }); @@ -501,3 +512,8 @@ const teamChatRoomId = async (teamId: number) => { return team.chatRoomId!; }; + +const byId = (a: number, b: number) => a - b; + +const byRoomId = (a: { roomId: number }, b: { roomId: number }) => + a.roomId - b.roomId; diff --git a/app/features/comp-analyzer/components/RangeVisualization.tsx b/app/features/comp-analyzer/components/RangeVisualization.tsx index 9920fa560..b77b18c72 100644 --- a/app/features/comp-analyzer/components/RangeVisualization.tsx +++ b/app/features/comp-analyzer/components/RangeVisualization.tsx @@ -165,13 +165,13 @@ function TrajectoryChart({ const groundY = yScale(0); - const xTicks = []; + const xTicks: number[] = []; const xStep = Math.ceil(maxRange / 5); for (let x = 0; x <= maxRange; x += xStep) { xTicks.push(x); } - const yTicks = []; + const yTicks: number[] = []; const yStep = Math.ceil((maxY - minY) / 4); for (let y = Math.ceil(minY); y <= maxY; y += yStep) { yTicks.push(y); diff --git a/app/features/comp-analyzer/core/damage-combinations.ts b/app/features/comp-analyzer/core/damage-combinations.ts index 54e3d1155..d68c25350 100644 --- a/app/features/comp-analyzer/core/damage-combinations.ts +++ b/app/features/comp-analyzer/core/damage-combinations.ts @@ -443,9 +443,7 @@ function isExcessiveCombo(combo: DamageCombo): boolean { ); const totalDamage = combo.totalDamage; - for (let i = 0; i < flatDamages.length; i++) { - const damage = flatDamages[i]; - + for (const damage of flatDamages) { const reducedDamage = totalDamage - damage; if (reducedDamage >= LETHAL_DAMAGE) { return true; diff --git a/app/features/img-export/components/ImageExportDialog.tsx b/app/features/img-export/components/ImageExportDialog.tsx index 4b532f67e..7221904bd 100644 --- a/app/features/img-export/components/ImageExportDialog.tsx +++ b/app/features/img-export/components/ImageExportDialog.tsx @@ -117,11 +117,13 @@ function ImageExportDialogContent({ let cancelled = false; - import("@zumer/snapdom").then(({ preCache }) => { - if (cancelled || !frameRef.current) return; + import("@zumer/snapdom") + .then(({ preCache }) => { + if (cancelled || !frameRef.current) return; - preCache(frameRef.current).catch(() => {}); - }); + return preCache(frameRef.current); + }) + .catch(() => {}); return () => { cancelled = true; diff --git a/app/features/map-list-generator/core/map-pool-serializer/serializer.ts b/app/features/map-list-generator/core/map-pool-serializer/serializer.ts index 1f028fb0c..5c475f71e 100644 --- a/app/features/map-list-generator/core/map-pool-serializer/serializer.ts +++ b/app/features/map-list-generator/core/map-pool-serializer/serializer.ts @@ -7,7 +7,7 @@ import type { MapPoolObject, ReadonlyMapPoolObject } from "./types"; export function mapPoolToSerializedString( mapPool: ReadonlyMapPoolObject, ): string { - const serializedModes = []; + const serializedModes: string[] = []; for (const mode of modesShort) { const stages = mapPool[mode]; diff --git a/app/features/map-list-generator/core/map-pool.ts b/app/features/map-list-generator/core/map-pool.ts index 17bccf6fa..98ea86770 100644 --- a/app/features/map-list-generator/core/map-pool.ts +++ b/app/features/map-list-generator/core/map-pool.ts @@ -42,11 +42,12 @@ export class MapPool { return this.asSerialized; } - // biome-ignore lint/suspicious/noAssignInExpressions: biome migration - return (this.asSerialized = + this.asSerialized = typeof this.source === "string" ? this.source - : mapPoolToSerializedString(this.source)); + : mapPoolToSerializedString(this.source); + + return this.asSerialized; } get parsed(): ReadonlyMapPoolObject { @@ -54,11 +55,12 @@ export class MapPool { return this.asObject; } - // biome-ignore lint/suspicious/noAssignInExpressions: biome migration - return (this.asObject = + this.asObject = typeof this.source === "string" ? serializedStringToMapPool(this.source) - : this.source); + : this.source; + + return this.asObject; } get dbList(): DbMapPoolList { diff --git a/app/features/notifications/core/notify.server.test.ts b/app/features/notifications/core/notify.server.test.ts index 23129d439..d91f115b0 100644 --- a/app/features/notifications/core/notify.server.test.ts +++ b/app/features/notifications/core/notify.server.test.ts @@ -257,7 +257,9 @@ describe("notify()", () => { expect(user10Notifications).toHaveLength(2); expect(user11Notifications).toHaveLength(2); - const types = user10Notifications.map((n) => n.type).sort(); + const types = user10Notifications + .map((n) => n.type) + .sort((a, b) => a.localeCompare(b)); expect(types).toEqual(["SCRIM_CANCELED", "SCRIM_SCHEDULED"]); }); diff --git a/app/features/scanner/node/resources.ts b/app/features/scanner/node/resources.ts index 13df55681..9c2911bab 100644 --- a/app/features/scanner/node/resources.ts +++ b/app/features/scanner/node/resources.ts @@ -25,7 +25,10 @@ async function loadAtlasLazy(name: string): Promise<() => GlyphSet | null> { const meta = JSON.parse(readFileSync(json, "utf8")) as AtlasMeta; const image = await readImage(png); let set: GlyphSet | null = null; - return () => (set ??= loadGlyphSet(image, meta)); + return () => { + set ??= loadGlyphSet(image, meta); + return set; + }; } /** Planner stage signatures; the (CPU) tile slicing runs on first access. */ @@ -36,7 +39,10 @@ async function loadPlannerStagesLazy(): Promise<() => PlannerStage[] | null> { const manifest = JSON.parse(readFileSync(json, "utf8")) as PlannerManifest; const atlas = await readImage(png); let stages: PlannerStage[] | null = null; - return () => (stages ??= loadPlannerStages(atlas, manifest)); + return () => { + stages ??= loadPlannerStages(atlas, manifest); + return stages; + }; } /** Requires loadOpenCV() to have resolved. */ diff --git a/app/features/scanner/scanner-schemas.ts b/app/features/scanner/scanner-schemas.ts index bb300852d..5a950ccb8 100644 --- a/app/features/scanner/scanner-schemas.ts +++ b/app/features/scanner/scanner-schemas.ts @@ -129,6 +129,7 @@ type MutuallyAssignable = [A] extends [B] // `true satisfies …` fails to compile the moment a schema and its core // interface disagree in either direction. +// biome-ignore-start lint/suspicious/noUnusedExpressions: type-level assertions, no runtime effect true satisfies MutuallyAssignable< v.InferOutput, ScannerMatchPlayer @@ -149,3 +150,4 @@ true satisfies MutuallyAssignable< v.InferOutput, ScannerMatch >; +// biome-ignore-end lint/suspicious/noUnusedExpressions: type-level assertions, no runtime effect diff --git a/app/features/scanner/tests/objective.test.ts b/app/features/scanner/tests/objective.test.ts index 91b993581..8d98d5f45 100644 --- a/app/features/scanner/tests/objective.test.ts +++ b/app/features/scanner/tests/objective.test.ts @@ -17,7 +17,7 @@ import { createScoreboardDetector } from "../core/detectors/scoreboard/index"; import { createScoreboardBattleLogReplayDetector } from "../core/detectors/scoreboard-battle-log-replay/index"; import { createScoreboardOwnDetector } from "../core/detectors/scoreboard-own/index"; import type { DetectedEvent, Detector } from "../core/detectors/types"; -import { hueDistance, hueOf } from "../core/ink-color"; +import { hueDistance, hueOf, type InkRgb } from "../core/ink-color"; import { type Fixture, isFieldSkipped, @@ -143,7 +143,7 @@ test("cast fixture pair: team ink hues identify sides across camera swaps", asyn ].map((name) => fixtures.find((fixture) => fixture.name === name)); assert.ok(pair[0] && pair[1], "cast fixture pair missing"); - const colors = []; + const colors: Array = []; for (const fixture of pair) { const { events } = await runDetectorOnFixture(detector, fixture!); const teamColor = ( diff --git a/app/features/scanner/worker/analyzer.worker.ts b/app/features/scanner/worker/analyzer.worker.ts index 9e7642000..620dc22d5 100644 --- a/app/features/scanner/worker/analyzer.worker.ts +++ b/app/features/scanner/worker/analyzer.worker.ts @@ -115,8 +115,10 @@ async function analyzeFrame( // ship back the exact analyzed pixels (lossless, capture resolution) so the // UI never re-grabs a later frame — encoded at most once per frame let encoded: Promise | null = null; - const frameBlob = () => - (encoded ??= canvas.convertToBlob({ type: "image/png" })); + const frameBlob = () => { + encoded ??= canvas.convertToBlob({ type: "image/png" }); + return encoded; + }; try { for (const detector of detectors) { diff --git a/app/features/sendouq/SQGroupRepository.server.ts b/app/features/sendouq/SQGroupRepository.server.ts index dcb974825..6cbf201ef 100644 --- a/app/features/sendouq/SQGroupRepository.server.ts +++ b/app/features/sendouq/SQGroupRepository.server.ts @@ -792,7 +792,7 @@ export function insertLike({ .execute(); } catch (error) { if (errorIsSqliteForeignKeyConstraintFailure(error)) { - throw new SendouQError(error.message); + throw new SendouQError(error.message, { cause: error }); } throw error; } @@ -829,7 +829,7 @@ export function insertSuggestion({ .execute(); } catch (error) { if (errorIsSqliteForeignKeyConstraintFailure(error)) { - throw new SendouQError(error.message); + throw new SendouQError(error.message, { cause: error }); } throw error; } diff --git a/app/features/sendouq/q-utils.server.ts b/app/features/sendouq/q-utils.server.ts index dfb26bc11..9b4a1223f 100644 --- a/app/features/sendouq/q-utils.server.ts +++ b/app/features/sendouq/q-utils.server.ts @@ -3,8 +3,8 @@ import * as SkillRepository from "~/features/mmr/SkillRepository.server"; import type { TieredSkill } from "~/features/mmr/tiered.server"; export class SendouQError extends Error { - constructor(message: string) { - super(message); + constructor(message: string, options?: ErrorOptions) { + super(message, options); this.name = "SendouQError"; } } diff --git a/app/features/tournament-bracket/core/Deadline.ts b/app/features/tournament-bracket/core/Deadline.ts index 197cb7b7a..775753180 100644 --- a/app/features/tournament-bracket/core/Deadline.ts +++ b/app/features/tournament-bracket/core/Deadline.ts @@ -1,6 +1,13 @@ const PREP_TIME_MINUTES = 6.5; const MINUTES_PER_GAME = 6.5; +type GameMarker = { + gameNumber: number; + percentage: number; + gameStartMinute: number; + maxMinute: number; +}; + /** Acceptable max duration of a match in minutes (preparation time + game time). */ export function totalMatchTime(maxGamesCount: number): number { return PREP_TIME_MINUTES + MINUTES_PER_GAME * maxGamesCount; @@ -15,14 +22,9 @@ export function progressPercentage( } /** Position of each game on the match timeline as a percentage. */ -export function gameMarkers(maxGamesCount: number): Array<{ - gameNumber: number; - percentage: number; - gameStartMinute: number; - maxMinute: number; -}> { +export function gameMarkers(maxGamesCount: number): GameMarker[] { const totalMinutes = totalMatchTime(maxGamesCount); - const markers = []; + const markers: GameMarker[] = []; for (let i = 1; i <= maxGamesCount; i++) { const gameStartMinute = PREP_TIME_MINUTES + MINUTES_PER_GAME * (i - 1); diff --git a/app/features/tournament-bracket/core/Progression.ts b/app/features/tournament-bracket/core/Progression.ts index 1d561e09f..22e80df13 100644 --- a/app/features/tournament-bracket/core/Progression.ts +++ b/app/features/tournament-bracket/core/Progression.ts @@ -207,10 +207,10 @@ export function validatedBrackets( try { parsed = toOutputBracketFormat(brackets); } catch (e) { - if ((e as { badBracketIdx: number }).badBracketIdx) { + if (e instanceof BadBracketError) { return { type: "PLACEMENTS_PARSE_ERROR", - bracketIdx: (e as { badBracketIdx: number }).badBracketIdx, + bracketIdx: e.bracketIdx, }; } @@ -372,6 +372,15 @@ export function bracketsToValidationError( return null; } +class BadBracketError extends Error { + readonly bracketIdx: number; + + constructor(bracketIdx: number) { + super(`Bracket at index ${bracketIdx} has invalid placements`); + this.bracketIdx = bracketIdx; + } +} + function toOutputBracketFormat(brackets: InputBracket[]): ParsedBracket[] { const result = brackets.map((bracket, bracketIdx) => { return { @@ -395,10 +404,10 @@ function toOutputBracketFormat(brackets: InputBracket[]): ParsedBracket[] { sourceBracket?.type === "swiss" && sourceBracket?.settings?.advanceThreshold; if (!isSwissWithEarlyAdvance) { - throw { badBracketIdx: bracketIdx }; + throw new BadBracketError(bracketIdx); } } else if (parsed === null) { - throw { badBracketIdx: bracketIdx }; + throw new BadBracketError(bracketIdx); } return { diff --git a/app/features/tournament-bracket/core/engine/swiss/pairing.ts b/app/features/tournament-bracket/core/engine/swiss/pairing.ts index c1b9cefb1..657386f76 100644 --- a/app/features/tournament-bracket/core/engine/swiss/pairing.ts +++ b/app/features/tournament-bracket/core/engine/swiss/pairing.ts @@ -185,7 +185,7 @@ export function pairUp(players: SwissPairingTeam[]) { const scoreSums = [ ...new Set( scoreGroups.flatMap((s, i, a) => { - const sums = []; + const sums: number[] = []; for (let j = i; j < a.length; j++) { sums.push(s + a[j]); } @@ -243,8 +243,7 @@ function generateWeightedPairs({ for (let i = 0; i < playerArray.length; i++) { const curr = playerArray[i]; const next = playerArray.slice(i + 1); - for (let j = 0; j < next.length; j++) { - const opp = next[j]; + for (const opp of next) { let wt = 75 - 75 / (scoreGroups.indexOf(Math.min(curr.score, opp.score)) + 2); wt += diff --git a/app/features/tournament-bracket/core/tests/test-utils.ts b/app/features/tournament-bracket/core/tests/test-utils.ts index 7757332ed..9a090f06b 100644 --- a/app/features/tournament-bracket/core/tests/test-utils.ts +++ b/app/features/tournament-bracket/core/tests/test-utils.ts @@ -29,7 +29,7 @@ export const tournamentCtxTeam = ( }; const nTeams = (n: number, startingId: number) => { - const teams = []; + const teams: TournamentData["ctx"]["teams"] = []; for (let i = 0; i < n; i++) { teams.push(tournamentCtxTeam(i + 1, tournamentCtxTeam(i + startingId))); } diff --git a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts index 9830f93ce..152780040 100644 --- a/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts +++ b/app/features/tournament-lfg/TournamentLFGRepository.server.test.ts @@ -246,8 +246,8 @@ describe("startLooking", () => { const roomsChangedUserIds = await startLooking(team.id); - expect(roomsChangedUserIds.sort()).toEqual( - [users.id(1), users.id(2)].sort(), + expect(roomsChangedUserIds.sort(byId)).toEqual( + [users.id(1), users.id(2)].sort(byId), ); const chatRoomId = await chatRoomIdOf(team.id); @@ -390,8 +390,8 @@ describe("mergeTeams", () => { maxGroupSize: 4, }); - expect(roomsChangedUserIds.sort()).toEqual( - [users.id(1), users.id(2)].sort(), + expect(roomsChangedUserIds.sort(byId)).toEqual( + [users.id(1), users.id(2)].sort(byId), ); }); @@ -664,3 +664,5 @@ describe("findAllSubsByTournamentId", () => { expect(subs).toHaveLength(0); }); }); + +const byId = (a: number, b: number) => a - b; diff --git a/app/features/tournament/TournamentTeamRepository.server.test.ts b/app/features/tournament/TournamentTeamRepository.server.test.ts index 008af015c..8788337a1 100644 --- a/app/features/tournament/TournamentTeamRepository.server.test.ts +++ b/app/features/tournament/TournamentTeamRepository.server.test.ts @@ -336,8 +336,8 @@ describe("TournamentTeamRepository", () => { TournamentTeamRepository.deleteById(team.id), ); - expect(roomsChangedUserIds.sort()).toEqual( - [ownerId(), memberId()].sort(), + expect(roomsChangedUserIds.sort(byId)).toEqual( + [ownerId(), memberId()].sort(byId), ); }); @@ -393,3 +393,5 @@ describe("TournamentTeamRepository", () => { }); }); }); + +const byId = (a: number, b: number) => a - b; diff --git a/app/features/user-page/UserRepository.test.ts b/app/features/user-page/UserRepository.test.ts index 56b3d61a5..8375fb96d 100644 --- a/app/features/user-page/UserRepository.test.ts +++ b/app/features/user-page/UserRepository.test.ts @@ -323,7 +323,9 @@ describe("UserRepository", () => { {}, ); - const teams = []; + const teams: Awaited< + ReturnType + >[] = []; for (const user of [topUser, topMate, lowUser, lowMate]) { teams.push( await TournamentTeamFactory.create( @@ -586,7 +588,9 @@ describe("UserRepository", () => { const tomorrow = await UserRepository.findAllPatronsForFooter(); expect(ids(today)).not.toEqual(ids(tomorrow)); - expect(ids(today).sort()).toEqual(ids(tomorrow).sort()); + expect(ids(today).sort((a, b) => a - b)).toEqual( + ids(tomorrow).sort((a, b) => a - b), + ); }); }); }); diff --git a/app/features/user-page/components/ResultsFiltersBar.tsx b/app/features/user-page/components/ResultsFiltersBar.tsx index ff3bf10c9..d82463b36 100644 --- a/app/features/user-page/components/ResultsFiltersBar.tsx +++ b/app/features/user-page/components/ResultsFiltersBar.tsx @@ -383,7 +383,7 @@ function YearSelect({ const selectableYears = () => { const currentYear = new Date().getFullYear(); - const result = []; + const result: number[] = []; for (let year = currentYear; year >= RESULTS_FIRST_YEAR; year--) { result.push(year); } diff --git a/app/features/user-page/core/widgets/portfolio-loaders.server.ts b/app/features/user-page/core/widgets/portfolio-loaders.server.ts index ad7be0431..f07fe6bce 100644 --- a/app/features/user-page/core/widgets/portfolio-loaders.server.ts +++ b/app/features/user-page/core/widgets/portfolio-loaders.server.ts @@ -8,6 +8,7 @@ import * as LFGRepository from "~/features/lfg/LFGRepository.server"; import * as LiveStreamRepository from "~/features/live-streams/LiveStreamRepository.server"; import { BANNED_MAPS } from "~/features/match-profile/banned-maps"; import * as MatchProfileRepository from "~/features/match-profile/MatchProfileRepository.server"; +import type { TierName } from "~/features/mmr/mmr-constants"; import { ordinalToSp } from "~/features/mmr/mmr-utils"; import { userSkills as _userSkills } from "~/features/mmr/tiered.server"; import * as TeamRepository from "~/features/team/TeamRepository.server"; @@ -54,7 +55,12 @@ export const WIDGET_LOADERS = { return null; } - let peakData = null; + let peakData: { + peakSp: number; + tierName: TierName; + isPlus: boolean; + season: number; + } | null = null; let maxOrdinal = Number.NEGATIVE_INFINITY; for (const season of seasonsParticipatedIn) { diff --git a/app/modules/tournament-map-list-generator/balanced-map-list.ts b/app/modules/tournament-map-list-generator/balanced-map-list.ts index 30f56de1a..d09bc1e11 100644 --- a/app/modules/tournament-map-list-generator/balanced-map-list.ts +++ b/app/modules/tournament-map-list-generator/balanced-map-list.ts @@ -330,7 +330,7 @@ function generateWithInput( function isNotFollowingModeOrder(stage: StageValidatorInput) { let currentIndex = 0; - for (let i = 0; i < mapList.length; i++) { + for (const _ of mapList) { currentIndex++; if (currentIndex === input.modesIncluded.length) currentIndex = 0; } diff --git a/app/routines/notifySeasonEnd.test.ts b/app/routines/notifySeasonEnd.test.ts index c38503ab2..adaea9477 100644 --- a/app/routines/notifySeasonEnd.test.ts +++ b/app/routines/notifySeasonEnd.test.ts @@ -56,7 +56,9 @@ describe("NotifySeasonEndRoutine", () => { }, }), ); - expect(notifiedUserIds().sort()).toEqual([users.id(1), users.id(2)].sort()); + expect(notifiedUserIds().sort((a, b) => a - b)).toEqual( + [users.id(1), users.id(2)].sort((a, b) => a - b), + ); }); test("does NOT notify a participant whose skill is approximate", async () => { diff --git a/app/utils/Test.ts b/app/utils/Test.ts index c6ac12acf..603c641d0 100644 --- a/app/utils/Test.ts +++ b/app/utils/Test.ts @@ -95,7 +95,12 @@ export function wrappedAction({ if (thrown instanceof Response) { if (thrown.status === 302) return thrown; - throw new Error(`Response thrown with status code: ${thrown.status}`); + throw new Error( + `Response thrown with status code: ${thrown.status}`, + { + cause: thrown, + }, + ); } throw thrown; @@ -145,7 +150,12 @@ export function wrappedLoader({ return data as T; } catch (thrown) { if (thrown instanceof Response) { - throw new Error(`Response thrown with status code: ${thrown.status}`); + throw new Error( + `Response thrown with status code: ${thrown.status}`, + { + cause: thrown, + }, + ); } throw thrown; diff --git a/app/utils/dates.ts b/app/utils/dates.ts index b0af3c4cd..a7bc06f53 100644 --- a/app/utils/dates.ts +++ b/app/utils/dates.ts @@ -168,7 +168,7 @@ export function nullPaddedDatesOfMonth({ month, year }: MonthYear) { } function datesOfMonth({ month, year }: MonthYear) { - const dates = []; + const dates: Date[] = []; const date = new Date(Date.UTC(year, month, 1)); while (date.getUTCMonth() === month) { dates.push(new Date(date)); diff --git a/app/utils/random.ts b/app/utils/random.ts index 5ff65bd36..6b6e09fb9 100644 --- a/app/utils/random.ts +++ b/app/utils/random.ts @@ -3,9 +3,8 @@ function cyrb128(str: string) { let h2 = 3144134277; let h3 = 1013904242; let h4 = 2773480762; - // biome-ignore lint/suspicious/noImplicitAnyLet: biome migration - for (let i = 0, k; i < str.length; i++) { - k = str.charCodeAt(i); + for (let i = 0; i < str.length; i++) { + const k = str.charCodeAt(i); h1 = h2 ^ Math.imul(h1 ^ k, 597399067); h2 = h3 ^ Math.imul(h2 ^ k, 2869860233); h3 = h4 ^ Math.imul(h3 ^ k, 951274213); diff --git a/biome.json b/biome.json index ab867a183..70759a085 100644 --- a/biome.json +++ b/biome.json @@ -34,7 +34,12 @@ "level": "error" }, "noSkippedTests": "error", - "noParametersOnlyUsedInRecursion": "error" + "noParametersOnlyUsedInRecursion": "error", + "useArraySortCompare": "error", + "noEvolvingTypes": "error", + "noReturnAssign": "error", + "noNestedPromises": "error", + "noUnusedExpressions": "error" }, "correctness": { "noUndeclaredVariables": "error", @@ -113,6 +118,10 @@ "noSubstr": "error", "useUnifiedTypeSignatures": "error", "useReadonlyClassProperties": "error", + "useForOf": "error", + "useErrorCause": "error", + "useThrowOnlyError": "error", + "noNamespace": "error", "useFilenamingConvention": { "level": "error", "options": { @@ -145,7 +154,8 @@ "fix": "safe", "level": "error" }, - "noUselessCatchBinding": "error" + "noUselessCatchBinding": "error", + "noForEach": "error" }, "performance": { "noSyncScripts": "error" @@ -182,6 +192,16 @@ } } }, + { + "includes": ["types/**/*.d.ts"], + "linter": { + "rules": { + "style": { + "noNamespace": "off" + } + } + } + }, { "includes": ["app/**", "!app/modules/search-params/**"], "plugins": ["./biome-plugins/no-raw-search-params.grit"] diff --git a/e2e/api-public.spec.ts b/e2e/api-public.spec.ts index edb91fd16..cfe6dd9ee 100644 --- a/e2e/api-public.spec.ts +++ b/e2e/api-public.spec.ts @@ -417,7 +417,9 @@ async function organizedTournament( startTimes: [dateToDatabaseTimestamp(addHours(new Date(), 2))], }); - const teams = []; + const teams: Awaited< + ReturnType + >[] = []; for (let teamNth = 0; teamNth < teamCount; teamNth++) { const roster = await factories.UserFactory.createMany(ROSTER_SIZE); teams.push( diff --git a/e2e/helpers/tournament.ts b/e2e/helpers/tournament.ts index d559ede98..f1144bf09 100644 --- a/e2e/helpers/tournament.ts +++ b/e2e/helpers/tournament.ts @@ -172,7 +172,9 @@ export async function createTeams( tournamentId: number, seeds: TeamSeed[], ) { - const teams = []; + const teams: Awaited< + ReturnType + >[] = []; for (const [i, seed] of seeds.entries()) { const presetMembers = seed.members ?? []; const rosterSize = seed.rosterSize ?? ROSTER_SIZE; diff --git a/e2e/leaderboards.spec.ts b/e2e/leaderboards.spec.ts index 819590541..879ee443e 100644 --- a/e2e/leaderboards.spec.ts +++ b/e2e/leaderboards.spec.ts @@ -102,7 +102,8 @@ async function seedLeaderboards(factories: Factories) { const alphaUserIds = [ADMIN_ID, ...mates.map((mate) => mate.id)]; const bravoUserIds = enemies.map((enemy) => enemy.id); - const matches = []; + const matches: Awaited>[] = + []; for (let i = 0; i < MATCHES_COUNT_NEEDED_FOR_LEADERBOARD; i++) { matches.push( await factories.SQMatchFactory.create( diff --git a/e2e/pages/builds/user-builds-page.ts b/e2e/pages/builds/user-builds-page.ts index 3697ad954..d59010243 100644 --- a/e2e/pages/builds/user-builds-page.ts +++ b/e2e/pages/builds/user-builds-page.ts @@ -55,7 +55,7 @@ export class UserBuildsPage { await this.locators.changeSortingButton.click(); const dialog = this.page.getByRole("dialog"); - for (let i = 0; i < DEFAULT_BUILD_SORT.length; i++) { + for (const _ of DEFAULT_BUILD_SORT) { await dialog.getByTestId("delete-sorting-button").click(); } await dialog.getByRole("combobox").selectOption(sort); diff --git a/e2e/tournament.spec.ts b/e2e/tournament.spec.ts index 55f804e76..2041174b2 100644 --- a/e2e/tournament.spec.ts +++ b/e2e/tournament.spec.ts @@ -349,7 +349,9 @@ test.describe("Tournament", () => { }); const captains = await factories.UserFactory.createMany(SEEDED_TEAM_COUNT); - const teams = []; + const teams: Awaited< + ReturnType + >[] = []; for (const [i, captain] of captains.entries()) { teams.push( await factories.TournamentTeamFactory.create({ diff --git a/migrations/20260815145515-leagues-as-normal-tournaments.ts b/migrations/20260815145515-leagues-as-normal-tournaments.ts index cf83e7869..86c84dbe8 100644 --- a/migrations/20260815145515-leagues-as-normal-tournaments.ts +++ b/migrations/20260815145515-leagues-as-normal-tournaments.ts @@ -73,6 +73,16 @@ const SEASONS: Season[] = [ }, ]; +type MigratedDivision = Season["divisions"][number] & { + tier: number | null; + castTwitchAccounts: string[] | null; + castedMatchesInfo: CastedMatchesInfo | null; + groupStageIdx: number; + playoffsIdx: number; + groupStageBracket: any; + playoffsBracket: any; +}; + async function migrateSeason(trx: Transaction, season: Season) { const signup = await trx .selectFrom("Tournament") @@ -83,7 +93,7 @@ async function migrateSeason(trx: Transaction, season: Season) { // databases without the production league data (dev, tests) if (!signup) return; - const divisions = []; + const divisions: MigratedDivision[] = []; for (const [idx, division] of season.divisions.entries()) { const row = await trx .selectFrom("Tournament") diff --git a/public/sw-2.js b/public/sw-2.js index 573519e47..e225f77ed 100644 --- a/public/sw-2.js +++ b/public/sw-2.js @@ -59,8 +59,7 @@ self.addEventListener("notificationclick", (event) => { event.waitUntil( clients.matchAll({ type: "window" }).then((windowClients) => { // Check if there is already a window/tab open with the target URL - for (let i = 0; i < windowClients.length; i++) { - const client = windowClients[i]; + for (const client of windowClients) { // If so, just focus it. if (client.url === targetUrl && "focus" in client) { return client.focus(); diff --git a/scripts/add-leaderboard-teams-to-tournament.ts b/scripts/add-leaderboard-teams-to-tournament.ts index a238df02d..0f44a3725 100644 --- a/scripts/add-leaderboard-teams-to-tournament.ts +++ b/scripts/add-leaderboard-teams-to-tournament.ts @@ -22,8 +22,10 @@ invariant( async function loadTournament() { try { return await tournamentFromDB(tournamentId); - } catch { - throw new Error(`Tournament with id ${tournamentId} not found`); + } catch (error) { + throw new Error(`Tournament with id ${tournamentId} not found`, { + cause: error, + }); } } diff --git a/scripts/check-translation-jsons.ts b/scripts/check-translation-jsons.ts index 86ddc2ba3..8dd96e3c0 100644 --- a/scripts/check-translation-jsons.ts +++ b/scripts/check-translation-jsons.ts @@ -67,8 +67,8 @@ for (const file of fileNames) { let otherLanguageContent: Record; try { otherLanguageContent = JSON.parse(otherRawContent); - } catch { - throw new Error(`failed to parse ${lang}/${file}`); + } catch (error) { + throw new Error(`failed to parse ${lang}/${file}`, { cause: error }); } const otherLanguageContentKeys = getKeysWithoutSuffix( @@ -276,7 +276,7 @@ function MDOverviewTable({ (name) => name !== "weapons.json" && name !== "gear.json", ); - const rows = []; + const rows: string[] = []; rows.push( `| Language | Total | ${relevantFiles.map(MD.inlineCode).join(" | ")} |`, @@ -285,7 +285,7 @@ function MDOverviewTable({ rows.push(`| :-- | :-: | ${relevantFiles.map(() => ":-:").join(" | ")} |`); for (const [lang, missingKeysObj] of Object.entries(missingTranslations)) { - const cells = []; + const cells: string[] = []; cells.push(MD.strong(lang)); diff --git a/scripts/download-prod-db.ts b/scripts/download-prod-db.ts index 644ba9b3a..3b417617c 100644 --- a/scripts/download-prod-db.ts +++ b/scripts/download-prod-db.ts @@ -123,6 +123,7 @@ async function main() { fs.rmSync(stagedPath, { force: true }); throw new Error( `Snapshot failed verification, no checkout was touched. The archive is kept at ${archivePath}\n${(error as Error).message}`, + { cause: error }, ); } diff --git a/scripts/scanner/scan-vod.ts b/scripts/scanner/scan-vod.ts index b387c645c..eb813dec2 100644 --- a/scripts/scanner/scan-vod.ts +++ b/scripts/scanner/scan-vod.ts @@ -134,6 +134,7 @@ function parseArgs(argv: string[]): { let duration: number | undefined; let outPath: string | undefined; let collectTelemetry = false; + // biome-ignore lint/style/useForOf: the index advances inside the loop to consume flag values for (let i = 0; i < argv.length; i++) { const arg = argv[i]!; if (arg === "--fps") fps = Number(argv[++i]);