chore(lint): ban broad double casts project-wide

- Extend the as-unknown-as structural rule beyond app and Vue code
- Remove existing broad double casts from core and tests
- Replace the vector base64 helper and XPath facade casts with precise code paths
This commit is contained in:
Danila Poyarkov 2026-05-06 00:29:23 +03:00
parent 73861b74d9
commit aa07e833cb
19 changed files with 49 additions and 50 deletions

View file

@ -763,13 +763,12 @@ const noTestCoreSourceImports = {
const noBroadDoubleCast = {
meta: {
docs: {
description: 'Disallow broad `as unknown as` casts in app and Vue SDK code'
description: 'Disallow broad `as unknown as` casts outside vendored code'
}
},
create(context) {
const file = normalizedFilename(context)
const applies = file.includes('/src/app/') || file.includes('/packages/vue/src/')
if (!applies) return {}
if (file.includes('/packages/core/src/kiwi/kiwi-schema/')) return {}
return {
TSAsExpression(node) {

View file

@ -276,7 +276,7 @@ export function importClipboardNodes(
populateAndApplyOverrides(
graph,
guidMap as unknown as Map<string, InstanceNodeChange>,
guidMap as Map<string, InstanceNodeChange>,
created,
blobs
)

View file

@ -98,7 +98,7 @@ export function installBasicNodeProxyAccessors(
graph(this, internals).updateNode(nodeId(this, internals), { width, height })
},
resizeWithoutConstraints(this: ProxyThis, width: number, height: number): void {
;(this as unknown as { resize(width: number, height: number): void }).resize(width, height)
;(this as { resize(width: number, height: number): void }).resize(width, height)
}
})
}

View file

@ -409,7 +409,7 @@ export function importNodeChanges(
populateAndApplyOverrides(
graph,
changeMap as unknown as Map<string, InstanceNodeChange>,
changeMap as Map<string, InstanceNodeChange>,
guidToNodeId,
blobs,
activeRootIds

View file

@ -21,7 +21,7 @@ function applyOverridePaints(ov: Record<string, unknown>, updates: Partial<Scene
if (ov.textData != null) {
const td = ov.textData as { characters?: string }
if (td.characters != null) updates.text = td.characters
const runs = importStyleRuns(ov as unknown as NodeChange)
const runs = importStyleRuns(ov as NodeChange)
if (runs.length > 0) updates.styleRuns = runs
}
if (ov.fillPaints != null) updates.fills = convertFills(ov.fillPaints as Paint[])

View file

@ -177,7 +177,7 @@ function exportTextData(node: SceneNode): NodeChange['textData'] {
if (style.fills && style.fills.length > 0) {
override.fillPaints = style.fills.map(fillToKiwiPaint)
}
overrideTable.push(override as unknown as NodeChange)
overrideTable.push(override as NodeChange)
}
return {

View file

@ -54,7 +54,7 @@ export function defineTool<P extends Record<string, ParamDef>>(def: {
params: P
execute: (figma: FigmaAPI, args: ResolvedParams<P>) => unknown
}): ToolDef {
return def as unknown as ToolDef
return def as ToolDef
}
export class NodeNotFoundError extends Error {

View file

@ -11,7 +11,7 @@ function uint8ArrayToBase64(bytes: Uint8Array): string {
let binary = ''
for (let index = 0; index < bytes.length; index += CHUNK_SIZE) {
const chunk = bytes.subarray(index, index + CHUNK_SIZE)
binary += String.fromCharCode.apply(null, chunk as unknown as number[])
binary += String.fromCharCode(...chunk)
}
return btoa(binary)
}

View file

@ -40,7 +40,7 @@ const QUERYABLE_ATTRS = [
interface XPathDocument {
nodeType: number
nodeName: string
documentElement: XPathNode
documentElement: XPathNode | null
_children?: XPathNode[]
}
@ -63,7 +63,7 @@ interface XPathNode {
prefix: null
_sceneNode: SceneNode
_attrs?: XPathAttr[]
_parent?: XPathNode | null
_parent?: XPathNode | XPathDocument | null
_children?: XPathNode[]
}
@ -79,7 +79,7 @@ function wrapNode(
namespaceURI: null,
prefix: null,
_sceneNode: node,
_parent: parent as XPathNode | null
_parent: parent
}
return wrapped
}
@ -88,9 +88,9 @@ function createDocument(graph: SceneGraph, rootNode: SceneNode): XPathDocument {
const doc: XPathDocument = {
nodeType: NODE_TYPES.DOCUMENT_NODE,
nodeName: '#document',
documentElement: null as unknown as XPathNode
documentElement: null
}
const root = wrapNode(graph, rootNode, doc as unknown as XPathNode)
const root = wrapNode(graph, rootNode, doc)
doc.documentElement = root
doc._children = [root]
return doc
@ -150,7 +150,7 @@ function siblingNode(
): XPathNode | null {
if (isDocument(node)) return null
const parent = node._parent
if (!parent) return null
if (!parent || isDocument(parent)) return null
const siblings = getChildren(graph, parent)
const index = siblings.indexOf(node)
if (index === -1) return null
@ -230,7 +230,7 @@ export async function queryByXPath(
if (targetPages.length === 0) return []
const { evaluateXPathToNodes } = await import('fontoxpath')
const domFacade = createDomFacade(graph) as unknown as IDomFacade
const domFacade = createDomFacade(graph) as IDomFacade
const results: SceneNode[] = []
for (const page of targetPages) {
@ -309,7 +309,7 @@ export async function matchByXPath(
node: SceneNode
): Promise<boolean> {
const { evaluateXPathToBoolean } = await import('fontoxpath')
const domFacade = createDomFacade(graph) as unknown as IDomFacade
const domFacade = createDomFacade(graph) as IDomFacade
const wrapped = wrapNode(graph, node)
try {
return evaluateXPathToBoolean(`self::*[${selector}]`, wrapped, domFacade)

View file

@ -31,7 +31,7 @@ test('autosave triggers after scene changes with a file handle', async () => {
}
const mockHandle = {
createWritable: async () => mockWritable
} as unknown as FileSystemFileHandle
} as FileSystemFileHandle
// Inject mock file handle via saveFigFileAs path:
// We access the internal closure by calling openFigFile with a mock

View file

@ -160,7 +160,7 @@ describe('mapUpdate', () => {
const update = {
sessionUpdate: 'available_commands_update',
availableCommands: []
} as unknown as SessionUpdate
} as SessionUpdate
const result = mapUpdate(update, TEXT_ID, false)
expect(result.chunks).toEqual([])
expect(result.textStarted).toBe(false)

View file

@ -13,7 +13,7 @@ describe('openExternalLink', () => {
beforeEach(() => {
mockOpen = vi.fn().mockReturnValue(null)
globalThis.window = { open: mockOpen } as unknown as Window & typeof globalThis
globalThis.window = { open: mockOpen } as Window & typeof globalThis
})
afterEach(async () => {

View file

@ -118,7 +118,7 @@ describe('fig-import: gradient fills', () => {
],
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
}
] as unknown as NodeChange['fillPaints']
] as NodeChange['fillPaints']
})
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
@ -147,7 +147,7 @@ describe('fig-import: gradient fills', () => {
],
transform: { m00: 0.5, m01: 0, m02: 0.5, m10: 0, m11: 0.5, m12: 0.5 }
}
] as unknown as NodeChange['fillPaints']
] as NodeChange['fillPaints']
})
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
@ -176,7 +176,7 @@ describe('fig-import: image fills', () => {
imageScaleMode: 'FILL',
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
}
] as unknown as NodeChange['fillPaints']
] as NodeChange['fillPaints']
})
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
@ -209,7 +209,7 @@ describe('fig-import: effects', () => {
spread: 0,
visible: true
}
] as unknown as NodeChange['effects']
] as NodeChange['effects']
})
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
@ -233,7 +233,7 @@ describe('fig-import: effects', () => {
spread: 0,
visible: true
}
] as unknown as NodeChange['effects']
] as NodeChange['effects']
})
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
@ -284,7 +284,7 @@ describe('fig-import: stroke options', () => {
strokeWeight: 2,
strokeAlign: 'CENTER',
dashPattern: [10, 5]
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.strokes[0].dashPattern).toEqual([10, 5])
@ -301,7 +301,7 @@ describe('fig-import: text properties', () => {
fontSize: 16,
textAlignHorizontal: 'CENTER',
textAutoResize: 'WIDTH_AND_HEIGHT'
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.textAutoResize).toBe('WIDTH_AND_HEIGHT')
@ -327,7 +327,7 @@ describe('fig-import: text properties', () => {
node('TEXT', 10, 1, {
textData: { characters: 'X' },
fontName: { family: 'Inter', style }
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.fontWeight).toBe(expected)
@ -346,7 +346,7 @@ describe('fig-import: arc data', () => {
endingAngle: Math.PI,
innerRadius: 0
}
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.arcData).toBeDefined()
@ -365,7 +365,7 @@ describe('fig-import: arc data', () => {
endingAngle: Math.PI * 2,
innerRadius: 0.5
}
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.arcData!.innerRadius).toBe(0.5)
@ -380,7 +380,7 @@ describe('fig-import: constraints', () => {
node('RECTANGLE', 10, 1, {
horizontalConstraint: 'STRETCH',
verticalConstraint: 'CENTER'
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.horizontalConstraint).toBe('STRETCH')
@ -402,7 +402,7 @@ describe('fig-import: blend mode', () => {
canvas(),
node('RECTANGLE', 10, 1, {
blendMode: 'MULTIPLY'
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.blendMode).toBe('MULTIPLY')
@ -426,7 +426,7 @@ describe('fig-import: independent stroke weights', () => {
borderBottomWeight: 2,
borderLeftWeight: 4,
borderStrokeWeightsIndependent: true
} as unknown as Partial<NodeChange>)
} as Partial<NodeChange>)
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
expect(n.borderTopWeight).toBe(2)
@ -460,7 +460,7 @@ describe('fig-import: multiple fills', () => {
],
transform: { m00: 1, m01: 0, m02: 0, m10: 0, m11: 1, m12: 0 }
}
] as unknown as NodeChange['fillPaints']
] as NodeChange['fillPaints']
})
])
const n = graph.getChildren(graph.getPages()[0].id)[0]
@ -550,7 +550,7 @@ describe('fig-import: variable asset refs', () => {
resolvedDataType: 'COLOR'
}
}
] as unknown as NodeChange['fillPaints']
] as NodeChange['fillPaints']
})
])
@ -576,7 +576,7 @@ describe('fig-import: component set detection', () => {
...node('FRAME', 10, 1),
name: 'Button',
componentPropDefs: [{ id: { sessionID: 0, localID: 1 }, name: 'State', type: 'VARIANT' }]
} as unknown as NodeChange,
} as NodeChange,
{
...node('SYMBOL', 11, 1),
parentIndex: { guid: { sessionID: 1, localID: 10 }, position: '!' },

View file

@ -68,7 +68,7 @@ function createRecordingProvider() {
registerFont(data: ArrayBuffer, family: string) {
registrations.push({ family, byteLength: data.byteLength })
}
} as unknown as TypefaceFontProvider
} as TypefaceFontProvider
return { provider, registrations }
}

