From aa07e833cb3f0d9f3240af167481efe03136f04c Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Wed, 6 May 2026 00:29:23 +0300 Subject: [PATCH] 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 --- lint/plugin.js | 5 ++- packages/core/src/clipboard.ts | 2 +- .../core/src/figma-api/accessors/basic.ts | 2 +- packages/core/src/kiwi/fig/import.ts | 2 +- .../kiwi/instance-overrides/symbol/props.ts | 2 +- .../core/src/kiwi/node-change/serialize.ts | 2 +- packages/core/src/tools/schema.ts | 2 +- packages/core/src/tools/vector/export.ts | 2 +- packages/core/src/xpath.ts | 16 +++++----- tests/e2e/autosave.spec.ts | 2 +- tests/engine/acp-transport.test.ts | 2 +- tests/engine/external-link.test.ts | 2 +- tests/engine/fig-import.test.ts | 32 +++++++++---------- tests/engine/fonts.test.ts | 2 +- tests/engine/pen-var-padding.test.ts | 4 +-- tests/engine/render-text.test.ts | 2 +- tests/engine/renderer-effects.test.ts | 2 +- tests/engine/variables-editor.test.ts | 2 +- tests/engine/vector.test.ts | 14 ++++---- 19 files changed, 49 insertions(+), 50 deletions(-) diff --git a/lint/plugin.js b/lint/plugin.js index 6e5c13959..e32f7a737 100644 --- a/lint/plugin.js +++ b/lint/plugin.js @@ -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) { diff --git a/packages/core/src/clipboard.ts b/packages/core/src/clipboard.ts index e45b66ec2..ff723fb66 100644 --- a/packages/core/src/clipboard.ts +++ b/packages/core/src/clipboard.ts @@ -276,7 +276,7 @@ export function importClipboardNodes( populateAndApplyOverrides( graph, - guidMap as unknown as Map, + guidMap as Map, created, blobs ) diff --git a/packages/core/src/figma-api/accessors/basic.ts b/packages/core/src/figma-api/accessors/basic.ts index 5fd8048a5..f80c01ed4 100644 --- a/packages/core/src/figma-api/accessors/basic.ts +++ b/packages/core/src/figma-api/accessors/basic.ts @@ -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) } }) } diff --git a/packages/core/src/kiwi/fig/import.ts b/packages/core/src/kiwi/fig/import.ts index d2be292b3..2a97d3a14 100644 --- a/packages/core/src/kiwi/fig/import.ts +++ b/packages/core/src/kiwi/fig/import.ts @@ -409,7 +409,7 @@ export function importNodeChanges( populateAndApplyOverrides( graph, - changeMap as unknown as Map, + changeMap as Map, guidToNodeId, blobs, activeRootIds diff --git a/packages/core/src/kiwi/instance-overrides/symbol/props.ts b/packages/core/src/kiwi/instance-overrides/symbol/props.ts index 752a0f407..a17f5d303 100644 --- a/packages/core/src/kiwi/instance-overrides/symbol/props.ts +++ b/packages/core/src/kiwi/instance-overrides/symbol/props.ts @@ -21,7 +21,7 @@ function applyOverridePaints(ov: Record, updates: Partial 0) updates.styleRuns = runs } if (ov.fillPaints != null) updates.fills = convertFills(ov.fillPaints as Paint[]) diff --git a/packages/core/src/kiwi/node-change/serialize.ts b/packages/core/src/kiwi/node-change/serialize.ts index 103c346d7..cfbadee05 100644 --- a/packages/core/src/kiwi/node-change/serialize.ts +++ b/packages/core/src/kiwi/node-change/serialize.ts @@ -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 { diff --git a/packages/core/src/tools/schema.ts b/packages/core/src/tools/schema.ts index 700f0e246..5bf6990d3 100644 --- a/packages/core/src/tools/schema.ts +++ b/packages/core/src/tools/schema.ts @@ -54,7 +54,7 @@ export function defineTool

>(def: { params: P execute: (figma: FigmaAPI, args: ResolvedParams

) => unknown }): ToolDef { - return def as unknown as ToolDef + return def as ToolDef } export class NodeNotFoundError extends Error { diff --git a/packages/core/src/tools/vector/export.ts b/packages/core/src/tools/vector/export.ts index 54f52b8f5..3e702de6e 100644 --- a/packages/core/src/tools/vector/export.ts +++ b/packages/core/src/tools/vector/export.ts @@ -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) } diff --git a/packages/core/src/xpath.ts b/packages/core/src/xpath.ts index 8db666bb3..1f250a606 100644 --- a/packages/core/src/xpath.ts +++ b/packages/core/src/xpath.ts @@ -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 { 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) diff --git a/tests/e2e/autosave.spec.ts b/tests/e2e/autosave.spec.ts index c0e40e651..b42844e02 100644 --- a/tests/e2e/autosave.spec.ts +++ b/tests/e2e/autosave.spec.ts @@ -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 diff --git a/tests/engine/acp-transport.test.ts b/tests/engine/acp-transport.test.ts index 13d91b34f..ba7de2987 100644 --- a/tests/engine/acp-transport.test.ts +++ b/tests/engine/acp-transport.test.ts @@ -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) diff --git a/tests/engine/external-link.test.ts b/tests/engine/external-link.test.ts index 1664dae9d..e9f04993c 100644 --- a/tests/engine/external-link.test.ts +++ b/tests/engine/external-link.test.ts @@ -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 () => { diff --git a/tests/engine/fig-import.test.ts b/tests/engine/fig-import.test.ts index 1ec1a2fa0..4a5e2e3df 100644 --- a/tests/engine/fig-import.test.ts +++ b/tests/engine/fig-import.test.ts @@ -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) + } as Partial) ]) 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) + } as Partial) ]) 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) + } as Partial) ]) 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) + } as Partial) ]) 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) + } as Partial) ]) 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) + } as Partial) ]) 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) + } as Partial) ]) 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) + } as Partial) ]) 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: '!' }, diff --git a/tests/engine/fonts.test.ts b/tests/engine/fonts.test.ts index ed50ff54e..0af267478 100644 --- a/tests/engine/fonts.test.ts +++ b/tests/engine/fonts.test.ts @@ -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 } } diff --git a/tests/engine/pen-var-padding.test.ts b/tests/engine/pen-var-padding.test.ts index d94a2c0f0..185760c1c 100644 --- a/tests/engine/pen-var-padding.test.ts +++ b/tests/engine/pen-var-padding.test.ts @@ -20,7 +20,7 @@ function makeNode(): SceneNode { paddingBottom: 0, paddingLeft: 0, boundVariables: {} - } as unknown as SceneNode + } as SceneNode } function makeVarContext(vars: Record): VarContext { @@ -45,7 +45,7 @@ function makeVarContext(vars: Record): VarContext { }, resolveString: () => '', setActiveTheme: () => {} - } as unknown as VarContext + } as VarContext } const ctx = makeVarContext({ diff --git a/tests/engine/render-text.test.ts b/tests/engine/render-text.test.ts index d44cae556..c20fdf230 100644 --- a/tests/engine/render-text.test.ts +++ b/tests/engine/render-text.test.ts @@ -55,7 +55,7 @@ function createMockRenderer(overrides: Partial> = {}) { buildParagraph: mock(() => paragraph), _paragraph: paragraph, ...overrides - } as unknown as SkiaRenderer & { _paragraph: ReturnType } + } as SkiaRenderer & { _paragraph: ReturnType } } function textNode(overrides: Partial = {}): SceneNode { diff --git a/tests/engine/renderer-effects.test.ts b/tests/engine/renderer-effects.test.ts index 3a3e16c16..2402b8fd3 100644 --- a/tests/engine/renderer-effects.test.ts +++ b/tests/engine/renderer-effects.test.ts @@ -125,7 +125,7 @@ function createMockRenderer(overrides: Partial = {}): SkiaRenderer isRectangularType: mock(() => true), worldViewport: { x: 0, y: 0, w: 1000, h: 1000 }, ...overrides - } as unknown as SkiaRenderer + } as SkiaRenderer } function createMockCanvas() { diff --git a/tests/engine/variables-editor.test.ts b/tests/engine/variables-editor.test.ts index 85248ebcb..c95603323 100644 --- a/tests/engine/variables-editor.test.ts +++ b/tests/engine/variables-editor.test.ts @@ -46,7 +46,7 @@ function createEditorHarness() { getVariable(id: string) { return variables.get(id) } - } as unknown as Editor + } as Editor return { editor, collection, collections, variables } } diff --git a/tests/engine/vector.test.ts b/tests/engine/vector.test.ts index 8bf121ad4..59bd23204 100644 --- a/tests/engine/vector.test.ts +++ b/tests/engine/vector.test.ts @@ -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') })