Add no-silent-catch lint rule, fix silent failures
- 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
This commit is contained in:
parent
e8f8a0b8e2
commit
353c867d03
|
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,8 +162,8 @@ export async function loadFont(family: string, style = 'Regular'): Promise<Array
|
|||
return buffer
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* fall through to Google Fonts */
|
||||
} catch (e) {
|
||||
console.warn(`Local font access failed for "${family}" ${style}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,8 +176,8 @@ export async function loadFont(family: string, style = 'Regular'): Promise<Array
|
|||
registerFontInBrowser(family, style, buffer)
|
||||
return buffer
|
||||
}
|
||||
} catch {
|
||||
/* fall through to bundled */
|
||||
} catch (e) {
|
||||
console.warn(`Google Fonts fetch failed for "${family}" ${style}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,8 +193,8 @@ export async function loadFont(family: string, style = 'Regular'): Promise<Array
|
|||
registerFontInBrowser(family, style, buffer)
|
||||
return buffer
|
||||
}
|
||||
} catch {
|
||||
/* no bundled font available */
|
||||
} catch (e) {
|
||||
console.warn(`Bundled font fetch failed for "${family}" ${style}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,11 +104,19 @@ function parseFigFileSync(buffer: ArrayBuffer): SceneGraph {
|
|||
return importNodeChanges(nodeChanges, blobs, images)
|
||||
}
|
||||
|
||||
const WORKER_TIMEOUT_MS = 30_000
|
||||
|
||||
function parseViaWorker(buffer: ArrayBuffer): Promise<SceneGraph> {
|
||||
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<FigParseResult & { error?: string }>) => {
|
||||
clearTimeout(timeout)
|
||||
worker.terminate()
|
||||
if (e.data.error) {
|
||||
reject(new Error(e.data.error))
|
||||
|
|
@ -120,8 +128,9 @@ function parseViaWorker(buffer: ArrayBuffer): Promise<SceneGraph> {
|
|||
}
|
||||
|
||||
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<SceneGraph> {
|
|||
|
||||
export async function parseFigFile(buffer: ArrayBuffer): Promise<SceneGraph> {
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,8 +172,8 @@ export class SkiaRenderer {
|
|||
auxStroke: Paint
|
||||
opacityPaint: Paint
|
||||
effectLayerPaint: Paint
|
||||
imageFilterCache = new Map<string, ImageFilter>()
|
||||
maskFilterCache = new Map<number, MaskFilter>()
|
||||
imageFilterCache = new Map<string, ImageFilter | null>()
|
||||
maskFilterCache = new Map<number, MaskFilter | null>()
|
||||
_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<string, SkPicture>()
|
||||
nodePictureCache = new Map<string, SkPicture | null>()
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -77,8 +77,8 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
req.resolve(payload)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed messages
|
||||
} catch (e) {
|
||||
console.warn('Malformed automation message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -173,8 +173,8 @@ export function connectAutomation(getStore: () => EditorStore) {
|
|||
})
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse WebSocket message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -294,8 +294,8 @@ export function useTextEdit(canvasRef: Ref<HTMLCanvasElement | null>, store: Edi
|
|||
insertText(text, node)
|
||||
resetBlink()
|
||||
}
|
||||
} catch {
|
||||
// Clipboard access denied
|
||||
} catch (e) {
|
||||
console.warn('Clipboard access denied:', e)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue