Skip to content

Commit 45cd1a8

Browse files
committed
Implement [LegacyFactoryFunction]
1 parent 8330bc3 commit 45cd1a8

File tree

8 files changed

+3800
-1659
lines changed

8 files changed

+3800
-1659
lines changed

README.md

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,11 @@ Creates a new instance of the wrapper class and corresponding implementation cla
282282

283283
This is useful inside implementation class files, where it is easiest to only deal with impls, not wrappers.
284284

285-
#### `new(globalObject)`
285+
#### `new(globalObject, newTarget)`
286286

287287
Creates a new instance of the wrapper class and corresponding implementation class, but without invoking the implementation class constructor logic. Then returns the implementation class.
288288

289-
This corresponds to the [Web IDL "new" algorithm](https://heycam.github.io/webidl/#new), and is useful when implementing specifications that initialize objects in different ways than their constructors do.
289+
This corresponds to the [WebIDL "create a new object implementing the interface"](https://heycam.github.io/webidl/#new) and ["internally create a new object implementing the interface"](https://heycam.github.io/webidl/#internally-create-a-new-object-implementing-the-interface) algorithms, and is useful when implementing specifications that initialize objects in different ways than their constructors do.
290290

291291
#### `setup(obj, globalObject, constructorArgs, privateData)`
292292

@@ -407,6 +407,24 @@ It is often useful for implementation classes to inherit from each other, if the
407407

408408
However, it is not required! The wrapper classes will have a correct inheritance chain, regardless of the implementation class inheritance chain. Just make sure that, either via inheritance or manual implementation, you implement all of the expected operations and attributes.
409409

410+
### The `[LegacyFactoryFunction]` extended attribute
411+
412+
For interfaces which have the `[LegacyFactoryFunction]` extended attribute, the implementation class file must contain the `legacyFactoryFunction` export, with the signature `(globalObject, legacyFactoryFunctionArgs, legacyFactoryFunctionName)`, which is used for:
413+
414+
- Setting up initial state that will always be used, such as caches or default values
415+
- Keep a reference to the relevant `globalObject` for later consumption.
416+
- Processing constructor arguments `legacyFactoryFunctionArgs` passed to the legacy factory function constructor, if the legacy factory function takes arguments.
417+
- Switching on the `legacyFactoryFunctionName`, if the interface defines multiple legacy factory functions, eg.:
418+
419+
```webidl
420+
[LegacyFactoryFunction=OverloadedLegacyFactoryFunction(DOMString arg1),
421+
LegacyFactoryFunction=OverloadedLegacyFactoryFunction(long arg1, long arg2),
422+
LegacyFactoryFunction=SimpleLegacyFactoryFunction(optional DOMString src)]
423+
interface SomeInterface {};
424+
```
425+
426+
The `legacyFactoryFunction` export is called with a `this` value of a new uninitialized implementation instance, which may be ignored by returning a different object, similarly to how constructors with overridden return types are implemented.
427+
410428
### The init export
411429

412430
In addition to the `implementation` export, for interfaces, your implementation class file can contain an `init` export. This would be a function taking as an argument an instance of the implementation class, and is called when any wrapper/implementation pairs are constructed (such as by the exports of the [generated wrapper module](https://github.com/jsdom/webidl2js#for-interfaces)). In particular, it is called even if they are constructed by [`new()`](newglobalobject), which does not invoke the implementation class constructor.
@@ -469,6 +487,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
469487
- `[Clamp]`
470488
- `[EnforceRange]`
471489
- `[Exposed]`
490+
- `[LegacyFactoryFunction]`
472491
- `[LegacyLenientThis]`
473492
- `[LegacyLenientSetter]`
474493
- `[LegacyNoInterfaceObject]`
@@ -494,7 +513,6 @@ Notable missing features include:
494513
- `[AllowShared]`
495514
- `[Default]` (for `toJSON()` operations)
496515
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
497-
- `[LegacyFactoryFunction]`
498516
- `[LegacyNamespace]`
499517
- `[LegacyTreatNonObjectAsNull]`
500518
- `[SecureContext]`

lib/constructs/interface.js

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,65 @@ const defaultClassMethodDescriptor = {
3333
writable: true
3434
};
3535

36+
class LegacyFactoryFunction {
37+
constructor(ctx, I, idl) {
38+
this.ctx = ctx;
39+
this.interface = I;
40+
this.idls = [idl];
41+
this.name = idl.rhs.value;
42+
}
43+
44+
generate() {
45+
const requires = new utils.RequiresMap(this.ctx);
46+
47+
let str = "";
48+
49+
if (!this.name) {
50+
throw new Error(`Internal error: this legacy factory function does not have a name (in interface ${this.interface.name})`);
51+
}
52+
53+
const overloads = Overloads.getEffectiveOverloads("legacy factory function", this.name, 0, this.interface);
54+
let minOp = overloads[0];
55+
for (let i = 1; i < overloads.length; ++i) {
56+
if (overloads[i].nameList.length < minOp.nameList.length) {
57+
minOp = overloads[i];
58+
}
59+
}
60+
61+
const argNames = minOp.nameList;
62+
63+
const conversions = Parameters.generateOverloadConversions(
64+
this.ctx, "legacy factory function", this.name, this.interface, `Failed to construct '${this.name}': `);
65+
requires.merge(conversions.requires);
66+
67+
const setupArgs = [
68+
"globalObject",
69+
conversions.hasArgs ? "args" : "[]",
70+
`"${this.name}"`
71+
];
72+
73+
str += `
74+
if (new.target === undefined) {
75+
throw new TypeError("Class constructor ${this.name} cannot be invoked without 'new'");
76+
}
77+
78+
${conversions.body}
79+
`;
80+
81+
// This implements the WebIDL legacy factory function behavior, as well as support for overridding
82+
// the return type, which is used by HTML's element legacy factory functions:
83+
str += `
84+
const thisArgument = exports.new(globalObject, new.target);
85+
const result = Impl.legacyFactoryFunction.call(thisArgument, ${formatArgs(setupArgs)});
86+
return utils.tryWrapperForImpl(utils.isObject(result) ? result : thisArgument);
87+
`;
88+
89+
this.interface.addLegacyFactoryFunction(this.name, argNames, str);
90+
91+
return { requires };
92+
}
93+
}
94+
3695
class Interface {
3796
constructor(ctx, idl, opts) {
3897
this.ctx = ctx;
@@ -54,6 +113,7 @@ class Interface {
54113
this.attributes = new Map();
55114
this.staticAttributes = new Map();
56115
this.constants = new Map();
116+
this.legacyFactoryFunctions = new Map();
57117

58118
this.indexedGetter = null;
59119
this.indexedSetter = null;
@@ -70,6 +130,7 @@ class Interface {
70130
this._outputStaticMethods = new Map();
71131
this._outputProperties = new Map();
72132
this._outputStaticProperties = new Map();
133+
this._outputLegacyFactoryFunctions = new Map();
73134

74135
const global = utils.getExtAttr(this.idl.extAttrs, "Global");
75136
this.isGlobal = Boolean(global);
@@ -181,6 +242,10 @@ class Interface {
181242
this._outputStaticMethods.set(propName, { type, args, body, descriptor });
182243
}
183244

245+
addLegacyFactoryFunction(name, args, body) {
246+
this._outputLegacyFactoryFunctions.set(name, { args, body });
247+
}
248+
184249
// whence is either "instance" or "prototype"
185250
addProperty(whence, propName, str, {
186251
configurable = true,
@@ -394,6 +459,22 @@ class Interface {
394459
throw new Error(msg);
395460
}
396461
}
462+
463+
for (const attr of this.idl.extAttrs) {
464+
if (attr.name === "LegacyFactoryFunction") {
465+
if (attr.rhs.type !== "identifier" || !attr.arguments) {
466+
throw new Error(`[LegacyFactoryFunction] must take a named argument list`);
467+
}
468+
469+
const name = attr.rhs.value;
470+
471+
if (!this.legacyFactoryFunctions.has(name)) {
472+
this.legacyFactoryFunctions.set(name, new LegacyFactoryFunction(this.ctx, this, attr));
473+
} else {
474+
this.legacyFactoryFunctions.get(name).idls.push(attr);
475+
}
476+
}
477+
}
397478
}
398479

399480
get supportsIndexedProperties() {
@@ -1125,17 +1206,25 @@ class Interface {
11251206

11261207
generateIface() {
11271208
this.str += `
1128-
function makeWrapper(globalObject) {
1209+
function makeWrapper(globalObject, newTarget) {
11291210
if (globalObject[ctorRegistrySymbol] === undefined) {
11301211
throw new Error('Internal error: invalid global object');
11311212
}
11321213
1133-
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
1134-
if (ctor === undefined) {
1135-
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
1214+
let prototype;
1215+
if (newTarget !== undefined) {
1216+
({ prototype } = newTarget);
1217+
}
1218+
1219+
if (!utils.isObject(prototype)) {
1220+
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
1221+
if (ctor === undefined) {
1222+
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
1223+
}
1224+
({ prototype } = ctor);
11361225
}
11371226
1138-
return Object.create(ctor.prototype);
1227+
return Object.create(prototype);
11391228
}
11401229
`;
11411230

@@ -1203,8 +1292,8 @@ class Interface {
12031292
return wrapper;
12041293
};
12051294
1206-
exports.new = globalObject => {
1207-
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject);
1295+
exports.new = (globalObject, newTarget) => {
1296+
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject, newTarget);
12081297
12091298
exports._internalSetup(wrapper, globalObject);
12101299
Object.defineProperty(wrapper, implSymbol, {
@@ -1314,6 +1403,11 @@ class Interface {
13141403
const data = member.generate();
13151404
this.requires.merge(data.requires);
13161405
}
1406+
1407+
for (const legacyFactoryFunction of this.legacyFactoryFunctions.values()) {
1408+
const data = legacyFactoryFunction.generate();
1409+
this.requires.merge(data.requires);
1410+
}
13171411
}
13181412

13191413
generateOffInstanceMethods() {
@@ -1478,6 +1572,31 @@ class Interface {
14781572
}
14791573
}
14801574

1575+
generateLegacyFactoryFunctions() {
1576+
for (const [name, { args, body }] of this._outputLegacyFactoryFunctions) {
1577+
this.str += `
1578+
{
1579+
function ${name}(${formatArgs(args)}) {
1580+
${body}
1581+
}
1582+
1583+
Object.defineProperty(${name}, "prototype", {
1584+
configurable: false,
1585+
enumerable: false,
1586+
writable: false,
1587+
value: ${this.name}.prototype
1588+
})
1589+
1590+
Object.defineProperty(globalObject, "${name}", {
1591+
configurable: true,
1592+
writable: true,
1593+
value: ${name}
1594+
});
1595+
}
1596+
`;
1597+
}
1598+
}
1599+
14811600
generateInstall() {
14821601
const { idl, name } = this;
14831602

@@ -1543,6 +1662,8 @@ class Interface {
15431662
}
15441663
}
15451664

1665+
this.generateLegacyFactoryFunctions();
1666+
15461667
this.str += `
15471668
};
15481669
`;

lib/overloads.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ function getOperations(type, A, I) {
1111
case "constructor": {
1212
return I.constructorOperations;
1313
}
14+
case "legacy factory function":
15+
return I.legacyFactoryFunctions.get(A).idls;
1416
}
1517
throw new RangeError(`${type}s are not yet supported`);
1618
}

0 commit comments

Comments
 (0)