typeChanges
How a type changed: its cases, its properties, or as a whole.
import { change, FLAGS, properties } from "#codebase/tcmts/cli/semver_checks"; import type { LintId } from "#codebase/tcmts/cli/semver_checks"; import type { TypeChecker } from "#codebase/tcmts/core/ts"; import { SyntaxKind as ts_SyntaxKind } from "typescript/ast"; import type { ClassDeclaration as ts_ClassDeclaration, InterfaceDeclaration as ts_InterfaceDeclaration, TypeAliasDeclaration as ts_TypeAliasDeclaration, } from "typescript/ast"; import { TypeFlags as ts_TypeFlags } from "typescript/sync"; import type { Symbol as ts_Symbol, Type as ts_Type, UnionOrIntersectionType as ts_UnionOrIntersectionType, } from "typescript/sync"; export function typeChanges(checker: TypeChecker, ot: ts_Type, nt: ts_Type, o: ts_Symbol, n: ts_Symbol, add: (l: LintId, d?: string) => void) { const str = (t: ts_Type) => checker.typeToString(t, undefined, FLAGS); const params = (s: ts_Symbol) => { const kinds = [ts_SyntaxKind.TypeAliasDeclaration, ts_SyntaxKind.InterfaceDeclaration, ts_SyntaxKind.ClassDeclaration]; const d = s.declarations?.find((x) => kinds.includes(x.kind))?.resolve() as ts_TypeAliasDeclaration | ts_InterfaceDeclaration | ts_ClassDeclaration | undefined; return d?.typeParameters?.length ?? 0; }; if (params(o) !== params(n)) return add("type_type_parameter_count_changed", `${params(o)} → ${params(n)}`); // a generic type mentions its own type parameters: compare it as written if (params(o)) return str(ot) === str(nt) ? undefined : add("type_changed", change("", str(ot), str(nt))); // cases and properties first: an added optional property, say, leaves the two types mutually assignable if (ot.flags & ts_TypeFlags.Union && nt.flags & ts_TypeFlags.Union) { const cases = (t: ts_Type) => new Map(((t as ts_UnionOrIntersectionType).getTypes() ?? []).map((x) => [str(x), x])); const [a, b] = [cases(ot), cases(nt)]; const added = [...b.keys()].filter((k) => !a.has(k)); const removed = [...a.keys()].filter((k) => !b.has(k)); if (added.length) add("type_union_member_added", added.join(" | ")); if (removed.length) add("type_union_member_removed", removed.join(" | ")); if (added.length || removed.length) return; } const isObject = (t: ts_Type) => !!(t.flags & (ts_TypeFlags.Object | ts_TypeFlags.Intersection)); if (isObject(ot) && isObject(nt)) { let found = 0; properties(checker, ot, nt, (l, d) => (found++, add(l, d))); if (found) return; } if (checker.isTypeAssignableTo(ot, nt) && checker.isTypeAssignableTo(nt, ot)) return; add("type_changed", change("", str(ot), str(nt))); }
uses
- tcmts.cli.semver_checks.FLAGS
- tcmts.cli.semver_checks.LintId
- tcmts.cli.semver_checks.change
- tcmts.cli.semver_checks.properties
- tcmts.core.ts.TypeChecker