mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Degrade a canonical form whose union mapped to the checked dynamic
Localized failure's canonical fallback classified a failing type as an exact mapping whenever its canonical form mapped to something other than the checked dynamic. A record or array holding a union that mapCanonical collapses to the checked dynamic (an arm such as {} or unknown) then counted as exact, so its slot got no SC6104 warning and its dynamic operations no attribution.
mapCanonical now notes such a collapse (CanonMapNotes), and canonicalType refuses the form when it happened, so the type degrades through the pushed-down form like any other.
This commit is contained in:
@@ -25,8 +25,21 @@ import {
|
||||
|
||||
const MAX_DEPTH = 24;
|
||||
|
||||
/** What a mapping did besides its result. */
|
||||
export interface CanonMapNotes {
|
||||
/** A union in the form mapped to the checked dynamic because an arm
|
||||
* did (`string | {}`): the result holds a dynamic part the form did not
|
||||
* spell as `unknown`. */
|
||||
collapsedToDynamic: boolean;
|
||||
}
|
||||
|
||||
/** The IR type of a canonical node, or null when no rule applies. */
|
||||
export function mapCanonical(c: Canon, ctx: TypeMapperCtx, depth = 0): IrType | null {
|
||||
export function mapCanonical(
|
||||
c: Canon,
|
||||
ctx: TypeMapperCtx,
|
||||
depth = 0,
|
||||
notes?: CanonMapNotes,
|
||||
): IrType | null {
|
||||
if (depth > MAX_DEPTH) return null;
|
||||
const viaChecker = (): IrType | null => (c.src === undefined ? null : mapType(c.src, ctx));
|
||||
switch (c.k) {
|
||||
@@ -55,13 +68,13 @@ export function mapCanonical(c: Canon, ctx: TypeMapperCtx, depth = 0): IrType |
|
||||
case "tuple":
|
||||
return viaChecker();
|
||||
case "array": {
|
||||
const elem = mapCanonical(c.elem, ctx, depth + 1);
|
||||
const elem = mapCanonical(c.elem, ctx, depth + 1, notes);
|
||||
return elem === null ? null : { kind: "array", elem };
|
||||
}
|
||||
case "union": {
|
||||
const byKey = new Map<string, IrType>();
|
||||
for (const arm of c.arms) {
|
||||
const mapped = mapCanonical(arm, ctx, depth + 1);
|
||||
const mapped = mapCanonical(arm, ctx, depth + 1, notes);
|
||||
if (mapped === null || mapped.kind === "void") return null;
|
||||
if (mapped.kind === "union") {
|
||||
const def = ctx.unions.get(mapped.unionId);
|
||||
@@ -71,7 +84,10 @@ export function mapCanonical(c: Canon, ctx: TypeMapperCtx, depth = 0): IrType |
|
||||
}
|
||||
const arms = [...byKey.values()];
|
||||
if (arms.length === 1) return arms[0]!;
|
||||
if (arms.some((a) => a.kind === "dyn")) return DYN;
|
||||
if (arms.some((a) => a.kind === "dyn")) {
|
||||
if (notes !== undefined) notes.collapsedToDynamic = true;
|
||||
return DYN;
|
||||
}
|
||||
arms.sort((a, b) => (typeKey(a) < typeKey(b) ? -1 : 1));
|
||||
return { kind: "union", unionId: ctx.unions.intern(arms) };
|
||||
}
|
||||
@@ -86,7 +102,7 @@ export function mapCanonical(c: Canon, ctx: TypeMapperCtx, depth = 0): IrType |
|
||||
if (props.some((p) => p.accessor)) return viaChecker();
|
||||
// `{}` holds any non-nullish value: the checked dynamic, as mapType.
|
||||
if (props.length === 0) return DYN;
|
||||
const structural = mapCanonicalRecord(props, ctx, depth);
|
||||
const structural = mapCanonicalRecord(props, ctx, depth, notes);
|
||||
// An object whose members do not map structurally (an interface over
|
||||
// Map or Set, methods) keeps its per-type recognition.
|
||||
return structural ?? viaChecker();
|
||||
@@ -100,10 +116,11 @@ function mapCanonicalRecord(
|
||||
props: readonly CanonProp[],
|
||||
ctx: TypeMapperCtx,
|
||||
depth: number,
|
||||
notes: CanonMapNotes | undefined,
|
||||
): IrType | null {
|
||||
const fields: { name: string; type: IrType }[] = [];
|
||||
for (const p of props) {
|
||||
let t = mapCanonical(p.type(), ctx, depth + 1);
|
||||
let t = mapCanonical(p.type(), ctx, depth + 1, notes);
|
||||
if (t === null) return null;
|
||||
if (p.optional && t.kind !== "dyn") {
|
||||
const armed = withUndefinedArm(t, ctx.unions);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
arrayRootOf,
|
||||
lowerWithArraySpecies,
|
||||
} from "./builtins/array-classes.js";
|
||||
import { canonShadowHook, mapCanonical } from "../canon-map.js";
|
||||
import { canonShadowHook, mapCanonical, type CanonMapNotes } from "../canon-map.js";
|
||||
import { Normalizer } from "../canon.js";
|
||||
import {
|
||||
compactDeclOrigins,
|
||||
@@ -10077,16 +10077,19 @@ export class Lowerer {
|
||||
}
|
||||
|
||||
/** The IR type of a type's canonical form (canon-map.ts), or null when
|
||||
* the form is irreducible, unbound, or only the checked dynamic. Runs
|
||||
* only at localized failure's commit points, so a program that compiles
|
||||
* today never consults it. */
|
||||
* the form is irreducible, unbound, only the checked dynamic, or holds a
|
||||
* union that mapped to the checked dynamic (such a slot is dynamic in
|
||||
* part, so it degrades like any other). Runs only at localized failure's
|
||||
* commit points, so a program that compiles today never consults it. */
|
||||
canonicalType(type: ts.Type): IrType | null {
|
||||
try {
|
||||
const resolve = this.typeCtx.resolveTypeParamTs;
|
||||
const c = new Normalizer(this.checker).canon(type, resolve ? (t) => resolve(t) : new Map());
|
||||
if (c.k === "opaque" || c.k === "param") return null;
|
||||
const { typeMemo: _strictMemo, ...strict } = this.typeCtx;
|
||||
const mapped = mapCanonical(c, { ...strict, canMemoizeType: () => false });
|
||||
const notes: CanonMapNotes = { collapsedToDynamic: false };
|
||||
const mapped = mapCanonical(c, { ...strict, canMemoizeType: () => false }, 0, notes);
|
||||
if (notes.collapsedToDynamic) return null;
|
||||
return mapped === null || mapped.kind === "void" || mapped.kind === "dyn" ? null : mapped;
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
||||
import { canonEqual, canonKey, Normalizer, type Canon } from "../src/frontend/canon.js";
|
||||
import { mapCanonical } from "../src/frontend/canon-map.js";
|
||||
import { mapCanonical, type CanonMapNotes } from "../src/frontend/canon-map.js";
|
||||
import {
|
||||
mapType,
|
||||
ShapeRegistry,
|
||||
@@ -76,6 +76,9 @@ beforeAll(() => {
|
||||
// Accessor members: one type, and a getter and setter of different types.
|
||||
export interface Gauge { get level(): number; set level(next: number) }
|
||||
export interface Cell { get value(): string; set value(next: string | number) }
|
||||
// A union with an arm that maps to the checked dynamic, inside a record.
|
||||
export type Collapsing = { tag: string | {}; name: string };
|
||||
export type Tight = { tag: string; name: string };
|
||||
// The same instantiations, each refined by an object type.
|
||||
export type RefinedMaps =
|
||||
| (Map<string, number> & { tag: number })[]
|
||||
@@ -319,6 +322,22 @@ describe("mapCanonical", () => {
|
||||
expect(mapCanonical(new Normalizer(checker).canon(cell), ctx())).toBeNull();
|
||||
expect(key(new Normalizer(checker).canon(cell))).toContain("get value");
|
||||
});
|
||||
test("a union that maps to the checked dynamic inside a record is noted", () => {
|
||||
const loose: CanonMapNotes = { collapsedToDynamic: false };
|
||||
const mapped = mapCanonical(
|
||||
new Normalizer(checker).canon(aliases.get("Collapsing")!),
|
||||
ctx(),
|
||||
0,
|
||||
loose,
|
||||
);
|
||||
expect(mapped?.kind).toBe("record");
|
||||
expect(loose.collapsedToDynamic).toBe(true);
|
||||
const tight: CanonMapNotes = { collapsedToDynamic: false };
|
||||
expect(
|
||||
mapCanonical(new Normalizer(checker).canon(aliases.get("Tight")!), ctx(), 0, tight)?.kind,
|
||||
).toBe("record");
|
||||
expect(tight.collapsedToDynamic).toBe(false);
|
||||
});
|
||||
test("shadow mode observes every successful top-level mapping", () => {
|
||||
const seen: string[] = [];
|
||||
const point = { kind: "object", className: "Point" } as const;
|
||||
|
||||
Reference in New Issue
Block a user