typeChanges

tcmts.cli.semver_checks.typeChanges · #pww3ilgh · term · runs anywhere

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

used by