View file

@ -20,7 +20,7 @@ function makeNode(): SceneNode {
paddingBottom: 0,
paddingLeft: 0,
boundVariables: {}
} as unknown as SceneNode
} as SceneNode
}
function makeVarContext(vars: Record<string, number>): VarContext {
@ -45,7 +45,7 @@ function makeVarContext(vars: Record<string, number>): VarContext {
},
resolveString: () => '',
setActiveTheme: () => {}
} as unknown as VarContext
} as VarContext
}
const ctx = makeVarContext({

View file

@ -55,7 +55,7 @@ function createMockRenderer(overrides: Partial<Record<string, unknown>> = {}) {
buildParagraph: mock(() => paragraph),
_paragraph: paragraph,
...overrides
} as unknown as SkiaRenderer & { _paragraph: ReturnType<typeof createMockParagraph> }
} as SkiaRenderer & { _paragraph: ReturnType<typeof createMockParagraph> }
}
function textNode(overrides: Partial<SceneNode> = {}): SceneNode {

View file

@ -125,7 +125,7 @@ function createMockRenderer(overrides: Partial<SkiaRenderer> = {}): SkiaRenderer
isRectangularType: mock(() => true),
worldViewport: { x: 0, y: 0, w: 1000, h: 1000 },
...overrides
} as unknown as SkiaRenderer
} as SkiaRenderer
}
function createMockCanvas() {

View file

@ -46,7 +46,7 @@ function createEditorHarness() {
getVariable(id: string) {
return variables.get(id)
}
} as unknown as Editor
} as Editor
return { editor, collection, collections, variables }
}

