Add strict lint rules to catch AI-generated slop at compile time
New rules: - typescript/no-unnecessary-condition: catches redundant ?., ??, and always-true/false branches (22 violations fixed) - typescript/consistent-return: mixed return/no-return in functions - typescript/no-unnecessary-type-parameters: unused generic params - typescript/prefer-for-of: use for-of instead of indexed loops - unicorn/no-nested-ternary: forbid nested ternaries - unicorn/consistent-existence-index-check: !== -1 over >= 0 - no-empty-function: no empty function bodies Fixed all 32 violations: removed unnecessary optional chaining on non-nullish values, tightened Record types to Partial<Record> where index access can return undefined, replaced nested ternary with lookup table, added exhaustive default cases.
This commit is contained in:
parent
f96fffb83f
commit
17faf4df93
|
|
@ -19,13 +19,19 @@
|
|||
}]
|
||||
}],
|
||||
|
||||
"no-empty-function": "error",
|
||||
|
||||
"typescript/no-explicit-any": "warn",
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
"typescript/no-floating-promises": "error",
|
||||
"typescript/no-misused-promises": "error",
|
||||
"typescript/consistent-type-imports": "error",
|
||||
"typescript/consistent-return": "error",
|
||||
"typescript/no-unnecessary-condition": "error",
|
||||
"typescript/no-unnecessary-type-assertion": "error",
|
||||
"typescript/no-unnecessary-type-parameters": "error",
|
||||
"typescript/prefer-optional-chain": "error",
|
||||
"typescript/prefer-for-of": "error",
|
||||
|
||||
"import/no-cycle": "error",
|
||||
"import/no-self-import": "error",
|
||||
|
|
@ -34,6 +40,8 @@
|
|||
|
||||
"unicorn/no-instanceof-array": "error",
|
||||
"unicorn/no-typeof-undefined": "error",
|
||||
"unicorn/no-nested-ternary": "error",
|
||||
"unicorn/consistent-existence-index-check": "error",
|
||||
|
||||
"vue/no-arrow-functions-in-watch": "error",
|
||||
"vue/no-deprecated-destroyed-lifecycle": "error",
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ export function startAutomationBridge(server: ViteServer) {
|
|||
if (provided !== authToken) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
await next()
|
||||
return next()
|
||||
})
|
||||
|
||||
app.post('/rpc', async (c) => {
|
||||
|
|
|
|||
|
|
@ -39,10 +39,10 @@ export function connectAutomation(getStore: () => EditorStore) {
|
|||
const store = getStore()
|
||||
|
||||
if (command === 'eval') {
|
||||
const code = (args as { code?: string })?.code
|
||||
const code = (args as { code?: string }).code
|
||||
if (!code) throw new Error('Missing "code" in args')
|
||||
const figma = makeFigma()
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () { /* noop */ }).constructor
|
||||
const wrappedCode = code.trim().startsWith('return')
|
||||
? code
|
||||
: `return (async () => { ${code} })()`
|
||||
|
|
@ -53,14 +53,14 @@ export function connectAutomation(getStore: () => EditorStore) {
|
|||
}
|
||||
|
||||
if (command === 'tool') {
|
||||
const toolName = (args as { name?: string })?.name
|
||||
const toolArgs = (args as { args?: Record<string, unknown> })?.args ?? {}
|
||||
const toolName = (args as { name?: string }).name
|
||||
const toolArgs = (args as { args?: Record<string, unknown> }).args ?? {}
|
||||
if (!toolName) throw new Error('Missing "name" in args')
|
||||
|
||||
if (toolName === 'render' && toolArgs.tree) {
|
||||
const tree = toolArgs.tree as Parameters<typeof renderTreeNode>[1]
|
||||
const result = renderTreeNode(store.graph, tree, {
|
||||
parentId: (toolArgs.parent_id as string) ?? store.state.currentPageId,
|
||||
parentId: (toolArgs.parent_id as string | undefined) ?? store.state.currentPageId,
|
||||
x: toolArgs.x as number | undefined,
|
||||
y: toolArgs.y as number | undefined
|
||||
})
|
||||
|
|
@ -96,7 +96,7 @@ export function connectAutomation(getStore: () => EditorStore) {
|
|||
)
|
||||
if (!data) throw new Error('Export failed')
|
||||
let binary = ''
|
||||
for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i])
|
||||
for (const byte of data) binary += String.fromCharCode(byte)
|
||||
const base64 = btoa(binary)
|
||||
return {
|
||||
ok: true,
|
||||
|
|
@ -113,7 +113,7 @@ export function connectAutomation(getStore: () => EditorStore) {
|
|||
nodeIds.length === 1
|
||||
? sceneNodeToJSX(nodeIds[0], store.graph, style)
|
||||
: selectionToJSX(nodeIds, store.graph, style)
|
||||
return { ok: true, result: { jsx: jsx ?? '' } }
|
||||
return { ok: true, result: { jsx } }
|
||||
}
|
||||
|
||||
const result = executeRpcCommand(store.graph, command, args ?? {})
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function handleSubmit(text: string) {
|
|||
const c = ensureChat()
|
||||
if (c) chat.value = markRaw(c)
|
||||
}
|
||||
chat.value?.sendMessage({ text }).catch(() => {})
|
||||
chat.value?.sendMessage({ text }).catch(() => { /* user-facing error handled by UI */ })
|
||||
}
|
||||
|
||||
function handleStop() {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ function onLayerRightClick(e: MouseEvent) {
|
|||
|
||||
function toggleExpand(id: string) {
|
||||
const idx = expanded.value.indexOf(id)
|
||||
if (idx >= 0) {
|
||||
if (idx !== -1) {
|
||||
expanded.value = expanded.value.filter((e) => e !== id)
|
||||
} else {
|
||||
expanded.value = [...expanded.value, id]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, h } from 'vue'
|
||||
import { ref, computed, watch, nextTick, h, type Component } from 'vue'
|
||||
import {
|
||||
DialogRoot,
|
||||
DialogPortal,
|
||||
|
|
@ -273,14 +273,13 @@ const columns = computed<ColumnDef<Variable>[]>(() => {
|
|||
cell: ({ row }) => {
|
||||
const v = row.original
|
||||
const iconClass = 'size-3.5 shrink-0 text-muted'
|
||||
const iconComponent =
|
||||
v.type === 'COLOR'
|
||||
? IconPalette
|
||||
: v.type === 'FLOAT'
|
||||
? IconHash
|
||||
: v.type === 'STRING'
|
||||
? IconType
|
||||
: IconToggleLeft
|
||||
const VARIABLE_TYPE_ICONS: Record<string, Component> = {
|
||||
COLOR: IconPalette,
|
||||
FLOAT: IconHash,
|
||||
STRING: IconType,
|
||||
BOOLEAN: IconToggleLeft
|
||||
}
|
||||
const iconComponent = VARIABLE_TYPE_ICONS[v.type] ?? IconToggleLeft
|
||||
const icon = h(iconComponent, { class: iconClass })
|
||||
|
||||
return h('div', { class: 'flex items-center gap-2' }, [
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const hasCornerRadius = computed(() => {
|
|||
|
||||
const visibilityState = computed(() => {
|
||||
const v = merged('visible')
|
||||
return v === MIXED ? 'mixed' : v ? 'visible' : 'hidden'
|
||||
return v === MIXED ? 'mixed' : (v ? 'visible' : 'hidden')
|
||||
})
|
||||
|
||||
function toggleVisibility() {
|
||||
|
|
|
|||
|
|
@ -494,7 +494,7 @@ export function useCanvasInput(
|
|||
|
||||
// Check proximity to first vertex for closing
|
||||
const first = store.state.penState.vertices[0]
|
||||
if (store.state.penState.vertices.length > 2 && first) {
|
||||
if (store.state.penState.vertices.length > 2) {
|
||||
const dist = Math.hypot(cx - first.x, cy - first.y)
|
||||
store.penSetClosingToFirst(dist < PEN_CLOSE_THRESHOLD)
|
||||
}
|
||||
|
|
@ -746,7 +746,6 @@ export function useCanvasInput(
|
|||
return
|
||||
}
|
||||
|
||||
if (d.type === 'marquee') {
|
||||
const minX = Math.min(d.startX, cx)
|
||||
const minY = Math.min(d.startY, cy)
|
||||
const maxX = Math.max(d.startX, cx)
|
||||
|
|
@ -766,7 +765,6 @@ export function useCanvasInput(
|
|||
store.select(hits)
|
||||
store.setMarquee({ x: minX, y: minY, width: maxX - minX, height: maxY - minY })
|
||||
}
|
||||
}
|
||||
|
||||
function applyResize(d: DragResize, cx: number, cy: number, constrain: boolean) {
|
||||
const { handle, origRect } = d
|
||||
|
|
@ -1067,9 +1065,9 @@ export function useCanvasInput(
|
|||
const allChildren = store.graph.getChildren(parent.id)
|
||||
let realIndex = 0
|
||||
let filteredCount = 0
|
||||
for (let i = 0; i < allChildren.length; i++) {
|
||||
if (store.state.selectedIds.has(allChildren[i].id)) continue
|
||||
if (allChildren[i].layoutPositioning === 'ABSOLUTE') {
|
||||
for (const child of allChildren) {
|
||||
if (store.state.selectedIds.has(child.id)) continue
|
||||
if (child.layoutPositioning === 'ABSOLUTE') {
|
||||
realIndex++
|
||||
continue
|
||||
}
|
||||
|
|
@ -1272,8 +1270,8 @@ export function useCanvasInput(
|
|||
const rect = canvas.getBoundingClientRect()
|
||||
pendingGesture = {
|
||||
scale: ge.scale,
|
||||
sx: (ge.clientX ?? rect.width / 2) - rect.left,
|
||||
sy: (ge.clientY ?? rect.height / 2) - rect.top
|
||||
sx: ge.clientX - rect.left,
|
||||
sy: ge.clientY - rect.top
|
||||
}
|
||||
if (!gestureRafId) {
|
||||
gestureRafId = requestAnimationFrame(flushGesture)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ async function initWebGPU(ck: CanvasKit): Promise<WebGPUContext | null> {
|
|||
const adapter = await navigator.gpu.requestAdapter()
|
||||
if (!adapter) return null
|
||||
const device = await adapter.requestDevice()
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- WebGPU CanvasKit API may not exist at runtime
|
||||
const deviceContext = asWebGPU(ck).MakeGPUDeviceContext?.(device)
|
||||
if (!deviceContext) return null
|
||||
return { device, deviceContext }
|
||||
|
|
@ -51,6 +52,7 @@ export function useCanvas(canvasRef: Ref<HTMLCanvasElement | null>, store: Edito
|
|||
if (!canvas || destroyed) return
|
||||
|
||||
ck = await getCanvasKit()
|
||||
// oxlint-disable-next-line typescript/no-unnecessary-condition -- async race: destroyed may change during await
|
||||
if (destroyed) return
|
||||
|
||||
if (getGpuBackend() === 'webgpu') {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,10 @@ function createModel(): LanguageModel {
|
|||
})
|
||||
return custom(effectiveModelID)
|
||||
}
|
||||
default: {
|
||||
const _exhaustive: never = providerID.value
|
||||
throw new Error(`Unknown provider: ${String(_exhaustive)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export async function openFileDialog() {
|
|||
|
||||
const store = useEditorStore()
|
||||
|
||||
const MENU_ACTIONS: Record<string, () => void> = {
|
||||
const MENU_ACTIONS: Partial<Record<string, () => void>> = {
|
||||
new: () => createTab(),
|
||||
open: () => void openFileDialog(),
|
||||
close: () => {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function useMultiProps() {
|
|||
const nodes = computed(() => store.selectedNodes.value)
|
||||
const isMulti = computed(() => nodes.value.length > 1)
|
||||
const active = computed(() => node.value || isMulti.value)
|
||||
const activeNode = computed(() => node.value ?? nodes.value[0] ?? null)
|
||||
const activeNode = computed(() => node.value ?? (nodes.value[0] as SceneNode | undefined) ?? null)
|
||||
|
||||
function merged<K extends keyof SceneNode>(key: K): MixedValue<SceneNode[K]> {
|
||||
const all = nodes.value
|
||||
|
|
@ -36,7 +36,7 @@ export function useMultiProps() {
|
|||
store.requestRender()
|
||||
}
|
||||
|
||||
function isArrayMixed<K extends keyof SceneNode>(key: K): boolean {
|
||||
function isArrayMixed(key: keyof SceneNode): boolean {
|
||||
const all = nodes.value
|
||||
if (all.length <= 1) return false
|
||||
const first = JSON.stringify(all[0][key])
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export const TOOLS: ToolDef[] = [
|
|||
{ key: 'HAND', label: 'Hand', shortcut: 'H' }
|
||||
]
|
||||
|
||||
export const TOOL_SHORTCUTS: Record<string, Tool> = {
|
||||
export const TOOL_SHORTCUTS: Partial<Record<string, Tool>> = {
|
||||
v: 'SELECT',
|
||||
f: 'FRAME',
|
||||
s: 'SECTION',
|
||||
|
|
@ -612,13 +612,14 @@ export function createEditorStore() {
|
|||
state.documentName = file.name.replace(/\.fig$/i, '')
|
||||
downloadName = file.name
|
||||
state.selectedIds = new Set()
|
||||
const firstPage = graph.getPages()[0]
|
||||
state.currentPageId = firstPage?.id ?? graph.rootId
|
||||
const firstPage = graph.getPages()[0] as SceneNode | undefined
|
||||
const pageId = firstPage?.id ?? graph.rootId
|
||||
state.currentPageId = pageId
|
||||
state.panX = 0
|
||||
state.panY = 0
|
||||
state.zoom = 1
|
||||
state.pageColor = { ...CANVAS_BG_COLOR }
|
||||
await loadFontsForNodes(graph.getChildren(firstPage?.id ?? graph.rootId).map((n) => n.id))
|
||||
await loadFontsForNodes(graph.getChildren(pageId).map((n) => n.id))
|
||||
requestRender()
|
||||
void startWatchingFile()
|
||||
} catch (e) {
|
||||
|
|
@ -1091,7 +1092,7 @@ export function createEditorStore() {
|
|||
const parentAbs = isTopLevel(parentId) ? { x: 0, y: 0 } : graph.getAbsolutePosition(parentId)
|
||||
|
||||
const direction: LayoutMode =
|
||||
nodes.length <= 1 ? 'VERTICAL' : maxX - minX >= maxY - minY ? 'HORIZONTAL' : 'VERTICAL'
|
||||
nodes.length <= 1 ? 'VERTICAL' : (maxX - minX >= maxY - minY ? 'HORIZONTAL' : 'VERTICAL')
|
||||
|
||||
const frame = graph.createNode('FRAME', parentId, {
|
||||
name: 'Frame',
|
||||
|
|
@ -2085,8 +2086,8 @@ export function createEditorStore() {
|
|||
const w = maxX - minX + padding * 2
|
||||
const h = maxY - minY + padding * 2
|
||||
|
||||
const viewW = window.innerWidth ?? 800
|
||||
const viewH = window.innerHeight ?? 600
|
||||
const viewW = window.innerWidth
|
||||
const viewH = window.innerHeight
|
||||
const zoom = Math.min(viewW / w, viewH / h, 1)
|
||||
|
||||
state.zoom = zoom
|
||||
|
|
@ -2114,8 +2115,8 @@ export function createEditorStore() {
|
|||
}
|
||||
|
||||
function zoomTo100() {
|
||||
const viewW = window.innerWidth ?? 800
|
||||
const viewH = window.innerHeight ?? 600
|
||||
const viewW = window.innerWidth
|
||||
const viewH = window.innerHeight
|
||||
const centerX = (-state.panX + viewW / 2) / state.zoom
|
||||
const centerY = (-state.panY + viewH / 2) / state.zoom
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function switchTab(tabId: string) {
|
|||
|
||||
export function closeTab(tabId: string) {
|
||||
const idx = tabsRef.value.findIndex((t) => t.id === tabId)
|
||||
if (idx < 0) return
|
||||
if (idx === -1) return
|
||||
|
||||
const wasActive = activeTabId.value === tabId
|
||||
tabsRef.value = tabsRef.value.filter((t) => t.id !== tabId)
|
||||
|
|
|
|||
Loading…
Reference in a new issue