Skip to content

Commit

Permalink
Implement [LegacyFactoryFunction]
Browse files Browse the repository at this point in the history
  • Loading branch information
ExE-Boss committed Jun 25, 2020
1 parent 1933ade commit f8cb248
Show file tree
Hide file tree
Showing 9 changed files with 6,344 additions and 3,958 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,11 @@ Creates a new instance of the wrapper class and corresponding implementation cla

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

#### `new(globalObject)`
#### `new(globalObject, newTarget)`

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.

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

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

Expand Down Expand Up @@ -421,6 +421,16 @@ It is often useful for implementation classes to inherit from each other, if the

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.

### The `[LegacyFactoryFunction]` extended attribute

For interfaces which have the `[LegacyFactoryFunction]` extended attribute, the implementation class file must contain the `legacyFactoryFunction` export, with the signature `(globalObject, ...legacyFactoryFunctionArgs)`, which is used for:

- Setting up initial state that will always be used, such as caches or default values
- Keep a reference to the relevant `globalObject` for later consumption.
- Processing constructor arguments `legacyFactoryFunctionArgs` passed to the legacy factory function constructor, if the legacy factory function takes arguments.

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 values are implemented.

### The init export

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.
Expand Down Expand Up @@ -484,6 +494,7 @@ webidl2js is implementing an ever-growing subset of the Web IDL specification. S
- `[Clamp]`
- `[EnforceRange]`
- `[Exposed]`
- `[LegacyFactoryFunction]`
- `[LegacyLenientThis]`
- `[LegacyLenientSetter]`
- `[LegacyNoInterfaceObject]`
Expand All @@ -510,7 +521,6 @@ Notable missing features include:
- `[AllowShared]`
- `[Default]` (for `toJSON()` operations)
- `[Global]`'s various consequences, including the named properties object and `[[SetPrototypeOf]]`
- `[LegacyFactoryFunction]`
- `[LegacyNamespace]`
- `[LegacyTreatNonObjectAsNull]`
- `[SecureContext]`
Expand Down
106 changes: 99 additions & 7 deletions lib/constructs/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Interface {
this.attributes = new Map();
this.staticAttributes = new Map();
this.constants = new Map();
this.legacyFactoryFunctions = [];

this.indexedGetter = null;
this.indexedSetter = null;
Expand Down Expand Up @@ -394,6 +395,28 @@ class Interface {
throw new Error(msg);
}
}

let legacyFactoryFunctionName;
for (const attr of this.idl.extAttrs) {
if (attr.name === "LegacyFactoryFunction") {
if (!attr.rhs || attr.rhs.type !== "identifier" || !attr.arguments) {
throw new Error(`[LegacyFactoryFunction] must take a named argument list`);
}

const name = attr.rhs.value;
if (legacyFactoryFunctionName === undefined) {
legacyFactoryFunctionName = name;
} else if (legacyFactoryFunctionName !== name) {
// This is currently valid, but not used anywhere, and there are plans to disallow it:
// https://github.com/jsdom/webidl2js/pull/213#issuecomment-621277733
throw new Error(
`Multiple [LegacyFactoryFunction] definitions with different names are not supported on ${this.name}`
);
}

this.legacyFactoryFunctions.push(attr);
}
}
}

get supportsIndexedProperties() {
Expand Down Expand Up @@ -1202,17 +1225,25 @@ class Interface {

generateIface() {
this.str += `
function makeWrapper(globalObject) {
function makeWrapper(globalObject, newTarget) {
if (globalObject[ctorRegistrySymbol] === undefined) {
throw new Error('Internal error: invalid global object');
}
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
if (ctor === undefined) {
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
let prototype;
if (newTarget !== undefined) {
({ prototype } = newTarget);
}
if (!utils.isObject(prototype)) {
const ctor = globalObject[ctorRegistrySymbol]["${this.name}"];
if (ctor === undefined) {
throw new Error('Internal error: constructor ${this.name} is not installed on the passed global object');
}
({ prototype } = ctor);
}
return Object.create(ctor.prototype);
return Object.create(prototype);
}
`;

Expand Down Expand Up @@ -1280,8 +1311,8 @@ class Interface {
return wrapper;
};
exports.new = globalObject => {
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject);
exports.new = (globalObject, newTarget) => {
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject, newTarget);
exports._internalSetup(wrapper, globalObject);
Object.defineProperty(wrapper, implSymbol, {
Expand Down Expand Up @@ -1555,6 +1586,65 @@ class Interface {
}
}

generateLegacyFactoryFunction() {
const { legacyFactoryFunctions } = this;
if (legacyFactoryFunctions.length === 0) {
return;
}

const name = legacyFactoryFunctions[0].rhs.value;

if (!name) {
throw new Error(`Internal error: The legacy factory function does not have a name (in interface ${this.name})`);
}

const overloads = Overloads.getEffectiveOverloads("legacy factory function", name, 0, this);
let minOp = overloads[0];
for (let i = 1; i < overloads.length; ++i) {
if (overloads[i].nameList.length < minOp.nameList.length) {
minOp = overloads[i];
}
}

const args = minOp.nameList;
const conversions = Parameters.generateOverloadConversions(
this.ctx, "legacy factory function", name, this, `Failed to construct '${name}': `);
this.requires.merge(conversions.requires);

const argsSpread = conversions.hasArgs ? "...args" : "";

this.str += `
function ${name}(${utils.formatArgs(args)}) {
if (new.target === undefined) {
throw new TypeError("Class constructor ${name} cannot be invoked without 'new'");
}
${conversions.body}
`;

// This implements the WebIDL legacy factory function behavior, as well as support for overridding
// the return type, which is used by HTML's element legacy factory functions:
this.str += `
const thisArgument = exports.new(globalObject, new.target);
const result = Impl.legacyFactoryFunction.call(thisArgument, globalObject, ${argsSpread});
return utils.tryWrapperForImpl(utils.isObject(result) ? result : thisArgument);
}
Object.defineProperty(${name}, "prototype", {
configurable: false,
enumerable: false,
writable: false,
value: ${this.name}.prototype
})
Object.defineProperty(globalObject, "${name}", {
configurable: true,
writable: true,
value: ${name}
});
`;
}

generateInstall() {
const { idl, name } = this;

Expand Down Expand Up @@ -1620,6 +1710,8 @@ class Interface {
}
}

this.generateLegacyFactoryFunction();

this.str += `
};
`;
Expand Down
2 changes: 2 additions & 0 deletions lib/overloads.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ function getOperations(type, A, I) {
case "constructor": {
return I.constructorOperations;
}
case "legacy factory function":
return I.legacyFactoryFunctions;
}
throw new RangeError(`${type}s are not yet supported`);
}
Expand Down
Loading

0 comments on commit f8cb248

Please sign in to comment.