mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-07 08:33:50 +08:00
Dispatch new.target over generic subclasses and own static names
A constructor's new.target dispatch skipped generic class families, so a generic subclass instantiated only in a body lowered later answered with the base's name, and `new.target === Base` held for its instances. A family now has its own arm, tested by its instanceof interval, which holds every instantiation: `new.target.name` reads the family's name and comparisons with a program class are false. Under an instantiation, the generic subclasses of its family are candidates too. `new.target` as a value refuses generic subclasses, which have no common class value. `new.target.name` reads a class's own static `name` field, which replaces its name (a subclass has its own name); a static method `name` or a non-string field is refused.
This commit is contained in:
@@ -43,7 +43,9 @@ function bindingOf(node: ts.Node): Binding {
|
||||
|
||||
interface Dispatch {
|
||||
info: ClassInfo;
|
||||
/** The constructor's class and its descendants, deepest first. */
|
||||
/** 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;
|
||||
}
|
||||
@@ -69,11 +71,23 @@ function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null {
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const classes = [info];
|
||||
// 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 || other.generic) continue;
|
||||
if (lowerer.isSubclassOf(other.def.name, info.def.name)) classes.push(other);
|
||||
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(
|
||||
@@ -84,7 +98,6 @@ function dispatchOf(lowerer: Lowerer, expr: ts.MetaProperty): Dispatch | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
classes.sort((a, b) => depth(b) - depth(a));
|
||||
let self: IrExpr | null = null;
|
||||
if (classes.length > 1) {
|
||||
const thisLocal = lowerer.resolveThis(expr);
|
||||
@@ -140,6 +153,13 @@ export function lowerNewTarget(lowerer: Lowerer, expr: ts.MetaProperty): IrExpr
|
||||
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",
|
||||
@@ -166,13 +186,37 @@ export function lowerNewTargetName(
|
||||
const dispatch = dispatchOf(lowerer, target);
|
||||
if (!dispatch) return null;
|
||||
const loc = locOf(access);
|
||||
return select(dispatch, STRING, loc, (c) => ({
|
||||
kind: "libCall",
|
||||
fn: "class.name",
|
||||
args: [classValueRef(lowerer, c, target)],
|
||||
type: STRING,
|
||||
loc,
|
||||
}));
|
||||
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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7364-new-target-late-instantiations.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/diagnostics/new-target-value-generic-subclass.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,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,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 @@
|
||||
{ "plain": 4300, "san": 17500 }
|
||||
Reference in New Issue
Block a user