Skip to content

Commit

Permalink
Implement [LegacyFactoryFunction]
Browse files Browse the repository at this point in the history
  • Loading branch information
ExE-Boss committed Mar 20, 2021
1 parent e025c84 commit 4a7471e
Show file tree
Hide file tree
Showing 9 changed files with 2,984 additions and 1,305 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,17 @@ 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 `(thisValue, globalObject, [...legacyFactoryFunctionArgs], { newTarget, wrapper })`, which is used for:

- Setting up initial state that will always be used, such as caches or default values
- `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.
- 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.
- `newTarget` holds a reference to the value of `new.target` that the legacy factor function was invoked with.
- `wrapper` holds a reference to the uninitialized wrapper instance, just like in `privateData` with the standard impl constructor.

### 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 +495,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 +522,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
92 changes: 92 additions & 0 deletions lib/constructs/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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 @@ -391,6 +392,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/heycam/webidl/issues/878
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 @@ -1644,6 +1667,73 @@ 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 argsParam = 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 thisValue = exports.new(globalObject, new.target);
const result = Impl.legacyFactoryFunction(thisValue, globalObject, ${argsParam}, {
wrapper: utils.wrapperForImpl(thisValue),
newTarget: new.target
});
return utils.tryWrapperForImpl(utils.isObject(result) ? result : thisValue);
}
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 @@ -1712,6 +1802,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 4a7471e

Please sign in to comment.