View file

@ -132,7 +132,7 @@ describe('normalizeVectorNetwork', () => {
],
segments: [{ start: 0, end: 1 }],
regions: []
} as unknown as VectorNetwork
} as VectorNetwork
const result = normalizeVectorNetwork(network)
expect(result.segments[0].tangentStart).toEqual({ x: 0, y: 0 })
expect(result.segments[0].tangentEnd).toEqual({ x: 0, y: 0 })
@ -146,7 +146,7 @@ describe('normalizeVectorNetwork', () => {
],
segments: [{ start: 0, end: 1, tangentStart: { x: 3, y: 4 } }],
regions: []
} as unknown as VectorNetwork
} as VectorNetwork
const result = normalizeVectorNetwork(network)
expect(result.segments[0].tangentStart).toEqual({ x: 3, y: 4 })
expect(result.segments[0].tangentEnd).toEqual({ x: 0, y: 0 })
@ -165,7 +165,7 @@ describe('normalizeVectorNetwork', () => {
{ start: 2, end: 0 }
],
regions: [{ windingRule: 'NONZERO', loops: [[0, 1, 2]] }]
} as unknown as VectorNetwork
} as VectorNetwork
const normalized = normalizeVectorNetwork(raw)
const blob = encodeVectorNetworkBlob(normalized)
@ -202,7 +202,7 @@ describe('validateVectorNetwork', () => {
],
segments: [{ start: 0, end: 1 }],
regions: []
} as unknown as VectorNetwork
} as VectorNetwork
expect(validateVectorNetwork(network)).toEqual([])
})
@ -211,14 +211,14 @@ describe('validateVectorNetwork', () => {
vertices: [{ x: 0, y: 0 }],
segments: [{ start: 0, end: 5 }],
regions: []
} as unknown as VectorNetwork
} as VectorNetwork
const errors = validateVectorNetwork(network)
expect(errors.length).toBe(1)
expect(errors[0]).toContain('end index 5 out of range')
})
test('rejects missing vertices array', () => {
const network = { segments: [], regions: [] } as unknown as VectorNetwork
const network = { segments: [], regions: [] } as VectorNetwork
const errors = validateVectorNetwork(network)
expect(errors[0]).toContain('vertices must be an array')
})
@ -228,7 +228,7 @@ describe('validateVectorNetwork', () => {
vertices: [{ x: 'a', y: 0 }],
segments: [],
regions: []
} as unknown as VectorNetwork
} as VectorNetwork
const errors = validateVectorNetwork(network)
expect(errors[0]).toContain('x and y must be numbers')
})