Skip to content

Commit 4a7471e

Browse files
committed
Implement [LegacyFactoryFunction]
1 parent e025c84 commit 4a7471e

9 files changed

+2984
-1305
lines changed

Diff for: README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,17 @@ It is often useful for implementation classes to inherit from each other, if the
421421

422422
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.
423423

424+
### The `[LegacyFactoryFunction]` extended attribute
425+
426+
For interfaces which have the `[LegacyFactoryFunction]` extended attribute, the implementation class file must contain the `legacyFactoryFunction` export, with the signature `(thisValue, globalObject, [...legacyFactoryFunctionArgs], { newTarget, wrapper })`, which is used for:
427+
428+
- Setting up initial state that will always be used, such as caches or default values
429+
- `thisValue` holds the value of a new uninitialized implementation instance, which may be ignored by returning a different object, similarly to how constructors with overridden return values are implemented.
430+
- Keep a reference to the relevant `globalObject` for later consumption.
431+
- Processing constructor arguments `legacyFactoryFunctionArgs` passed to the legacy factory function constructor, if the legacy factory function takes arguments.
432+
- `newTarget` holds a reference to the value of `new.target` that the legacy factor function was invoked with.
433+
- `wrapper` holds a reference to the uninitialized wrapper instance, just like in `privateData` with the standard impl constructor.
434+
424435
### The init export
425436

426437
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.
@@ -484,6 +495,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
484495
- `[Clamp]`
485496
- `[EnforceRange]`
486497
- `[Exposed]`
498+
- `[LegacyFactoryFunction]`
487499
- `[LegacyLenientThis]`
488500
- `[LegacyLenientSetter]`
489501
- `[LegacyNoInterfaceObject]`
@@ -510,7 +522,6 @@ Notable missing features include:
510522
- `[AllowShared]`
511523
- `[Default]` (for `toJSON()` operations)
512524
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
513-
- `[LegacyFactoryFunction]`
514525
- `[LegacyNamespace]`
515526
- `[LegacyTreatNonObjectAsNull]`
516527
- `[SecureContext]`

Diff for: lib/constructs/interface.js

+92
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Interface {
4646
this.attributes = new Map();
4747
this.staticAttributes = new Map();
4848
this.constants = new Map();
49+
this.legacyFactoryFunctions = [];
4950

5051
this.indexedGetter = null;
5152
this.indexedSetter = null;
@@ -391,6 +392,28 @@ class Interface {
391392
throw new Error(msg);
392393
}
393394
}
395+
396+
let legacyFactoryFunctionName;
397+
for (const attr of this.idl.extAttrs) {
398+
if (attr.name === "LegacyFactoryFunction") {
399+
if (!attr.rhs || attr.rhs.type !== "identifier" || !attr.arguments) {
400+
throw new Error(`[LegacyFactoryFunction] must take a named argument list`);
401+
}
402+
403+
const name = attr.rhs.value;
404+
if (legacyFactoryFunctionName === undefined) {
405+
legacyFactoryFunctionName = name;
406+
} else if (legacyFactoryFunctionName !== name) {
407+
// This is currently valid, but not used anywhere, and there are plans to disallow it:
408+
// https://github.com/heycam/webidl/issues/878
409+
throw new Error(
410+
`Multiple [LegacyFactoryFunction] definitions with different names are not supported on ${this.name}`
411+
);
412+
}
413+
414+
this.legacyFactoryFunctions.push(attr);
415+
}
416+
}
394417
}
395418

396419
get supportsIndexedProperties() {
@@ -1644,6 +1667,73 @@ class Interface {
16441667
`;
16451668
}
16461669

1670+
generateLegacyFactoryFunction() {
1671+
const { legacyFactoryFunctions } = this;
1672+
if (legacyFactoryFunctions.length === 0) {
1673+
return;
1674+
}
1675+
1676+
const name = legacyFactoryFunctions[0].rhs.value;
1677+
1678+
if (!name) {
1679+
throw new Error(`Internal error: The legacy factory function does not have a name (in interface ${this.name})`);
1680+
}
1681+
1682+
const overloads = Overloads.getEffectiveOverloads("legacy factory function", name, 0, this);
1683+
let minOp = overloads[0];
1684+
for (let i = 1; i < overloads.length; ++i) {
1685+
if (overloads[i].nameList.length < minOp.nameList.length) {
1686+
minOp = overloads[i];
1687+
}
1688+
}
1689+
1690+
const args = minOp.nameList;
1691+
const conversions = Parameters.generateOverloadConversions(
1692+
this.ctx,
1693+
"legacy factory function",
1694+
name,
1695+
this,
1696+
`Failed to construct '${name}': `
1697+
);
1698+
this.requires.merge(conversions.requires);
1699+
1700+
const argsParam = conversions.hasArgs ? "args" : "[]";
1701+
1702+
this.str += `
1703+
function ${name}(${utils.formatArgs(args)}) {
1704+
if (new.target === undefined) {
1705+
throw new TypeError("Class constructor ${name} cannot be invoked without 'new'");
1706+
}
1707+
1708+
${conversions.body}
1709+
`;
1710+
1711+
// This implements the WebIDL legacy factory function behavior, as well as support for overridding
1712+
// the return type, which is used by HTML's element legacy factory functions:
1713+
this.str += `
1714+
const thisValue = exports.new(globalObject, new.target);
1715+
const result = Impl.legacyFactoryFunction(thisValue, globalObject, ${argsParam}, {
1716+
wrapper: utils.wrapperForImpl(thisValue),
1717+
newTarget: new.target
1718+
});
1719+
return utils.tryWrapperForImpl(utils.isObject(result) ? result : thisValue);
1720+
}
1721+
1722+
Object.defineProperty(${name}, "prototype", {
1723+
configurable: false,
1724+
enumerable: false,
1725+
writable: false,
1726+
value: ${this.name}.prototype
1727+
})
1728+
1729+
Object.defineProperty(globalObject, "${name}", {
1730+
configurable: true,
1731+
writable: true,
1732+
value: ${name}
1733+
});
1734+
`;
1735+
}
1736+
16471737
generateInstall() {
16481738
const { idl, name } = this;
16491739

@@ -1712,6 +1802,8 @@ class Interface {
17121802
}
17131803
}
17141804

1805+
this.generateLegacyFactoryFunction();
1806+
17151807
this.str += `
17161808
};
17171809
`;

Diff for: lib/overloads.js

+2
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;
1416
}
1517
throw new RangeError(`${type}s are not yet supported`);
1618
}

0 commit comments

Comments
 (0)