From 353c867d0310fb9c3980b7ba56383c7fc279f28f Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Fri, 13 Mar 2026 10:03:12 +0300 Subject: [PATCH] Add no-silent-catch lint rule, fix silent failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New oxlint rule: open-pencil/no-silent-catch — errors on empty catch blocks - Replace all 8 empty catch blocks with console.warn() logging - Add worker timeout (30s) and main-thread fallback for .fig parsing - Fix null crash in renderer filter/picture cache cleanup - Buffer copy before worker transfer for safe fallback --- lint/plugin.js | 28 ++++++++++++++++++++++++++ oxlint.json | 6 ++++-- packages/core/src/clipboard.ts | 4 ++-- packages/core/src/fonts.ts | 12 +++++------ packages/core/src/kiwi/fig-file.ts | 19 +++++++++++++++-- packages/core/src/renderer/renderer.ts | 14 ++++++------- src/automation/bridge.ts | 4 ++-- src/automation/server.ts | 4 ++-- src/composables/use-text-edit.ts | 4 ++-- src/stores/editor.ts | 4 ++-- 10 files changed, 72 insertions(+), 27 deletions(-) diff --git a/lint/plugin.js b/lint/plugin.js index ae90d635b..1e62e2c4d 100644 --- a/lint/plugin.js +++ b/lint/plugin.js @@ -187,6 +187,33 @@ const noRawConsoleFormat = { }, } +const noSilentCatch = { + meta: { + docs: { + description: + 'Disallow empty catch blocks — log a warning or re-throw instead of silently swallowing errors', + }, + }, + create(context) { + return { + CatchClause(node) { + const body = node.body + if (!body || !body.body) return + const stmts = body.body.filter( + (s) => s.type !== 'EmptyStatement', + ) + if (stmts.length === 0) { + context.report({ + node, + message: + 'Empty catch block silently swallows errors. Add console.warn(), re-throw, or an explicit // oxlint-ignore-next-line comment.', + }) + } + }, + } + }, +} + const plugin = { meta: { name: 'open-pencil' }, rules: { @@ -195,6 +222,7 @@ const plugin = { 'no-math-random': noMathRandom, 'no-hand-rolled-color': noHandRolledColor, 'no-raw-console-format': noRawConsoleFormat, + 'no-silent-catch': noSilentCatch, }, } diff --git a/oxlint.json b/oxlint.json index f284e793f..ad197e7f4 100644 --- a/oxlint.json +++ b/oxlint.json @@ -78,14 +78,16 @@ "open-pencil/no-structuredclone-scene-arrays": "error", "open-pencil/no-math-random": "error", "open-pencil/no-hand-rolled-color": "error", - "open-pencil/no-raw-console-format": "off" + "open-pencil/no-raw-console-format": "off", + "open-pencil/no-silent-catch": "error" }, "overrides": [ { "files": ["**/*.test.ts", "**/*.test.tsx"], "rules": { "typescript/no-explicit-any": "off", - "typescript/no-non-null-assertion": "off" + "typescript/no-non-null-assertion": "off", + "open-pencil/no-silent-catch": "off" } }, diff --git a/packages/core/src/clipboard.ts b/packages/core/src/clipboard.ts index 52f552274..c536c6895 100644 --- a/packages/core/src/clipboard.ts +++ b/packages/core/src/clipboard.ts @@ -374,8 +374,8 @@ export function parseOpenPencilClipboard( } return { nodes: decoded.nodes, images } } - } catch { - // Not our format + } catch (e) { + console.warn('Failed to parse OpenPencil clipboard data:', e) } return null } diff --git a/packages/core/src/fonts.ts b/packages/core/src/fonts.ts index 94889f938..b373ae704 100644 --- a/packages/core/src/fonts.ts +++ b/packages/core/src/fonts.ts @@ -162,8 +162,8 @@ export async function loadFont(family: string, style = 'Regular'): Promise { return new Promise((resolve, reject) => { const worker = new Worker(new URL('./fig-parse-worker.ts', import.meta.url), { type: 'module' }) + const timeout = setTimeout(() => { + worker.terminate() + reject(new Error('Worker timed out parsing .fig file')) + }, WORKER_TIMEOUT_MS) + worker.onmessage = (e: MessageEvent) => { + clearTimeout(timeout) worker.terminate() if (e.data.error) { reject(new Error(e.data.error)) @@ -120,8 +128,9 @@ function parseViaWorker(buffer: ArrayBuffer): Promise { } worker.onerror = (err) => { + clearTimeout(timeout) worker.terminate() - reject(new Error(err.message)) + reject(new Error(err.message || 'Worker failed to parse .fig file')) } worker.postMessage(buffer, [buffer]) @@ -130,7 +139,13 @@ function parseViaWorker(buffer: ArrayBuffer): Promise { export async function parseFigFile(buffer: ArrayBuffer): Promise { if (typeof Worker !== 'undefined' && typeof window !== 'undefined') { - return parseViaWorker(buffer) + const copy = buffer.slice(0) + try { + return await parseViaWorker(buffer) + } catch (e) { + console.warn('Worker parsing failed, falling back to main thread:', e) + return parseFigFileSync(copy) + } } return parseFigFileSync(buffer) } diff --git a/packages/core/src/renderer/renderer.ts b/packages/core/src/renderer/renderer.ts index 7b5395d57..558d3bd5b 100644 --- a/packages/core/src/renderer/renderer.ts +++ b/packages/core/src/renderer/renderer.ts @@ -172,8 +172,8 @@ export class SkiaRenderer { auxStroke: Paint opacityPaint: Paint effectLayerPaint: Paint - imageFilterCache = new Map() - maskFilterCache = new Map() + imageFilterCache = new Map() + maskFilterCache = new Map() _tmpColor = new Float32Array(4) _tmpRect = new Float32Array(4) textFont: Font | null = null @@ -191,7 +191,7 @@ export class SkiaRenderer { scenePicture: SkPicture | null = null scenePictureVersion = -1 scenePicturePageId: string | null = null - nodePictureCache = new Map() + nodePictureCache = new Map() readonly labelCache = new LabelCache() readonly profiler: RenderProfiler @@ -430,7 +430,7 @@ export class SkiaRenderer { invalidateAllPictures(): void { this.invalidateScenePicture() - for (const pic of this.nodePictureCache.values()) pic.delete() + for (const pic of this.nodePictureCache.values()) pic?.delete() this.nodePictureCache.clear() } @@ -848,11 +848,11 @@ export class SkiaRenderer { this.penVertexFill.delete() this.penVertexStroke.delete() this.effectLayerPaint.delete() - for (const filter of this.imageFilterCache.values()) filter.delete() + for (const filter of this.imageFilterCache.values()) filter?.delete() this.imageFilterCache.clear() - for (const filter of this.maskFilterCache.values()) filter.delete() + for (const filter of this.maskFilterCache.values()) filter?.delete() this.maskFilterCache.clear() - for (const pic of this.nodePictureCache.values()) pic.delete() + for (const pic of this.nodePictureCache.values()) pic?.delete() this.nodePictureCache.clear() this.scenePicture?.delete() this._flashPaint?.delete() diff --git a/src/automation/bridge.ts b/src/automation/bridge.ts index 74299c58a..99badbe52 100644 --- a/src/automation/bridge.ts +++ b/src/automation/bridge.ts @@ -77,8 +77,8 @@ export function startAutomationBridge(server: ViteServer) { req.resolve(payload) } } - } catch { - // ignore malformed messages + } catch (e) { + console.warn('Malformed automation message:', e) } } diff --git a/src/automation/server.ts b/src/automation/server.ts index 2474a9de3..d138f1482 100644 --- a/src/automation/server.ts +++ b/src/automation/server.ts @@ -173,8 +173,8 @@ export function connectAutomation(getStore: () => EditorStore) { }) ) } - } catch { - // ignore parse errors + } catch (e) { + console.warn('Failed to parse WebSocket message:', e) } } diff --git a/src/composables/use-text-edit.ts b/src/composables/use-text-edit.ts index 8098f267a..cb6802b01 100644 --- a/src/composables/use-text-edit.ts +++ b/src/composables/use-text-edit.ts @@ -294,8 +294,8 @@ export function useTextEdit(canvasRef: Ref, store: Edi insertText(text, node) resetBlink() } - } catch { - // Clipboard access denied + } catch (e) { + console.warn('Clipboard access denied:', e) } } diff --git a/src/stores/editor.ts b/src/stores/editor.ts index fc26604ce..c166df530 100644 --- a/src/stores/editor.ts +++ b/src/stores/editor.ts @@ -222,8 +222,8 @@ export function createEditorStore() { if (!state.autosaveEnabled) return try { await writeFile(await buildFigFile()) - } catch { - // silently fail — user can still save manually + } catch (e) { + console.warn('Autosave failed:', e) } }, AUTOSAVE_DELAY) }