From 6cb4490137b411a955d67a0519f68757dd9eb25c Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Mon, 3 Aug 2026 23:47:29 -0700 Subject: [PATCH] Ban the null literal via ESLint and replace existing usages with undefined or truthiness checks (#4401) --- frontend/eslint.config.js | 9 ++++++++- frontend/src/utility-functions/colors.ts | 11 +++++------ frontend/src/utility-functions/input.ts | 2 +- frontend/src/utility-functions/persistence.ts | 6 +++--- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index a8cd8e699d..38752e68d0 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -73,6 +73,13 @@ export default defineConfig([ ], }, ], + "no-restricted-syntax": [ + "error", + { + selector: "Literal[raw='null']", + message: "Use `undefined` instead of `null`.", + }, + ], // TypeScript plugin config (for TS-specific linting) "@typescript-eslint/indent": "off", @@ -96,7 +103,7 @@ export default defineConfig([ "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "never" }], "@typescript-eslint/consistent-indexed-object-style": ["error", "record"], "@typescript-eslint/consistent-generic-constructors": ["error", "constructor"], - "@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead." } }], + "@typescript-eslint/no-restricted-types": ["error", { types: { null: "Use `undefined` instead of `null`." } }], // Prettier plugin config (for validating and fixing formatting) "prettier/prettier": "error", diff --git a/frontend/src/utility-functions/colors.ts b/frontend/src/utility-functions/colors.ts index 196c01b177..362ec5297c 100644 --- a/frontend/src/utility-functions/colors.ts +++ b/frontend/src/utility-functions/colors.ts @@ -28,7 +28,7 @@ export function createSRgba8FromHsva(h: number, s: number, v: number, a: number) // COLOR UTILITY FUNCTIONS export function isSRgba8(value: unknown): value is SRGBA8 { - return typeof value === "object" && value !== null && "red" in value; + return !!value && typeof value === "object" && "red" in value; } // Parse a CSS color string into an `SRGBA8`. Uses a canvas to delegate parsing to the browser. @@ -183,11 +183,11 @@ export function contrastingOutlineFactor(value: FillChoice, proximityCol // GRADIENT UTILITY FUNCTIONS export function isGradientStops(value: unknown): value is GradientStops { - return typeof value === "object" && value !== null && "color" in value && Array.isArray(value.color); + return !!value && typeof value === "object" && "color" in value && Array.isArray(value.color); } export function isGradientRamp(value: unknown): value is GradientRamp { - return typeof value === "object" && value !== null && "stops" in value && isGradientStops(value.stops); + return !!value && typeof value === "object" && "stops" in value && isGradientStops(value.stops); } // FILL CHOICE UTILITY FUNCTIONS @@ -203,8 +203,7 @@ export function fillChoiceGradient(value: FillChoice): GradientStops { - if (value === "None" || value === undefined || value === null) return "None"; - if (typeof value === "object" && value !== null && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid }; - if (typeof value === "object" && value !== null && "Gradient" in value && isGradientRamp(value.Gradient)) return { Gradient: value.Gradient }; + if (value && typeof value === "object" && "Solid" in value && isSRgba8(value.Solid)) return { Solid: value.Solid }; + if (value && typeof value === "object" && "Gradient" in value && isGradientRamp(value.Gradient)) return { Gradient: value.Gradient }; return "None"; } diff --git a/frontend/src/utility-functions/input.ts b/frontend/src/utility-functions/input.ts index e0642075ff..222ea4f364 100644 --- a/frontend/src/utility-functions/input.ts +++ b/frontend/src/utility-functions/input.ts @@ -353,7 +353,7 @@ function targetIsTextField(target: EventTarget | HTMLElement | undefined): boole function potentiallyRestoreCanvasFocus(e: Event) { const appElement = window.document.querySelector("[data-app-container]"); - const app = appElement instanceof HTMLElement ? appElement : null; + const app = appElement instanceof HTMLElement ? appElement : undefined; const newInCanvasArea = (e.target instanceof Element && e.target.closest("[data-viewport], [data-viewport-container], [data-graph]")) instanceof Element && diff --git a/frontend/src/utility-functions/persistence.ts b/frontend/src/utility-functions/persistence.ts index 3ef37380f9..c591753ba6 100644 --- a/frontend/src/utility-functions/persistence.ts +++ b/frontend/src/utility-functions/persistence.ts @@ -147,7 +147,7 @@ async function migrateToNewFormat() { if (oldDocuments) { Object.values(oldDocuments).forEach((value) => { const oldEntry: unknown = value; - if (typeof oldEntry !== "object" || oldEntry === null) return; + if (!oldEntry || typeof oldEntry !== "object") return; if (!("documentId" in oldEntry) || !("document" in oldEntry) || !("details" in oldEntry)) return; // Extract the document ID, handling bigint, number, and string formats @@ -168,7 +168,7 @@ async function migrateToNewFormat() { // Extract document details, handling camelCase from the old shipped format const details: unknown = oldEntry.details; - if (typeof details !== "object" || details === null) return; + if (!details || typeof details !== "object") return; let name = ""; if ("name" in details && typeof details.name === "string") name = details.name; @@ -193,7 +193,7 @@ async function migrateToNewFormat() { // TODO: Eventually remove this document upgrade code function extractIsSavedFromUnknown(details: unknown): boolean { - if (typeof details !== "object" || details === null) return false; + if (!details || typeof details !== "object") return false; // Old camelCase format if ("isSaved" in details) return Boolean(details.isSaved);