mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Support new.target in class constructors (#1917)
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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<number, string>();
|
||||
|
||||
@@ -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<ts.Node>,
|
||||
): 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<T> extends Box<T>` under Box<number>).
|
||||
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,
|
||||
}));
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7364-new-target-late-instantiations.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7560-new-target-constructors.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7561-new-target-generic-registry.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-late-local-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-local-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-plain-function.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-value-generic-subclass.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-value-signatures.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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<T> extends Plain {
|
||||
readonly v: T;
|
||||
constructor(v: T) {
|
||||
super();
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
class Box<T> {
|
||||
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<T> extends Box<T> {}
|
||||
class Leaf extends Tagged<number> {}
|
||||
|
||||
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<T> {
|
||||
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<number>(1);
|
||||
const p = new Plain();
|
||||
console.log(n.label, new Sub<string>("s").label, p.label, n.plain, p.plain);
|
||||
const t = new Tagged<number>(1);
|
||||
const s = new Tagged<string>("a");
|
||||
const b = new Box<number>(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<boolean>(true).label);
|
||||
}
|
||||
|
||||
// The bases construct here first; their subclasses only in later().
|
||||
console.log(new Plain().label, new Box<boolean>(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();
|
||||
@@ -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);
|
||||
@@ -0,0 +1,32 @@
|
||||
// new.target in generic classes and as a construction registry key.
|
||||
class Box<T> {
|
||||
readonly label: string;
|
||||
readonly value: T;
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
this.label = `${new.target.name}<${typeof value}>`;
|
||||
}
|
||||
}
|
||||
class NumBox extends Box<number> {
|
||||
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(","));
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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<T> extends A { readonly v: T | undefined = undefined; }
|
||||
console.log(new B<number>().t.name);
|
||||
@@ -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);
|
||||
@@ -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 | }
|
||||
@@ -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(); }
|
||||
@@ -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);
|
||||
@@ -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<T> extends A { readonly v: T | undefined = undefined; }
|
||||
@@ -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(); } }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4300, "san": 17500 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6000, "san": 26000 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4500, "san": 23500 }
|
||||
Reference in New Issue
Block a user