callable

tcmts.cli.semver_checks.callable · #fk52ts6y · term · runs anywhere

How call (or construct) signatures changed, from the side of a caller.

import { change, fits, FLAGS, LINTS, params, sig } from "#codebase/tcmts/cli/semver_checks";
import type { LintId, Param } from "#codebase/tcmts/cli/semver_checks";
import type { TypeChecker } from "#codebase/tcmts/core/ts";
import { SignatureKind as ts_SignatureKind } from "typescript/sync";
import type { Type as ts_Type } from "typescript/sync";

export function callable(checker: TypeChecker, ot: ts_Type, nt: ts_Type, kind: ts_SignatureKind, add: (l: LintId, d?: string) => void) {
  const [os, ns] = [checker.getSignaturesOfType(ot, kind), checker.getSignaturesOfType(nt, kind)];
  if (!os.length) return;
  const ctor = kind === ts_SignatureKind.Construct;
  if (!ns.length) return add(ctor ? "constructor_incompatible" : "function_became_value");
  const str = (t: ts_Type) => checker.typeToString(t, undefined, FLAGS);
  if (os.length === 1 && ns.length === 1) {
    const [a, b] = [os[0]!, ns[0]!];
    if (a.getTypeParameters().length !== b.getTypeParameters().length) return add(ctor ? "constructor_incompatible" : "function_type_parameter_count_changed");
    // generic signatures' parameters mention their own type parameters: compare them whole
    if (a.getTypeParameters().length) return fits(checker, b, a) ? undefined : add(ctor ? "constructor_incompatible" : "function_signature_incompatible", `${sig(checker, a)} → ${sig(checker, b)}`);
    const before = params(checker, a);
    const after = params(checker, b);
    const lint = (l: LintId, d?: string) => add(ctor && LINTS[l].level === "major" ? "constructor_incompatible" : l, d);
    const required = (ps: Param[]) => ps.filter((x) => !x.optional && !x.rest).length;
    if (required(after) > required(before)) {
      const madeRequired = after.find((x, i) => before[i] && before[i]!.optional && !x.optional && !x.rest);
      if (madeRequired) lint("function_parameter_made_required", madeRequired.name);
      else lint("function_required_parameter_added", after.filter((x, i) => !x.optional && !x.rest && i >= required(before)).map((x) => `${x.name}: ${str(x.type)}`).join(", "));
    }
    if (after.length < before.length && !after.some((x) => x.rest)) lint("function_parameter_removed", before.slice(after.length).map((x) => x.name).join(", "));
    for (let i = 0; i < Math.min(before.length, after.length); i++) {
      const [x, y] = [before[i]!, after[i]!];
      if (!checker.isTypeAssignableTo(x.type, y.type)) lint("function_parameter_type_narrowed", change(`${x.name}: `, str(x.type), str(y.type)));
      else if (!checker.isTypeAssignableTo(y.type, x.type)) lint("function_parameter_type_widened", change(`${x.name}: `, str(x.type), str(y.type)));
      if (!x.optional && y.optional) lint("function_parameter_made_optional", y.name);
    }
    if (after.length > before.length && after.slice(before.length).every((x) => x.optional || x.rest)) lint("function_optional_parameter_added", after.slice(before.length).map((x) => x.name).join(", "));
    const [r, s] = [checker.getReturnTypeOfSignature(a), checker.getReturnTypeOfSignature(b)];
    if (!checker.isTypeAssignableTo(s, r)) lint("function_return_type_widened", change("", str(r), str(s)));
    else if (!checker.isTypeAssignableTo(r, s)) lint("function_return_type_narrowed", change("", str(r), str(s)));
    return;
  }
  // overloads: every old signature needs a new one its calls fit
  const unmatched = os.filter((a) => !ns.some((b) => fits(checker, b, a)));
  if (unmatched.length) add(ctor ? "constructor_incompatible" : "function_signature_incompatible", unmatched.map((a) => sig(checker, a)).join("; "));
  else if (ns.length > os.length || ns.some((b) => !os.some((a) => fits(checker, a, b)))) add("function_signature_extended");
}

uses

used by