From 955eeae2a7659d9541fc4a54d1dff8a4ea433c40 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 13 Sep 2024 13:18:06 +0200 Subject: [PATCH] refactor: enable no-unsafe-assignment rule (@miodec) (#5874) Co-authored-by: Nad Alaba <37968805+NadAlaba@users.noreply.github.com> Co-authored-by: Christian Fehmer Co-authored-by: Igor Bedesqui Co-authored-by: amarnathsama <63007641+amarnathsama@users.noreply.github.com> --- backend/__tests__/utils/pb.spec.ts | 96 ++++++++++++++++++- backend/src/api/controllers/user.ts | 2 +- backend/src/api/routes/swagger.ts | 2 +- backend/src/api/ts-rest-adapter.ts | 2 +- backend/src/dal/new-quotes.ts | 26 +++-- backend/src/dal/result.ts | 2 +- backend/src/init/configuration.ts | 6 +- backend/src/init/email-client.ts | 8 +- backend/src/init/firebase-admin.ts | 6 +- backend/src/middlewares/auth.ts | 16 +--- backend/src/middlewares/configuration.ts | 5 +- backend/src/services/weekly-xp-leaderboard.ts | 12 ++- backend/src/utils/daily-leaderboards.ts | 6 +- backend/src/utils/misc.ts | 2 +- backend/src/utils/pb.ts | 39 ++++---- backend/src/workers/email-worker.ts | 13 +-- backend/src/workers/later-worker.ts | 4 +- .../src/ts/ape/adapters/ts-rest-adapter.ts | 2 +- .../ts/controllers/analytics-controller.ts | 12 ++- .../src/ts/controllers/captcha-controller.ts | 3 +- .../src/ts/controllers/pw-ad-controller.ts | 4 +- .../src/ts/elements/account/result-filters.ts | 7 ++ frontend/src/ts/elements/leaderboards.ts | 1 + .../ts/elements/settings/settings-group.ts | 2 +- .../src/ts/elements/test-activity-calendar.ts | 6 +- frontend/src/ts/elements/test-activity.ts | 3 + frontend/src/ts/event-handlers/global.ts | 4 +- frontend/src/ts/modals/edit-preset.ts | 23 ++--- frontend/src/ts/modals/quote-report.ts | 1 + frontend/src/ts/modals/quote-search.ts | 1 + frontend/src/ts/modals/quote-submit.ts | 1 + frontend/src/ts/modals/user-report.ts | 1 + frontend/src/ts/modals/word-filter.ts | 3 + frontend/src/ts/pages/account-settings.ts | 2 +- frontend/src/ts/pages/settings.ts | 4 +- frontend/src/ts/test/poetry.ts | 8 +- frontend/src/ts/test/test-logic.ts | 4 +- frontend/src/ts/test/wikipedia.ts | 7 +- .../src/ts/utils/local-storage-with-schema.ts | 2 +- frontend/src/ts/utils/misc.ts | 21 ++++ frontend/src/ts/utils/results.ts | 4 +- frontend/src/ts/utils/url-handler.ts | 35 ++++--- packages/eslint-config/index.js | 2 +- 43 files changed, 281 insertions(+), 129 deletions(-) diff --git a/backend/__tests__/utils/pb.spec.ts b/backend/__tests__/utils/pb.spec.ts index 28c9f77f3..79c80693d 100644 --- a/backend/__tests__/utils/pb.spec.ts +++ b/backend/__tests__/utils/pb.spec.ts @@ -57,10 +57,15 @@ describe("Pb Utils", () => { mode2: "15", } as unknown as Result; - const run = pb.checkAndUpdatePb(userPbs, undefined, result); + const run = pb.checkAndUpdatePb( + userPbs, + {} as MonkeyTypes.LbPersonalBests, + result + ); expect(run.isPb).toBe(true); expect(run.personalBests?.["time"]?.["15"]?.[0]).not.toBe(undefined); + expect(run.lbPersonalBests).not.toBe({}); }); it("should not override default pb when saving numbers test", () => { const userPbs: PersonalBests = { @@ -111,4 +116,93 @@ describe("Pb Utils", () => { ); }); }); + describe("updateLeaderboardPersonalBests", () => { + const userPbs: PersonalBests = { + time: { + "15": [ + { + acc: 100, + consistency: 100, + difficulty: "normal", + lazyMode: false, + language: "english", + numbers: false, + punctuation: false, + raw: 100, + timestamp: 0, + wpm: 100, + }, + { + acc: 100, + consistency: 100, + difficulty: "normal", + lazyMode: false, + language: "spanish", + numbers: false, + punctuation: false, + raw: 100, + timestamp: 0, + wpm: 100, + }, + ], + }, + words: {}, + custom: {}, + quote: {}, + zen: {}, + }; + it("should update leaderboard personal bests if they dont exist or the structure is incomplete", () => { + const lbpbstartingvalues = [ + undefined, + {}, + { time: {} }, + { time: { "15": {} } }, + { time: { "15": { english: {} } } }, + ]; + + const result15 = { + mode: "time", + mode2: "15", + } as unknown as Result; + + for (const lbPb of lbpbstartingvalues) { + const lbPbPb = pb.updateLeaderboardPersonalBests( + userPbs, + _.cloneDeep(lbPb) as MonkeyTypes.LbPersonalBests, + result15 + ); + + expect(lbPbPb).toEqual({ + time: { + "15": { + english: { + acc: 100, + consistency: 100, + difficulty: "normal", + lazyMode: false, + language: "english", + numbers: false, + punctuation: false, + raw: 100, + timestamp: 0, + wpm: 100, + }, + spanish: { + acc: 100, + consistency: 100, + difficulty: "normal", + lazyMode: false, + language: "spanish", + numbers: false, + punctuation: false, + raw: 100, + timestamp: 0, + wpm: 100, + }, + }, + }, + }); + } + }); + }); }); diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index d4b38a291..33f3e4268 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -198,7 +198,7 @@ export async function sendVerificationEmail( JSON.stringify({ decodedTokenEmail: email, userInfoEmail: userInfo.email, - stack: e.stack, + stack: e.stack as unknown, }), userInfo.uid ); diff --git a/backend/src/api/routes/swagger.ts b/backend/src/api/routes/swagger.ts index 0bd008129..ba4b90c5a 100644 --- a/backend/src/api/routes/swagger.ts +++ b/backend/src/api/routes/swagger.ts @@ -8,7 +8,7 @@ function addSwaggerMiddlewares(app: Application): void { const openApiSpec = __dirname + "/../../static/api/openapi.json"; let spec = {}; try { - spec = JSON.parse(readFileSync(openApiSpec, "utf8")); + spec = JSON.parse(readFileSync(openApiSpec, "utf8")) as string; } catch (err) { Logger.warning( `Cannot read openApi specification from ${openApiSpec}. Swagger stats will not fully work.` diff --git a/backend/src/api/ts-rest-adapter.ts b/backend/src/api/ts-rest-adapter.ts index d86575651..a48313473 100644 --- a/backend/src/api/ts-rest-adapter.ts +++ b/backend/src/api/ts-rest-adapter.ts @@ -22,7 +22,7 @@ export function callController< query: all.query as TQuery, params: all.params as TParams, raw: all.req, - ctx: all.req["ctx"], + ctx: all.req["ctx"] as MonkeyTypes.Context, }; const result = await handler(req); diff --git a/backend/src/dal/new-quotes.ts b/backend/src/dal/new-quotes.ts index 357cce367..8814cebf6 100644 --- a/backend/src/dal/new-quotes.ts +++ b/backend/src/dal/new-quotes.ts @@ -8,6 +8,20 @@ import MonkeyError from "../utils/error"; import { compareTwoStrings } from "string-similarity"; import { ApproveQuote, Quote } from "@monkeytype/contracts/schemas/quotes"; +type JsonQuote = { + text: string; + britishText?: string; + source: string; + length: number; + id: number; +}; + +type QuoteData = { + language: string; + quotes: JsonQuote[]; + groups: [number, number][]; +}; + const PATH_TO_REPO = "../../../../monkeytype-new-quotes"; let git; @@ -71,11 +85,11 @@ export async function add( let similarityScore = -1; if (existsSync(fileDir)) { const quoteFile = await readFile(fileDir); - const quoteFileJSON = JSON.parse(quoteFile.toString()); + const quoteFileJSON = JSON.parse(quoteFile.toString()) as QuoteData; quoteFileJSON.quotes.every((old) => { - if (compareTwoStrings(old.text as string, quote.text) > 0.9) { + if (compareTwoStrings(old.text, quote.text) > 0.9) { duplicateId = old.id; - similarityScore = compareTwoStrings(old.text as string, quote.text); + similarityScore = compareTwoStrings(old.text, quote.text); return false; } return true; @@ -155,9 +169,9 @@ export async function approve( await git.pull("upstream", "master"); if (existsSync(fileDir)) { const quoteFile = await readFile(fileDir); - const quoteObject = JSON.parse(quoteFile.toString()); + const quoteObject = JSON.parse(quoteFile.toString()) as QuoteData; quoteObject.quotes.every((old) => { - if (compareTwoStrings(old.text as string, quote.text) > 0.8) { + if (compareTwoStrings(old.text, quote.text) > 0.8) { throw new MonkeyError(409, "Duplicate quote"); } }); @@ -168,7 +182,7 @@ export async function approve( } }); quote.id = maxid + 1; - quoteObject.quotes.push(quote); + quoteObject.quotes.push(quote as JsonQuote); writeFileSync(fileDir, JSON.stringify(quoteObject, null, 2)); message = `Added quote to ${language}.json.`; } else { diff --git a/backend/src/dal/result.ts b/backend/src/dal/result.ts index 57946bf7e..dd1d80364 100644 --- a/backend/src/dal/result.ts +++ b/backend/src/dal/result.ts @@ -87,7 +87,7 @@ export async function getLastResult( export async function getResultByTimestamp( uid: string, - timestamp + timestamp: number ): Promise { return await getResultCollection().findOne({ uid, timestamp }); } diff --git a/backend/src/init/configuration.ts b/backend/src/init/configuration.ts index 662a2f191..6a59d4df1 100644 --- a/backend/src/init/configuration.ts +++ b/backend/src/init/configuration.ts @@ -25,14 +25,14 @@ function mergeConfigurations( const commonKeys = _.intersection(_.keys(base), _.keys(source)); commonKeys.forEach((key) => { - const baseValue = base[key]; - const sourceValue = source[key]; + const baseValue = base[key] as object; + const sourceValue = source[key] as object; const isBaseValueObject = _.isPlainObject(baseValue); const isSourceValueObject = _.isPlainObject(sourceValue); if (isBaseValueObject && isSourceValueObject) { - merge(baseValue as object, sourceValue as object); + merge(baseValue, sourceValue); } else if (identity(baseValue) === identity(sourceValue)) { base[key] = sourceValue; } diff --git a/backend/src/init/email-client.ts b/backend/src/init/email-client.ts index 3ccaa2a2e..3c4874392 100644 --- a/backend/src/init/email-client.ts +++ b/backend/src/init/email-client.ts @@ -103,14 +103,16 @@ export async function sendEmail( html: template, }; - let result; + type Result = { response: string; accepted: string[] }; + + let result: Result; try { - result = await transporter.sendMail(mailOptions); + result = (await transporter.sendMail(mailOptions)) as Result; } catch (e) { recordEmail(templateName, "fail"); return { success: false, - message: e.message, + message: e.message as string, }; } diff --git a/backend/src/init/firebase-admin.ts b/backend/src/init/firebase-admin.ts index 1e1687658..d380eab31 100644 --- a/backend/src/init/firebase-admin.ts +++ b/backend/src/init/firebase-admin.ts @@ -29,11 +29,9 @@ export function init(): void { encoding: "utf8", flag: "r", }) - ); + ) as ServiceAccount; admin.initializeApp({ - credential: admin.credential.cert( - serviceAccount as unknown as ServiceAccount - ), + credential: admin.credential.cert(serviceAccount), }); Logger.success("Firebase app initialized"); } diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 15d1fab99..b2aff30c8 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -188,33 +188,27 @@ async function authenticateWithBearerToken( email: decodedToken.email ?? "", }; } catch (error) { - const errorCode = error?.errorInfo?.code; + const errorCode = error?.errorInfo?.code as string | undefined; - if (errorCode?.includes("auth/id-token-expired") as boolean | undefined) { + if (errorCode?.includes("auth/id-token-expired")) { throw new MonkeyError( 401, "Token expired - please login again", "authenticateWithBearerToken" ); - } else if ( - errorCode?.includes("auth/id-token-revoked") as boolean | undefined - ) { + } else if (errorCode?.includes("auth/id-token-revoked")) { throw new MonkeyError( 401, "Token revoked - please login again", "authenticateWithBearerToken" ); - } else if ( - errorCode?.includes("auth/user-not-found") as boolean | undefined - ) { + } else if (errorCode?.includes("auth/user-not-found")) { throw new MonkeyError( 404, "User not found", "authenticateWithBearerToken" ); - } else if ( - errorCode?.includes("auth/argument-error") as boolean | undefined - ) { + } else if (errorCode?.includes("auth/argument-error")) { throw new MonkeyError( 400, "Incorrect Bearer token format", diff --git a/backend/src/middlewares/configuration.ts b/backend/src/middlewares/configuration.ts index 256618efa..07e8f233a 100644 --- a/backend/src/middlewares/configuration.ts +++ b/backend/src/middlewares/configuration.ts @@ -53,11 +53,12 @@ function getValue( path: ConfigurationPath ): boolean { const keys = (path as string).split("."); - let result = configuration; + let result: unknown = configuration; for (const key of keys) { - if (result === undefined || result === null) + if (result === undefined || result === null) { throw new MonkeyError(500, `Invalid configuration path: "${path}"`); + } result = result[key]; } diff --git a/backend/src/services/weekly-xp-leaderboard.ts b/backend/src/services/weekly-xp-leaderboard.ts index e52b56ad8..2933d7f4a 100644 --- a/backend/src/services/weekly-xp-leaderboard.ts +++ b/backend/src/services/weekly-xp-leaderboard.ts @@ -129,14 +129,14 @@ export class WeeklyXpLeaderboard { this.getThisWeeksXpLeaderboardKeys(); // @ts-expect-error - const [results, scores]: string[][] = await connection.getResults( + const [results, scores] = (await connection.getResults( 2, // How many of the arguments are redis keys (https://redis.io/docs/manual/programmability/lua-api/) weeklyXpLeaderboardScoresKey, weeklyXpLeaderboardResultsKey, minRank, maxRank, "true" - ); + )) as string[][]; if (results === undefined) { throw new Error( @@ -183,13 +183,17 @@ export class WeeklyXpLeaderboard { // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error // @ts-ignore - const [[, rank], [, totalXp], [, count], [, result]] = await connection + const [[, rank], [, totalXp], [, count], [, result]] = (await connection .multi() .zrevrank(weeklyXpLeaderboardScoresKey, uid) .zscore(weeklyXpLeaderboardScoresKey, uid) .zcard(weeklyXpLeaderboardScoresKey) .hget(weeklyXpLeaderboardResultsKey, uid) - .exec(); + .exec()) as [ + [null, number | null], + [null, string | null], + [null, number | null] + ]; if (rank === null) { return null; diff --git a/backend/src/utils/daily-leaderboards.ts b/backend/src/utils/daily-leaderboards.ts index b99fed503..e77bfe401 100644 --- a/backend/src/utils/daily-leaderboards.ts +++ b/backend/src/utils/daily-leaderboards.ts @@ -121,14 +121,14 @@ export class DailyLeaderboard { this.getTodaysLeaderboardKeys(); // @ts-expect-error - const [results]: string[][] = await connection.getResults( + const [results] = (await connection.getResults( 2, leaderboardScoresKey, leaderboardResultsKey, minRank, maxRank, "false" - ); + )) as string[][]; if (results === undefined) { throw new Error( @@ -198,7 +198,7 @@ export class DailyLeaderboard { count: count ?? 0, rank: rank + 1, entry: { - ...JSON.parse(result ?? "null"), + ...(JSON.parse(result ?? "null") as LeaderboardEntry), }, }; } diff --git a/backend/src/utils/misc.ts b/backend/src/utils/misc.ts index 985784642..1db2bcafc 100644 --- a/backend/src/utils/misc.ts +++ b/backend/src/utils/misc.ts @@ -285,7 +285,7 @@ export function formatSeconds( } export function intersect(a: T[], b: T[], removeDuplicates = false): T[] { - let t; + let t: T[]; // eslint-disable-next-line @typescript-eslint/no-unused-expressions if (b.length > a.length) (t = b), (b = a), (a = t); // indexOf to loop over shorter const filtered = a.filter(function (e) { diff --git a/backend/src/utils/pb.ts b/backend/src/utils/pb.ts index b46f5e0f2..383daa7cb 100644 --- a/backend/src/utils/pb.ts +++ b/backend/src/utils/pb.ts @@ -59,7 +59,14 @@ export function checkAndUpdatePb( } if (!_.isNil(lbPersonalBests)) { - updateLeaderboardPersonalBests(userPb, lbPersonalBests, result); + const newLbPb = updateLeaderboardPersonalBests( + userPb, + lbPersonalBests, + result + ); + if (newLbPb !== null) { + lbPersonalBests = newLbPb; + } } return { @@ -165,26 +172,20 @@ function buildPersonalBest(result: Result): PersonalBest { }; } -function updateLeaderboardPersonalBests( +export function updateLeaderboardPersonalBests( userPersonalBests: PersonalBests, lbPersonalBests: MonkeyTypes.LbPersonalBests, result: Result -): void { +): MonkeyTypes.LbPersonalBests | null { if (!shouldUpdateLeaderboardPersonalBests(result)) { - return; + return null; } - const mode = result.mode; const mode2 = result.mode2; - - lbPersonalBests[mode] = lbPersonalBests[mode] ?? {}; - const lbMode2 = lbPersonalBests[mode][mode2] as MonkeyTypes.LbPersonalBests; - if (lbMode2 === undefined || Array.isArray(lbMode2)) { - lbPersonalBests[mode][mode2] = {}; - } - + const lbPb = lbPersonalBests ?? {}; + lbPb[mode] ??= {}; + lbPb[mode][mode2] ??= {}; const bestForEveryLanguage = {}; - userPersonalBests[mode][mode2].forEach((pb: PersonalBest) => { const language = pb.language; if ( @@ -194,18 +195,18 @@ function updateLeaderboardPersonalBests( bestForEveryLanguage[language] = pb; } }); - _.each(bestForEveryLanguage, (pb: PersonalBest, language: string) => { - const languageDoesNotExist = - lbPersonalBests[mode][mode2][language] === undefined; - + const languageDoesNotExist = lbPb[mode][mode2][language] === undefined; + const languageIsEmpty = _.isEmpty(lbPb[mode][mode2][language]); if ( languageDoesNotExist || - lbPersonalBests[mode][mode2][language].wpm < pb.wpm + languageIsEmpty || + lbPb[mode][mode2][language].wpm < pb.wpm ) { - lbPersonalBests[mode][mode2][language] = pb; + lbPb[mode][mode2][language] = pb; } }); + return lbPb; } function shouldUpdateLeaderboardPersonalBests(result: Result): boolean { diff --git a/backend/src/workers/email-worker.ts b/backend/src/workers/email-worker.ts index a6d70db56..4cf03915c 100644 --- a/backend/src/workers/email-worker.ts +++ b/backend/src/workers/email-worker.ts @@ -2,18 +2,15 @@ import _ from "lodash"; import IORedis from "ioredis"; import { Worker, Job, type ConnectionOptions } from "bullmq"; import Logger from "../utils/logger"; -import EmailQueue, { - type EmailTaskContexts, - type EmailType, -} from "../queues/email-queue"; +import EmailQueue, { EmailTask, type EmailType } from "../queues/email-queue"; import { sendEmail } from "../init/email-client"; import { recordTimeToCompleteJob } from "../utils/prometheus"; import { addLog } from "../dal/logs"; -async function jobHandler(job: Job): Promise { - const type: EmailType = job.data.type; - const email: string = job.data.email; - const ctx: EmailTaskContexts[typeof type] = job.data.ctx; +async function jobHandler(job: Job>): Promise { + const type = job.data.type; + const email = job.data.email; + const ctx = job.data.ctx; Logger.info(`Starting job: ${type}`); diff --git a/backend/src/workers/later-worker.ts b/backend/src/workers/later-worker.ts index 2621cd53c..a669a3b3e 100644 --- a/backend/src/workers/later-worker.ts +++ b/backend/src/workers/later-worker.ts @@ -180,8 +180,8 @@ async function handleWeeklyXpLeaderboardResults( await addToInboxBulk(mailEntries, inboxConfig); } -async function jobHandler(job: Job): Promise { - const { taskName, ctx }: LaterTask = job.data; +async function jobHandler(job: Job>): Promise { + const { taskName, ctx } = job.data; Logger.info(`Starting job: ${taskName}`); diff --git a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts index c72468d72..bba9590d9 100644 --- a/frontend/src/ts/ape/adapters/ts-rest-adapter.ts +++ b/frontend/src/ts/ape/adapters/ts-rest-adapter.ts @@ -41,7 +41,7 @@ function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{ : AbortSignal.timeout(timeout), }); - const body = await response.json(); + const body = (await response.json()) as object; if (response.status >= 400) { console.error(`${request.method} ${request.path} failed`, { status: response.status, diff --git a/frontend/src/ts/controllers/analytics-controller.ts b/frontend/src/ts/controllers/analytics-controller.ts index 90b71dab8..56fbc7eaf 100644 --- a/frontend/src/ts/controllers/analytics-controller.ts +++ b/frontend/src/ts/controllers/analytics-controller.ts @@ -9,6 +9,11 @@ import { createErrorMessage } from "../utils/misc"; let analytics: AnalyticsType; +type AcceptedCookies = { + security: boolean; + analytics: boolean; +}; + export async function log( eventName: string, params?: Record @@ -21,12 +26,9 @@ export async function log( } const lsString = localStorage.getItem("acceptedCookies"); -let acceptedCookies: { - security: boolean; - analytics: boolean; -} | null; +let acceptedCookies; if (lsString !== undefined && lsString !== null && lsString !== "") { - acceptedCookies = JSON.parse(lsString); + acceptedCookies = JSON.parse(lsString) as AcceptedCookies; } else { acceptedCookies = null; } diff --git a/frontend/src/ts/controllers/captcha-controller.ts b/frontend/src/ts/controllers/captcha-controller.ts index 610580d3b..46aee7b37 100644 --- a/frontend/src/ts/controllers/captcha-controller.ts +++ b/frontend/src/ts/controllers/captcha-controller.ts @@ -13,12 +13,13 @@ export function render( } //@ts-expect-error + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const widgetId = grecaptcha.render(element, { sitekey: siteKey, callback, }); - captchas[id] = widgetId; + captchas[id] = widgetId as number; } export function reset(id: string): void { diff --git a/frontend/src/ts/controllers/pw-ad-controller.ts b/frontend/src/ts/controllers/pw-ad-controller.ts index 6c7f91f5c..dd5378838 100644 --- a/frontend/src/ts/controllers/pw-ad-controller.ts +++ b/frontend/src/ts/controllers/pw-ad-controller.ts @@ -143,8 +143,9 @@ export function init(): void { headOfDocument.appendChild(rampScript); window._pwGA4PageviewId = "".concat(Date.now()); - // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/no-unsafe-assignment window.dataLayer = window.dataLayer || []; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment window.gtag = // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions window.gtag || @@ -156,6 +157,7 @@ export function init(): void { gtag("config", "G-KETCPNHRJF", { send_page_view: false }); gtag("event", "ramp_js", { send_to: "G-KETCPNHRJF", + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment pageview_id: window._pwGA4PageviewId, }); } diff --git a/frontend/src/ts/elements/account/result-filters.ts b/frontend/src/ts/elements/account/result-filters.ts index abedd150c..c77797aef 100644 --- a/frontend/src/ts/elements/account/result-filters.ts +++ b/frontend/src/ts/elements/account/result-filters.ts @@ -350,10 +350,12 @@ export function updateActive(): void { } for (const [id, select] of Object.entries(groupSelects)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const ss = select; const group = getGroup(id as ResultFiltersGroup); const everythingSelected = Object.values(group).every((v) => v === true); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const newData = ss.store.getData(); const allOption = $( @@ -682,8 +684,10 @@ function adjustScrollposition( group: ResultFiltersGroup, topItem: number = 0 ): void { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const slimSelect = groupSelects[group]; if (slimSelect === undefined) return; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const listElement = slimSelect.render.content.list; const topListItem = listElement.children.item(topItem) as HTMLElement; @@ -779,6 +783,7 @@ export async function appendButtons( ); if (el) { el.innerHTML = html; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select groupSelects["language"] = new SlimSelect({ select: el.querySelector(".languageSelect") as HTMLSelectElement, settings: { @@ -840,6 +845,7 @@ export async function appendButtons( ); if (el) { el.innerHTML = html; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select groupSelects["funbox"] = new SlimSelect({ select: el.querySelector(".funboxSelect") as HTMLSelectElement, settings: { @@ -897,6 +903,7 @@ export async function appendButtons( ); if (el) { el.innerHTML = html; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select groupSelects["tags"] = new SlimSelect({ select: el.querySelector(".tagsSelect") as HTMLSelectElement, settings: { diff --git a/frontend/src/ts/elements/leaderboards.ts b/frontend/src/ts/elements/leaderboards.ts index 02c0810cf..e8c23aa11 100644 --- a/frontend/src/ts/elements/leaderboards.ts +++ b/frontend/src/ts/elements/leaderboards.ts @@ -712,6 +712,7 @@ export function show(): void { Config.typingSpeedUnit + '
accuracy
' ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select languageSelector = new SlimSelect({ select: "#leaderboardsWrapper #leaderboards .leaderboardsTop .buttonGroup.timeRange .languageSelect", diff --git a/frontend/src/ts/elements/settings/settings-group.ts b/frontend/src/ts/elements/settings/settings-group.ts index 75bf4f20a..2ae136bf5 100644 --- a/frontend/src/ts/elements/settings/settings-group.ts +++ b/frontend/src/ts/elements/settings/settings-group.ts @@ -120,7 +120,7 @@ export default class SettingsGroup { select.value = this.configValue as string; //@ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unnecessary-type-assertion -- TODO: update slim-select const ss = select.slim as SlimSelect | undefined; ss?.store.setSelectedBy("value", [this.configValue as string]); ss?.render.renderValues(); diff --git a/frontend/src/ts/elements/test-activity-calendar.ts b/frontend/src/ts/elements/test-activity-calendar.ts index 95b3f08d2..a2209ddf0 100644 --- a/frontend/src/ts/elements/test-activity-calendar.ts +++ b/frontend/src/ts/elements/test-activity-calendar.ts @@ -57,9 +57,9 @@ export class TestActivityCalendar implements MonkeyTypes.TestActivityCalendar { lastDay: Date ): (number | null | undefined)[] { //fill calendar with enough values - const values: (number | null | undefined)[] = new Array( - Math.max(0, 386 - data.length) - ).fill(undefined); + const values = new Array(Math.max(0, 386 - data.length)).fill( + undefined + ) as (number | null | undefined)[]; values.push(...data); //discard values outside the calendar range diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 634edda71..9bf035140 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -18,6 +18,7 @@ export function init( } $("#testActivity").removeClass("hidden"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select yearSelector = getYearSelector(); initYearSelector("current", userSignUpDate?.getFullYear() || 2022); update(calendar); @@ -84,6 +85,7 @@ export function initYearSelector( } } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select const yearSelect = getYearSelector(); yearSelect.setData(years); years.length > 1 ? yearSelect.enable() : yearSelect.disable(); @@ -102,6 +104,7 @@ function updateMonths(months: MonkeyTypes.TestActivityMonth[]): void { function getYearSelector(): SlimSelect { if (yearSelector !== undefined) return yearSelector; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select yearSelector = new SlimSelect({ select: "#testActivity .yearSelect", settings: { diff --git a/frontend/src/ts/event-handlers/global.ts b/frontend/src/ts/event-handlers/global.ts index 8b91c46b2..d1376761d 100644 --- a/frontend/src/ts/event-handlers/global.ts +++ b/frontend/src/ts/event-handlers/global.ts @@ -46,7 +46,7 @@ window.onerror = function (message, url, line, column, error): void { window.onunhandledrejection = function (e): void { if (Misc.isDevEnvironment()) { - const message = e.reason.message ?? e.reason; + const message = (e.reason.message ?? e.reason) as string; Notifications.add(`${message}`, -1, { customTitle: "DEV: Unhandled rejection", duration: 5, @@ -54,6 +54,6 @@ window.onunhandledrejection = function (e): void { console.error(e); } void log("error", { - error: e.reason.stack ?? "", + error: (e.reason.stack ?? "") as string, }); }; diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index eee5e777b..b0124e80a 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -139,7 +139,9 @@ function addCheckboxListeners(): void { `#editPresetModal .modal .changePresetToCurrentCheckbox input` ); presetToCurrentCheckbox.on("change", async () => { - state.setPresetToCurrent = presetToCurrentCheckbox.prop("checked"); + state.setPresetToCurrent = presetToCurrentCheckbox.prop( + "checked" + ) as boolean; await updateEditPresetUI(); }); } @@ -212,9 +214,9 @@ async function apply(): Promise { "data-preset-id" ) as string; - const updateConfig: boolean = $("#editPresetModal .modal label input").prop( + const updateConfig = $("#editPresetModal .modal label input").prop( "checked" - ); + ) as boolean; const snapshotPresets = DB.getSnapshot()?.presets ?? []; @@ -474,14 +476,13 @@ function getPartialConfigChanges( state.checkboxes.get(getSettingGroup(settingName)) === true ) .forEach((settingName) => { - //@ts-expect-error this is fine - activeConfigChanges[settingName] = - //@ts-expect-error this is fine - configChanges[settingName] !== undefined - ? //@ts-expect-error this is fine - configChanges[settingName] - : //@ts-expect-error this is fine - defaultConfig[settingName]; + const safeSettingName = settingName as keyof MonkeyTypes.ConfigChanges; + const newValue = + configChanges[safeSettingName] !== undefined + ? configChanges[safeSettingName] + : defaultConfig[safeSettingName]; + // @ts-expect-error cant figure this one out, but it works + activeConfigChanges[safeSettingName] = newValue; }); return activeConfigChanges; } diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index 693432057..5b4cf7ba0 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -46,6 +46,7 @@ export async function show( $("#quoteReportModal .reason").val("Grammatical error"); $("#quoteReportModal .comment").val(""); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select state.reasonSelect = new SlimSelect({ select: "#quoteReportModal .reason", settings: { diff --git a/frontend/src/ts/modals/quote-search.ts b/frontend/src/ts/modals/quote-search.ts index 53f95b38b..42456ffda 100644 --- a/frontend/src/ts/modals/quote-search.ts +++ b/frontend/src/ts/modals/quote-search.ts @@ -273,6 +273,7 @@ export async function show(showOptions?: ShowOptions): Promise { $("#quoteSearchModal .goToQuoteApprove").addClass("hidden"); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select lengthSelect = new SlimSelect({ select: "#quoteSearchModal .quoteLengthFilter", diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index 20a6b7c58..3f0c1aa5e 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -65,6 +65,7 @@ export async function show(showOptions: ShowOptions): Promise { ); await initDropdown(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select select = new SlimSelect({ select: "#quoteSubmitModal .newQuoteLanguage", }); diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index 8d7a78117..3c926afcf 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -51,6 +51,7 @@ export async function show(options: ShowOptions): Promise { "Inappropriate name"; (modalEl.querySelector(".comment") as HTMLTextAreaElement).value = ""; + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select select = new SlimSelect({ select: modalEl.querySelector(".reason") as HTMLElement, settings: { diff --git a/frontend/src/ts/modals/word-filter.ts b/frontend/src/ts/modals/word-filter.ts index e954eab02..84b1e75d3 100644 --- a/frontend/src/ts/modals/word-filter.ts +++ b/frontend/src/ts/modals/word-filter.ts @@ -159,18 +159,21 @@ export async function show(showOptions?: ShowOptions): Promise { void modal.show({ ...showOptions, beforeAnimation: async (modalEl) => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select languageSelect = new SlimSelect({ select: "#wordFilterModal .languageInput", settings: { contentLocation: modalEl, }, }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select layoutSelect = new SlimSelect({ select: "#wordFilterModal .layoutInput", settings: { contentLocation: modal.getModal(), }, }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- TODO: update slim-select presetSelect = new SlimSelect({ select: "#wordFilterModal .presetInput", settings: { diff --git a/frontend/src/ts/pages/account-settings.ts b/frontend/src/ts/pages/account-settings.ts index 403c1bc7a..2e51be5b4 100644 --- a/frontend/src/ts/pages/account-settings.ts +++ b/frontend/src/ts/pages/account-settings.ts @@ -183,7 +183,7 @@ export function updateUI(): void { } $(".page.pageAccountSettings").on("click", ".tabs button", (event) => { - state.activeTab = $(event.target).data("tab"); + state.activeTab = $(event.target).data("tab") as State["activeTab"]; updateTabs(); }); diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts index 24dc593f2..ff78a658b 100644 --- a/frontend/src/ts/pages/settings.ts +++ b/frontend/src/ts/pages/settings.ts @@ -1062,7 +1062,7 @@ $(".pageSettings .section.tags").on( "click", ".tagsList .tag .tagButton", (e) => { - const target = e.currentTarget; + const target = e.currentTarget as HTMLElement; const tagid = $(target).parent(".tag").attr("data-id") as string; TagController.toggle(tagid); $(target).toggleClass("active"); @@ -1073,7 +1073,7 @@ $(".pageSettings .section.presets").on( "click", ".presetsList .preset .presetButton", async (e) => { - const target = e.currentTarget; + const target = e.currentTarget as HTMLElement; const presetid = $(target).parent(".preset").attr("data-id") as string; await PresetController.apply(presetid); void update(); diff --git a/frontend/src/ts/test/poetry.ts b/frontend/src/ts/test/poetry.ts index ae9d19954..ab104ac10 100644 --- a/frontend/src/ts/test/poetry.ts +++ b/frontend/src/ts/test/poetry.ts @@ -49,8 +49,12 @@ export async function getPoem(): Promise
{ try { const response = await fetch(apiURL); - const data = await response.json(); - const poemObj: PoemObject = data[0]; + const data = (await response.json()) as PoemObject[]; + const poemObj = data[0]; + + if (!poemObj) { + return false; + } const words: string[] = []; diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 4a55a952f..cd5be47cc 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1182,9 +1182,9 @@ async function saveResult( } if (data.insertedId !== undefined) { - const result: MonkeyTypes.FullResult = JSON.parse( + const result = JSON.parse( JSON.stringify(completedEvent) - ); + ) as MonkeyTypes.FullResult; result._id = data.insertedId; if (data.isPb !== undefined && data.isPb) { result.isPb = true; diff --git a/frontend/src/ts/test/wikipedia.ts b/frontend/src/ts/test/wikipedia.ts index 2e74959ad..d34fbb58f 100644 --- a/frontend/src/ts/test/wikipedia.ts +++ b/frontend/src/ts/test/wikipedia.ts @@ -268,7 +268,7 @@ export async function getSection(language: string): Promise
{ let pageid = 0; if (randomPostReq.status === 200) { - const postObj: Post = await randomPostReq.json(); + const postObj = (await randomPostReq.json()) as Post; sectionObj.title = postObj.title; sectionObj.author = postObj.author; pageid = postObj.pageid; @@ -286,8 +286,9 @@ export async function getSection(language: string): Promise
{ sectionReq.onload = (): void => { if (sectionReq.readyState === 4) { if (sectionReq.status === 200) { - let sectionText: string = JSON.parse(sectionReq.responseText).query - .pages[pageid.toString()].extract; + let sectionText = JSON.parse(sectionReq.responseText).query.pages[ + pageid.toString() + ].extract as string; // Converting to one paragraph sectionText = sectionText.replace(/<\/p>

+/g, " "); diff --git a/frontend/src/ts/utils/local-storage-with-schema.ts b/frontend/src/ts/utils/local-storage-with-schema.ts index 6cee13390..be26f3458 100644 --- a/frontend/src/ts/utils/local-storage-with-schema.ts +++ b/frontend/src/ts/utils/local-storage-with-schema.ts @@ -25,7 +25,7 @@ export class LocalStorageWithSchema { return this.fallback; } - let jsonParsed; + let jsonParsed: unknown; try { jsonParsed = JSON.parse(value); } catch (e) { diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 28a9d89a1..28e8828da 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -8,6 +8,7 @@ import { Mode2, PersonalBests, } from "@monkeytype/contracts/schemas/shared"; +import { ZodError, ZodSchema } from "zod"; export function kogasa(cov: number): number { return ( @@ -684,4 +685,24 @@ export function isObject(obj: unknown): obj is Record { return typeof obj === "object" && !Array.isArray(obj) && obj !== null; } +/** + * Parse a JSON string into an object and validate it against a schema + * @param input JSON string + * @param schema Zod schema to validate the JSON against + * @returns The parsed JSON object + */ +export function parseJsonWithSchema(input: string, schema: ZodSchema): T { + try { + const jsonParsed = JSON.parse(input) as unknown; + const zodParsed = schema.parse(jsonParsed); + return zodParsed; + } catch (error) { + if (error instanceof ZodError) { + throw new Error(error.errors.map((err) => err.message).join("\n")); + } else { + throw error; + } + } +} + // DO NOT ALTER GLOBAL OBJECTSONSTRUCTOR, IT WILL BREAK RESULT HASHES diff --git a/frontend/src/ts/utils/results.ts b/frontend/src/ts/utils/results.ts index e389eef9a..29b135383 100644 --- a/frontend/src/ts/utils/results.ts +++ b/frontend/src/ts/utils/results.ts @@ -19,9 +19,9 @@ export async function syncNotSignedInLastResult(uid: string): Promise { return; } - const result: MonkeyTypes.FullResult = JSON.parse( + const result = JSON.parse( JSON.stringify(notSignedInLastResult) - ); + ) as MonkeyTypes.FullResult; result._id = response.body.data.insertedId; if (response.body.data.isPb) { result.isPb = true; diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 38fef3afd..dc05b8c63 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -65,37 +65,32 @@ export async function linkDiscord(hashOverride: string): Promise { } } +const customThemeUrlDataSchema = z.object({ + c: CustomThemeColorsSchema, + i: z.string().optional(), + s: CustomBackgroundSizeSchema.optional(), + f: CustomBackgroundFilterSchema.optional(), +}); + export function loadCustomThemeFromUrl(getOverride?: string): void { const getValue = Misc.findGetParameter("customTheme", getOverride); if (getValue === null) return; - let decoded = null; + let decoded: z.infer; try { - decoded = JSON.parse(atob(getValue)); + decoded = Misc.parseJsonWithSchema( + atob(getValue), + customThemeUrlDataSchema + ); } catch (e) { console.log("Custom theme URL decoding failed", e); Notifications.add( - "Failed to load theme from URL: could not decode theme", + "Failed to load theme from URL: " + (e as Error).message, 0 ); return; } - const decodedSchema = z.object({ - c: CustomThemeColorsSchema, - i: z.string().optional(), - s: CustomBackgroundSizeSchema.optional(), - f: CustomBackgroundFilterSchema.optional(), - }); - - const parsed = decodedSchema.safeParse(decoded); - if (!parsed.success) { - Notifications.add("Failed to load theme from URL: invalid data schema", 0); - return; - } - - decoded = parsed.data; - let colorArray: CustomThemeColors | undefined; let image: string | undefined; let size: CustomBackgroundSize | undefined; @@ -151,7 +146,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { const getValue = Misc.findGetParameter("testSettings", getOverride); if (getValue === null) return; - const de: SharedTestSettings = JSON.parse(decompressFromURI(getValue) ?? ""); + const de = JSON.parse( + decompressFromURI(getValue) ?? "" + ) as SharedTestSettings; const applied: Record = {}; diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js index d89eea361..f49363209 100644 --- a/packages/eslint-config/index.js +++ b/packages/eslint-config/index.js @@ -80,12 +80,12 @@ module.exports = { "@typescript-eslint/restrict-plus-operands": "off", // TODO: enable at some point - "@typescript-eslint/no-unsafe-assignment": "off", //~63 "@typescript-eslint/no-unsafe-call": "off", //~76 "@typescript-eslint/no-unsafe-member-access": "off", //~105 // "@typescript-eslint/no-unsafe-argument": "error", + "@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", "@typescript-eslint/no-confusing-void-expression": [ "error",