diff --git a/packages/compiler/src/frontend/lowering/class-protocols.ts b/packages/compiler/src/frontend/lowering/class-protocols.ts index 4de7f03ad..172224053 100644 --- a/packages/compiler/src/frontend/lowering/class-protocols.ts +++ b/packages/compiler/src/frontend/lowering/class-protocols.ts @@ -6,6 +6,8 @@ export class ClassProtocols { private implemented: Set | null = null; private readonly methods = new Map(); private readonly answers = new Map(); + /** The program classes each Pick or Omit view accepts (classesAcceptedBy). */ + private readonly accepted = new Map(); /** What markIdentityFlows learned about the program, while it runs. */ private facts: FlowFacts | null = null; @@ -414,6 +416,14 @@ export class ClassProtocols { for (const part of ts.constituentTypes(type)) add(part); return; } + // A Pick or Omit view's values are instances of the class it views, + // or of any other program class that has the view's members. + if (this.pickOfProgramClass(type)) { + add(type.getAliasTypeArguments()[0]!); + for (const other of this.classesAcceptedBy(type)) + for (const related of this.classFamily(other, facts)) out.add(related); + return; + } const symbol = type.getSymbol(); if (symbol === undefined) return; if (this.isProgramClassInstance(type)) { @@ -429,6 +439,30 @@ export class ClassProtocols { return out; } + /** The program classes with methods whose instances are assignable to + * `type`. */ + private classesAcceptedBy(type: ts.Type): ts.Symbol[] { + const known = this.accepted.get(type); + if (known !== undefined) return known; + this.index(); + const found = new Set(); + for (const entries of this.methods.values()) + for (const declaration of entries) { + const symbol = declaration.name + ? this.checker.getSymbolAtLocation(declaration.name) + : undefined; + if ( + symbol !== undefined && + !found.has(symbol) && + this.checker.isTypeAssignableTo(this.checker.getDeclaredTypeOfSymbol(symbol), type) + ) + found.add(symbol); + } + const result = [...found]; + this.accepted.set(type, result); + return result; + } + /** A class symbol with its base classes and the program classes that * extend it. */ private classFamily(symbol: ts.Symbol, facts: FlowFacts): ts.Symbol[] { @@ -570,6 +604,20 @@ export class ClassProtocols { } } + /** `Pick` or `Omit` over a program class instance type: a + * view of the class's own members, spelled inline instead of as an + * interface. Like an interface the class matches, a view that exposes a + * method needs a live receiver (see protocol). */ + private pickOfProgramClass(type: ts.Type): boolean { + const alias = type.getAliasSymbol(); + if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit")) + return false; + if (!alias.declarations.every((declaration) => declaration.getSourceFile().isDeclarationFile)) + return false; + const source = type.getAliasTypeArguments()[0]; + return source !== undefined && this.isProgramClassInstance(source); + } + private isProgramClassInstance(type: ts.Type): boolean { const symbol = type.getSymbol(); return ( @@ -592,6 +640,8 @@ export class ClassProtocols { return element !== undefined && this.carriesIdentity(element, marked); } if (this.isProgramClassInstance(type)) return true; + // A live Pick or Omit view of a program class holds the instance itself. + if (this.pickOfProgramClass(type) && this.usesCheckedIdentity(type)) return true; const symbol = type.getSymbol(); if (symbol === undefined) return false; if (marked.has(symbol)) return true; @@ -704,12 +754,13 @@ export class ClassProtocols { const properties = this.checker.getPropertiesOfType(type); if (symbol && protocols.has(symbol)) return true; if ( - !symbol || - !symbol.declarations.some( - (declaration) => - ts.isInterfaceDeclaration(declaration) && - !ts.getSourceFileOfNode(declaration).isDeclarationFile, - ) + !this.pickOfProgramClass(type) && + (!symbol || + !symbol.declarations.some( + (declaration) => + ts.isInterfaceDeclaration(declaration) && + !ts.getSourceFileOfNode(declaration).isDeclarationFile, + )) ) return false; for (const property of properties) { diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 880cccc8b..ac5949656 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -275,10 +275,13 @@ export function genericCallInstance( { const rdecl = rsig ? rsig.declaration : undefined; if ( + rsig && rdecl && (ts.isFunctionDeclaration(rdecl) || ts.isMethodDeclaration(rdecl)) && !rdecl.body ) { + const overload = overloadCallInstance(lowerer, expr, info, rsig, rdecl); + if (overload) return overload; lowerer.unsupported( "SC1090", expr, @@ -313,14 +316,7 @@ export function genericCallInstance( fenceGenericSignatureResult(lowerer, expr, retTs); lowerer.badType(expr, retTs); } - // A body that can return an indexed read that misses returns undefined - // there, as in JavaScript, whatever the declared result says. An async - // body's promise is always present and settles with undefined there. - const returnType = lowerer.runtimeOptionalGenericReturns.has(info.decl) - ? mappedReturn.kind === "promise" - ? { kind: "promise" as const, inner: lowerer.runtimeOptionalType(mappedReturn.inner) } - : lowerer.runtimeOptionalType(mappedReturn) - : mappedReturn; + const returnType = runtimeReturnType(lowerer, info, mappedReturn); // keyof-constrained type parameters (`K extends keyof T`): the bound // LITERAL is semantic — the instance body reads the named field — so @@ -331,23 +327,11 @@ export function genericCallInstance( // share one runtime-keyed instance per IR signature, exactly the // widened discipline. const named = staticNameBindings(lowerer, expr, info); - const keyofTps = keyofConstrainedTypeParams(info); - if (keyofTps.size > 0) { + if (keyofConstrainedTypeParams(info).size > 0) { const tsBindings = new Map(); const bindings = lowerer.inferTypeParamBindings(expr, info, rsig, tsBindings); - const litKey = info.typeParams - .filter((tp) => keyofTps.has(tp)) - .map((tp) => { - const bound = tsBindings.get(tp); - return bound?.isStringLiteralType() - ? JSON.stringify(bound.value) - : bound?.isNumberLiteralType() - ? String(bound.value) - : "*"; - }) - .join(","); return internGenericInstance(lowerer, expr, info, params, returnType, () => bindings, { - extraKey: `@${litKey}${named?.key ?? ""}`, + extraKey: `${keyofLiteralKey(info, tsBindings)}${named?.key ?? ""}`, tsBindings, ...(named ? { staticFnNames: named.names } : {}), }); @@ -363,6 +347,110 @@ export function genericCallInstance( ); } +/** A call that resolved to an OVERLOAD signature of a generic function + * or method with an implementation (`request(p: WithHandler): + * Promise; request(p: Plain): Promise; request(p: Params) + * {...}`): the implementation monomorphizes under the bindings the + * overload's resolved signature proves for its type parameters (the + * structural unification every generic call uses, run against the + * overload; unbound parameters take their declared defaults). The + * instance's parameters and result are the IMPLEMENTATION's under those + * bindings — the body was checked against them — and the call's + * arguments and result coerce across, exactly as a non-generic overload + * call reconciles with its implementation (parameters the overload omits + * must be optional in the implementation). Null when the overload does + * not belong to this implementation or the arities do not line up. */ +function overloadCallInstance( + lowerer: Lowerer, + expr: ts.CallExpression, + info: GenericFnInfo, + rsig: ts.Signature, + rdecl: ts.FunctionDeclaration | ts.MethodDeclaration, +): GenericInstance | null { + const impl = info.decl; + if ( + rdecl.parent !== impl.parent || + rdecl.name === undefined || + impl.name === undefined || + rdecl.name.getText() !== impl.name.getText() || + rdecl.parameters.nodes.length > impl.parameters.nodes.length || + impl.parameters.nodes + .slice(rdecl.parameters.nodes.length) + .some((p) => p.questionToken === undefined && p.initializer === undefined) || + rdecl.parameters.nodes.some((p) => p.dotDotDotToken !== undefined) + ) + return null; + const declSig = lowerer.checker.getSignatureFromDeclaration(impl); + if (!declSig) return null; + const tsBindings = new Map(); + const bindings = new Map(); + unifySignatureBindings(lowerer, info, rsig, bindings, tsBindings); + // Explicit type arguments name the OVERLOAD's type parameters, which + // need not match the implementation's in order: the resolved overload + // already carries them, so unification binds first, and an explicit + // argument binds only a still-unbound implementation parameter of the + // same name. + const overloadTps = rdecl.typeParameters?.nodes ?? []; + expr.typeArguments?.nodes.forEach((ta, i) => { + const name = overloadTps[i]?.name.text; + const tp = info.typeParams.find((p) => ts.symbolName(p) === name); + if (!tp || bindings.has(tp)) return; + const taT = lowerer.checker.getTypeFromTypeNode(ta); + const mapped = lowerer.mapTypeOf(taT); + if (mapped) { + bindings.set(tp, mapped); + tsBindings.set(tp, taT); + } + }); + bindDefaultTypeParams(lowerer, info.typeParams, impl.typeParameters?.nodes, bindings, tsBindings); + // A type parameter the overload never mentions (a handler the overload + // omits) is `unknown`, the checker's own answer for an uninferred one. + for (const tp of info.typeParams) { + if (bindings.has(tp)) continue; + bindings.set(tp, DYN); + tsBindings.set(tp, lowerer.checker.getUnknownType()); + } + const previous = lowerer.typeParamBindings; + const previousTs = lowerer.typeParamTsBindings; + lowerer.typeParamBindings = bindings; + lowerer.typeParamTsBindings = tsBindings; + let params: ParamShape[]; + let returnType: IrType | null; + try { + params = paramShapes( + lowerer, + impl.parameters.nodes, + declSig, + (_param, i) => expr.arguments.nodes[i] ?? expr, + ); + returnType = lowerer.mapTypeOf(lowerer.checker.getReturnTypeOfSignature(declSig)); + } finally { + lowerer.typeParamBindings = previous; + lowerer.typeParamTsBindings = previousTs; + } + if (!returnType) return null; + // keyof-constrained parameters key their instance on the bound literal, + // as on the implementation-signature path. + const runtimeReturn = runtimeReturnType(lowerer, info, returnType); + return internGenericInstance(lowerer, expr, info, params, runtimeReturn, () => bindings, { + ...(keyofConstrainedTypeParams(info).size > 0 + ? { extraKey: keyofLiteralKey(info, tsBindings) } + : {}), + tsBindings, + }); +} + +/** An instance's result type: a body that can return an indexed read + * that misses returns undefined there, as in JavaScript, whatever the + * declared result says. An async body's promise is always present and + * settles with undefined there. */ +function runtimeReturnType(lowerer: Lowerer, info: GenericFnInfo, mapped: IrType): IrType { + if (!lowerer.runtimeOptionalGenericReturns.has(info.decl)) return mapped; + return mapped.kind === "promise" + ? { kind: "promise", inner: lowerer.runtimeOptionalType(mapped.inner) } + : lowerer.runtimeOptionalType(mapped); +} + /** A generic factory whose result is one of its own local classes * (`function base(v: T) { return class { value: T = v }; }`). Each * instance has its own class, so the instance is keyed on its bindings and @@ -596,6 +684,25 @@ function keyofConstrainedTypeParams(info: GenericFnInfo): Set { return out; } +/** The instance-key suffix of the literals bound to `info`'s + * keyof-constrained type parameters (`@"a"`; `*` for a non-literal + * binding): two literals can map to one IR signature while their bodies + * read different fields. */ +function keyofLiteralKey(info: GenericFnInfo, tsBindings: Map): string { + const keyofTps = keyofConstrainedTypeParams(info); + const literals = info.typeParams + .filter((tp) => keyofTps.has(tp)) + .map((tp) => { + const bound = tsBindings.get(tp); + return bound?.isStringLiteralType() + ? JSON.stringify(bound.value) + : bound?.isNumberLiteralType() + ? String(bound.value) + : "*"; + }); + return `@${literals.join(",")}`; +} + /** Type-parameter symbol → concrete IR type for one instantiation. * Explicit type arguments bind directly; the rest come from structurally * matching each DECLARED param/return type (which mentions the type @@ -736,6 +843,27 @@ function unifySignatureBindings( // so match it against the whole remaining union. Removing null // too is safe only when no undeclared null arm remains. unify(dParts[0]!, lowerer.checker.getNonNullableType(inst), depth + 1); + } else if (dParts.length > 1 && iParts.length === dParts.length) { + // `T | Promise` against `Res | Promise`: each declared + // part that names a generic target (Promise, an interface, an + // alias) pairs with the one instantiated part of the same target; + // a single remaining bare type parameter takes the single + // remaining instantiated part. + const remaining = [...iParts]; + const bare: ts.Type[] = []; + for (const d of dParts) { + if (d.flags & ts.TypeFlags.TypeParameter) { + bare.push(d); + continue; + } + const target = d.getSymbol() ?? d.getAliasSymbol(); + if (target === undefined) return; + const matches = remaining.filter((i) => (i.getSymbol() ?? i.getAliasSymbol()) === target); + if (matches.length !== 1) return; + remaining.splice(remaining.indexOf(matches[0]!), 1); + unify(d, matches[0]!, depth + 1); + } + if (bare.length === 1 && remaining.length === 1) unify(bare[0]!, remaining[0]!, depth + 1); } return; } diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index b1b4086cd..12f7409b9 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -3367,6 +3367,8 @@ function lowerDynReceiverMethodCall( ? lowerer.checker.getNonNullableType(rawRecvTs) : rawRecvTs; if (lowerer.classImplementedProtocol(recvTs)) { + const viewed = pickViewGenericMethodCall(lowerer, call, access, recvTs); + if (viewed) return viewed; const property = lowerer.checker.getPropertyOfType(recvTs, access.name.text); if ( property && @@ -10411,6 +10413,53 @@ function maybePromiseHandlerResult( return stmt; } +/** `view.m(args)` where `view` is a `Pick` / `Omit` view of a + * program class C and `m` is one of C's GENERIC methods (its overloads + * included): generic methods have no dispatchable method value, so the + * view's live receiver is extracted as the C instance it holds (a checked + * extraction — a value of another class throws Node-style TypeError + * text naming C) and the call monomorphizes statically against C, the + * same lowering a C-typed receiver gets. Null for other receivers. */ +function pickViewGenericMethodCall( + lowerer: Lowerer, + call: ts.CallExpression, + access: ts.PropertyAccessExpression, + recvTs: ts.Type, +): IrExpr | null { + const alias = recvTs.getAliasSymbol(); + if (alias === undefined || (ts.symbolName(alias) !== "Pick" && ts.symbolName(alias) !== "Omit")) + return null; + if (!alias.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return null; + const source = recvTs.getAliasTypeArguments()[0]; + const symbol = source?.getSymbol(); + const info = symbol ? lowerer.classBySymbol.get(symbol) : undefined; + if (!info || call.questionDotToken || access.questionDotToken) return null; + const found = findGenericMethodOn(lowerer, info, access.name.text); + if (!found) return null; + const loc = locOf(access.expression); + const lowered = lowerer.lowerExpr(access.expression); + const instance: IrType = { kind: "object", className: info.def.name }; + // A view that statically holds an instance of another class (a + // structurally compatible one) calls that class's method in Node, not + // C's: the call is refused rather than extracted as a C instance. + if ( + lowered.type.kind === "object" && + lowered.type.className !== info.def.name && + !lowerer.isSubclassOf(lowered.type.className, info.def.name) + ) { + lowerer.unsupported( + "SC1090", + access, + `calling the generic method '${access.name.text}' through a '${ts.symbolName(alias)}' view of '${info.def.name}' that holds a '${lowerer.fmt(lowered.type)}'`, + ); + } + const receiver: IrExpr = + lowered.type.kind === "object" + ? lowered + : { kind: "dynCheck", value: lowerer.coerceToExpected(lowered, DYN), type: instance, loc }; + return lowerClassGenericMethodCall(lowerer, call, access, info, found, receiver); +} + /** `Object.assign(err, ...sources)` in TypeScript onto an error-hierarchy * target (`Object.assign(new Error(message), details)`): the sources' * own members land on the error — the runtime copy over its diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts index c9a1cc9df..02f2f4cfb 100644 --- a/packages/compiler/src/frontend/type-mapper.ts +++ b/packages/compiler/src/frontend/type-mapper.ts @@ -2138,7 +2138,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null { (ts.isInterfaceDeclaration(d) || ts.isTypeAliasDeclaration(d) || ts.isTypeLiteralNode(d) || - ts.isMappedTypeNode(d)), + ts.isMappedTypeNode(d) || + isDeclaredDataClass(d) || + (decls.some(isDeclaredDataClass) && isTypeOnlyNamespace(d))), ); const aliasDecls = packageOnly(widened.getAliasSymbol()); const ownDecls = packageOnly(widened.getSymbol()); @@ -6520,6 +6522,48 @@ function isMappedShape(t: ts.Type): boolean { * of a package the program does not compile. Its pure type shapes — * interfaces, aliases, type literals — describe data a program can build * itself (`import type` from a types-only package such as type-fest). */ +/** A class an SDK declares only to describe data (aws-sdk's + * `declare class PresignedPost { url: string; fields: Fields }`, which + * arrives as JSON and is never constructed): public instance fields and + * nothing else — no constructor, methods, accessors, index signatures, + * static, private or protected members, base classes, implemented + * interfaces or type parameters. Its instances are exactly the records + * of those fields, so the type maps structurally like an interface. */ +function isDeclaredDataClass(d: ts.Node): boolean { + if (!ts.isClassDeclaration(d) || !d.getSourceFile().isDeclarationFile) return false; + if (d.heritageClauses !== undefined && d.heritageClauses.nodes.length > 0) return false; + if (d.typeParameters !== undefined && d.typeParameters.nodes.length > 0) return false; + return d.members.nodes.every( + (member) => + ts.isPropertyDeclaration(member) && + !ts.isPrivateIdentifier(member.name) && + !(member.modifiers?.nodes ?? []).some( + (modifier) => + modifier.kind === ts.SyntaxKind.StaticKeyword || + modifier.kind === ts.SyntaxKind.PrivateKeyword || + modifier.kind === ts.SyntaxKind.ProtectedKeyword || + modifier.kind === ts.SyntaxKind.AbstractKeyword, + ), + ); +} + +/** A namespace merged into a declared data class that holds only types + * (`namespace PresignedPost { interface Fields { … } }`): it adds no value + * the program could reach. */ +function isTypeOnlyNamespace(d: ts.Node): boolean { + if (!ts.isModuleDeclaration(d)) return false; + const body = d.body; + if (body === undefined) return true; + if (ts.isModuleDeclaration(body)) return isTypeOnlyNamespace(body); + if (!ts.isModuleBlock(body)) return false; + return body.statements.nodes.every( + (statement) => + ts.isInterfaceDeclaration(statement) || + ts.isTypeAliasDeclaration(statement) || + isTypeOnlyNamespace(statement), + ); +} + function packageTypeFile(sf: ts.SourceFile, ctx: Pick): boolean { return ( sf.isDeclarationFile && /[\\/]node_modules[\\/]/.test(sf.fileName) && !ctx.isStdlibFile(sf) @@ -7287,10 +7331,11 @@ function mapRecordTypeInner( let v: IrType | null = null; for (const info of indexInfos) { if (!stringKey(info.keyType) && !(info.keyType.flags & ts.TypeFlags.Number)) return null; - const iv = mapType(info.type, ctx); - - // JSONValue>` (a package's own JSON alias) and `Record` - + // An `any`-valued signature (`{ [key: string]: any }`, an SDK's + // `Dictionary`) holds whatever the program stores: its + // overflow values are checked-dynamic, exactly `Record`'s. + const iv = info.type.flags & ts.TypeFlags.Any ? DYN : mapType(info.type, ctx); if (!iv || !isSupportedIndexValue(iv)) return null; if (v !== null && !typeEquals(v, iv)) return null; v = iv; @@ -7517,6 +7562,22 @@ function mapRecordTypeInner( pt = arrayOf(unitOnlyUnion(ctx.unions)); } if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) pt = DYN; + // An `any[]` field (`list: any[]` in an SDK's message type, also + // optional or nullable) is the checked-dynamic array, exactly as + // `unknown[]` maps; the checked-dynamic value holds the units too. + // Declared members only: an object literal's inferred `any[]` member + // keeps its existing story. + if ( + pt === null && + declaredMember(p) && + (fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every( + (part) => + isAnyArrayType(part, checker) || + (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0, + ) && + !isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam) + ) + pt = DYN; // Unit-only FIELDS (`{ msg?: undefined }` — the discriminated-union // absent-field idiom — and `{ p: undefined }` spellings): the // unit-only union. The runtime value is the interned unit, JSON @@ -7613,6 +7674,10 @@ function dynSubsumableUnionArm(arm: IrType, ctx: TypeMapperCtx): boolean { return true; case "record": case "array": + // `T | Promise` with T bound to `unknown`: the checked-dynamic value + // holds a promise by reference, so `unknown` absorbs the promise arm + // exactly as the checker's own union reduction does. + case "promise": return canConvertToDyn( arm, (id) => ctx.shapes.get(id), @@ -7907,15 +7972,118 @@ export function describeRecordMemberBlocker(widened: ts.Type, ctx: TypeMapperCtx if (erasedOverloadMemberType(fieldTs, ctx) !== undefined) continue; return `the record shape is supported, but its member '${ts.symbolName(p)}' ${describeOverloadSetBlocker(fieldTs, ctx)}`; } - let pt = isGenericCallableMemberType(fieldTs, checker) - ? erasedGenericMemberType(fieldTs, ctx) - : mapType(fieldTs, ctx); - if (pt === undefined) continue; - if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) - pt = unitOnlyUnion(ctx.unions); - if (!pt || pt.kind === "void") { - return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`; + if (memberMaps(fieldTs, ctx, p)) continue; + // Name the deepest member the failure comes from (`order.deployment. + // builds[].config`): a large message type otherwise points only at + // its outermost member. + const inner = innerBlockedMember(fieldTs, ctx, new Set()); + if (inner !== null && inner.path !== "") { + return `the record shape is supported, but its member '${ts.symbolName(p)}${inner.path}' has type '${checker.typeToString(inner.type)}', which does not compile`; } + return `the record shape is supported, but its member '${ts.symbolName(p)}' has type '${checker.typeToString(fieldTs)}', which does not compile`; } return null; } + +/** A member an interface, type literal or class declares (not one an + * object literal's inferred type carries). */ +function declaredMember(member: ts.Symbol): boolean { + return ( + member.declarations.length > 0 && + member.declarations.every((d) => ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) + ); +} + +/** Whether a record member of type `fieldTs` maps, by the member rules of + * mapRecordTypeInner (generic members erase; `any` and `any[]` members + * are checked-dynamic; unit-only members are the unit union). Overloaded + * members answer true: describeOverloadSetBlocker tells their story. */ +function memberMaps(fieldTs: ts.Type, ctx: TypeMapperCtx, member?: ts.Symbol): boolean { + const { checker } = ctx; + if (isOverloadedCallableMemberType(fieldTs, checker)) return true; + const pt = isGenericCallableMemberType(fieldTs, checker) + ? erasedGenericMemberType(fieldTs, ctx) + : mapType(fieldTs, ctx); + if (pt === undefined) return true; + if (pt === null && (fieldTs.flags & ts.TypeFlags.Any) !== 0) return true; + if ( + pt === null && + (member === undefined || declaredMember(member)) && + (fieldTs.isUnionType() ? ts.constituentTypes(fieldTs) : [fieldTs]).every( + (part) => + isAnyArrayType(part, checker) || + (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Null)) !== 0, + ) && + !isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam) + ) + return true; + if (pt?.kind === "void" && isUnitOnlyTsType(fieldTs, ctx.resolveTypeParam)) return true; + return pt !== null && pt.kind !== "void"; +} + +/** The member path inside the unmapped type `t` that its failure comes + * from, and that member's type: `.name` steps into record members, `[]` + * into array elements, `()` into a function's return; union arms and + * promise payloads are transparent. The empty path means `t` itself is + * the blocker; null means `t` maps (or is `any`). */ +function innerBlockedMember( + t: ts.Type, + ctx: TypeMapperCtx, + seen: Set, +): { path: string; type: ts.Type } | null { + const { checker } = ctx; + // `any` is no component to name: the member holding it is the blocker. + if ((t.flags & ts.TypeFlags.Any) !== 0 || mapType(t, ctx) !== null) return null; + const here = { path: "", type: t }; + if (seen.has(t) || seen.size >= 16) return here; + seen.add(t); + const step = (prefix: string, inner: ts.Type): { path: string; type: ts.Type } => { + const found = innerBlockedMember(inner, ctx, seen); + return found === null ? here : { path: prefix + found.path, type: found.type }; + }; + if (t.isUnionType()) { + const UNIT = ts.TypeFlags.Undefined | ts.TypeFlags.Void | ts.TypeFlags.Null; + for (const part of ts.constituentTypes(t)) { + if ((part.flags & UNIT) !== 0 || mapType(part, ctx) !== null) continue; + return step("", part); + } + return here; + } + if ((t.flags & ts.TypeFlags.Object) === 0) return here; + if (checker.isArrayType(t)) { + const elem = checker.getTypeArguments(t as ts.TypeReference)[0]; + return elem === undefined ? here : step("[]", elem); + } + const symbol = t.getSymbol(); + if ( + symbol !== undefined && + ts.symbolName(symbol) === "Promise" && + symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile())) + ) { + const payload = checker.getTypeArguments(t as ts.TypeReference)[0]; + return payload === undefined ? here : step("", payload); + } + const calls = checker.getCallSignatures(t); + if (calls.length === 1) { + const sig = calls[0]!; + if (sig.getTypeParameters().length > 0) return here; + if (sig.getParameters().some((p) => mapType(checker.getTypeOfSymbol(p), ctx) === null)) + return here; + return step("()", checker.getReturnTypeOfSignature(sig)); + } + if ( + calls.length > 0 || + checker.getConstructSignatures(t).length > 0 || + checker.isTupleType(t) || + checker.getIndexInfosOfType(t).length > 0 + ) + return here; + for (const p of checker.getPropertiesOfType(t)) { + if ((p.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) !== 0) continue; + if (ts.symbolName(p).startsWith("__@")) continue; + const fieldTs = checker.getTypeOfSymbol(p); + if (memberMaps(fieldTs, ctx, p)) continue; + return step(`.${ts.symbolName(p)}`, fieldTs); + } + return here; +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json new file mode 100644 index 000000000..70a5823f4 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6412-any-valued-dictionaries.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6412-any-valued-dictionaries.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json new file mode 100644 index 000000000..83fe76101 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6413-sdk-declared-data-class/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6413-sdk-declared-data-class/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json new file mode 100644 index 000000000..c5b440992 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6414-pick-class-method-views.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6414-pick-class-method-views.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json new file mode 100644 index 000000000..98806a53c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6495-generic-method-overloads.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6495-generic-method-overloads.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json new file mode 100644 index 000000000..8bc601bd6 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6496-overload-signatures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6496-overload-signatures.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json new file mode 100644 index 000000000..06e7bb17f --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6497-generic-overload-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6497-generic-overload-bindings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json new file mode 100644 index 000000000..b52b29f92 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/nested-member-blocker-path.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/nested-member-blocker-path.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json deleted file mode 100644 index 1515ceeb9..000000000 --- a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/overload-fences.ts.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "order": [ - "/tests/diagnostics/overload-fences.ts" - ], - "diags": [] -} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json new file mode 100644 index 000000000..1dafe9a3a --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/pick-view-generic-methods.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/pick-view-generic-methods.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6412-any-valued-dictionaries.ts b/tests/corpus/6412-any-valued-dictionaries.ts new file mode 100644 index 000000000..0cbdc6aad --- /dev/null +++ b/tests/corpus/6412-any-valued-dictionaries.ts @@ -0,0 +1,50 @@ +// Message types whose members are typed with `any`: string-keyed +// dictionaries of `any` values (`{ [key: string]: any }`, a `Dictionary` alias used bare), and `any[]` lists, also optional, nested in +// arrays of records. Values keep whatever the program or JSON stored. +type Dictionary = { [key: string]: T }; + +interface Build { + use: string; + src?: string; + config?: Dictionary; +} +interface Order { + id: string; + builds?: Build[]; + upload: { url: string; fields: { [key: string]: any } }; + tags: any[]; + extra?: any[]; +} + +function tags(): Dictionary { + return { a: 1, b: "x", c: [1, 2] }; +} +async function meta(): Promise { + return { n: 3 }; +} + +async function main(): Promise { + const order: Order = JSON.parse( + '{"id":"o1","builds":[{"use":"node","config":{"zero":true,"n":2}},{"use":"static","src":"a/**"}],' + + '"upload":{"url":"u","fields":{"k":"v","n":2}},"tags":[1,"two",null]}', + ); + console.log(order.id, order.upload.url, String(order.upload.fields.k), order.upload.fields["n"] + 1); + for (const build of order.builds ?? []) { + console.log(build.use, build.src ?? "-", JSON.stringify(build.config ?? null)); + } + console.log(order.tags.length, order.extra === undefined, JSON.stringify(order.tags)); + + const t = tags(); + for (const k of Object.keys(t)) console.log(k, JSON.stringify(t[k])); + t.d = { deep: true }; + console.log(JSON.stringify(t)); + const m = await meta(); + console.log(m.n * 2, JSON.stringify(m)); + + const made: Order = { id: "o2", upload: { url: "w", fields: {} }, tags: [], extra: [0] }; + made.upload.fields.signature = "s"; + made.tags.push({ k: 1 }); + console.log(JSON.stringify(made)); +} +void main(); diff --git a/tests/corpus/6413-sdk-declared-data-class/main.ts b/tests/corpus/6413-sdk-declared-data-class/main.ts new file mode 100644 index 000000000..32509da57 --- /dev/null +++ b/tests/corpus/6413-sdk-declared-data-class/main.ts @@ -0,0 +1,27 @@ +// A types-only SDK package declares the shape of JSON data as a class with +// fields only (`declare class UploadForm { url: string; fields: ... }`, +// with a merged namespace holding the fields interface). The program never +// constructs it: values arrive through JSON.parse or object literals, so +// they compile as records of the declared fields. +import type { UploadForm } from "upload-sdk"; + +interface Order { + id: string; + upload?: UploadForm; + forms: UploadForm[]; +} + +function describe(form: UploadForm | undefined): string { + if (!form) return "none"; + return `${form.url} ${form.fields.Policy} ${form.fields["X-Signature"]} ${form.fields["key"] ?? "-"} ${Object.keys(form.fields).join(",")}`; +} + +const order = JSON.parse( + '{"id":"o1","upload":{"url":"u","fields":{"Policy":"p","X-Signature":"s","key":"k"}},"forms":[]}', +) as Order; +console.log(order.id, describe(order.upload)); +const made: UploadForm = { url: "v", fields: { Policy: "q", "X-Signature": "t" } }; +order.forms.push(made); +made.fields.extra = "e"; +console.log(describe(made), JSON.stringify(order.forms)); +console.log(describe(undefined)); diff --git a/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts new file mode 100644 index 000000000..5c72897ab --- /dev/null +++ b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/index.d.ts @@ -0,0 +1,13 @@ +export declare class UploadForm { + /** The URL the form posts to. */ + url: string; + /** The hidden form fields. */ + fields: UploadForm.Fields; +} +export declare namespace UploadForm { + interface Fields { + Policy: string; + "X-Signature": string; + [key: string]: string; + } +} diff --git a/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json new file mode 100644 index 000000000..093d34b91 --- /dev/null +++ b/tests/corpus/6413-sdk-declared-data-class/node_modules/upload-sdk/package.json @@ -0,0 +1 @@ +{ "name": "upload-sdk", "version": "1.0.0", "types": "index.d.ts" } diff --git a/tests/corpus/6414-pick-class-method-views.ts b/tests/corpus/6414-pick-class-method-views.ts new file mode 100644 index 000000000..6bbbbf780 --- /dev/null +++ b/tests/corpus/6414-pick-class-method-views.ts @@ -0,0 +1,83 @@ +// `Pick` and `Omit` over a class, used as structural parameter +// and field types: a view of the instance's own members. Calls through the +// view run the instance's methods on the instance itself, so its state is +// shared with every other reference. +class ApiClient { + private readonly base: string; + calls = 0; + constructor(base: string) { + this.base = base; + } + request(path: string): Promise { + this.calls++; + return Promise.resolve(this.base + path); + } + close(): void { + console.log("closed after", this.calls); + } +} + +interface Context { + client: Pick; + name: string; +} + +async function run(ctx: Context): Promise { + console.log(ctx.name, await ctx.client.request("/x"), await ctx.client.request("/y")); +} +function finish(client: Pick): void { + client.close(); +} +function count(client: Omit): number { + return client.calls; +} + +const client = new ApiClient("https://h"); +void run({ client, name: "ctx" }).then(() => { + finish(client); + console.log(count(client), client.calls); +}); + +// A live view that flows into another structural type keeps the instance: +// reads through the second view see the instance's later writes. +interface Counter { + calls: number; +} +function counted(view: Pick): Counter { + void view.request("/z"); + return view; +} +const second = new ApiClient("https://k"); +const counter = counted(second); +second.calls += 10; +const holder: { counter: Counter } = { counter: counted(second) }; +second.calls = 100; +console.log("counter", counter.calls, holder.counter.calls, second.calls); + +// A view also accepts other classes that have its members. Their instances +// flow through it the same way. +class Meter { + label = "meter"; + reads = 0; + read(): number { + return this.reads; + } +} +class Gauge { + reads = 0; + read(): number { + return this.reads; + } +} +interface Reading { + reads: number; +} +function reading(view: Pick): Reading { + console.log("read", view.read()); + return view; +} +const gauge = new Gauge(); +const result = reading(gauge); +gauge.reads = 50; +const meter = new Meter(); +console.log("reading", result.reads, reading(meter).reads, meter.label); diff --git a/tests/corpus/6495-generic-method-overloads.ts b/tests/corpus/6495-generic-method-overloads.ts new file mode 100644 index 000000000..34bcfa712 --- /dev/null +++ b/tests/corpus/6495-generic-method-overloads.ts @@ -0,0 +1,54 @@ +// Calls that resolve to OVERLOAD signatures of a generic method or +// function: the implementation monomorphizes under what the overload +// proves for its type parameters (a parameter the overload never mentions +// is unknown), directly and through a `Pick` view of the class, whose +// live receiver keeps the instance's state. `T | Promise` callbacks +// bind T from either arm. +interface Reply { + code: number; + text: string; +} +type Mapper = (reply: Reply) => T | Promise; + +class Store { + loads = 0; + async load(key: string, map: Mapper): Promise; + async load(key: string): Promise; + async load(key: string, map?: Mapper): Promise { + this.loads++; + const reply: Reply = { code: 200, text: key }; + if (map === undefined) return reply; + return await map(reply); + } +} + +interface Services { + store: Pick; +} + +function pick(items: T[], index: number): T; +function pick(items: T[]): T; +function pick(items: T[], index = 0): T { + return items[index]!; +} + +async function settle(reply: Reply, map?: Mapper): Promise { + return map ? await map(reply) : reply; +} + +async function main(): Promise { + const store = new Store(); + const plain = await store.load("alpha"); + console.log(plain.code, plain.text); + const length = await store.load("beta", (reply) => reply.text.length); + console.log(length * 2); + const services: Services = { store }; + const viaView = await services.store.load("gamma"); + const upper = await services.store.load("delta", async (reply) => reply.text.toUpperCase()); + console.log(viaView.text, upper, store.loads); + console.log(pick(["x", "y"]), pick([1, 2, 3], 2)); + console.log(JSON.stringify(await settle({ code: 1, text: "s" }))); + console.log(await settle({ code: 2, text: "tt" }, (reply) => reply.code + reply.text.length)); +} + +main(); diff --git a/tests/diagnostics/overload-fences.ts b/tests/corpus/6496-overload-signatures.ts similarity index 66% rename from tests/diagnostics/overload-fences.ts rename to tests/corpus/6496-overload-signatures.ts index 0cf1f7e15..5902d8c19 100644 --- a/tests/diagnostics/overload-fences.ts +++ b/tests/corpus/6496-overload-signatures.ts @@ -1,7 +1,7 @@ -// The overload fences that REMAIN now that overload signatures lower: -// generic overload calls (the per-instantiation body would be typed -// against a signature it was never checked under), and the multi- -// signature type surfacing where a binding needs it spelled. +// Overload signatures: a generic overload call monomorphizes the +// implementation under the bindings the overload proves (a parameter the +// overload omits is optional in the implementation), and an immutable +// alias of an overloaded declaration projects onto the implementation. function twice(x: T): T[]; function twice(x: T, n: number): T[]; function twice(x: T, n?: number): T[] { diff --git a/tests/corpus/6497-generic-overload-bindings.ts b/tests/corpus/6497-generic-overload-bindings.ts new file mode 100644 index 000000000..0d33acaae --- /dev/null +++ b/tests/corpus/6497-generic-overload-bindings.ts @@ -0,0 +1,47 @@ +// Bindings of generic overload calls: explicit type arguments name the +// overload's type parameters, which can be declared in another order than +// the implementation's, and keyof-constrained parameters bound to +// different literals through an overload read their own fields. +function tag(value: A, label: B): string; +function tag(value: A, label?: B): string { + return `${typeof value}:${typeof label}:${String(value)}${String(label)}`; +} +console.log(tag(5, "x")); +console.log(tag("y", true)); + +class Labels { + prefix = "#"; + render(value: A, label: B): string; + render(value: A, label?: B): string { + return `${this.prefix}${typeof value}/${typeof label}`; + } +} +console.log(new Labels().render("z", 7)); + +interface Row { + a: number; + b: number; + name: string; +} +function read(row: T, key: K): T[K]; +function read(row: T, key: K, fallback: T[K]): T[K]; +function read(row: T, key: K, fallback?: T[K]): T[K] { + const value = row[key]; + return value === undefined ? fallback! : value; +} +const row: Row = { a: 1, b: 2, name: "r" }; +console.log(read(row, "a"), read(row, "b"), read(row, "name")); +console.log(read(row, "b", 0) + read(row, "a", 0)); + +// An overload call whose implementation returns an indexed read that +// misses returns undefined, as the implementation's own calls do. +function at(items: T[], index: number): T; +function at(items: T[], index: number, label: string): T; +function at(items: T[], index: number, label?: string): T { + if (label !== undefined) console.log(label); + return items[index]; +} +console.log(at([1, 2, 3], 1), at([1, 2, 3], 5)); +console.log(at(["a", "b"], 0, "strings"), at(["a", "b"], 9, "strings")); +const missing = at([{ id: 1 }], 3); +console.log(missing === undefined, typeof missing); diff --git a/tests/diagnostics/nested-member-blocker-path.ts b/tests/diagnostics/nested-member-blocker-path.ts new file mode 100644 index 000000000..60edd053c --- /dev/null +++ b/tests/diagnostics/nested-member-blocker-path.ts @@ -0,0 +1,19 @@ +// A record blocked by a member deep inside it names the member's path: +// through a function's result (`()`), the promise and union around it, +// record members (`.name`) and array elements (`[]`). +interface Job { + id: string; + handle: Promise & { cancel(): void }; +} +interface Order { + id: string; + jobs: Job[]; +} +interface Provider { + name: string; + poll: () => Promise; +} +function makeProvider(): Provider { + return { name: "p", poll: async () => undefined }; +} +console.log(makeProvider().name); diff --git a/tests/diagnostics/pick-view-generic-methods.ts b/tests/diagnostics/pick-view-generic-methods.ts new file mode 100644 index 000000000..912867dc6 --- /dev/null +++ b/tests/diagnostics/pick-view-generic-methods.ts @@ -0,0 +1,16 @@ +// A generic method called through a `Pick` view of a class compiles +// against that class. A view that holds an instance of a structurally +// compatible, unrelated class is refused: Node calls that class's own +// method. +class Store { + load(key: string, map: (text: string) => T): T { + return map(key); + } +} +class Mirror { + load(key: string, map: (text: string) => T): T { + return map(`${key}!`); + } +} +const view: Pick = new Mirror(); +console.log(view.load("k", (text) => text.length)); diff --git a/tests/harness/__snapshots__/nested-member-blocker-path.ts.txt b/tests/harness/__snapshots__/nested-member-blocker-path.ts.txt new file mode 100644 index 000000000..e2d83b47c --- /dev/null +++ b/tests/harness/__snapshots__/nested-member-blocker-path.ts.txt @@ -0,0 +1,17 @@ +nested-member-blocker-path.ts:16:10 - error SC2009: values of type 'Provider' cannot be compiled: the record shape is supported, but its member 'poll().jobs[].handle' has type 'Promise & { cancel(): void; }', which does not compile + + 15 | } + 16 | function makeProvider(): Provider { + | ^~~~~~~~~~~~ + 17 | return { name: "p", poll: async () => undefined }; + + hint: the outer shape is supported — the named component is the blocker; restate it inside the compilable set (number, string, boolean, arrays, Maps, Sets, RegExp, functions, classes, records, unions of those, and 'unknown') + +nested-member-blocker-path.ts:19:13 - error SC2004: uses of 'makeProvider' inherit the blocker on its declaration + + 18 | } + 19 | console.log(makeProvider().name); + | ^~~~~~~~~~~~ + 20 | + + hint: the declaration of 'makeProvider' did not compile — fix the diagnostic reported there and these sites clear with it \ No newline at end of file diff --git a/tests/harness/__snapshots__/overload-fences.ts.txt b/tests/harness/__snapshots__/overload-fences.ts.txt deleted file mode 100644 index 6c4b2f405..000000000 --- a/tests/harness/__snapshots__/overload-fences.ts.txt +++ /dev/null @@ -1,6 +0,0 @@ -overload-fences.ts:12:13 - error SC1090: calls selecting an overload signature of a generic function (only the implementation signature monomorphizes) is not supported yet - - 11 | } - 12 | console.log(twice("q").length); - | ^~~~~~~~~~ - 13 | function pick(x: "a"): string; \ No newline at end of file diff --git a/tests/harness/__snapshots__/pick-view-generic-methods.ts.txt b/tests/harness/__snapshots__/pick-view-generic-methods.ts.txt new file mode 100644 index 000000000..59e7ed642 --- /dev/null +++ b/tests/harness/__snapshots__/pick-view-generic-methods.ts.txt @@ -0,0 +1,6 @@ +pick-view-generic-methods.ts:16:13 - error SC1090: calling the generic method 'load' through a 'Pick' view of 'Store' that holds a 'Mirror' is not supported yet + + 15 | const view: Pick = new Mirror(); + 16 | console.log(view.load("k", (text) => text.length)); + | ^~~~~~~~~ + 17 | \ No newline at end of file diff --git a/tests/harness/corpus-costs/6495-generic-method-overloads.ts.json b/tests/harness/corpus-costs/6495-generic-method-overloads.ts.json new file mode 100644 index 000000000..89eeb5b0c --- /dev/null +++ b/tests/harness/corpus-costs/6495-generic-method-overloads.ts.json @@ -0,0 +1 @@ +{ "plain": 2783, "san": 2211 } diff --git a/tests/harness/corpus-costs/6496-overload-signatures.ts.json b/tests/harness/corpus-costs/6496-overload-signatures.ts.json new file mode 100644 index 000000000..5b1f193e3 --- /dev/null +++ b/tests/harness/corpus-costs/6496-overload-signatures.ts.json @@ -0,0 +1 @@ +{ "plain": 2032, "san": 1924 } diff --git a/tests/harness/corpus-costs/6497-generic-overload-bindings.ts.json b/tests/harness/corpus-costs/6497-generic-overload-bindings.ts.json new file mode 100644 index 000000000..a94adc8be --- /dev/null +++ b/tests/harness/corpus-costs/6497-generic-overload-bindings.ts.json @@ -0,0 +1 @@ +{ "plain": 2061, "san": 1900 }