From 6b0d891a00c62e8de408702ee7f8762e84637811 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Fri, 15 Mar 2024 21:27:39 +1300 Subject: [PATCH 1/3] perf: switch to using an iterative stack --- src/calculate.ts | 966 ++++++++++++++++++++++++++++------------------- src/utils.ts | 7 + 2 files changed, 585 insertions(+), 388 deletions(-) diff --git a/src/calculate.ts b/src/calculate.ts index 56aed5f6..1a08e407 100644 --- a/src/calculate.ts +++ b/src/calculate.ts @@ -14,18 +14,19 @@ import { } from "ts-api-utils"; import ts from "typescript"; -import { max, min, clamp } from "./compare"; +import { max, min } from "./compare"; import { Immutability } from "./immutability"; import { - type TypeData, - type TypeSpecifier, cacheData, + cast, getCachedData, getTypeData, hasSymbol, isTypeNode, propertyNameToString, typeMatchesSpecifier, + type TypeData, + type TypeSpecifier, } from "./utils"; /** @@ -37,10 +38,6 @@ export type ImmutabilityOverrides = ReadonlyArray<{ from?: Immutability; }>; -type TypeReferenceData = TypeData & { - type: ts.TypeReference; -}; - /** * Get the default overrides that are applied. */ @@ -74,6 +71,11 @@ export function getDefaultOverrides(): ImmutabilityOverrides { */ export type ImmutabilityCache = WeakMap; +type ImmutabilityLimits = { + min: Immutability; + max: Immutability; +}; + /** * A global cache that can be used between consumers. */ @@ -118,12 +120,106 @@ export function getTypeImmutability( ); } +type Snapshot = + | SnapshotBase + | SnapshotChildren + | SnapshotWithLimits + | SnapshotCheckDone + | SnapshotCheckOverride; + +type SnapshotBase = { + immutability: Immutability; + stage: SnapshotStage; + typeData: Readonly; +}; + +type SnapshotWithLimits = SnapshotBase & { + limits: ImmutabilityLimits; +}; + +type SnapshotChildren = SnapshotBase & { + stage: SnapshotStage.ReduceChildren; + children: ReadonlyArray<{ immutability: Immutability }>; + childrenReducer: (a: Immutability, b: Immutability) => Immutability; +}; + +// eslint-disable-next-line functional/no-mixed-types +type SnapshotCheckDone = { + stage: SnapshotStage.CheckDone; + snapshot: Snapshot; + notDoneAction: (snapshot: SnapshotWithLimits) => void; +}; + +type SnapshotCheckOverride = { + snapshot: Snapshot; + stage: SnapshotStage.CheckOverride; + override: ImmutabilityOverrides[number]; +}; + +/** + * The stage of a snapshot. + */ +const enum SnapshotStage { + Init, + ReduceChildren, + ObjectProperties, + ObjectTypeReference, + ObjectIndexSignature, + CheckDone, + CheckOverride, + Done, +} + +function createSnapshot(typeData: TypeData): SnapshotBase { + return { + typeData, + stage: SnapshotStage.Init, + immutability: Immutability.Calculating, + }; +} + +function createChildrenReducer( + current: SnapshotBase, + children: SnapshotChildren["children"], + childrenReducer: SnapshotChildren["childrenReducer"], +): SnapshotChildren { + return { + typeData: current.typeData, + children, + childrenReducer, + stage: SnapshotStage.ReduceChildren, + immutability: Immutability.Calculating, + }; +} + +function createDoneChecker( + snapshot: Snapshot, + notDoneAction: SnapshotCheckDone["notDoneAction"], +): SnapshotCheckDone { + return { + snapshot, + notDoneAction, + stage: SnapshotStage.CheckDone, + }; +} + +function createOverrideChecker( + snapshot: Snapshot, + override: ImmutabilityOverrides[number], +): SnapshotCheckOverride { + return { + snapshot, + override, + stage: SnapshotStage.CheckOverride, + }; +} + /** * Get the immutability of the given type data. */ function getTypeImmutabilityHelper( program: ts.Program, - typeData: Readonly, + td: Readonly, overrides: ImmutabilityOverrides, useCache: ImmutabilityCache | boolean, maxImmutability: Immutability, @@ -135,296 +231,318 @@ function getTypeImmutabilityHelper( ? new WeakMap() : useCache; - const cached = getCachedData(program, cache, typeData); - if (cached !== undefined) { - return cached; - } + const checker = program.getTypeChecker(); - const override = getOverride(program, typeData, overrides); - const overrideTo = override?.to; - const overrideFrom = override?.from; + const m_stack: Snapshot[] = [createSnapshot(td)]; + let m_PreviousImmutability = Immutability.Unknown; + let m_current: Snapshot; + do { + m_current = m_stack.pop() ?? assert.fail(); - // Early escape if we don't need to check the override from. - if (overrideTo !== undefined && overrideFrom === undefined) { - cacheData(program, cache, typeData, overrideTo); - return overrideTo; - } + switch (m_current.stage) { + case SnapshotStage.Init: { + handleStageInit(m_current); + break; + } + case SnapshotStage.ReduceChildren: { + reduceChildren(m_current); + break; + } + case SnapshotStage.CheckDone: { + handleTypeObjectCheckDone(m_current); + assert("snapshot" in m_current); + m_current = m_current.snapshot; + assert(m_current.stage !== SnapshotStage.CheckDone); + break; + } + case SnapshotStage.ObjectTypeReference: { + handleTypeObjectTypeReference(m_current); + break; + } + case SnapshotStage.ObjectIndexSignature: { + handleTypeObjectIndexSignature(m_current); + break; + } + case SnapshotStage.CheckOverride: { + handleCheckOverride(m_current); + assert("snapshot" in m_current); + m_current = m_current.snapshot; + assert(m_current.stage !== SnapshotStage.CheckOverride); + break; + } + case SnapshotStage.Done: { + break; + } + default: { + assert.fail("Unexpected snapshot stage"); + } + } - cacheData(program, cache, typeData, Immutability.Calculating); + assert("immutability" in m_current); + if (m_current.immutability !== Immutability.Calculating) { + cacheData(program, cache, m_current.typeData, m_current.immutability); + m_PreviousImmutability = m_current.immutability; + } + } while (m_stack.length > 0); - const immutability = calculateTypeImmutability( - program, - typeData, - overrides, - cache, - maxImmutability, - ); + assert(m_current.immutability !== Immutability.Calculating); + return m_current.immutability; + + /** + * The first stage for all types. + */ + function handleStageInit(m_snapshot: Snapshot): void { + assert(m_snapshot.stage === SnapshotStage.Init); + + const cached = getCachedData(program, cache, m_snapshot.typeData); + if (cached !== undefined) { + m_snapshot.immutability = cached; + return; + } + + const override = getOverride(m_snapshot); + if (override?.to !== undefined) { + // Early escape if we don't need to check the override from. + if (override.from === undefined) { + m_snapshot.immutability = override.to; + cacheData(program, cache, m_snapshot.typeData, m_snapshot.immutability); + return; + } + + m_stack.push(createOverrideChecker(m_snapshot, override)); + } + + assert(m_snapshot.immutability === Immutability.Calculating); + cacheData(program, cache, m_snapshot.typeData, m_snapshot.immutability); + + if (isUnionType(m_snapshot.typeData.type)) { + handleTypeUnion(m_snapshot); + return; + } + + if (isIntersectionType(m_snapshot.typeData.type)) { + handleTypeIntersection(m_snapshot); + return; + } + + if (isConditionalType(m_snapshot.typeData.type)) { + handleTypeConditional(m_snapshot); + return; + } + + if (isFunction(m_snapshot.typeData.type)) { + handleTypeFunction(m_snapshot); + return; + } + + if (checker.isTupleType(m_snapshot.typeData.type)) { + handleTypeTuple(m_snapshot); + return; + } + + if (checker.isArrayType(m_snapshot.typeData.type)) { + handleTypeArray(m_snapshot); + return; + } + + if (isObjectType(m_snapshot.typeData.type)) { + handleTypeObject(m_snapshot); + return; + } + + // Must be a primitive. + handleTypePrimitive(m_snapshot); + } + + function handleCheckOverride(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.CheckOverride); + assert("override" in m_snapshot); + assert(m_snapshot.override.from !== undefined); + assert("immutability" in m_snapshot.snapshot); - if (overrideTo !== undefined) { - assert(overrideFrom !== undefined); if ( - (overrideFrom <= immutability && immutability <= overrideTo) || - (overrideFrom >= immutability && immutability >= overrideTo) + (m_snapshot.override.from <= m_snapshot.snapshot.immutability && + m_snapshot.snapshot.immutability <= m_snapshot.override.to) || + (m_snapshot.override.from >= m_snapshot.snapshot.immutability && + m_snapshot.snapshot.immutability >= m_snapshot.override.to) ) { - cacheData(program, cache, typeData, overrideTo); - return overrideTo; + m_snapshot.snapshot.immutability = m_snapshot.override.to; } } - cacheData(program, cache, typeData, immutability); - return immutability; -} + function handleTypeUnion(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); + assert(isUnionType(m_snapshot.typeData.type)); -/** - * Get the override for the type if it has one. - */ -function getOverride( - program: ts.Program, - typeData: Readonly, - overrides: ImmutabilityOverrides, -) { - return overrides.find((potentialOverride) => - typeMatchesSpecifier(typeData, potentialOverride.type, program), - ); -} + const children = m_snapshot.typeData.type.types.map((type, index) => { + const typeNode = + m_snapshot.typeData.typeNode !== null && + ts.isUnionTypeNode(m_snapshot.typeData.typeNode) + ? m_snapshot.typeData.typeNode.types[index] + : undefined; // TODO: can we safely get a union type node nested within a different type node? -/** - * Calculated the immutability of the given type. - */ -function calculateTypeImmutability( - program: ts.Program, - typeData: Readonly, - overrides: ImmutabilityOverrides, - cache: ImmutabilityCache, - maxImmutability: Immutability, -): Immutability { - // Union? - if (isUnionType(typeData.type)) { - return typeData.type.types - .map((type, index) => { - const typeNode = - typeData.typeNode !== null && ts.isUnionTypeNode(typeData.typeNode) - ? typeData.typeNode.types[index] - : undefined; // TODO: can we safely get a union type node nested within a different type node? - - return getTypeImmutabilityHelper( - program, - getTypeData(type, typeNode), - overrides, - cache, - maxImmutability, - ); - }) - .reduce(min); - } + return createSnapshot(getTypeData(type, typeNode)); + }); - // Intersection? - if (isIntersectionType(typeData.type)) { - return objectImmutability( - program, - typeData, - overrides, - cache, - maxImmutability, - ); + m_stack.push(createChildrenReducer(m_snapshot, children, min), ...children); } - // Conditional? - if (isConditionalType(typeData.type)) { - return [typeData.type.root.node.trueType, typeData.type.root.node.falseType] - .map((typeNode) => { - const checker = program.getTypeChecker(); - const type = checker.getTypeFromTypeNode(typeNode); - - return getTypeImmutabilityHelper( - program, - getTypeData(type, typeNode), - overrides, - cache, - maxImmutability, - ); - }) - .reduce(min); - } + function handleTypeIntersection(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); - // (Non-namespace) Function? - if ( - typeData.type.getCallSignatures().length > 0 && - typeData.type.getProperties().length === 0 - ) { - return Immutability.Immutable; + handleTypeObject(m_snapshot); } - const checker = program.getTypeChecker(); + function handleTypeConditional(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); + assert(isConditionalType(m_snapshot.typeData.type)); - // Tuple? - if (checker.isTupleType(typeData.type)) { - if (!typeData.type.target.readonly) { - return Immutability.Mutable; - } + const children = [ + m_snapshot.typeData.type.root.node.trueType, + m_snapshot.typeData.type.root.node.falseType, + ].map((typeNode) => { + const type = checker.getTypeFromTypeNode(typeNode); + return createSnapshot(getTypeData(type, typeNode)); + }); - return arrayImmutability( - program, - typeData as Readonly< - TypeData & { - type: ts.TypeReference; - } - >, - overrides, - cache, - maxImmutability, - ); + m_stack.push(createChildrenReducer(m_snapshot, children, min), ...children); } - // Array? - if (checker.isArrayType(typeData.type)) { - return arrayImmutability( - program, - typeData as Readonly< - TypeData & { - type: ts.TypeReference; - } - >, - overrides, - cache, - maxImmutability, - ); - } + function handleTypeFunction(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); - // Other type of object? - if (isObjectType(typeData.type)) { - return objectImmutability( - program, - typeData, - overrides, - cache, - maxImmutability, - ); + m_snapshot.immutability = Immutability.Immutable; } - // Must be a primitive. - return Immutability.Immutable; -} + function handleTypeTuple(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); + assert(checker.isTupleType(m_snapshot.typeData.type)); -/** - * Get the immutability of the given array. - */ -function arrayImmutability( - program: ts.Program, - typeData: Readonly< - TypeData & { - type: ts.TypeReference; + if (!m_snapshot.typeData.type.target.readonly) { + m_snapshot.immutability = Immutability.Mutable; + return; } - >, - overrides: ImmutabilityOverrides, - cache: ImmutabilityCache, - maxImmutability: Immutability, -): Immutability { - const shallowImmutability = objectImmutability( - program, - typeData, - overrides, - cache, - maxImmutability, - ); - if ( - shallowImmutability <= Immutability.ReadonlyShallow || - shallowImmutability >= maxImmutability - ) { - return shallowImmutability; + handleTypeArray(m_snapshot); } - const deepImmutability = typeArgumentsImmutability( - program, - typeData as TypeReferenceData, - overrides, - cache, - maxImmutability, - ); + function handleTypeArray(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); - return clamp( - Immutability.ReadonlyShallow, - deepImmutability, - shallowImmutability, - ); -} + m_stack.push( + createDoneChecker(m_snapshot, (m_objectType) => { + assert("limits" in m_objectType); -/** - * Get the immutability of the given object. - */ -function objectImmutability( - program: ts.Program, - typeData: Readonly, - overrides: ImmutabilityOverrides, - cache: ImmutabilityCache, - maxImmutability: Immutability, -): Immutability { - const checker = program.getTypeChecker(); + m_objectType.stage = SnapshotStage.Done; + m_objectType.immutability = max( + m_objectType.limits.min, + m_objectType.limits.max, + ); + m_stack.push(m_objectType); + }), + createDoneChecker(m_snapshot, (m_objectType) => { + assert("limits" in m_objectType); - let m_maxImmutability = maxImmutability; - let m_minImmutability = Immutability.Mutable; - - const properties = typeData.type.getProperties(); - // eslint-disable-next-line functional/no-conditional-statements - if (properties.length > 0) { - // eslint-disable-next-line functional/no-loop-statements - for (const property of properties) { - if ( - isPropertyReadonlyInType( - typeData.type, - property.getEscapedName(), - checker, - ) || - // Ignore "length" for tuples. - // TODO: Report this issue to upstream. - ((property.escapedName as string) === "length" && - checker.isTupleType(typeData.type)) - ) { - continue; - } + if (isTypeReferenceWithTypeArguments(m_objectType.typeData.type)) { + handleTypeArguments(m_snapshot); + } + }), + ); - const name = ts.getNameOfDeclaration(property.valueDeclaration); - if (name !== undefined && ts.isPrivateIdentifier(name)) { - continue; - } + handleTypeObject(m_snapshot); + } + + function handleTypeObject(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.Init); + assert(!("limits" in m_snapshot), "Limits already set"); + assert(cast(m_snapshot)); + + m_snapshot.stage = SnapshotStage.ObjectProperties; + m_snapshot.limits = { + max: maxImmutability, + min: Immutability.Mutable, + }; + + m_stack.push( + createDoneChecker(m_snapshot, (m_objectType) => { + assert(m_objectType.stage === SnapshotStage.ObjectProperties); + assert("limits" in m_objectType); + + if (isTypeReferenceWithTypeArguments(m_objectType.typeData.type)) { + m_objectType.stage = SnapshotStage.ObjectTypeReference; + m_stack.push(m_objectType); + return; + } - const declarations = property.getDeclarations() ?? []; - if (declarations.length > 0) { + m_objectType.stage = SnapshotStage.ObjectIndexSignature; + m_stack.push(m_objectType); + }), + ); + + const properties = m_snapshot.typeData.type.getProperties(); + if (properties.length > 0) { + for (const property of properties) { if ( - declarations.some( - (declaration) => - hasSymbol(declaration) && - isSymbolFlagSet(declaration.symbol, ts.SymbolFlags.Method), - ) + isPropertyReadonlyInType( + m_snapshot.typeData.type, + property.getEscapedName(), + checker, + ) || + // Ignore "length" for tuples. + // TODO: Report this issue to upstream. + ((property.escapedName as string) === "length" && + checker.isTupleType(m_snapshot.typeData.type)) ) { - m_maxImmutability = min(m_maxImmutability, Immutability.ReadonlyDeep); continue; } - if ( - declarations.every( - (declaration) => - ts.isPropertySignature(declaration) && - declaration.type !== undefined && - ts.isFunctionTypeNode(declaration.type), - ) - ) { - m_maxImmutability = min(m_maxImmutability, Immutability.ReadonlyDeep); + const name = ts.getNameOfDeclaration(property.valueDeclaration); + if (name !== undefined && ts.isPrivateIdentifier(name)) { continue; } - } - return Immutability.Mutable; - } + const declarations = property.getDeclarations() ?? []; + if (declarations.length > 0) { + if ( + declarations.some( + (declaration) => + hasSymbol(declaration) && + isSymbolFlagSet(declaration.symbol, ts.SymbolFlags.Method), + ) + ) { + m_snapshot.limits.max = min( + m_snapshot.limits.max, + Immutability.ReadonlyDeep, + ); + continue; + } + + if ( + declarations.every( + (declaration) => + ts.isPropertySignature(declaration) && + declaration.type !== undefined && + ts.isFunctionTypeNode(declaration.type), + ) + ) { + m_snapshot.limits.max = min( + m_snapshot.limits.max, + Immutability.ReadonlyDeep, + ); + continue; + } + } - m_minImmutability = Immutability.ReadonlyShallow; + m_snapshot.immutability = Immutability.Mutable; + return; + } + } const propertyNodes = new Map( - typeData.typeNode !== null && - hasType(typeData.typeNode) && - typeData.typeNode.type !== undefined && - ts.isTypeLiteralNode(typeData.typeNode.type) - ? typeData.typeNode.type.members + m_snapshot.typeData.typeNode !== null && + hasType(m_snapshot.typeData.typeNode) && + m_snapshot.typeData.typeNode.type !== undefined && + ts.isTypeLiteralNode(m_snapshot.typeData.typeNode.type) + ? m_snapshot.typeData.typeNode.type.members .map((member): [string, ts.TypeNode] | undefined => member.name === undefined || !hasType(member) || @@ -436,170 +554,242 @@ function objectImmutability( : [], ); - // eslint-disable-next-line functional/no-loop-statements - for (const property of properties) { - const propertyType = getTypeOfPropertyOfType( - checker, - typeData.type, - property, - ); - if ( - propertyType === undefined || - (isIntrinsicType(propertyType) && - propertyType.intrinsicName === "error") - ) { - continue; - } + const children = properties + .map((property) => { + const propertyType = getTypeOfPropertyOfType( + checker, + m_snapshot.typeData.type, + property, + ); + if ( + propertyType === undefined || + (isIntrinsicType(propertyType) && + propertyType.intrinsicName === "error") + ) { + return null; + } - const propertyTypeNode = propertyNodes.get( - property.getEscapedName() as string, - ); + const propertyTypeNode = propertyNodes.get( + property.getEscapedName() as string, + ); - const result = getTypeImmutabilityHelper( - program, - getTypeData(propertyType, propertyTypeNode), - overrides, - cache, - maxImmutability, + return createSnapshot(getTypeData(propertyType, propertyTypeNode)); + }) + .filter((snapshot): snapshot is SnapshotBase => snapshot !== null); + + if (children.length > 0) { + m_snapshot.limits.min = Immutability.ReadonlyShallow; + + m_stack.push( + createChildrenReducer(m_snapshot, children, min), + ...children, ); - m_maxImmutability = min(m_maxImmutability, result); - if (m_minImmutability >= m_maxImmutability) { - return m_minImmutability; - } } } - if (isTypeReference(typeData.type)) { - const result = typeArgumentsImmutability( - program, - typeData as TypeReferenceData, - overrides, - cache, - maxImmutability, + function handleTypeObjectTypeReference(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.ObjectTypeReference); + assert("limits" in m_snapshot); + + m_stack.push( + createDoneChecker(m_snapshot, (m_objectType) => { + assert(m_objectType.stage === SnapshotStage.ObjectTypeReference); + m_objectType.stage = SnapshotStage.ObjectIndexSignature; + m_stack.push(m_objectType); + }), ); - m_maxImmutability = min(m_maxImmutability, result); - if (m_minImmutability >= m_maxImmutability) { - return m_minImmutability; - } + handleTypeArguments(m_snapshot); } - const types = isIntersectionType(typeData.type) - ? typeData.type.types - : [typeData.type]; + function handleTypeObjectIndexSignature(m_snapshot: Snapshot) { + assert(m_snapshot.stage === SnapshotStage.ObjectIndexSignature); + assert("limits" in m_snapshot); + assert( + m_snapshot.typeData.typeNode === null || + isIntersectionType(m_snapshot.typeData.type) === + ts.isIntersectionTypeNode(m_snapshot.typeData.typeNode), + ); - const typeNodes = - typeData.typeNode === null - ? undefined - : ts.isIntersectionTypeNode(typeData.typeNode) - ? typeData.typeNode.types - : [typeData.typeNode]; + const [types, typeNodes] = isIntersectionType(m_snapshot.typeData.type) + ? [ + m_snapshot.typeData.type.types, + (m_snapshot.typeData.typeNode as ts.IntersectionTypeNode | null) + ?.types, + ] + : [ + [m_snapshot.typeData.type], + m_snapshot.typeData.typeNode === null + ? undefined + : [m_snapshot.typeData.typeNode], + ]; + + m_stack.push( + createDoneChecker(m_snapshot, (m_objectType) => { + assert(m_objectType.stage === SnapshotStage.ObjectIndexSignature); + + m_objectType.stage = SnapshotStage.Done; + m_objectType.immutability = max( + m_objectType.limits.min, + m_objectType.limits.max, + ); + m_stack.push(m_objectType); + }), + createDoneChecker(m_snapshot, (m_objectType) => { + assert(m_objectType.stage === SnapshotStage.ObjectIndexSignature); + assert("limits" in m_objectType); + + const children = types.flatMap((type, index) => + createIndexSignatureSnapshots( + m_objectType, + ts.IndexKind.Number, + getTypeData(type, typeNodes?.[index]), + ), + ); + if (children.length > 0) { + m_stack.push( + createChildrenReducer(m_objectType, children, max), + ...children, + ); + } + }), + ); - const stringIndexSigImmutability = types - .map((type, index) => - indexSignatureImmutability( - program, - getTypeData(type, typeNodes?.[index]), + const children = types.flatMap((type, index) => + createIndexSignatureSnapshots( + m_snapshot, ts.IndexKind.String, - overrides, - cache, - maxImmutability, + getTypeData(type, typeNodes?.[index]), ), - ) - .reduce(max); - - m_maxImmutability = min(stringIndexSigImmutability, m_maxImmutability); - if (m_minImmutability >= m_maxImmutability) { - return m_minImmutability; + ); + if (children.length > 0) { + m_stack.push( + createChildrenReducer(m_snapshot, children, max), + ...children, + ); + } } - const numberIndexSigImmutability = types - .map((type, index) => - indexSignatureImmutability( - program, - getTypeData(type, typeNodes?.[index]), - ts.IndexKind.Number, - overrides, - cache, - maxImmutability, - ), - ) - .reduce(max); + function handleTypeObjectCheckDone(m_snapshot: Snapshot) { + assert("snapshot" in m_snapshot); + assert("limits" in m_snapshot.snapshot); + assert("notDoneAction" in m_snapshot); + + if (m_PreviousImmutability !== Immutability.Calculating) { + m_snapshot.snapshot.limits.max = min( + m_snapshot.snapshot.limits.max, + m_PreviousImmutability, + ); + if (m_snapshot.snapshot.limits.min >= m_snapshot.snapshot.limits.max) { + m_snapshot.snapshot.immutability = m_snapshot.snapshot.limits.min; + return; + } + } - m_maxImmutability = min(numberIndexSigImmutability, m_maxImmutability); - if (m_minImmutability >= m_maxImmutability) { - return m_minImmutability; + m_snapshot.notDoneAction(m_snapshot.snapshot); } - return max(m_minImmutability, m_maxImmutability); -} + function handleTypeArguments(m_snapshot: Snapshot) { + assert("typeData" in m_snapshot); + assert(isTypeReferenceWithTypeArguments(m_snapshot.typeData.type)); -/** - * Get the immutability of the given type arguments. - */ -function typeArgumentsImmutability( - program: ts.Program, - typeData: Readonly, - overrides: ImmutabilityOverrides, - cache: ImmutabilityCache, - maxImmutability: Immutability, -): Immutability { - if ( - typeData.type.typeArguments !== undefined && - typeData.type.typeArguments.length > 0 - ) { - return typeData.type.typeArguments - .map((type) => - getTypeImmutabilityHelper( - program, - getTypeData(type, undefined), // TODO: can we get a type node for this? - overrides, - cache, - maxImmutability, - ), - ) - .reduce(min); + const children = m_snapshot.typeData.type.typeArguments.map((type) => + createSnapshot(getTypeData(type, undefined)), + ); + m_stack.push(createChildrenReducer(m_snapshot, children, min), ...children); } - return Immutability.Unknown; -} + function createIndexSignatureSnapshots( + m_snapshot: SnapshotBase, + kind: ts.IndexKind, + typeData: TypeData, + ): Array> { + const indexInfo = checker.getIndexInfoOfType(typeData.type, kind); + if (indexInfo === undefined) { + m_snapshot.immutability = Immutability.Unknown; + return []; + } -/** - * Get the immutability of the given index signature. - */ -function indexSignatureImmutability( - program: ts.Program, - typeData: Readonly, - kind: ts.IndexKind, - overrides: ImmutabilityOverrides, - cache: ImmutabilityCache, - maxImmutability: Immutability, -): Immutability { - const checker = program.getTypeChecker(); - const indexInfo = checker.getIndexInfoOfType(typeData.type, kind); - if (indexInfo === undefined) { - return Immutability.Unknown; - } + if (maxImmutability <= Immutability.ReadonlyShallow) { + m_snapshot.immutability = Immutability.ReadonlyShallow; + return []; + } - if (maxImmutability <= Immutability.ReadonlyShallow) { - return Immutability.ReadonlyShallow; - } + if (indexInfo.isReadonly) { + if (indexInfo.type === typeData.type) { + m_snapshot.immutability = maxImmutability; + return []; + } + + const child = createSnapshot( + getTypeData(indexInfo.type, undefined), // TODO: can we get a type node for this? + ); - if (indexInfo.isReadonly) { - if (indexInfo.type === typeData.type) { - return maxImmutability; + return [ + createChildrenReducer( + m_snapshot, + [{ immutability: Immutability.ReadonlyShallow }, child], + max, + ), + child, + ]; } - return max( - Immutability.ReadonlyShallow, - getTypeImmutabilityHelper( + m_snapshot.immutability = Immutability.Mutable; + return []; + } + + function handleTypePrimitive(m_snapshot: Snapshot) { + assert("immutability" in m_snapshot); + m_snapshot.immutability = Immutability.Immutable; + } + + /** + * Get the override for the type if it has one. + */ + function getOverride(m_snapshot: Snapshot) { + assert("typeData" in m_snapshot); + return overrides.find((potentialOverride) => + typeMatchesSpecifier( + m_snapshot.typeData, + potentialOverride.type, program, - getTypeData(indexInfo.type, undefined), // TODO: can we get a type node for this? - overrides, - cache, - maxImmutability, ), ); } - return Immutability.Mutable; + function reduceChildren(m_snapshot: Snapshot): void { + assert("children" in m_snapshot && "childrenReducer" in m_snapshot); + assert(cast(m_snapshot)); + + m_snapshot.immutability = ( + m_snapshot.children[0] ?? assert.fail("no children") + ).immutability; + for (let m_index = 1; m_index < m_snapshot.children.length; m_index++) { + m_snapshot.immutability = m_snapshot.childrenReducer( + m_snapshot.immutability, + m_snapshot.children[m_index]!.immutability, + ); + } + } +} + +/** + * Is type a (non-namespace) function? + */ +function isFunction(type: ts.Type) { + return ( + type.getCallSignatures().length > 0 && type.getProperties().length === 0 + ); +} + +function isTypeReferenceWithTypeArguments( + type: ts.Type, +): type is ts.TypeReference & { + typeArguments: NonNullable; +} { + return ( + isTypeReference(type) && + type.typeArguments !== undefined && + type.typeArguments.length > 0 + ); } diff --git a/src/utils.ts b/src/utils.ts index 5502a368..ee65e43d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -349,3 +349,10 @@ function qualifiedNameToString(qualifiedName: ts.QualifiedName): string { qualifiedName.right, )}`; } + +/** + * Cast the type. + */ +export function cast(value: U): value is T { + return true; +} From 9fcddc4f20186c20b74a7243acd57acbe6e9f902 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Mon, 18 Mar 2024 02:28:37 +1300 Subject: [PATCH 2/3] refactor: make more manageable --- src/calculate.ts | 1067 ++++++++++++++++++++++++---------------------- src/utils.ts | 25 +- 2 files changed, 572 insertions(+), 520 deletions(-) diff --git a/src/calculate.ts b/src/calculate.ts index 1a08e407..0751764d 100644 --- a/src/calculate.ts +++ b/src/calculate.ts @@ -9,7 +9,6 @@ import { isObjectType, isPropertyReadonlyInType, isSymbolFlagSet, - isTypeReference, isUnionType, } from "ts-api-utils"; import ts from "typescript"; @@ -18,11 +17,12 @@ import { max, min } from "./compare"; import { Immutability } from "./immutability"; import { cacheData, - cast, getCachedData, getTypeData, hasSymbol, + isFunction, isTypeNode, + isTypeReferenceWithTypeArguments, propertyNameToString, typeMatchesSpecifier, type TypeData, @@ -120,99 +120,78 @@ export function getTypeImmutability( ); } -type Snapshot = - | SnapshotBase - | SnapshotChildren - | SnapshotWithLimits - | SnapshotCheckDone - | SnapshotCheckOverride; +type TaskState = + | TaskStateTriage + | TaskStateChildrenReducer + | TaskStateWithLimits + | TaskStateCheckDone + | TaskStateApplyOverride + | TaskStateDone; -type SnapshotBase = { +type TaskStateBase = { + stage: TaskStateStage; immutability: Immutability; - stage: SnapshotStage; typeData: Readonly; }; -type SnapshotWithLimits = SnapshotBase & { +type TaskStateTriage = TaskStateBase & { + stage: TaskStateStage.Triage; +}; + +type TaskStateWithLimits = TaskStateBase & { limits: ImmutabilityLimits; + stage: + | TaskStateStage.ObjectIndexSignature + | TaskStateStage.ObjectProperties + | TaskStateStage.ObjectTypeReference + | TaskStateStage.Done; }; -type SnapshotChildren = SnapshotBase & { - stage: SnapshotStage.ReduceChildren; +type TaskStateChildrenReducer = TaskStateBase & { + stage: TaskStateStage.ReduceChildren; children: ReadonlyArray<{ immutability: Immutability }>; childrenReducer: (a: Immutability, b: Immutability) => Immutability; }; // eslint-disable-next-line functional/no-mixed-types -type SnapshotCheckDone = { - stage: SnapshotStage.CheckDone; - snapshot: Snapshot; - notDoneAction: (snapshot: SnapshotWithLimits) => void; +type TaskStateCheckDone = { + stage: TaskStateStage.CheckDone; + taskState: TaskStateWithLimits; + notDoneAction: () => void; }; -type SnapshotCheckOverride = { - snapshot: Snapshot; - stage: SnapshotStage.CheckOverride; +type TaskStateApplyOverride = { + stage: TaskStateStage.ApplyOverride; + taskState: Exclude; override: ImmutabilityOverrides[number]; }; +type TaskStateDone = TaskStateBase & { + stage: TaskStateStage.Done; +}; + /** - * The stage of a snapshot. + * The stage of a taskState. */ -const enum SnapshotStage { - Init, +const enum TaskStateStage { + Triage, ReduceChildren, ObjectProperties, ObjectTypeReference, ObjectIndexSignature, CheckDone, - CheckOverride, + ApplyOverride, Done, } -function createSnapshot(typeData: TypeData): SnapshotBase { - return { - typeData, - stage: SnapshotStage.Init, - immutability: Immutability.Calculating, - }; -} - -function createChildrenReducer( - current: SnapshotBase, - children: SnapshotChildren["children"], - childrenReducer: SnapshotChildren["childrenReducer"], -): SnapshotChildren { - return { - typeData: current.typeData, - children, - childrenReducer, - stage: SnapshotStage.ReduceChildren, - immutability: Immutability.Calculating, - }; -} - -function createDoneChecker( - snapshot: Snapshot, - notDoneAction: SnapshotCheckDone["notDoneAction"], -): SnapshotCheckDone { - return { - snapshot, - notDoneAction, - stage: SnapshotStage.CheckDone, - }; -} +type Stack = TaskState[]; -function createOverrideChecker( - snapshot: Snapshot, - override: ImmutabilityOverrides[number], -): SnapshotCheckOverride { - return { - snapshot, - override, - stage: SnapshotStage.CheckOverride, - }; -} +type Parameters = Readonly<{ + program: ts.Program; + overrides: ImmutabilityOverrides; + cache: ImmutabilityCache; + immutabilityLimits: Readonly; +}>; /** * Get the immutability of the given type data. @@ -231,565 +210,621 @@ function getTypeImmutabilityHelper( ? new WeakMap() : useCache; - const checker = program.getTypeChecker(); + const parameters: Parameters = { + program, + overrides, + cache, + immutabilityLimits: { + min: Immutability.Mutable, + max: maxImmutability, + }, + }; - const m_stack: Snapshot[] = [createSnapshot(td)]; + const m_stack: Stack = [createNewTaskState(td)]; let m_PreviousImmutability = Immutability.Unknown; - let m_current: Snapshot; + let m_state: TaskState; do { - m_current = m_stack.pop() ?? assert.fail(); + m_state = m_stack.pop() ?? assert.fail(); - switch (m_current.stage) { - case SnapshotStage.Init: { - handleStageInit(m_current); + switch (m_state.stage) { + case TaskStateStage.Triage: { + taskTriage(parameters, m_stack, m_state); break; } - case SnapshotStage.ReduceChildren: { - reduceChildren(m_current); + case TaskStateStage.ReduceChildren: { + taskReduceChildren(m_state); break; } - case SnapshotStage.CheckDone: { - handleTypeObjectCheckDone(m_current); - assert("snapshot" in m_current); - m_current = m_current.snapshot; - assert(m_current.stage !== SnapshotStage.CheckDone); + case TaskStateStage.ObjectTypeReference: { + taskObjectTypeReference(m_stack, m_state); break; } - case SnapshotStage.ObjectTypeReference: { - handleTypeObjectTypeReference(m_current); + case TaskStateStage.ObjectIndexSignature: { + taskObjectIndexSignature(parameters, m_stack, m_state); break; } - case SnapshotStage.ObjectIndexSignature: { - handleTypeObjectIndexSignature(m_current); + case TaskStateStage.ApplyOverride: { + taskApplyOverride(m_state); + m_state = m_state.taskState; break; } - case SnapshotStage.CheckOverride: { - handleCheckOverride(m_current); - assert("snapshot" in m_current); - m_current = m_current.snapshot; - assert(m_current.stage !== SnapshotStage.CheckOverride); + case TaskStateStage.CheckDone: { + taskCheckDone(m_state, m_PreviousImmutability); + m_state = m_state.taskState; break; } - case SnapshotStage.Done: { + case TaskStateStage.Done: { break; } default: { - assert.fail("Unexpected snapshot stage"); + assert.fail("Unexpected taskState stage"); } } - assert("immutability" in m_current); - if (m_current.immutability !== Immutability.Calculating) { - cacheData(program, cache, m_current.typeData, m_current.immutability); - m_PreviousImmutability = m_current.immutability; + if (m_state.immutability !== Immutability.Calculating) { + cacheData(program, cache, m_state.typeData, m_state.immutability); + m_PreviousImmutability = m_state.immutability; } } while (m_stack.length > 0); - assert(m_current.immutability !== Immutability.Calculating); - return m_current.immutability; - - /** - * The first stage for all types. - */ - function handleStageInit(m_snapshot: Snapshot): void { - assert(m_snapshot.stage === SnapshotStage.Init); + if (m_state.immutability === Immutability.Calculating) { + assert.fail('Tried to return immutability of "Calculating"'); + // @ts-expect-error Unreachable Code + return Immutability.Unknown; + } + return m_state.immutability; +} - const cached = getCachedData(program, cache, m_snapshot.typeData); - if (cached !== undefined) { - m_snapshot.immutability = cached; - return; - } +function createNewTaskState(typeData: TypeData): TaskStateTriage { + return { + typeData, + stage: TaskStateStage.Triage, + immutability: Immutability.Calculating, + }; +} - const override = getOverride(m_snapshot); - if (override?.to !== undefined) { - // Early escape if we don't need to check the override from. - if (override.from === undefined) { - m_snapshot.immutability = override.to; - cacheData(program, cache, m_snapshot.typeData, m_snapshot.immutability); - return; - } +function createChildrenReducerTaskState( + parent: TaskStateBase, + children: TaskStateChildrenReducer["children"], + childrenReducer: TaskStateChildrenReducer["childrenReducer"], +): TaskStateChildrenReducer { + return { + typeData: parent.typeData, + children, + childrenReducer, + stage: TaskStateStage.ReduceChildren, + immutability: Immutability.Calculating, + }; +} - m_stack.push(createOverrideChecker(m_snapshot, override)); - } +function createCheckDoneTaskState( + taskState: TaskStateCheckDone["taskState"], + notDoneAction: TaskStateCheckDone["notDoneAction"], +): TaskStateCheckDone { + return { + taskState, + notDoneAction, + stage: TaskStateStage.CheckDone, + }; +} - assert(m_snapshot.immutability === Immutability.Calculating); - cacheData(program, cache, m_snapshot.typeData, m_snapshot.immutability); +function createApplyOverrideTaskState( + taskState: TaskStateApplyOverride["taskState"], + override: ImmutabilityOverrides[number], +): TaskStateApplyOverride { + return { + taskState, + override, + stage: TaskStateStage.ApplyOverride, + }; +} - if (isUnionType(m_snapshot.typeData.type)) { - handleTypeUnion(m_snapshot); - return; - } +/** + * Get the override for the type if it has one. + */ +function getOverride(parameters: Parameters, typeData: TypeData) { + return parameters.overrides.find((potentialOverride) => + typeMatchesSpecifier(typeData, potentialOverride.type, parameters.program), + ); +} - if (isIntersectionType(m_snapshot.typeData.type)) { - handleTypeIntersection(m_snapshot); - return; - } +/** + * The first stage for all types. + */ +function taskTriage( + parameters: Parameters, + m_stack: Stack, + m_state: TaskStateTriage, +): void { + const cached = getCachedData( + parameters.program, + parameters.cache, + m_state.typeData, + ); + if (cached !== undefined) { + m_state.immutability = cached; + return; + } - if (isConditionalType(m_snapshot.typeData.type)) { - handleTypeConditional(m_snapshot); + const override = getOverride(parameters, m_state.typeData); + if (override?.to !== undefined) { + // Early escape if we don't need to check the override from. + if (override.from === undefined) { + m_state.immutability = override.to; + cacheData( + parameters.program, + parameters.cache, + m_state.typeData, + m_state.immutability, + ); return; } - if (isFunction(m_snapshot.typeData.type)) { - handleTypeFunction(m_snapshot); - return; - } + m_stack.push(createApplyOverrideTaskState(m_state, override)); + } - if (checker.isTupleType(m_snapshot.typeData.type)) { - handleTypeTuple(m_snapshot); - return; - } + assert(m_state.immutability === Immutability.Calculating); + cacheData( + parameters.program, + parameters.cache, + m_state.typeData, + m_state.immutability, + ); - if (checker.isArrayType(m_snapshot.typeData.type)) { - handleTypeArray(m_snapshot); - return; - } + if (isUnionType(m_state.typeData.type)) { + handleTypeUnion(m_stack, m_state); + return; + } - if (isObjectType(m_snapshot.typeData.type)) { - handleTypeObject(m_snapshot); - return; - } + if (isIntersectionType(m_state.typeData.type)) { + handleTypeIntersection(parameters, m_stack, m_state); + return; + } - // Must be a primitive. - handleTypePrimitive(m_snapshot); + if (isConditionalType(m_state.typeData.type)) { + handleTypeConditional(parameters, m_stack, m_state); + return; } - function handleCheckOverride(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.CheckOverride); - assert("override" in m_snapshot); - assert(m_snapshot.override.from !== undefined); - assert("immutability" in m_snapshot.snapshot); - - if ( - (m_snapshot.override.from <= m_snapshot.snapshot.immutability && - m_snapshot.snapshot.immutability <= m_snapshot.override.to) || - (m_snapshot.override.from >= m_snapshot.snapshot.immutability && - m_snapshot.snapshot.immutability >= m_snapshot.override.to) - ) { - m_snapshot.snapshot.immutability = m_snapshot.override.to; - } + if (isFunction(m_state.typeData.type)) { + handleTypeFunction(m_state); + return; } - function handleTypeUnion(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); - assert(isUnionType(m_snapshot.typeData.type)); + const checker = parameters.program.getTypeChecker(); - const children = m_snapshot.typeData.type.types.map((type, index) => { - const typeNode = - m_snapshot.typeData.typeNode !== null && - ts.isUnionTypeNode(m_snapshot.typeData.typeNode) - ? m_snapshot.typeData.typeNode.types[index] - : undefined; // TODO: can we safely get a union type node nested within a different type node? + if (checker.isTupleType(m_state.typeData.type)) { + handleTypeTuple(parameters, m_stack, m_state); + return; + } - return createSnapshot(getTypeData(type, typeNode)); - }); + if (checker.isArrayType(m_state.typeData.type)) { + handleTypeArray(parameters, m_stack, m_state); + return; + } - m_stack.push(createChildrenReducer(m_snapshot, children, min), ...children); + if (isObjectType(m_state.typeData.type)) { + handleTypeObject(parameters, m_stack, m_state); + return; } - function handleTypeIntersection(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); + // Must be a primitive. + handleTypePrimitive(m_state); +} - handleTypeObject(m_snapshot); - } +function taskObjectTypeReference(m_stack: Stack, m_state: TaskStateWithLimits) { + m_stack.push( + createCheckDoneTaskState(m_state, () => { + m_state.stage = TaskStateStage.ObjectIndexSignature; + m_stack.push(m_state); + }), + ); + handleTypeArguments(m_stack, m_state); +} - function handleTypeConditional(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); - assert(isConditionalType(m_snapshot.typeData.type)); +function taskObjectIndexSignature( + parameters: Parameters, + m_stack: Stack, + m_state: TaskStateWithLimits, +) { + assert( + m_state.typeData.typeNode === null || + isIntersectionType(m_state.typeData.type) === + ts.isIntersectionTypeNode(m_state.typeData.typeNode), + ); - const children = [ - m_snapshot.typeData.type.root.node.trueType, - m_snapshot.typeData.type.root.node.falseType, - ].map((typeNode) => { - const type = checker.getTypeFromTypeNode(typeNode); - return createSnapshot(getTypeData(type, typeNode)); - }); + const [types, typeNodes] = isIntersectionType(m_state.typeData.type) + ? [ + m_state.typeData.type.types, + (m_state.typeData.typeNode as ts.IntersectionTypeNode | null)?.types, + ] + : [ + [m_state.typeData.type], + m_state.typeData.typeNode === null + ? undefined + : [m_state.typeData.typeNode], + ]; + + m_stack.push( + createCheckDoneTaskState(m_state, () => { + m_state.stage = TaskStateStage.Done; + m_state.immutability = max(m_state.limits.min, m_state.limits.max); + m_stack.push(m_state); + }), + createCheckDoneTaskState(m_state, () => { + const children = types.flatMap((type, index) => + createIndexSignatureTaskStates( + parameters, + m_state, + ts.IndexKind.Number, + getTypeData(type, typeNodes?.[index]), + ), + ); + if (children.length > 0) { + m_stack.push( + createChildrenReducerTaskState(m_state, children, max), + ...children, + ); + } + }), + ); - m_stack.push(createChildrenReducer(m_snapshot, children, min), ...children); + const children = types.flatMap((type, index) => + createIndexSignatureTaskStates( + parameters, + m_state, + ts.IndexKind.String, + getTypeData(type, typeNodes?.[index]), + ), + ); + if (children.length > 0) { + m_stack.push( + createChildrenReducerTaskState(m_state, children, max), + ...children, + ); } +} - function handleTypeFunction(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); +function taskApplyOverride(m_state: TaskStateApplyOverride) { + assert( + m_state.override.from !== undefined, + "Override should have already been applied", + ); - m_snapshot.immutability = Immutability.Immutable; + if ( + (m_state.override.from <= m_state.taskState.immutability && + m_state.taskState.immutability <= m_state.override.to) || + (m_state.override.from >= m_state.taskState.immutability && + m_state.taskState.immutability >= m_state.override.to) + ) { + m_state.taskState.immutability = m_state.override.to; } +} - function handleTypeTuple(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); - assert(checker.isTupleType(m_snapshot.typeData.type)); - - if (!m_snapshot.typeData.type.target.readonly) { - m_snapshot.immutability = Immutability.Mutable; +function taskCheckDone( + m_state: TaskStateCheckDone, + immutability: Immutability, +) { + if (immutability !== Immutability.Calculating) { + m_state.taskState.limits.max = min( + m_state.taskState.limits.max, + immutability, + ); + if (m_state.taskState.limits.min >= m_state.taskState.limits.max) { + m_state.taskState.immutability = m_state.taskState.limits.min; return; } - handleTypeArray(m_snapshot); } - function handleTypeArray(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); - - m_stack.push( - createDoneChecker(m_snapshot, (m_objectType) => { - assert("limits" in m_objectType); - - m_objectType.stage = SnapshotStage.Done; - m_objectType.immutability = max( - m_objectType.limits.min, - m_objectType.limits.max, - ); - m_stack.push(m_objectType); - }), - createDoneChecker(m_snapshot, (m_objectType) => { - assert("limits" in m_objectType); + m_state.notDoneAction(); +} - if (isTypeReferenceWithTypeArguments(m_objectType.typeData.type)) { - handleTypeArguments(m_snapshot); - } - }), +function taskReduceChildren(m_state: TaskStateChildrenReducer): void { + m_state.immutability = ( + m_state.children[0] ?? assert.fail("no children") + ).immutability; + for (let m_index = 1; m_index < m_state.children.length; m_index++) { + m_state.immutability = m_state.childrenReducer( + m_state.immutability, + m_state.children[m_index]!.immutability, ); - - handleTypeObject(m_snapshot); } +} - function handleTypeObject(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.Init); - assert(!("limits" in m_snapshot), "Limits already set"); - assert(cast(m_snapshot)); +function handleTypeUnion(m_stack: Stack, m_state: TaskStateTriage) { + assert(isUnionType(m_state.typeData.type)); - m_snapshot.stage = SnapshotStage.ObjectProperties; - m_snapshot.limits = { - max: maxImmutability, - min: Immutability.Mutable, - }; + const children = m_state.typeData.type.types.map((type, index) => { + const typeNode = + m_state.typeData.typeNode !== null && + ts.isUnionTypeNode(m_state.typeData.typeNode) + ? m_state.typeData.typeNode.types[index] + : undefined; // TODO: can we safely get a union type node nested within a different type node? - m_stack.push( - createDoneChecker(m_snapshot, (m_objectType) => { - assert(m_objectType.stage === SnapshotStage.ObjectProperties); - assert("limits" in m_objectType); - - if (isTypeReferenceWithTypeArguments(m_objectType.typeData.type)) { - m_objectType.stage = SnapshotStage.ObjectTypeReference; - m_stack.push(m_objectType); - return; - } + return createNewTaskState(getTypeData(type, typeNode)); + }); - m_objectType.stage = SnapshotStage.ObjectIndexSignature; - m_stack.push(m_objectType); - }), - ); + m_stack.push( + createChildrenReducerTaskState(m_state, children, min), + ...children, + ); +} - const properties = m_snapshot.typeData.type.getProperties(); - if (properties.length > 0) { - for (const property of properties) { - if ( - isPropertyReadonlyInType( - m_snapshot.typeData.type, - property.getEscapedName(), - checker, - ) || - // Ignore "length" for tuples. - // TODO: Report this issue to upstream. - ((property.escapedName as string) === "length" && - checker.isTupleType(m_snapshot.typeData.type)) - ) { - continue; - } +function handleTypeIntersection( + parameters: Parameters, + m_stack: Stack, + m_state: TaskState, +) { + assert(m_state.stage === TaskStateStage.Triage); - const name = ts.getNameOfDeclaration(property.valueDeclaration); - if (name !== undefined && ts.isPrivateIdentifier(name)) { - continue; - } + handleTypeObject(parameters, m_stack, m_state); +} - const declarations = property.getDeclarations() ?? []; - if (declarations.length > 0) { - if ( - declarations.some( - (declaration) => - hasSymbol(declaration) && - isSymbolFlagSet(declaration.symbol, ts.SymbolFlags.Method), - ) - ) { - m_snapshot.limits.max = min( - m_snapshot.limits.max, - Immutability.ReadonlyDeep, - ); - continue; - } - - if ( - declarations.every( - (declaration) => - ts.isPropertySignature(declaration) && - declaration.type !== undefined && - ts.isFunctionTypeNode(declaration.type), - ) - ) { - m_snapshot.limits.max = min( - m_snapshot.limits.max, - Immutability.ReadonlyDeep, - ); - continue; - } - } +function handleTypeConditional( + parameters: Parameters, + m_stack: Stack, + m_state: TaskState, +) { + assert(m_state.stage === TaskStateStage.Triage); + assert(isConditionalType(m_state.typeData.type)); + + const checker = parameters.program.getTypeChecker(); + const children = [ + m_state.typeData.type.root.node.trueType, + m_state.typeData.type.root.node.falseType, + ].map((typeNode) => { + const type = checker.getTypeFromTypeNode(typeNode); + return createNewTaskState(getTypeData(type, typeNode)); + }); + + m_stack.push( + createChildrenReducerTaskState(m_state, children, min), + ...children, + ); +} - m_snapshot.immutability = Immutability.Mutable; - return; - } - } +function handleTypeFunction(m_state: TaskState) { + assert(m_state.stage === TaskStateStage.Triage); - const propertyNodes = new Map( - m_snapshot.typeData.typeNode !== null && - hasType(m_snapshot.typeData.typeNode) && - m_snapshot.typeData.typeNode.type !== undefined && - ts.isTypeLiteralNode(m_snapshot.typeData.typeNode.type) - ? m_snapshot.typeData.typeNode.type.members - .map((member): [string, ts.TypeNode] | undefined => - member.name === undefined || - !hasType(member) || - member.type === undefined - ? undefined - : [propertyNameToString(member.name), member.type], - ) - .filter((v: T | undefined): v is T => v !== undefined) - : [], - ); + m_state.immutability = Immutability.Immutable; +} - const children = properties - .map((property) => { - const propertyType = getTypeOfPropertyOfType( - checker, - m_snapshot.typeData.type, - property, - ); - if ( - propertyType === undefined || - (isIntrinsicType(propertyType) && - propertyType.intrinsicName === "error") - ) { - return null; - } +function handleTypeTuple( + parameters: Parameters, + m_stack: Stack, + m_state: TaskState, +) { + assert(m_state.stage === TaskStateStage.Triage); + assert( + parameters.program.getTypeChecker().isTupleType(m_state.typeData.type), + ); - const propertyTypeNode = propertyNodes.get( - property.getEscapedName() as string, - ); + if (!m_state.typeData.type.target.readonly) { + m_state.immutability = Immutability.Mutable; + return; + } + handleTypeArray(parameters, m_stack, m_state); +} - return createSnapshot(getTypeData(propertyType, propertyTypeNode)); - }) - .filter((snapshot): snapshot is SnapshotBase => snapshot !== null); +function handleTypeArray( + parameters: Parameters, + m_stack: Stack, + m_state: TaskState, +) { + assert(m_state.stage === TaskStateStage.Triage); + + // It will have limits after being processed by `handleTypeObject`. + const m_stateWithLimits = m_state as unknown as TaskStateWithLimits; + m_stack.push( + createCheckDoneTaskState(m_stateWithLimits, () => { + m_stateWithLimits.stage = TaskStateStage.Done; + m_stateWithLimits.immutability = max( + m_stateWithLimits.limits.min, + m_stateWithLimits.limits.max, + ); + m_stack.push(m_stateWithLimits); + }), - if (children.length > 0) { - m_snapshot.limits.min = Immutability.ReadonlyShallow; + createCheckDoneTaskState(m_stateWithLimits, () => { + if (isTypeReferenceWithTypeArguments(m_stateWithLimits.typeData.type)) { + handleTypeArguments(m_stack, m_stateWithLimits); + } + }), + ); - m_stack.push( - createChildrenReducer(m_snapshot, children, min), - ...children, - ); - } - } + handleTypeObject(parameters, m_stack, m_state); +} - function handleTypeObjectTypeReference(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.ObjectTypeReference); - assert("limits" in m_snapshot); +function handleTypeObject( + parameters: Parameters, + m_stack: Stack, + m_state: TaskStateTriage, +) { + // Add limits. + const m_stateWithLimits = m_state as unknown as TaskStateWithLimits; + m_stateWithLimits.stage = TaskStateStage.ObjectProperties; + m_stateWithLimits.limits = { + ...parameters.immutabilityLimits, + }; - m_stack.push( - createDoneChecker(m_snapshot, (m_objectType) => { - assert(m_objectType.stage === SnapshotStage.ObjectTypeReference); - m_objectType.stage = SnapshotStage.ObjectIndexSignature; - m_stack.push(m_objectType); - }), - ); - handleTypeArguments(m_snapshot); - } + m_stack.push( + createCheckDoneTaskState(m_stateWithLimits, () => { + if (isTypeReferenceWithTypeArguments(m_stateWithLimits.typeData.type)) { + m_stateWithLimits.stage = TaskStateStage.ObjectTypeReference; + m_stack.push(m_stateWithLimits); + return; + } - function handleTypeObjectIndexSignature(m_snapshot: Snapshot) { - assert(m_snapshot.stage === SnapshotStage.ObjectIndexSignature); - assert("limits" in m_snapshot); - assert( - m_snapshot.typeData.typeNode === null || - isIntersectionType(m_snapshot.typeData.type) === - ts.isIntersectionTypeNode(m_snapshot.typeData.typeNode), - ); + m_stateWithLimits.stage = TaskStateStage.ObjectIndexSignature; + m_stack.push(m_stateWithLimits); + }), + ); - const [types, typeNodes] = isIntersectionType(m_snapshot.typeData.type) - ? [ - m_snapshot.typeData.type.types, - (m_snapshot.typeData.typeNode as ts.IntersectionTypeNode | null) - ?.types, - ] - : [ - [m_snapshot.typeData.type], - m_snapshot.typeData.typeNode === null - ? undefined - : [m_snapshot.typeData.typeNode], - ]; + const checker = parameters.program.getTypeChecker(); - m_stack.push( - createDoneChecker(m_snapshot, (m_objectType) => { - assert(m_objectType.stage === SnapshotStage.ObjectIndexSignature); + const properties = m_stateWithLimits.typeData.type.getProperties(); + if (properties.length > 0) { + for (const property of properties) { + if ( + isPropertyReadonlyInType( + m_stateWithLimits.typeData.type, + property.getEscapedName(), + checker, + ) || + // Ignore "length" for tuples. + // TODO: Report this issue to upstream. + ((property.escapedName as string) === "length" && + checker.isTupleType(m_stateWithLimits.typeData.type)) + ) { + continue; + } - m_objectType.stage = SnapshotStage.Done; - m_objectType.immutability = max( - m_objectType.limits.min, - m_objectType.limits.max, - ); - m_stack.push(m_objectType); - }), - createDoneChecker(m_snapshot, (m_objectType) => { - assert(m_objectType.stage === SnapshotStage.ObjectIndexSignature); - assert("limits" in m_objectType); - - const children = types.flatMap((type, index) => - createIndexSignatureSnapshots( - m_objectType, - ts.IndexKind.Number, - getTypeData(type, typeNodes?.[index]), - ), - ); - if (children.length > 0) { - m_stack.push( - createChildrenReducer(m_objectType, children, max), - ...children, + const name = ts.getNameOfDeclaration(property.valueDeclaration); + if (name !== undefined && ts.isPrivateIdentifier(name)) { + continue; + } + + const declarations = property.getDeclarations() ?? []; + if (declarations.length > 0) { + if ( + declarations.some( + (declaration) => + hasSymbol(declaration) && + isSymbolFlagSet(declaration.symbol, ts.SymbolFlags.Method), + ) + ) { + m_stateWithLimits.limits.max = min( + m_stateWithLimits.limits.max, + Immutability.ReadonlyDeep, ); + continue; } - }), - ); - const children = types.flatMap((type, index) => - createIndexSignatureSnapshots( - m_snapshot, - ts.IndexKind.String, - getTypeData(type, typeNodes?.[index]), - ), - ); - if (children.length > 0) { - m_stack.push( - createChildrenReducer(m_snapshot, children, max), - ...children, - ); + if ( + declarations.every( + (declaration) => + ts.isPropertySignature(declaration) && + declaration.type !== undefined && + ts.isFunctionTypeNode(declaration.type), + ) + ) { + m_stateWithLimits.limits.max = min( + m_stateWithLimits.limits.max, + Immutability.ReadonlyDeep, + ); + continue; + } + } + + m_stateWithLimits.immutability = Immutability.Mutable; + return; } } - function handleTypeObjectCheckDone(m_snapshot: Snapshot) { - assert("snapshot" in m_snapshot); - assert("limits" in m_snapshot.snapshot); - assert("notDoneAction" in m_snapshot); + const propertyNodes = new Map( + m_stateWithLimits.typeData.typeNode !== null && + hasType(m_stateWithLimits.typeData.typeNode) && + m_stateWithLimits.typeData.typeNode.type !== undefined && + ts.isTypeLiteralNode(m_stateWithLimits.typeData.typeNode.type) + ? m_stateWithLimits.typeData.typeNode.type.members + .map((member): [string, ts.TypeNode] | undefined => + member.name === undefined || + !hasType(member) || + member.type === undefined + ? undefined + : [propertyNameToString(member.name), member.type], + ) + .filter((v: T | undefined): v is T => v !== undefined) + : [], + ); - if (m_PreviousImmutability !== Immutability.Calculating) { - m_snapshot.snapshot.limits.max = min( - m_snapshot.snapshot.limits.max, - m_PreviousImmutability, + const children = properties + .map((property) => { + const propertyType = getTypeOfPropertyOfType( + checker, + m_stateWithLimits.typeData.type, + property, ); - if (m_snapshot.snapshot.limits.min >= m_snapshot.snapshot.limits.max) { - m_snapshot.snapshot.immutability = m_snapshot.snapshot.limits.min; - return; + if ( + propertyType === undefined || + (isIntrinsicType(propertyType) && + propertyType.intrinsicName === "error") + ) { + return null; } - } - m_snapshot.notDoneAction(m_snapshot.snapshot); - } + const propertyTypeNode = propertyNodes.get( + property.getEscapedName() as string, + ); + + return createNewTaskState(getTypeData(propertyType, propertyTypeNode)); + }) + .filter((taskState): taskState is TaskStateTriage => taskState !== null); - function handleTypeArguments(m_snapshot: Snapshot) { - assert("typeData" in m_snapshot); - assert(isTypeReferenceWithTypeArguments(m_snapshot.typeData.type)); + if (children.length > 0) { + m_stateWithLimits.limits.min = Immutability.ReadonlyShallow; - const children = m_snapshot.typeData.type.typeArguments.map((type) => - createSnapshot(getTypeData(type, undefined)), + m_stack.push( + createChildrenReducerTaskState(m_stateWithLimits, children, min), + ...children, ); - m_stack.push(createChildrenReducer(m_snapshot, children, min), ...children); } +} - function createIndexSignatureSnapshots( - m_snapshot: SnapshotBase, - kind: ts.IndexKind, - typeData: TypeData, - ): Array> { - const indexInfo = checker.getIndexInfoOfType(typeData.type, kind); - if (indexInfo === undefined) { - m_snapshot.immutability = Immutability.Unknown; - return []; - } - - if (maxImmutability <= Immutability.ReadonlyShallow) { - m_snapshot.immutability = Immutability.ReadonlyShallow; - return []; - } - - if (indexInfo.isReadonly) { - if (indexInfo.type === typeData.type) { - m_snapshot.immutability = maxImmutability; - return []; - } +function handleTypeArguments(m_stack: Stack, m_state: TaskStateWithLimits) { + assert(isTypeReferenceWithTypeArguments(m_state.typeData.type)); - const child = createSnapshot( - getTypeData(indexInfo.type, undefined), // TODO: can we get a type node for this? - ); + const children = m_state.typeData.type.typeArguments.map((type) => + createNewTaskState(getTypeData(type, undefined)), + ); + m_stack.push( + createChildrenReducerTaskState(m_state, children, min), + ...children, + ); +} - return [ - createChildrenReducer( - m_snapshot, - [{ immutability: Immutability.ReadonlyShallow }, child], - max, - ), - child, - ]; - } +function handleTypePrimitive(m_state: TaskStateTriage) { + m_state.immutability = Immutability.Immutable; +} - m_snapshot.immutability = Immutability.Mutable; +function createIndexSignatureTaskStates( + parameters: Parameters, + m_state: TaskStateBase, + kind: ts.IndexKind, + typeData: TypeData, +): Array> { + const checker = parameters.program.getTypeChecker(); + const indexInfo = checker.getIndexInfoOfType(typeData.type, kind); + if (indexInfo === undefined) { + m_state.immutability = Immutability.Unknown; return []; } - function handleTypePrimitive(m_snapshot: Snapshot) { - assert("immutability" in m_snapshot); - m_snapshot.immutability = Immutability.Immutable; + if (parameters.immutabilityLimits.max <= Immutability.ReadonlyShallow) { + m_state.immutability = Immutability.ReadonlyShallow; + return []; } - /** - * Get the override for the type if it has one. - */ - function getOverride(m_snapshot: Snapshot) { - assert("typeData" in m_snapshot); - return overrides.find((potentialOverride) => - typeMatchesSpecifier( - m_snapshot.typeData, - potentialOverride.type, - program, - ), + if (indexInfo.isReadonly) { + if (indexInfo.type === typeData.type) { + m_state.immutability = parameters.immutabilityLimits.max; + return []; + } + + const child = createNewTaskState( + getTypeData(indexInfo.type, undefined), // TODO: can we get a type node for this? ); - } - function reduceChildren(m_snapshot: Snapshot): void { - assert("children" in m_snapshot && "childrenReducer" in m_snapshot); - assert(cast(m_snapshot)); - - m_snapshot.immutability = ( - m_snapshot.children[0] ?? assert.fail("no children") - ).immutability; - for (let m_index = 1; m_index < m_snapshot.children.length; m_index++) { - m_snapshot.immutability = m_snapshot.childrenReducer( - m_snapshot.immutability, - m_snapshot.children[m_index]!.immutability, - ); - } + return [ + createChildrenReducerTaskState( + m_state, + [{ immutability: Immutability.ReadonlyShallow }, child], + max, + ), + child, + ]; } -} -/** - * Is type a (non-namespace) function? - */ -function isFunction(type: ts.Type) { - return ( - type.getCallSignatures().length > 0 && type.getProperties().length === 0 - ); -} - -function isTypeReferenceWithTypeArguments( - type: ts.Type, -): type is ts.TypeReference & { - typeArguments: NonNullable; -} { - return ( - isTypeReference(type) && - type.typeArguments !== undefined && - type.typeArguments.length > 0 - ); + m_state.immutability = Immutability.Mutable; + return []; } diff --git a/src/utils.ts b/src/utils.ts index ee65e43d..0b8f84b8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import path from "node:path"; -import { isIntrinsicErrorType } from "ts-api-utils"; +import { isIntrinsicErrorType, isTypeReference } from "ts-api-utils"; import ts from "typescript"; import { typeToString, type TypeName } from "./type-to-string"; @@ -351,8 +351,25 @@ function qualifiedNameToString(qualifiedName: ts.QualifiedName): string { } /** - * Cast the type. + * Is type a (non-namespace) function? */ -export function cast(value: U): value is T { - return true; +export function isFunction(type: ts.Type) { + return ( + type.getCallSignatures().length > 0 && type.getProperties().length === 0 + ); +} + +/** + * Is type a type reference with type arguments? + */ +export function isTypeReferenceWithTypeArguments( + type: ts.Type, +): type is ts.TypeReference & { + typeArguments: NonNullable; +} { + return ( + isTypeReference(type) && + type.typeArguments !== undefined && + type.typeArguments.length > 0 + ); } From bb513cbada8dd8ea02d431543a0d30ebabe0e5e2 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Tue, 19 Mar 2024 22:28:56 +1300 Subject: [PATCH 3/3] docs: add calculate basic docs --- src/calculate.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/src/calculate.ts b/src/calculate.ts index 0751764d..5a18ac3a 100644 --- a/src/calculate.ts +++ b/src/calculate.ts @@ -71,6 +71,9 @@ export function getDefaultOverrides(): ImmutabilityOverrides { */ export type ImmutabilityCache = WeakMap; +/** + * The bounds on the immutability of a type. + */ type ImmutabilityLimits = { min: Immutability; max: Immutability; @@ -120,6 +123,9 @@ export function getTypeImmutability( ); } +/** + * The different states a task can be in. + */ type TaskState = | TaskStateTriage | TaskStateChildrenReducer @@ -275,6 +281,9 @@ function getTypeImmutabilityHelper( return m_state.immutability; } +/** + * Create the state for a new task. + */ function createNewTaskState(typeData: TypeData): TaskStateTriage { return { typeData, @@ -283,6 +292,9 @@ function createNewTaskState(typeData: TypeData): TaskStateTriage { }; } +/** + * Create the state for a new task that reduces the children task states. + */ function createChildrenReducerTaskState( parent: TaskStateBase, children: TaskStateChildrenReducer["children"], @@ -297,6 +309,10 @@ function createChildrenReducerTaskState( }; } +/** + * Create the state for a new task that checks if the previous task has found + * the type's immutability. If it hasn't the given action is called. + */ function createCheckDoneTaskState( taskState: TaskStateCheckDone["taskState"], notDoneAction: TaskStateCheckDone["notDoneAction"], @@ -308,6 +324,10 @@ function createCheckDoneTaskState( }; } +/** + * Create the state for a new task that applies an override if the from + * immutability check matches. + */ function createApplyOverrideTaskState( taskState: TaskStateApplyOverride["taskState"], override: ImmutabilityOverrides[number], @@ -329,7 +349,7 @@ function getOverride(parameters: Parameters, typeData: TypeData) { } /** - * The first stage for all types. + * Process the state and create any new next task that need to be used to process it. */ function taskTriage( parameters: Parameters, @@ -412,6 +432,10 @@ function taskTriage( handleTypePrimitive(m_state); } +/** + * We know we're dealling with a TypeReference, check its type arguments. + * If we're not done, move on to the ObjectIndexSignature task. + */ function taskObjectTypeReference(m_stack: Stack, m_state: TaskStateWithLimits) { m_stack.push( createCheckDoneTaskState(m_state, () => { @@ -422,6 +446,9 @@ function taskObjectTypeReference(m_stack: Stack, m_state: TaskStateWithLimits) { handleTypeArguments(m_stack, m_state); } +/** + * We know we're dealling with an object, check its index signatures. + */ function taskObjectIndexSignature( parameters: Parameters, m_stack: Stack, @@ -451,6 +478,7 @@ function taskObjectIndexSignature( m_state.immutability = max(m_state.limits.min, m_state.limits.max); m_stack.push(m_state); }), + createCheckDoneTaskState(m_state, () => { const children = types.flatMap((type, index) => createIndexSignatureTaskStates( @@ -485,6 +513,9 @@ function taskObjectIndexSignature( } } +/** + * Apply an override if its criteria are met. + */ function taskApplyOverride(m_state: TaskStateApplyOverride) { assert( m_state.override.from !== undefined, @@ -501,6 +532,9 @@ function taskApplyOverride(m_state: TaskStateApplyOverride) { } } +/** + * Check if we're found the type's immutability. + */ function taskCheckDone( m_state: TaskStateCheckDone, immutability: Immutability, @@ -519,6 +553,9 @@ function taskCheckDone( m_state.notDoneAction(); } +/** + * Reduce the children's immutability values to a single value. + */ function taskReduceChildren(m_state: TaskStateChildrenReducer): void { m_state.immutability = ( m_state.children[0] ?? assert.fail("no children") @@ -531,6 +568,9 @@ function taskReduceChildren(m_state: TaskStateChildrenReducer): void { } } +/** + * Handle a type we know is a union. + */ function handleTypeUnion(m_stack: Stack, m_state: TaskStateTriage) { assert(isUnionType(m_state.typeData.type)); @@ -550,6 +590,9 @@ function handleTypeUnion(m_stack: Stack, m_state: TaskStateTriage) { ); } +/** + * Handle a type we know is an intersection. + */ function handleTypeIntersection( parameters: Parameters, m_stack: Stack, @@ -560,6 +603,9 @@ function handleTypeIntersection( handleTypeObject(parameters, m_stack, m_state); } +/** + * Handle a type we know is a conditional type. + */ function handleTypeConditional( parameters: Parameters, m_stack: Stack, @@ -583,12 +629,18 @@ function handleTypeConditional( ); } +/** + * Handle a type we know is a non-namespace function. + */ function handleTypeFunction(m_state: TaskState) { assert(m_state.stage === TaskStateStage.Triage); m_state.immutability = Immutability.Immutable; } +/** + * Handle a type we know is a tuple. + */ function handleTypeTuple( parameters: Parameters, m_stack: Stack, @@ -606,6 +658,9 @@ function handleTypeTuple( handleTypeArray(parameters, m_stack, m_state); } +/** + * Handle a type we know is an array (this includes tuples). + */ function handleTypeArray( parameters: Parameters, m_stack: Stack, @@ -635,6 +690,9 @@ function handleTypeArray( handleTypeObject(parameters, m_stack, m_state); } +/** + * Handle a type that all we know is that it's an object. + */ function handleTypeObject( parameters: Parameters, m_stack: Stack, @@ -771,6 +829,9 @@ function handleTypeObject( } } +/** + * Handle the type arguments of a type reference. + */ function handleTypeArguments(m_stack: Stack, m_state: TaskStateWithLimits) { assert(isTypeReferenceWithTypeArguments(m_state.typeData.type)); @@ -783,10 +844,16 @@ function handleTypeArguments(m_stack: Stack, m_state: TaskStateWithLimits) { ); } +/** + * Handle a primitive type. + */ function handleTypePrimitive(m_state: TaskStateTriage) { m_state.immutability = Immutability.Immutable; } +/** + * Create the task states for analyzing an object's index signatures. + */ function createIndexSignatureTaskStates( parameters: Parameters, m_state: TaskStateBase,