chore(lint): ban broad function types
- Add structural lint rules for broad Function usage and globalThis deletion outside tests - Type design JSX component factories with an explicit callable signature - Replace remaining broad Kiwi serialization test records with unknown values
This commit is contained in:
parent
fae190a673
commit
4d3e16c0a2
|
|
@ -805,6 +805,57 @@ const noUnknownRecordDoubleCast = {
|
|||
}
|
||||
}
|
||||
|
||||
const noFunctionType = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow the broad Function type; use an explicit callable signature'
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
TSTypeReference(node) {
|
||||
if (node.typeName?.type !== 'Identifier' || node.typeName.name !== 'Function') return
|
||||
context.report({
|
||||
node,
|
||||
message: 'Use an explicit function signature instead of the broad Function type.'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const noReflectDeleteGlobalThisOutsideTests = {
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Disallow Reflect.deleteProperty(globalThis, ...) outside tests'
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
const file = normalizedFilename(context)
|
||||
if (file.includes('/tests/')) return {}
|
||||
|
||||
return {
|
||||
CallExpression(node) {
|
||||
if (node.callee?.type !== 'MemberExpression') return
|
||||
if (node.callee.object?.type !== 'Identifier' || node.callee.object.name !== 'Reflect')
|
||||
return
|
||||
if (
|
||||
node.callee.property?.type !== 'Identifier' ||
|
||||
node.callee.property.name !== 'deleteProperty'
|
||||
)
|
||||
return
|
||||
const firstArg = node.arguments?.[0]
|
||||
if (firstArg?.type !== 'Identifier' || firstArg.name !== 'globalThis') return
|
||||
context.report({
|
||||
node,
|
||||
message:
|
||||
'Do not mutate globalThis outside tests; isolate platform state behind a boundary.'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const noTsSuppressionComments = {
|
||||
meta: {
|
||||
docs: {
|
||||
|
|
@ -1342,6 +1393,8 @@ const plugin = {
|
|||
'no-broad-double-cast': noBroadDoubleCast,
|
||||
'no-unknown-record-double-cast': noUnknownRecordDoubleCast,
|
||||
'no-ts-suppression-comments': noTsSuppressionComments,
|
||||
'no-function-type': noFunctionType,
|
||||
'no-reflect-delete-global-this-outside-tests': noReflectDeleteGlobalThisOutsideTests,
|
||||
'no-core-browser-globals': noCoreBrowserGlobals,
|
||||
'no-direct-graph-emitter-subscriptions': noDirectGraphEmitterSubscriptions,
|
||||
'no-on-unmounted-in-composition-roots': noOnUnmountedInCompositionRoots,
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@
|
|||
"open-pencil/no-broad-double-cast": "error",
|
||||
"open-pencil/no-unknown-record-double-cast": "error",
|
||||
"open-pencil/no-ts-suppression-comments": "error",
|
||||
"open-pencil/no-function-type": "error",
|
||||
"open-pencil/no-reflect-delete-global-this-outside-tests": "error",
|
||||
"open-pencil/no-core-browser-globals": "error",
|
||||
"open-pencil/no-function-alias-imports": "error",
|
||||
"open-pencil/max-composition-root-lines": ["error", { "max": 260 }]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@
|
|||
"rules": {
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["packages/core/src/kiwi/kiwi-schema/**"],
|
||||
"rules": {
|
||||
"typescript/no-explicit-any": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
|
|
@ -39,6 +45,9 @@
|
|||
"open-pencil/no-broad-double-cast": "error",
|
||||
"open-pencil/no-unknown-record-double-cast": "error",
|
||||
"open-pencil/no-ts-suppression-comments": "error",
|
||||
"open-pencil/no-function-type": "error",
|
||||
"open-pencil/no-reflect-delete-global-this-outside-tests": "error",
|
||||
"typescript/no-explicit-any": "error",
|
||||
"open-pencil/no-core-browser-globals": "error",
|
||||
"open-pencil/no-direct-graph-emitter-subscriptions": "error",
|
||||
"open-pencil/no-on-unmounted-in-composition-roots": "error",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
export type ComponentType = (props: Record<string, unknown>) => ReactNode
|
||||
|
||||
export type ReactElement = {
|
||||
type: string | Function
|
||||
type: string | ComponentType
|
||||
props: Record<string, unknown> & { children?: ReactNode[] }
|
||||
}
|
||||
|
||||
|
|
@ -8,7 +10,7 @@ export type ReactNode = ReactElement | string | number | null | undefined | Reac
|
|||
export type FC<P = Record<string, unknown>> = (props: P) => ReactElement
|
||||
|
||||
export function createElement(
|
||||
type: string | Function,
|
||||
type: string | ComponentType,
|
||||
props: Record<string, unknown> | null,
|
||||
...children: ReactNode[]
|
||||
): ReactElement {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { SceneGraph } from '#core/scene-graph'
|
|||
* Build a component function from a JSX string using sucrase.
|
||||
* Works in both Node/Bun and the browser (no native bindings).
|
||||
*/
|
||||
export function buildComponent(jsxString: string): () => unknown {
|
||||
export function buildComponent(jsxString: string): React.ComponentType {
|
||||
const trimmed = jsxString.trim()
|
||||
|
||||
const aliases = `
|
||||
|
|
@ -37,7 +37,7 @@ export function buildComponent(jsxString: string): () => unknown {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line typescript-eslint/no-implied-eval -- sucrase output must be evaluated at runtime
|
||||
return new Function('React', code)(React) as () => unknown
|
||||
return new Function('React', code)(React) as React.ComponentType
|
||||
}
|
||||
|
||||
interface RenderJSXOptions {
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ const DRAW_METHODS = [
|
|||
] as const
|
||||
|
||||
type DrawMethod = (typeof DRAW_METHODS)[number]
|
||||
type DrawFunction = (...args: unknown[]) => void
|
||||
|
||||
export class DrawCallCounter {
|
||||
count = 0
|
||||
|
||||
private originals = new Map<DrawMethod, (...args: unknown[]) => void>()
|
||||
private originals = new Map<DrawMethod, DrawFunction>()
|
||||
private gl: WebGL2RenderingContext | null
|
||||
|
||||
constructor(gl: WebGL2RenderingContext | null) {
|
||||
|
|
@ -18,10 +19,10 @@ export class DrawCallCounter {
|
|||
if (!gl) return
|
||||
|
||||
for (const method of DRAW_METHODS) {
|
||||
const original = gl[method].bind(gl) as (...args: unknown[]) => void
|
||||
const original = gl[method].bind(gl) as DrawFunction
|
||||
this.originals.set(method, original)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- monkey-patching overloaded WebGL draw methods
|
||||
;(gl[method] as Function) = (...args: unknown[]) => {
|
||||
;(gl[method] as DrawFunction) = (...args: unknown[]) => {
|
||||
this.count++
|
||||
original(...args)
|
||||
}
|
||||
|
|
@ -39,7 +40,7 @@ export class DrawCallCounter {
|
|||
if (!gl) return
|
||||
|
||||
for (const [method, fn] of this.originals) {
|
||||
;(gl[method] as Function) = fn
|
||||
;(gl[method] as DrawFunction) = fn
|
||||
}
|
||||
this.originals.clear()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
{ value: 100 },
|
||||
graph,
|
||||
blobs
|
||||
) as Record<string, any>[]
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
// changes[0] = parent, changes[1] = child
|
||||
const childNc = changes.find((nc) => nc.name === 'Child')!
|
||||
|
|
@ -99,7 +99,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
{ value: 100 },
|
||||
graph,
|
||||
blobs
|
||||
) as Record<string, any>[]
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const absNc = changes.find((nc) => nc.name === 'AbsChild')!
|
||||
expect(absNc).toBeDefined()
|
||||
|
|
@ -134,7 +134,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
{ value: 100 },
|
||||
graph,
|
||||
blobs
|
||||
) as Record<string, any>[]
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const childNc = changes.find((nc) => nc.name === 'Child')!
|
||||
expect(childNc).toBeDefined()
|
||||
|
|
@ -170,7 +170,7 @@ describe('Fix 1: auto-layout child transforms', () => {
|
|||
{ value: 100 },
|
||||
graph,
|
||||
blobs
|
||||
) as Record<string, any>[]
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const itemNc = changes.find((nc) => nc.name === 'Item')!
|
||||
expect(itemNc.transform.m02).toBe(0)
|
||||
|
|
@ -193,7 +193,7 @@ describe('Fix 2: frameMaskDisabled is inverse of clipsContent', () => {
|
|||
height: 100
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].frameMaskDisabled).toBe(true)
|
||||
})
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ describe('Fix 2: frameMaskDisabled is inverse of clipsContent', () => {
|
|||
clipsContent: true
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].frameMaskDisabled).toBe(false)
|
||||
})
|
||||
|
||||
|
|
@ -223,7 +223,7 @@ describe('Fix 2: frameMaskDisabled is inverse of clipsContent', () => {
|
|||
clipsContent: false
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].frameMaskDisabled).toBe(true)
|
||||
})
|
||||
|
||||
|
|
@ -264,7 +264,7 @@ describe('Fix 3: bordersTakeSpace serialization', () => {
|
|||
strokesIncludedInLayout: true
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].bordersTakeSpace).toBe(true)
|
||||
})
|
||||
|
||||
|
|
@ -281,7 +281,7 @@ describe('Fix 3: bordersTakeSpace serialization', () => {
|
|||
strokesIncludedInLayout: false
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].bordersTakeSpace).toBe(false)
|
||||
})
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ describe('Fix 3: bordersTakeSpace serialization', () => {
|
|||
strokesIncludedInLayout: true
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].bordersTakeSpace).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
|
@ -321,7 +321,7 @@ describe('Fix 4: text lineHeight serialization', () => {
|
|||
lineHeight: 24
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].lineHeight).toEqual({ value: 24, units: 'PIXELS' })
|
||||
})
|
||||
|
||||
|
|
@ -340,7 +340,7 @@ describe('Fix 4: text lineHeight serialization', () => {
|
|||
// lineHeight not set — defaults to null
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
// ceil(16 * 1.2) = ceil(19.2) = 20
|
||||
expect(changes[0].lineHeight).toEqual({ value: 20, units: 'PIXELS' })
|
||||
})
|
||||
|
|
@ -359,7 +359,7 @@ describe('Fix 4: text lineHeight serialization', () => {
|
|||
fontSize: 14
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
// ceil(14 * 1.2) = ceil(16.8) = 17
|
||||
expect(changes[0].lineHeight).toEqual({ value: 17, units: 'PIXELS' })
|
||||
})
|
||||
|
|
@ -469,7 +469,7 @@ describe('Fix 5: font family normalization in derivedTextData', () => {
|
|||
fontSize: 14
|
||||
})
|
||||
|
||||
const changes = toKiwi(node, graph) as Record<string, any>[]
|
||||
const changes = toKiwi(node, graph) as Record<string, unknown>[]
|
||||
expect(changes[0].fontName.family).toBe('DM Sans')
|
||||
})
|
||||
})
|
||||
|
|
@ -539,7 +539,7 @@ describe('Integration: auto-layout component with all fixes', () => {
|
|||
{ value: 100 },
|
||||
graph,
|
||||
blobs
|
||||
) as Record<string, any>[]
|
||||
) as Record<string, unknown>[]
|
||||
|
||||
const cardNc = changes[0]
|
||||
const titleNc = changes.find((nc) => nc.name === 'Title')!
|
||||
|
|
|
|||
Loading…
Reference in a new issue