Fix to use more accurate types with custom isArray() and add lint (#5261)

* Fix to use more accurate types with custom isArray()

* Add lint against Array.isArray()
This commit is contained in:
Jonathan Tran
2025-02-05 09:01:45 -05:00
committed by GitHub
parent 336f4f27ba
commit f7ee248a26
14 changed files with 70 additions and 38 deletions

View File

@ -29,6 +29,13 @@
{ {
"name": "isNaN", "name": "isNaN",
"message": "Use Number.isNaN() instead." "message": "Use Number.isNaN() instead."
},
],
"no-restricted-syntax": [
"error",
{
"selector": "CallExpression[callee.object.name='Array'][callee.property.name='isArray']",
"message": "Use isArray() in lib/utils.ts instead of Array.isArray()."
} }
], ],
"semi": [ "semi": [

View File

@ -0,0 +1,7 @@
/**
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
*/
export function isArray(val: any): val is unknown[] {
// eslint-disable-next-line no-restricted-syntax
return Array.isArray(val)
}

View File

@ -2,6 +2,7 @@ import { Text } from '@codemirror/state'
import { Marked } from '@ts-stack/markdown' import { Marked } from '@ts-stack/markdown'
import type * as LSP from 'vscode-languageserver-protocol' import type * as LSP from 'vscode-languageserver-protocol'
import { isArray } from '../lib/utils'
// takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset // takes a function and executes it after the wait time, if the function is called again before the wait time is up, the timer is reset
export function deferExecution<T>(func: (args: T) => any, wait: number) { export function deferExecution<T>(func: (args: T) => any, wait: number) {
@ -45,7 +46,7 @@ export function offsetToPos(doc: Text, offset: number) {
export function formatMarkdownContents( export function formatMarkdownContents(
contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[] contents: LSP.MarkupContent | LSP.MarkedString | LSP.MarkedString[]
): string { ): string {
if (Array.isArray(contents)) { if (isArray(contents)) {
return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('') return contents.map((c) => formatMarkdownContents(c) + '\n\n').join('')
} else if (typeof contents === 'string') { } else if (typeof contents === 'string') {
return Marked.parse(contents) return Marked.parse(contents)

View File

@ -22,6 +22,7 @@ import {
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { openExternalBrowserIfDesktop } from 'lib/openWindow' import { openExternalBrowserIfDesktop } from 'lib/openWindow'
import { commandBarActor } from 'machines/commandBarMachine' import { commandBarActor } from 'machines/commandBarMachine'
import { isArray } from 'lib/utils'
export function Toolbar({ export function Toolbar({
className = '', className = '',
@ -121,7 +122,7 @@ export function Toolbar({
return toolbarConfig[currentMode].items.map((maybeIconConfig) => { return toolbarConfig[currentMode].items.map((maybeIconConfig) => {
if (maybeIconConfig === 'break') { if (maybeIconConfig === 'break') {
return 'break' return 'break'
} else if (Array.isArray(maybeIconConfig)) { } else if (isArray(maybeIconConfig)) {
return maybeIconConfig.map(resolveItemConfig) return maybeIconConfig.map(resolveItemConfig)
} else { } else {
return resolveItemConfig(maybeIconConfig) return resolveItemConfig(maybeIconConfig)
@ -180,7 +181,7 @@ export function Toolbar({
className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80" className="h-5 w-[1px] block bg-chalkboard-30 dark:bg-chalkboard-80"
/> />
) )
} else if (Array.isArray(maybeIconConfig)) { } else if (isArray(maybeIconConfig)) {
// A button with a dropdown // A button with a dropdown
return ( return (
<ActionButtonDropdown <ActionButtonDropdown

View File

@ -7,6 +7,7 @@ import { trap } from 'lib/trap'
import { codeToIdSelections } from 'lib/selections' import { codeToIdSelections } from 'lib/selections'
import { codeRefFromRange } from 'lang/std/artifactGraph' import { codeRefFromRange } from 'lang/std/artifactGraph'
import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm' import { defaultSourceRange, SourceRange, topLevelRange } from 'lang/wasm'
import { isArray } from 'lib/utils'
export function AstExplorer() { export function AstExplorer() {
const { context } = useModelingContext() const { context } = useModelingContext()
@ -166,12 +167,12 @@ function DisplayObj({
{Object.entries(obj).map(([key, value]) => { {Object.entries(obj).map(([key, value]) => {
if (filterKeys.includes(key)) { if (filterKeys.includes(key)) {
return null return null
} else if (Array.isArray(value)) { } else if (isArray(value)) {
return ( return (
<li key={key}> <li key={key}>
{`${key}: [`} {`${key}: [`}
<DisplayBody <DisplayBody
body={value} body={value as any}
filterKeys={filterKeys} filterKeys={filterKeys}
node={node} node={node}
/> />

View File

@ -14,6 +14,7 @@ import {
} from '@codemirror/state' } from '@codemirror/state'
import { EditorView } from '@codemirror/view' import { EditorView } from '@codemirror/view'
import { oneDark } from '@codemirror/theme-one-dark' import { oneDark } from '@codemirror/theme-one-dark'
import { isArray } from 'lib/utils'
//reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts //reference: https://github.com/sachinraja/rodemirror/blob/main/src/use-first-render.ts
const useFirstRender = () => { const useFirstRender = () => {
@ -86,6 +87,18 @@ const CodeEditor = forwardRef<CodeEditorRef, CodeEditorProps>((props, ref) => {
return <div ref={editor}></div> return <div ref={editor}></div>
}) })
/**
* The extensions type is quite weird. We need a special helper to preserve the
* readonly array type.
*
* @see https://github.com/microsoft/TypeScript/issues/17002
*/
function isExtensionArray(
extensions: Extension
): extensions is readonly Extension[] {
return isArray(extensions)
}
export function useCodeMirror(props: UseCodeMirror) { export function useCodeMirror(props: UseCodeMirror) {
const { const {
onCreateEditor, onCreateEditor,
@ -103,7 +116,7 @@ export function useCodeMirror(props: UseCodeMirror) {
const isFirstRender = useFirstRender() const isFirstRender = useFirstRender()
const targetExtensions = useMemo(() => { const targetExtensions = useMemo(() => {
let exts = Array.isArray(extensions) ? extensions : [] let exts = isExtensionArray(extensions) ? extensions : []
if (theme === 'dark') { if (theme === 'dark') {
exts = [...exts, oneDark] exts = [...exts, oneDark]
} else if (theme === 'light') { } else if (theme === 'light') {

View File

@ -9,6 +9,7 @@ import {
import { Range, Extension, Text } from '@codemirror/state' import { Range, Extension, Text } from '@codemirror/state'
import { NodeProp, Tree } from '@lezer/common' import { NodeProp, Tree } from '@lezer/common'
import { language, syntaxTree } from '@codemirror/language' import { language, syntaxTree } from '@codemirror/language'
import { isArray } from 'lib/utils'
interface PickerState { interface PickerState {
from: number from: number
@ -79,7 +80,7 @@ function discoverColorsInKCL(
) )
if (maybeWidgetOptions) { if (maybeWidgetOptions) {
if (Array.isArray(maybeWidgetOptions)) { if (isArray(maybeWidgetOptions)) {
console.error('Unexpected nested overlays') console.error('Unexpected nested overlays')
ret.push(...maybeWidgetOptions) ret.push(...maybeWidgetOptions)
} else { } else {
@ -150,7 +151,7 @@ function colorPickersDecorations(
return return
} }
if (!Array.isArray(maybeWidgetOptions)) { if (!isArray(maybeWidgetOptions)) {
widgets.push( widgets.push(
Decoration.widget({ Decoration.widget({
widget: new ColorPickerWidget(maybeWidgetOptions), widget: new ColorPickerWidget(maybeWidgetOptions),

View File

@ -36,6 +36,7 @@ import {
import { err, trap } from 'lib/trap' import { err, trap } from 'lib/trap'
import { Selection, Selections } from 'lib/selections' import { Selection, Selections } from 'lib/selections'
import { KclCommandValue } from 'lib/commandTypes' import { KclCommandValue } from 'lib/commandTypes'
import { isArray } from 'lib/utils'
import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph' import { Artifact, getSweepFromSuspectedPath } from 'lang/std/artifactGraph'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg } from 'lang/util' import { findKwArg } from 'lang/util'
@ -866,10 +867,7 @@ export async function deleteEdgeTreatment(
if (!inPipe) { if (!inPipe) {
const varDecPathStep = varDec.shallowPath[1] const varDecPathStep = varDec.shallowPath[1]
if ( if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
!Array.isArray(varDecPathStep) ||
typeof varDecPathStep[0] !== 'number'
) {
return new Error( return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]' 'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
) )
@ -935,7 +933,7 @@ export async function deleteEdgeTreatment(
if (err(pipeExpressionNode)) return pipeExpressionNode if (err(pipeExpressionNode)) return pipeExpressionNode
// Ensure that the PipeExpression.body is an array // Ensure that the PipeExpression.body is an array
if (!Array.isArray(pipeExpressionNode.node.body)) { if (!isArray(pipeExpressionNode.node.body)) {
return new Error('PipeExpression body is not an array') return new Error('PipeExpression body is not an array')
} }
@ -945,10 +943,7 @@ export async function deleteEdgeTreatment(
// Remove VariableDeclarator if PipeExpression.body is empty // Remove VariableDeclarator if PipeExpression.body is empty
if (pipeExpressionNode.node.body.length === 0) { if (pipeExpressionNode.node.body.length === 0) {
const varDecPathStep = varDec.shallowPath[1] const varDecPathStep = varDec.shallowPath[1]
if ( if (!isArray(varDecPathStep) || typeof varDecPathStep[0] !== 'number') {
!Array.isArray(varDecPathStep) ||
typeof varDecPathStep[0] !== 'number'
) {
return new Error( return new Error(
'Invalid shallowPath structure: expected a number at shallowPath[1][0]' 'Invalid shallowPath structure: expected a number at shallowPath[1][0]'
) )

View File

@ -27,7 +27,7 @@ import {
import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils' import { getNodePathFromSourceRange } from 'lang/queryAstNodePathUtils'
import { createIdentifier, splitPathAtLastIndex } from './modifyAst' import { createIdentifier, splitPathAtLastIndex } from './modifyAst'
import { getSketchSegmentFromSourceRange } from './std/sketchConstraints' import { getSketchSegmentFromSourceRange } from './std/sketchConstraints'
import { getAngle } from '../lib/utils' import { getAngle, isArray } from '../lib/utils'
import { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch' import { ARG_TAG, getArgForEnd, getFirstArg } from './std/sketch'
import { import {
getConstraintLevelFromSourceRange, getConstraintLevelFromSourceRange,
@ -112,7 +112,7 @@ export function getNodeFromPath<T>(
} }
if ( if (
typeof stopAt !== 'undefined' && typeof stopAt !== 'undefined' &&
(Array.isArray(stopAt) (isArray(stopAt)
? stopAt.includes(currentNode.type) ? stopAt.includes(currentNode.type)
: currentNode.type === stopAt) : currentNode.type === stopAt)
) { ) {
@ -167,6 +167,7 @@ export function getNodeFromPathCurry(
type KCLNode = Node< type KCLNode = Node<
| Expr | Expr
| ExpressionStatement | ExpressionStatement
| ImportStatement
| VariableDeclaration | VariableDeclaration
| VariableDeclarator | VariableDeclarator
| ReturnStatement | ReturnStatement
@ -263,10 +264,14 @@ export function traverse(
// hmm this smell // hmm this smell
_traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']]) _traverse(_node.object, [...pathToNode, ['object', 'MemberExpression']])
_traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']]) _traverse(_node.property, [...pathToNode, ['property', 'MemberExpression']])
} else if ('body' in _node && Array.isArray(_node.body)) { } else if (_node.type === 'ImportStatement') {
_node.body.forEach((expression, index) => // Do nothing.
} else if ('body' in _node && isArray(_node.body)) {
// TODO: Program should have a type field, but it currently doesn't.
const program = node as Node<Program>
program.body.forEach((expression, index) => {
_traverse(expression, [...pathToNode, ['body', ''], [index, 'index']]) _traverse(expression, [...pathToNode, ['body', ''], [index, 'index']])
) })
} }
option?.leave?.(_node) option?.leave?.(_node)
} }

View File

@ -60,7 +60,7 @@ import {
mutateObjExpProp, mutateObjExpProp,
findUniqueName, findUniqueName,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { roundOff, getLength, getAngle } from 'lib/utils' import { roundOff, getLength, getAngle, isArray } from 'lib/utils'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { perpendicularDistance } from 'sketch-helpers' import { perpendicularDistance } from 'sketch-helpers'
import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator' import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
@ -96,7 +96,7 @@ export function createFirstArg(
sketchFn: ToolTip, sketchFn: ToolTip,
val: Expr | [Expr, Expr] | [Expr, Expr, Expr] val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
): Expr | Error { ): Expr | Error {
if (Array.isArray(val)) { if (isArray(val)) {
if ( if (
[ [
'angledLine', 'angledLine',

View File

@ -57,7 +57,7 @@ import {
getSketchSegmentFromPathToNode, getSketchSegmentFromPathToNode,
getSketchSegmentFromSourceRange, getSketchSegmentFromSourceRange,
} from './sketchConstraints' } from './sketchConstraints'
import { getAngle, roundOff, normaliseAngle } from '../../lib/utils' import { getAngle, roundOff, normaliseAngle, isArray } from '../../lib/utils'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { findKwArg, findKwArgAny } from 'lang/util' import { findKwArg, findKwArgAny } from 'lang/util'
@ -122,7 +122,7 @@ function createCallWrapper(
tag?: Expr, tag?: Expr,
valueUsedInTransform?: number valueUsedInTransform?: number
): CreatedSketchExprResult { ): CreatedSketchExprResult {
if (Array.isArray(val)) { if (isArray(val)) {
if (tooltip === 'line') { if (tooltip === 'line') {
const labeledArgs = [createLabeledArg('end', createArrayExpression(val))] const labeledArgs = [createLabeledArg('end', createArrayExpression(val))]
if (tag) { if (tag) {
@ -1330,12 +1330,12 @@ export function getRemoveConstraintsTransform(
// check if the function has no constraints // check if the function has no constraints
const isTwoValFree = const isTwoValFree =
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val) isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) { if (isTwoValFree) {
return false return false
} }
const isOneValFree = const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val) !isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isOneValFree) { if (isOneValFree) {
return transformInfo return transformInfo
} }
@ -1649,7 +1649,7 @@ export function getConstraintType(
// and for one val sketch functions that the arg is NOT locked down // and for one val sketch functions that the arg is NOT locked down
// these conditions should have been checked previously. // these conditions should have been checked previously.
// completely locked down or not locked down at all does not depend on the fnName so we can check that first // completely locked down or not locked down at all does not depend on the fnName so we can check that first
const isArr = Array.isArray(val) const isArr = isArray(val)
if (!isArr) { if (!isArr) {
if (fnName === 'xLine') return 'yRelative' if (fnName === 'xLine') return 'yRelative'
if (fnName === 'yLine') return 'xRelative' if (fnName === 'yLine') return 'xRelative'
@ -2113,9 +2113,9 @@ export function getConstraintLevelFromSourceRange(
// check if the function has no constraints // check if the function has no constraints
const isTwoValFree = const isTwoValFree =
Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val) isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
const isOneValFree = const isOneValFree =
!Array.isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val) !isArray(firstArg.val) && isLiteralArrayOrStatic(firstArg.val)
if (isTwoValFree) return { level: 'free', range: range } if (isTwoValFree) return { level: 'free', range: range }
if (isOneValFree) return { level: 'partial', range: range } if (isOneValFree) return { level: 'partial', range: range }
@ -2128,7 +2128,7 @@ export function isLiteralArrayOrStatic(
): boolean { ): boolean {
if (!val) return false if (!val) return false
if (Array.isArray(val)) { if (isArray(val)) {
const a = val[0] const a = val[0]
const b = val[1] const b = val[1]
return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b) return isLiteralArrayOrStatic(a) && isLiteralArrayOrStatic(b)
@ -2142,7 +2142,7 @@ export function isLiteralArrayOrStatic(
export function isNotLiteralArrayOrStatic( export function isNotLiteralArrayOrStatic(
val: Expr | [Expr, Expr] | [Expr, Expr, Expr] val: Expr | [Expr, Expr] | [Expr, Expr, Expr]
): boolean { ): boolean {
if (Array.isArray(val)) { if (isArray(val)) {
const a = val[0] const a = val[0]
const b = val[1] const b = val[1]
return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b) return isNotLiteralArrayOrStatic(a) && isNotLiteralArrayOrStatic(b)

View File

@ -12,7 +12,7 @@ import {
NumericSuffix, NumericSuffix,
} from './wasm' } from './wasm'
import { filterArtifacts } from 'lang/std/artifactGraph' import { filterArtifacts } from 'lang/std/artifactGraph'
import { isOverlap } from 'lib/utils' import { isArray, isOverlap } from 'lib/utils'
export function updatePathToNodeFromMap( export function updatePathToNodeFromMap(
oldPath: PathToNode, oldPath: PathToNode,
@ -40,8 +40,8 @@ export function isCursorInSketchCommandRange(
predicate: (artifact) => { predicate: (artifact) => {
return selectionRanges.graphSelections.some( return selectionRanges.graphSelections.some(
(selection) => (selection) =>
Array.isArray(selection?.codeRef?.range) && isArray(selection?.codeRef?.range) &&
Array.isArray(artifact?.codeRef?.range) && isArray(artifact?.codeRef?.range) &&
isOverlap(selection?.codeRef?.range, artifact.codeRef.range) isOverlap(selection?.codeRef?.range, artifact.codeRef.range)
) )
}, },

View File

@ -16,7 +16,7 @@ import { isDesktop } from 'lib/isDesktop'
import { useRef } from 'react' import { useRef } from 'react'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { toSync } from 'lib/utils' import { isArray, toSync } from 'lib/utils'
import { reportRejection } from 'lib/trap' import { reportRejection } from 'lib/trap'
import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType' import { CameraProjectionType } from 'wasm-lib/kcl/bindings/CameraProjectionType'
import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus' import { OnboardingStatus } from 'wasm-lib/kcl/bindings/OnboardingStatus'
@ -240,7 +240,7 @@ export function createSettings() {
if ( if (
inputRef.current && inputRef.current &&
inputRefVal && inputRefVal &&
!Array.isArray(inputRefVal) !isArray(inputRefVal)
) { ) {
updateValue(inputRefVal) updateValue(inputRefVal)
} else { } else {

View File

@ -11,6 +11,7 @@ export const uuidv4 = v4
* A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`. * A safer type guard for arrays since the built-in Array.isArray() asserts `any[]`.
*/ */
export function isArray(val: any): val is unknown[] { export function isArray(val: any): val is unknown[] {
// eslint-disable-next-line no-restricted-syntax
return Array.isArray(val) return Array.isArray(val)
} }