diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index ba3fcba05..e41978229 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -238,7 +238,7 @@ export function returnsOnlyThisValue(member: ts.MethodDeclaration): boolean { return valid; } -function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { +export function functionLocalClass(decl: ts.ClassLikeDeclaration): boolean { for ( let parent: ts.Node | undefined = decl.parent; parent && !ts.isSourceFile(parent); diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ac8b02b69..2bde5f2aa 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -1,4 +1,10 @@ import { keyedHelperName } from "./stable-names.js"; +import { + isNewTarget, + lowerNewTarget, + lowerNewTargetComparison, + lowerNewTargetName, +} from "./new-target.js"; import { lowerWebAssemblyValue, refuseWebAssemblyUse } from "./builtins/webassembly.js"; import { lowerAwaitedUniformPromiseAll } from "./builtins/promises.js"; import { dynUndefinedExpr, nodeThrowExpr, numLit, strLit, varRef } from "../../ir/build.js"; @@ -713,7 +719,8 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isPrefixUnaryExpression(expr)) return lowerer.lowerPrefixUnary(expr); // `x++` / `x--` in expression position: yields the OLD value. if (ts.isPostfixUnaryExpression(expr)) return lowerIncDec(lowerer, expr, false); - if (ts.isBinaryExpression(expr)) return lowerer.lowerBinary(expr); + if (ts.isBinaryExpression(expr)) + return lowerNewTargetComparison(lowerer, expr) ?? lowerer.lowerBinary(expr); if (ts.isCallExpression(expr)) { // `h.return?.()` on a helper object: the method exists, so the // optional call is the call. @@ -729,13 +736,15 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr { if (ts.isElementAccessExpression(expr)) return lowerer.lowerElementAccess(expr); if (ts.isConditionalExpression(expr)) return lowerTernary(lowerer, expr); - if (ts.isPropertyAccessExpression(expr)) return lowerPropertyAccessExpr(lowerer, expr, loc); + if (ts.isPropertyAccessExpression(expr)) + return lowerNewTargetName(lowerer, expr) ?? lowerPropertyAccessExpr(lowerer, expr, loc); // Meta-properties, named: `new.target` reflects HOW a function was // invoked (compiled functions are never constructors of themselves — // no runtime invocation record exists), and `import.meta`/ // `import.defer` are module-loader surface a native binary does not // carry. + if (isNewTarget(expr)) return lowerNewTarget(lowerer, expr); if (ts.isMetaProperty(expr)) { const name = expr.keywordToken === ts.SyntaxKind.NewKeyword ? "new.target" : `import.${expr.name.text}`; diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 80db45f3a..6023c24aa 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -2188,6 +2188,9 @@ export class Lowerer { * and erased instances). */ erasingDepth = 0; localClassInstantiations: { owner: ts.FunctionLikeDeclaration; name: string }[] = []; + /** The function-local classes of the program with an `extends` clause, whether lowering collected + * them yet or not (new-target.ts); found on first use. */ + functionLocalSubclasses: ts.ClassLikeDeclaration[] | undefined = undefined; /** Instance types (by checker type id) of the local classes generic * factory calls return, and the per-instance class each names. */ readonly localClassInstanceTypes = new Map(); diff --git a/packages/compiler/src/frontend/lowering/new-target.ts b/packages/compiler/src/frontend/lowering/new-target.ts new file mode 100644 index 000000000..83994ca6f --- /dev/null +++ b/packages/compiler/src/frontend/lowering/new-target.ts @@ -0,0 +1,357 @@ +/* `new.target`. Compiled code has no invocation record, but inside a class + * constructor `new.target` is always the class of the object under + * construction: a compiled `new` allocates the instance with its concrete + * class before any constructor of the chain runs, and every constructor of + * the chain receives that instance as `this`. The program's class + * hierarchy is closed, so the value is a static dispatch over the + * constructor's class and its descendants: the deepest class the instance + * is an instance of is its exact class. + * + * The checker rejects `new.target` in methods, accessors, static blocks and + * field initializers. Plain functions (constructible in JavaScript) keep + * the refusal. */ +import * as ts from "../ts7/adapter.js"; +import { BOOL, STRING, type IrExpr, type IrType, type SrcLoc } from "../../ir/ir.js"; +import { varRef } from "../../ir/build.js"; +import { locOf } from "../program.js"; +import type { Lowerer } from "./lowerer.js"; +import type { ClassInfo } from "./lower-classes.js"; +import { classValueRef, functionLocalClass } from "./lower-classes.js"; + +export function isNewTarget(node: ts.Node): node is ts.MetaProperty { + return ( + ts.isMetaProperty(node) && + node.keywordToken === ts.SyntaxKind.NewKeyword && + node.name.text === "target" + ); +} + +/** Where `new.target` is bound: a class constructor, or a plain function + * (refused). */ +type Binding = + | { kind: "constructor"; declaration: ts.ConstructorDeclaration } + | { kind: "function" }; + +function bindingOf(node: ts.Node): Binding { + for (let p = node.parent; p !== undefined; p = p.parent) { + if (ts.isArrowFunction(p)) continue; + if (ts.isConstructorDeclaration(p)) return { kind: "constructor", declaration: p }; + if (ts.isFunctionLike(p) || ts.isSourceFile(p)) return { kind: "function" }; + } + return { kind: "function" }; +} + +interface Dispatch { + info: ClassInfo; + /** The constructor's descendants, deepest first, then its class. A + * generic family stands for its instantiations, also those that later + * bodies instantiate: its interval holds them all. */ + classes: ClassInfo[]; + self: IrExpr | null; +} + +function depth(info: ClassInfo): number { + let d = 0; + for (let c = info.base; c; c = c.base) d++; + return d; +} + +/** The function-local classes of the program that extend a class. */ +function functionLocalSubclasses(lowerer: Lowerer): ts.ClassLikeDeclaration[] { + let found = lowerer.functionLocalSubclasses; + if (found === undefined) { + const classes: ts.ClassLikeDeclaration[] = []; + for (const sf of lowerer.moduleOrder) + ts.walkPreorder(sf, (node) => { + if ( + (ts.isClassDeclaration(node) || ts.isClassExpression(node)) && + baseExpressionOf(node) !== undefined && + functionLocalClass(node) + ) + classes.push(node); + }); + lowerer.functionLocalSubclasses = found = classes; + } + return found; +} + +function baseExpressionOf(node: ts.ClassLikeDeclaration): ts.Expression | undefined { + let base = node.heritageClauses?.nodes.find( + (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword, + )?.types?.nodes[0]?.expression; + while (base !== undefined && ts.isParenthesizedExpression(base)) base = base.expression; + return base; +} + +/** Whether the class `node` extends the class of `info` (for an + * instantiation, its family), directly or through other classes, as far as + * its `extends` clauses name classes. */ +function derivesFrom( + lowerer: Lowerer, + node: ts.ClassLikeDeclaration, + info: ClassInfo, + seen: Set, +): boolean { + if (seen.has(node)) return false; + seen.add(node); + const base = baseExpressionOf(node); + const name = + base === undefined + ? undefined + : ts.isIdentifier(base) + ? base + : ts.isPropertyAccessExpression(base) + ? base.name + : undefined; + let symbol = name === undefined ? undefined : lowerer.checker.getSymbolAtLocation(name); + if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) + symbol = lowerer.checker.getAliasedSymbol(symbol); + if (symbol === undefined) return false; + const named = lowerer.classBySymbol.get(symbol); + if (named !== undefined) + return ( + named === info || + named === info.genericInstance?.family || + lowerer.isSubclassOf(named.def.name, info.def.name) + ); + // A class lowering has not collected: through its declaration. + for (const declaration of symbol.declarations) { + let cls: ts.Node | undefined = declaration; + if (ts.isVariableDeclaration(cls)) { + cls = cls.initializer; + while (cls !== undefined && ts.isParenthesizedExpression(cls)) cls = cls.expression; + } + if ( + cls !== undefined && + (ts.isClassDeclaration(cls) || ts.isClassExpression(cls)) && + derivesFrom(lowerer, cls, info, seen) + ) + return true; + } + return false; +} + +/** `node`'s name, quoted, for diagnostics. */ +function localClassName(node: ts.ClassLikeDeclaration): string { + if (node.name) return `'${node.name.text}'`; + const parent = node.parent; + return parent && ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) + ? `'${parent.name.text}'` + : "a class expression"; +} + +/** The classes `new.target` can denote in the constructor being lowered, + * or null after a diagnostic. */ +function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null { + const binding = bindingOf(expr); + if (binding.kind !== "constructor") return null; + const info = lowerer.currentClass; + const cls = binding.declaration.parent; + if (!info || (info.decl !== null && info.decl !== cls)) { + lowerer.unsupported( + "SC1090", + expr, + "'new.target' in this constructor (its class has no lowering here)", + ); + return null; + } + // An instantiation's subclasses can also be instantiations of generic + // subclasses of its family (`class G extends Box` under Box). + const family = info.genericInstance?.family; + const descendants: ClassInfo[] = []; + for (const other of lowerer.classes.values()) { + if (other === info) continue; + if ( + lowerer.isSubclassOf(other.def.name, info.def.name) || + (family !== undefined && + other.generic !== undefined && + other !== family && + lowerer.isSubclassOf(other.def.name, family.def.name)) + ) + descendants.push(other); + } + descendants.sort((a, b) => depth(b) - depth(a)); + const classes = [...descendants, info]; + for (const c of classes) { + if (c.localClass) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' where '${c.def.jsName ?? c.def.name}' is a function-local class (each evaluation creates a new class)`, + ); + return null; + } + } + // Lowering collects a function-local class with the body declaring it, + // which may come after this constructor. + for (const local of functionLocalSubclasses(lowerer)) { + if (!derivesFrom(lowerer, local, info, new Set())) continue; + lowerer.unsupported( + "SC1090", + expr, + `'new.target' where ${localClassName(local)} is a function-local class (each evaluation creates a new class)`, + ); + } + let self: IrExpr | null = null; + if (classes.length > 1) { + const thisLocal = lowerer.resolveThis(expr); + if (!thisLocal || thisLocal.type.kind !== "object") { + lowerer.unsupported( + "SC1090", + expr, + "'new.target' where the constructor's 'this' has no class representation", + ); + return null; + } + self = varRef(thisLocal.id, thisLocal.type, locOf(expr)); + } + return { info, classes, self }; +} + +/** A conditional chain selecting `arm(D)` for the exact class D of the + * instance under construction. */ +function select( + dispatch: Dispatch, + type: IrType, + loc: SrcLoc, + arm: (info: ClassInfo) => IrExpr, +): IrExpr { + const { classes, self } = dispatch; + let result = arm(classes[classes.length - 1]!); + for (let i = classes.length - 2; i >= 0; i--) { + const c = classes[i]!; + result = { + kind: "ternary", + cond: { kind: "instanceOf", value: self!, className: c.def.name, type: BOOL, loc }, + then: arm(c), + else_: result, + type, + loc, + }; + } + return result; +} + +function refuseFunction(lowerer: Lowerer, expr: ts.MetaProperty): never { + return lowerer.unsupported( + "SC1090", + expr, + "'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor)", + ); +} + +/** `new.target` as a value. */ +export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr { + const loc = locOf(expr); + if (bindingOf(expr).kind === "function") refuseFunction(lowerer, expr); + const dispatch = dispatchOf(lowerer, expr)!; + const type: IrType = { kind: "classval", className: dispatch.info.def.name }; + for (const c of dispatch.classes) { + if (c.generic) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' as a value where '${c.def.jsName ?? c.def.name}' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead)`, + ); + } + if (c !== dispatch.info && !lowerer.ctorAbiEquals(c, dispatch.info)) { + lowerer.unsupported( + "SC1090", + expr, + `'new.target' as a value where '${c.def.jsName ?? c.def.name}' constructs with a different signature than '${dispatch.info.def.jsName ?? dispatch.info.def.name}' (read 'new.target.name' or compare 'new.target' with a class instead)`, + ); + } + } + return select(dispatch, type, loc, (c) => + lowerer.coerceToExpected(classValueRef(lowerer, c, expr), type), + ); +} + +/** `new.target.name`: the exact class's name, without materializing a + * class value (subclasses may construct with other signatures). */ +export function lowerNewTargetName( + lowerer: Lowerer, + access: ts.PropertyAccessExpression, +): IrExpr | null { + if (access.questionDotToken || access.name.text !== "name") return null; + let target: ts.Expression = access.expression; + while (ts.isParenthesizedExpression(target)) target = target.expression; + if (!isNewTarget(target) || bindingOf(target).kind !== "constructor") return null; + const dispatch = dispatchOf(lowerer, target); + if (!dispatch) return null; + const loc = locOf(access); + return select(dispatch, STRING, loc, (c) => { + // An own static `name` field replaces the class's name; a subclass has + // its own name. An instantiation's statics are its family's. + const owner = c.genericInstance?.family ?? c; + const field = owner.staticFields.find((f) => f.name === "name"); + if (field !== undefined) { + if (field.type.kind !== "string") + lowerer.unsupported( + "SC1090", + access, + `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static 'name' that is not a string`, + ); + return { kind: "varRef", localId: field.globalId, type: field.type, loc }; + } + if (owner.staticMethods?.has("name") || owner.genericStatics?.has("name")) + lowerer.unsupported( + "SC1090", + access, + `'new.target.name' where '${c.def.jsName ?? c.def.name}' declares a static method 'name'`, + ); + // A generic family's instantiations share its name. + if (c.generic) + return { kind: "strLit", value: c.def.jsName ?? c.generic.baseName, type: STRING, loc }; + return { + kind: "libCall", + fn: "class.name", + args: [classValueRef(lowerer, c, target)], + type: STRING, + loc, + }; + }); +} + +/** `new.target === C` (and `!==`, `==`, `!=`, either operand order) with a + * program class named by the other operand: an exact-class test. */ +export function lowerNewTargetComparison( + lowerer: Lowerer, + expr: ts.BinaryExpression, +): IrExpr | null { + const op = expr.operatorToken.kind; + const negated = + op === ts.SyntaxKind.ExclamationEqualsEqualsToken || + op === ts.SyntaxKind.ExclamationEqualsToken; + if ( + !negated && + op !== ts.SyntaxKind.EqualsEqualsEqualsToken && + op !== ts.SyntaxKind.EqualsEqualsToken + ) + return null; + const strip = (e: ts.Expression): ts.Expression => { + while (ts.isParenthesizedExpression(e)) e = e.expression; + return e; + }; + const left = strip(expr.left); + const right = strip(expr.right); + const target = isNewTarget(left) ? left : isNewTarget(right) ? right : null; + if (!target || bindingOf(target).kind !== "constructor") return null; + const other = target === left ? right : left; + if (!ts.isIdentifier(other)) return null; + // A constructor's new.target is never undefined. + if (other.text === "undefined" && (lowerer.typeOf(other).flags & ts.TypeFlags.Undefined) !== 0) + return { kind: "boolLit", value: negated, type: BOOL, loc: locOf(expr) }; + const symbol = lowerer.resolveValueSymbol(other); + const named = symbol ? lowerer.classBySymbol.get(symbol) : undefined; + if (!named || named.generic || named.localClass) return null; + const dispatch = dispatchOf(lowerer, target); + if (!dispatch) return null; + const loc = locOf(expr); + return select(dispatch, BOOL, loc, (c) => ({ + kind: "boolLit", + value: (c === named) !== negated, + type: BOOL, + loc, + })); +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json new file mode 100644 index 000000000..20d761c6c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7364-new-target-late-instantiations.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7364-new-target-late-instantiations.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json new file mode 100644 index 000000000..b4426af2c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7560-new-target-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7560-new-target-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json new file mode 100644 index 000000000..dc4eab819 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7561-new-target-generic-registry.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7561-new-target-generic-registry.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json new file mode 100644 index 000000000..7046e28d5 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-late-local-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-late-local-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json new file mode 100644 index 000000000..4f597a8a2 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-local-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-local-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json new file mode 100644 index 000000000..9f424457c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-plain-function.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-plain-function.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json new file mode 100644 index 000000000..1abcce3ad --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-generic-subclass.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-value-generic-subclass.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json new file mode 100644 index 000000000..249f8f4bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/new-target-value-signatures.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/new-target-value-signatures.ts" + ], + "diags": [] +} diff --git a/tests/corpus/7364-new-target-late-instantiations.ts b/tests/corpus/7364-new-target-late-instantiations.ts new file mode 100644 index 000000000..3fa7ff94a --- /dev/null +++ b/tests/corpus/7364-new-target-late-instantiations.ts @@ -0,0 +1,71 @@ +// new.target in a constructor whose subclasses are generic classes +// instantiated only in bodies lowered later, and classes whose own static +// `name` field replaces their name. +class Plain { + readonly label: string; + readonly plain: boolean; + constructor() { + this.label = new.target.name; + this.plain = new.target === Plain; + } +} +class Sub extends Plain { + readonly v: T; + constructor(v: T) { + super(); + this.v = v; + } +} + +class Box { + readonly label: string; + readonly exact: boolean; + readonly value: T; + constructor(value: T) { + this.value = value; + this.label = new.target.name; + this.exact = new.target === Leaf; + } +} +class Tagged extends Box {} +class Leaf extends Tagged {} + +class Named { + static name = "custom"; + readonly label: string; + constructor() { + this.label = new.target.name; + } +} +class Child extends Named {} +class Other extends Named { + static name = "other"; +} +class Family { + static name = "family"; + readonly label: string; + readonly v: T; + constructor(v: T) { + this.label = new.target.name; + this.v = v; + } +} + +function later(): void { + const n = new Sub(1); + const p = new Plain(); + console.log(n.label, new Sub("s").label, p.label, n.plain, p.plain); + const t = new Tagged(1); + const s = new Tagged("a"); + const b = new Box(2); + const l = new Leaf(3); + console.log(t.label, s.label, b.label, l.label, t.exact, l.exact, b.exact); + console.log(new Family(true).label); +} + +// The bases construct here first; their subclasses only in later(). +console.log(new Plain().label, new Box(false).label); +console.log(new Named().label, new Child().label, new Other().label); +Named.name = "changed"; +console.log(new Named().label, new Child().label, new Other().label); +later(); diff --git a/tests/corpus/7560-new-target-constructors.ts b/tests/corpus/7560-new-target-constructors.ts new file mode 100644 index 000000000..9e94200bf --- /dev/null +++ b/tests/corpus/7560-new-target-constructors.ts @@ -0,0 +1,55 @@ +// new.target in class constructors: the class under construction, through +// inherited and explicit subclass constructors, Error subclasses, arrows, +// identity comparisons and class values. +abstract class AppError extends Error { + abstract readonly status: number; + constructor(message: string) { + super(message); + this.name = new.target.name; + } +} +class NotFound extends AppError { + readonly status = 404; +} +class Invalid extends AppError { + readonly status = 400; + readonly field: string; + constructor(field: string, detail: number) { + super(`bad ${field} ${detail}`); + this.field = field; + } +} +class Strict extends Invalid {} +const errors: AppError[] = [new NotFound("missing"), new Invalid("isbn", 3), new Strict("title", 1)]; +for (const e of errors) console.log(e.name, e.status, e.message, String(e), e instanceof Invalid); + +class Shape { + readonly kind: string; + readonly exact: boolean; + constructor() { + this.kind = new.target.name; + this.exact = new.target === Shape; + const probe = () => new.target !== Circle; + console.log("probe", probe(), new.target == Square, new.target === undefined); + } + describe(): string { + return `${this.kind}${this.exact ? " (exact)" : ""}`; + } +} +class Circle extends Shape {} +class Square extends Shape { + constructor() { + super(); + console.log("square ctor sees", new.target.name); + } +} +for (const s of [new Shape(), new Circle(), new Square()]) console.log(s.describe()); + +class Node2 { + static make(): Node2 { return new this(); } + readonly origin: typeof Node2; + constructor() { this.origin = new.target; } + where(): string | undefined { return typeof (() => 1)() === "number" ? String(this.origin.name) : undefined; } +} +class Leaf extends Node2 {} +console.log(new Node2().origin === Node2, new Leaf().origin === Leaf, new Leaf().where(), Node2.make().origin.name); diff --git a/tests/corpus/7561-new-target-generic-registry.ts b/tests/corpus/7561-new-target-generic-registry.ts new file mode 100644 index 000000000..17866d143 --- /dev/null +++ b/tests/corpus/7561-new-target-generic-registry.ts @@ -0,0 +1,32 @@ +// new.target in generic classes and as a construction registry key. +class Box { + readonly label: string; + readonly value: T; + constructor(value: T) { + this.value = value; + this.label = `${new.target.name}<${typeof value}>`; + } +} +class NumBox extends Box { + constructor(n: number) { + super(n * 2); + } +} +const a = new Box("s"); +const b = new NumBox(4); +console.log(a.label, b.label, b.value); + +class Registry { + static readonly created: string[] = []; + constructor() { + Registry.created.push(new.target.name); + if (new.target === Registry) console.log("plain registry"); + } +} +class Users extends Registry {} +class Books extends Registry {} +new Users(); +new Books(); +new Registry(); +new Books(); +console.log(Registry.created.join(",")); diff --git a/tests/diagnostics/new-target-late-local-subclass.ts b/tests/diagnostics/new-target-late-local-subclass.ts new file mode 100644 index 000000000..658a7148e --- /dev/null +++ b/tests/diagnostics/new-target-late-local-subclass.ts @@ -0,0 +1,15 @@ +// A function-local subclass refuses new.target in its base's constructor +// also when lowering collects it after that constructor: make() is lowered +// after the top-level code that constructs Base. +class Base { + readonly label: string; + constructor() { + this.label = new.target.name; + } +} +console.log(new Base().label); +function make(): Base { + const Local = class extends Base {}; + return new Local(); +} +console.log(make().label); diff --git a/tests/diagnostics/new-target-local-subclass.ts b/tests/diagnostics/new-target-local-subclass.ts new file mode 100644 index 000000000..a5f153049 --- /dev/null +++ b/tests/diagnostics/new-target-local-subclass.ts @@ -0,0 +1,5 @@ +// A function-local subclass makes a new class per evaluation, so the +// base constructor's new.target has no static dispatch. +class A { constructor() { console.log(new.target.name); } } +function make() { class Local extends A {} return new Local(); } +make(); diff --git a/tests/diagnostics/new-target-plain-function.ts b/tests/diagnostics/new-target-plain-function.ts new file mode 100644 index 000000000..da592e084 --- /dev/null +++ b/tests/diagnostics/new-target-plain-function.ts @@ -0,0 +1,3 @@ +// new.target in a plain function has no invocation record to read. +function F(this: unknown) { console.log(new.target === undefined); } +F.call(null); diff --git a/tests/diagnostics/new-target-value-generic-subclass.ts b/tests/diagnostics/new-target-value-generic-subclass.ts new file mode 100644 index 000000000..2107a39a5 --- /dev/null +++ b/tests/diagnostics/new-target-value-generic-subclass.ts @@ -0,0 +1,5 @@ +// new.target as a class value where a subclass is generic: its +// instantiations have no common class value. +class A { readonly t: typeof A; constructor() { this.t = new.target; } } +class B extends A { readonly v: T | undefined = undefined; } +console.log(new B().t.name); diff --git a/tests/diagnostics/new-target-value-signatures.ts b/tests/diagnostics/new-target-value-signatures.ts new file mode 100644 index 000000000..af0c906e5 --- /dev/null +++ b/tests/diagnostics/new-target-value-signatures.ts @@ -0,0 +1,5 @@ +// new.target as a class value needs every subclass to construct with the +// base's signature; B's constructor takes a parameter A's does not. +class A { readonly t: typeof A; constructor() { this.t = new.target; } } +class B extends A { constructor(readonly n: number) { super(); } } +console.log(new B(1).t.name); diff --git a/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt new file mode 100644 index 000000000..feb663813 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-late-local-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-late-local-subclass.ts:7:18 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet + + 6 | constructor() { + 7 | this.label = new.target.name; + | ^~~~~~~~~~ + 8 | } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-local-subclass.ts.txt b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt new file mode 100644 index 000000000..0f1d465b1 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-local-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-local-subclass.ts:3:39 - error SC1090: 'new.target' where 'Local' is a function-local class (each evaluation creates a new class) is not supported yet + + 2 | // base constructor's new.target has no static dispatch. + 3 | class A { constructor() { console.log(new.target.name); } } + | ^~~~~~~~~~ + 4 | function make() { class Local extends A {} return new Local(); } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-plain-function.ts.txt b/tests/harness/__snapshots__/new-target-plain-function.ts.txt new file mode 100644 index 000000000..5005f5915 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-plain-function.ts.txt @@ -0,0 +1,6 @@ +new-target-plain-function.ts:2:41 - error SC1090: 'new.target' in a plain function (a constructible function's invocation record does not exist in compiled code; use a class constructor) is not supported yet + + 1 | // new.target in a plain function has no invocation record to read. + 2 | function F(this: unknown) { console.log(new.target === undefined); } + | ^~~~~~~~~~ + 3 | F.call(null); \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt new file mode 100644 index 000000000..0e5cf2e52 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-value-generic-subclass.ts.txt @@ -0,0 +1,6 @@ +new-target-value-generic-subclass.ts:3:58 - error SC1090: 'new.target' as a value where 'B' is a generic subclass (its instantiations have no common class value; read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet + + 2 | // instantiations have no common class value. + 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } } + | ^~~~~~~~~~ + 4 | class B extends A { readonly v: T | undefined = undefined; } \ No newline at end of file diff --git a/tests/harness/__snapshots__/new-target-value-signatures.ts.txt b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt new file mode 100644 index 000000000..9c38e4d51 --- /dev/null +++ b/tests/harness/__snapshots__/new-target-value-signatures.ts.txt @@ -0,0 +1,6 @@ +new-target-value-signatures.ts:3:58 - error SC1090: 'new.target' as a value where 'B' constructs with a different signature than 'A' (read 'new.target.name' or compare 'new.target' with a class instead) is not supported yet + + 2 | // base's signature; B's constructor takes a parameter A's does not. + 3 | class A { readonly t: typeof A; constructor() { this.t = new.target; } } + | ^~~~~~~~~~ + 4 | class B extends A { constructor(readonly n: number) { super(); } } \ No newline at end of file diff --git a/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json new file mode 100644 index 000000000..725cf769c --- /dev/null +++ b/tests/harness/corpus-costs/7364-new-target-late-instantiations.ts.json @@ -0,0 +1 @@ +{ "plain": 4300, "san": 17500 } diff --git a/tests/harness/corpus-costs/7560-new-target-constructors.ts.json b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json new file mode 100644 index 000000000..2c7339a24 --- /dev/null +++ b/tests/harness/corpus-costs/7560-new-target-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 6000, "san": 26000 } diff --git a/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json new file mode 100644 index 000000000..eceea988d --- /dev/null +++ b/tests/harness/corpus-costs/7561-new-target-generic-registry.ts.json @@ -0,0 +1 @@ +{ "plain": 4500, "san": 23500 }