webidl2js
Version:
Auto-generates class structures for WebIDL specifications
1,548 lines (1,384 loc) • 53.5 kB
JavaScript
"use strict";
const utils = require("../utils");
const Attribute = require("./attribute");
const Constant = require("./constant");
const Iterable = require("./iterable");
const AsyncIterable = require("./async-iterable");
const Operation = require("./operation");
const Types = require("../types");
const Overloads = require("../overloads");
const Parameters = require("../parameters");
function isNamed(idl) {
return idl.arguments[0].idlType.idlType === "DOMString";
}
function isIndexed(idl) {
return idl.arguments[0].idlType.idlType === "unsigned long";
}
const defaultObjectLiteralDescriptor = {
configurable: true,
enumerable: true,
writable: true
};
const defaultClassMethodDescriptor = {
configurable: true,
enumerable: false,
writable: true
};
class Interface {
constructor(ctx, idl, opts) {
this.ctx = ctx;
this.idl = idl;
this.name = idl.name;
this.str = null;
this.opts = opts;
this.requires = new utils.RequiresMap(ctx);
this.included = [];
this.constructorOperations = [];
this.operations = new Map();
this.staticOperations = new Map();
this.attributes = new Map();
this.staticAttributes = new Map();
this.constants = new Map();
this.indexedGetter = null;
this.indexedSetter = null;
this.namedGetter = null;
this.namedSetter = null;
this.namedDeleter = null;
this.stringifier = null;
this.iterable = null;
this._analyzed = false;
this._needsUnforgeablesObject = false;
this._outputMethods = new Map();
this._outputStaticMethods = new Map();
this._outputProperties = new Map();
this._outputStaticProperties = new Map();
const global = utils.getExtAttr(this.idl.extAttrs, "Global");
this.isGlobal = Boolean(global);
if (global && !global.rhs) {
throw new Error(`[Global] must take an identifier or an identifier list in interface ${this.name}`);
}
const exposed = utils.getExtAttr(this.idl.extAttrs, "Exposed");
if (!exposed) {
throw new Error(`Interface ${this.name} lacks [Exposed]`);
}
if (!exposed.rhs || (exposed.rhs.type !== "identifier" && exposed.rhs.type !== "identifier-list")) {
throw new Error(`[Exposed] must take an identifier or an identifier list in interface ${this.name}`);
}
if (exposed.rhs.type === "identifier") {
this.exposed = new Set([exposed.rhs.value]);
} else {
this.exposed = new Set(exposed.rhs.value.map(token => token.value));
}
const legacyWindowAlias = utils.getExtAttr(this.idl.extAttrs, "LegacyWindowAlias");
if (legacyWindowAlias) {
if (utils.getExtAttr(this.idl.extAttrs, "LegacyNoInterfaceObject")) {
throw new Error(`Interface ${this.name} has [LegacyWindowAlias] and [LegacyNoInterfaceObject]`);
}
if (!this.exposed.has("Window")) {
throw new Error(`Interface ${this.name} has [LegacyWindowAlias] without [Exposed=Window]`);
}
if (!legacyWindowAlias.rhs ||
(legacyWindowAlias.rhs.type !== "identifier" && legacyWindowAlias.rhs.type !== "identifier-list")) {
throw new Error(`[LegacyWindowAlias] must take an identifier or an identifier list in interface ${this.name}`);
}
if (legacyWindowAlias.rhs.type === "identifier") {
this.legacyWindowAliases = new Set([legacyWindowAlias.rhs.value]);
} else {
this.legacyWindowAliases = new Set(legacyWindowAlias.rhs.value.map(token => token.value));
}
} else {
this.legacyWindowAliases = null;
}
}
// whence is either "instance", "prototype" or "unforgeables"
// type is either "regular", "get", or "set"
addMethod(whence, propName, args, body, type = "regular", {
configurable = true,
enumerable = typeof propName === "string",
writable = type === "regular" ? true : undefined
} = {}) {
if (whence !== "instance" && whence !== "prototype" && whence !== "unforgeables") {
throw new Error(`Internal error: Invalid whence ${whence}`);
}
if (type !== "regular") {
const existing = this._outputMethods.get(propName);
if (existing !== undefined) {
if (type === "get") {
existing.body[0] = body;
} else {
existing.args = args;
existing.body[1] = body;
}
return;
}
const pair = new Array(2);
pair[type === "get" ? 0 : 1] = body;
body = pair;
type = "accessor";
}
const descriptor = { configurable, enumerable, writable };
this._outputMethods.set(propName, { whence, type, args, body, descriptor });
if (whence === "unforgeables" && !this.isGlobal) {
this._needsUnforgeablesObject = true;
}
}
// type is either "regular", "get", or "set"
addStaticMethod(propName, args, body, type = "regular", {
configurable = true,
enumerable = typeof propName === "string",
writable = type === "regular" ? true : undefined
} = {}) {
if (type !== "regular") {
const existing = this._outputStaticMethods.get(propName);
if (existing !== undefined) {
if (type === "get") {
existing.body[0] = body;
} else {
existing.args = args;
existing.body[1] = body;
}
return;
}
const pair = new Array(2);
pair[type === "get" ? 0 : 1] = body;
body = pair;
type = "accessor";
}
const descriptor = { configurable, enumerable, writable };
this._outputStaticMethods.set(propName, { type, args, body, descriptor });
}
// whence is either "instance" or "prototype"
addProperty(whence, propName, str, {
configurable = true,
enumerable = typeof propName === "string",
writable = true
} = {}) {
if (whence !== "instance" && whence !== "prototype") {
throw new Error(`Internal error: Invalid whence ${whence}`);
}
const descriptor = { configurable, enumerable, writable };
this._outputProperties.set(propName, { whence, body: str, descriptor });
}
addStaticProperty(propName, str, {
configurable = true,
enumerable = typeof propName === "string",
writable = true
} = {}) {
const descriptor = { configurable, enumerable, writable };
this._outputStaticProperties.set(propName, { body: str, descriptor });
}
_analyzeMembers() {
const handleSpecialOperations = member => {
if (member.type === "operation") {
if (member.special === "getter") {
let msg = `Invalid getter ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`;
if (member.parent.name !== this.name) {
msg += ` (defined in ${member.parent.name})`;
}
msg += ": ";
if (member.arguments.length !== 1) {
throw new Error(`${msg}1 argument should be present, found ${member.arguments.length}`);
}
if (isIndexed(member)) {
if (this.indexedGetter) {
throw new Error(`${msg}duplicated indexed getter`);
}
this.indexedGetter = member;
} else if (isNamed(member)) {
this.namedGetter = member;
} else {
throw new Error(`${msg}getter is neither indexed nor named`);
}
}
if (member.special === "setter") {
let msg = `Invalid setter ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`;
if (member.parent.name !== this.name) {
msg += ` (defined in ${member.parent.name})`;
}
msg += ": ";
if (member.arguments.length !== 2) {
throw new Error(`${msg}2 arguments should be present, found ${member.arguments.length}`);
}
if (isIndexed(member)) {
if (this.indexedSetter) {
throw new Error(`${msg}duplicated indexed setter`);
}
this.indexedSetter = member;
} else if (isNamed(member)) {
this.namedSetter = member;
} else {
throw new Error(`${msg}setter is neither indexed nor named`);
}
}
if (member.special === "deleter") {
let msg = `Invalid deleter ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`;
if (member.parent.name !== this.name) {
msg += ` (defined in ${member.parent.name})`;
}
msg += ": ";
if (member.arguments.length !== 1) {
throw new Error(`${msg}1 arguments should be present, found ${member.arguments.length}`);
}
if (isNamed(member)) {
this.namedDeleter = member;
} else {
throw new Error(`${msg}deleter is not named`);
}
}
}
};
for (const member of this.members()) {
let key;
switch (member.type) {
case "constructor":
this.constructorOperations.push(member);
break;
case "operation":
key = member.special === "static" ? "staticOperations" : "operations";
if (member.name) {
if (!this[key].has(member.name)) {
this[key].set(member.name, new Operation(this.ctx, this, member));
} else {
this[key].get(member.name).idls.push(member);
}
}
break;
case "attribute":
key = member.special === "static" ? "staticAttributes" : "attributes";
this[key].set(member.name, new Attribute(this.ctx, this, member));
break;
case "const":
this.constants.set(member.name, new Constant(this.ctx, this, member));
break;
case "iterable":
if (this.iterable) {
throw new Error(`Interface ${this.name} has more than one iterable declaration`);
}
this.iterable = member.async ?
new AsyncIterable(this.ctx, this, member) :
new Iterable(this.ctx, this, member);
break;
default:
if (!this.ctx.options.suppressErrors) {
throw new Error(`Unknown IDL member type "${member.type}" in interface ${this.name}`);
}
}
handleSpecialOperations(member);
if (member.special === "stringifier") {
let msg = `Invalid stringifier ${member.name ? `"${member.name}" ` : ""}on interface ${this.name}`;
if (member.parent.name !== this.name) {
msg += ` (defined in ${member.parent.name})`;
}
msg += ": ";
if (member.type === "operation") {
if (!member.idlType) {
member.idlType = { idlType: "DOMString" };
}
if (!member.arguments) {
member.arguments = [];
}
if (member.arguments.length > 0) {
throw new Error(`${msg}takes more than zero arguments`);
}
if (member.idlType.idlType !== "DOMString" || member.idlType.nullable) {
throw new Error(`${msg}returns something other than a plain DOMString`);
}
if (this.stringifier) {
throw new Error(`${msg}duplicated stringifier`);
}
const op = new Operation(this.ctx, this, member);
op.name = "toString";
this.operations.set("toString", op);
} else if (member.type === "attribute") {
if (member.special === "static") {
throw new Error(`${msg}keyword cannot be placed on static attribute`);
}
if ((member.idlType.idlType !== "DOMString" && member.idlType.idlType !== "USVString") ||
member.idlType.nullable) {
throw new Error(`${msg}attribute can only be of type DOMString or USVString`);
}
// Implemented in Attribute class.
} else {
throw new Error(`${msg}keyword placed on incompatible type ${member.type}`);
}
this.stringifier = member;
}
}
for (const member of this.inheritedMembers()) {
if (this.iterable && member.type === "iterable") {
throw new Error(`Iterable interface ${this.name} inherits from another iterable interface ` +
`${member.parent.name}`);
}
handleSpecialOperations(member);
}
// https://heycam.github.io/webidl/#dfn-reserved-identifier
const forbiddenMembers = new Set(["constructor", "toString"]);
if (this.iterable) {
if (!this.iterable.isAsync) {
if (this.iterable.isValue) {
if (!this.supportsIndexedProperties) {
throw new Error(`A value iterator cannot be declared on ${this.name} which does not support indexed ` +
"properties");
}
} else if (this.iterable.isPair && this.supportsIndexedProperties) {
throw new Error(`A pair iterator cannot be declared on ${this.name} which supports indexed properties`);
}
}
for (const n of ["entries", "forEach", "keys", "values"]) {
forbiddenMembers.add(n);
}
}
for (const member of this.allMembers()) {
if (forbiddenMembers.has(member.name)) {
let msg = `${member.name} is forbidden in interface ${this.name}`;
if (member.parent.name !== this.name) {
msg += ` (defined in ${member.parent.name})`;
}
throw new Error(msg);
}
}
}
get supportsIndexedProperties() {
return this.indexedGetter !== null;
}
get supportsNamedProperties() {
return this.namedGetter !== null;
}
get isLegacyPlatformObj() {
return !this.isGlobal && (this.supportsIndexedProperties || this.supportsNamedProperties);
}
includes(source) {
const mixin = this.ctx.interfaceMixins.get(source);
if (!mixin) {
if (this.ctx.options.suppressErrors) {
return;
}
throw new Error(`${source} interface mixin not found (included in ${this.name})`);
}
this.included.push(source);
}
generateInstallIteratorPrototype() {
const { iterable } = this;
if (!iterable) {
return;
}
if (iterable.isAsync) {
this.requires.addRaw("newObjectInRealm", "utils.newObjectInRealm");
this.str += `
ctorRegistry["${this.name} AsyncIterator"] =
Object.create(ctorRegistry["%AsyncIteratorPrototype%"], {
[Symbol.toStringTag]: {
value: "${this.name} AsyncIterator",
configurable: true
}
});
utils.define(ctorRegistry["${this.name} AsyncIterator"], {
next() {
const internal = this && this[utils.iterInternalSymbol];
if (!internal) {
return globalObject.Promise.reject(new globalObject.TypeError("next() called on a value that is not a ${this.name} async iterator object"));
}
const nextSteps = () => {
if (internal.isFinished) {
return globalObject.Promise.resolve(newObjectInRealm(globalObject, { value: undefined, done: true }));
}
const nextPromise = internal.target[implSymbol][utils.asyncIteratorNext](this);
return nextPromise.then(
next => {
internal.ongoingPromise = null;
if (next === utils.asyncIteratorEOI) {
internal.isFinished = true;
return newObjectInRealm(globalObject, { value: undefined, done: true });
}`;
if (iterable.isPair) {
this.str += `
return newObjectInRealm(globalObject, utils.iteratorResult(next.map(utils.tryWrapperForImpl), kind));
`;
} else {
this.str += `
return newObjectInRealm(globalObject, { value: utils.tryWrapperForImpl(next), done: false });
`;
}
this.str += `
},
reason => {
internal.ongoingPromise = null;
internal.isFinished = true;
throw reason;
}
);
};
internal.ongoingPromise = internal.ongoingPromise ?
internal.ongoingPromise.then(nextSteps, nextSteps) :
nextSteps();
return internal.ongoingPromise;
},
`;
if (iterable.hasReturnSteps) {
this.str += `
return(value) {
const internal = this && this[utils.iterInternalSymbol];
if (!internal) {
return globalObject.Promise.reject(new globalObject.TypeError("return() called on a value that is not a ${this.name} async iterator object"));
}
const returnSteps = () => {
if (internal.isFinished) {
return globalObject.Promise.resolve(newObjectInRealm(globalObject, { value, done: true }));
}
internal.isFinished = true;
return internal.target[implSymbol][utils.asyncIteratorReturn](this, value);
};
internal.ongoingPromise = internal.ongoingPromise ?
internal.ongoingPromise.then(returnSteps, returnSteps) :
returnSteps();
return internal.ongoingPromise.then(() => newObjectInRealm(globalObject, { value, done: true }));
}
`;
}
this.str += `
});
`;
} else if (iterable.isPair) {
this.requires.addRaw("newObjectInRealm", "utils.newObjectInRealm");
this.str += `
ctorRegistry["${this.name} Iterator"] =
Object.create(ctorRegistry["%IteratorPrototype%"], {
[Symbol.toStringTag]: {
configurable: true,
value: "${this.name} Iterator"
}
});
utils.define(
ctorRegistry["${this.name} Iterator"],
{
next() {
const internal = this && this[utils.iterInternalSymbol];
if (!internal) {
throw new globalObject.TypeError("next() called on a value that is not a ${this.name} iterator object");
}
const { target, kind, index } = internal;
const values = Array.from(target[implSymbol]);
const len = values.length;
if (index >= len) {
return newObjectInRealm(globalObject, { value: undefined, done: true });
}
const pair = values[index];
internal.index = index + 1;
return newObjectInRealm(globalObject, utils.iteratorResult(pair.map(utils.tryWrapperForImpl), kind));
}
}
);
`;
}
}
// All interfaces an object of this interface implements.
* allInterfaces(seen = new Set([this.name]), root = this.name) {
yield this.name;
if (this.idl.inheritance && this.ctx.interfaces.has(this.idl.inheritance)) {
if (seen.has(this.idl.inheritance)) {
throw new Error(`${root} has an inheritance cycle`);
}
seen.add(this.idl.inheritance);
yield* this.ctx.interfaces.get(this.idl.inheritance).allInterfaces(seen, root);
}
}
// Members that will be own properties of this interface's prototype object,
// i.e. members from this interface and its mixins.
* members() {
yield* this.idl.members;
for (const mixin of this.included) {
yield* this.ctx.interfaceMixins.get(mixin).idl.members;
}
}
// Members inherited from this interface's prototype chain.
* inheritedMembers(seen = new Set([this.name]), root = this.name) {
if (this.idl.inheritance && this.ctx.interfaces.has(this.idl.inheritance)) {
if (seen.has(this.idl.inheritance)) {
throw new Error(`${root} has an inheritance cycle`);
}
seen.add(this.idl.inheritance);
yield* this.ctx.interfaces.get(this.idl.inheritance).allMembers(seen, root);
}
}
* allMembers(seen = new Set([this.name]), root = this.name) {
for (const mixin of this.included) {
yield* this.ctx.interfaceMixins.get(mixin).idl.members;
}
for (const iface of this.allInterfaces(seen, root)) {
yield* this.ctx.interfaces.get(iface).idl.members;
}
}
generateRequires() {
this.requires.addRaw("implSymbol", "utils.implSymbol");
this.requires.addRaw("ctorRegistrySymbol", "utils.ctorRegistrySymbol");
if (this.idl.inheritance !== null) {
this.requires.addRelative(this.idl.inheritance);
}
this.str = `
${this.requires.generate()}
${this.str}
`;
}
generateExport() {
this.str += `
exports.is = value => {
return utils.isObject(value) && Object.hasOwn(value, implSymbol) && value[implSymbol] instanceof Impl.implementation;
};
exports.isImpl = value => {
return utils.isObject(value) && value instanceof Impl.implementation;
};
exports.convert = (globalObject, value, { context = "The provided value" } = {}) => {
if (exports.is(value)) {
return utils.implForWrapper(value);
}
throw new globalObject.TypeError(\`\${context} is not of type '${this.name}'.\`);
};
`;
if (this.iterable) {
if (this.iterable.isAsync) {
this.str += `
exports.createDefaultAsyncIterator = (globalObject, target, kind) => {
const ctorRegistry = globalObject[ctorRegistrySymbol];
const asyncIteratorPrototype = ctorRegistry["${this.name} AsyncIterator"];
const iterator = Object.create(asyncIteratorPrototype);
Object.defineProperty(iterator, utils.iterInternalSymbol, {
value: { target, kind, ongoingPromise: null, isFinished: false },
configurable: true
});
return iterator;
};
`;
} else if (this.iterable.isPair) {
this.str += `
exports.createDefaultIterator = (globalObject, target, kind) => {
const ctorRegistry = globalObject[ctorRegistrySymbol];
const iteratorPrototype = ctorRegistry["${this.name} Iterator"];
const iterator = Object.create(iteratorPrototype);
Object.defineProperty(iterator, utils.iterInternalSymbol, {
value: { target, kind, index: 0 },
configurable: true
});
return iterator;
};
`;
}
}
}
generateLegacyProxyHandler() {
const hasIndexedSetter = this.indexedSetter !== null;
const hasNamedSetter = this.namedSetter !== null;
const hasNamedDeleter = this.namedDeleter !== null;
const overrideBuiltins = Boolean(utils.getExtAttr(this.idl.extAttrs, "LegacyOverrideBuiltins"));
const supportsPropertyIndex = (O, index, indexedValue) => {
let unsupportedValue = utils.getExtAttr(this.indexedGetter.extAttrs, "WebIDL2JSValueAsUnsupported");
if (unsupportedValue) {
unsupportedValue = unsupportedValue.rhs.value;
}
if (unsupportedValue) {
const func = this.indexedGetter.name ? `.${this.indexedGetter.name}` : "[utils.indexedGet]";
const value = indexedValue || `${O}[implSymbol]${func}(${index})`;
return `${value} !== ${unsupportedValue}`;
}
return `${O}[implSymbol][utils.supportsPropertyIndex](${index})`;
};
const supportsPropertyName = (O, P, namedValue) => {
let unsupportedValue = utils.getExtAttr(this.namedGetter.extAttrs, "WebIDL2JSValueAsUnsupported");
if (unsupportedValue) {
unsupportedValue = unsupportedValue.rhs.value;
}
if (unsupportedValue) {
const func = this.namedGetter.name ? `.${this.namedGetter.name}` : "[utils.namedGet]";
const value = namedValue || `${O}[implSymbol]${func}(${P})`;
return `${value} !== ${unsupportedValue}`;
}
return `${O}[implSymbol][utils.supportsPropertyName](${P})`;
};
// "named property visibility algorithm"
// If `supports` is true then skip the supportsPropertyName check.
function namedPropertyVisible(P, O, supports = false) {
const conditions = [];
if (!supports) {
conditions.push(supportsPropertyName(O, P));
}
if (overrideBuiltins) {
conditions.push(`!Object.hasOwn(${O}, ${P})`);
} else {
// TODO: create a named properties object.
conditions.push(`!(${P} in ${O})`);
}
return conditions.join(" && ");
}
// "invoke an indexed property setter"
const invokeIndexedSetter = (O, P, V) => {
const arg = this.indexedSetter.arguments[1];
const conv = Types.generateTypeConversion(
this.ctx,
"indexedValue",
arg.idlType,
arg.extAttrs,
this.name,
`"Failed to set the " + index + " property on '${this.name}': The provided value"`
);
this.requires.merge(conv.requires);
const prolog = `
const index = ${P} >>> 0;
let indexedValue = ${V};
${conv.body}
`;
let invocation;
if (!this.indexedSetter.name) {
invocation = `
const creating = !(${supportsPropertyIndex(O, "index")});
if (creating) {
${O}[implSymbol][utils.indexedSetNew](index, indexedValue);
} else {
${O}[implSymbol][utils.indexedSetExisting](index, indexedValue);
}
`;
} else {
invocation = `
${O}[implSymbol].${this.indexedSetter.name}(index, indexedValue);
`;
}
if (utils.hasCEReactions(this.indexedSetter)) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires: this.requires
});
}
return prolog + invocation;
};
// "invoke a named property setter"
const invokeNamedSetter = (O, P, V) => {
const arg = this.namedSetter.arguments[1];
const conv = Types.generateTypeConversion(
this.ctx,
"namedValue",
arg.idlType,
arg.extAttrs,
this.name,
`"Failed to set the '" + ${P} + "' property on '${this.name}': The provided value"`
);
this.requires.merge(conv.requires);
const prolog = `
let namedValue = ${V};
${conv.body}
`;
let invocation;
if (!this.namedSetter.name) {
invocation = `
const creating = !(${supportsPropertyName(O, P)});
if (creating) {
${O}[implSymbol][utils.namedSetNew](${P}, namedValue);
} else {
${O}[implSymbol][utils.namedSetExisting](${P}, namedValue);
}
`;
} else {
invocation = `
${O}[implSymbol].${this.namedSetter.name}(${P}, namedValue);
`;
}
if (utils.hasCEReactions(this.namedSetter)) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires: this.requires
});
}
return prolog + invocation;
};
this.str += `
const proxyHandlerCache = new WeakMap();
class ProxyHandler {
constructor(globalObject) {
this._globalObject = globalObject;
}
`;
// [[Get]] (necessary because of proxy semantics)
this.str += `
get(target, P, receiver) {
if (typeof P === "symbol") {
return Reflect.get(target, P, receiver);
}
const desc = this.getOwnPropertyDescriptor(target, P);
if (desc === undefined) {
const parent = Object.getPrototypeOf(target);
if (parent === null) {
return undefined;
}
return Reflect.get(target, P, receiver);
}
if (!desc.get && !desc.set) {
return desc.value;
}
const getter = desc.get;
if (getter === undefined) {
return undefined;
}
return Reflect.apply(getter, receiver, []);
}
`;
// [[HasProperty]] (necessary because of proxy semantics)
this.str += `
has(target, P) {
if (typeof P === "symbol") {
return Reflect.has(target, P);
}
const desc = this.getOwnPropertyDescriptor(target, P);
if (desc !== undefined) {
return true;
}
const parent = Object.getPrototypeOf(target);
if (parent !== null) {
return Reflect.has(parent, P);
}
return false;
}
`;
// [[OwnPropertyKeys]]
// Loosely defined by https://heycam.github.io/webidl/#legacy-platform-object-property-enumeration, but with finer
// points tuned according to Firefox until https://github.com/heycam/webidl/issues/400 is resolved.
this.str += `
ownKeys(target) {
const keys = new Set();
`;
if (this.supportsIndexedProperties) {
this.str += `
for (const key of target[implSymbol][utils.supportedPropertyIndices]) {
keys.add(\`\${key}\`);
}
`;
}
if (this.supportsNamedProperties) {
this.str += `
for (const key of target[implSymbol][utils.supportedPropertyNames]) {
if (${namedPropertyVisible("key", "target", true)}) {
keys.add(\`\${key}\`);
}
}
`;
}
this.str += `
for (const key of Reflect.ownKeys(target)) {
keys.add(key);
}
return [...keys];
}
`;
// [[GetOwnProperty]]
this.str += `
getOwnPropertyDescriptor(target, P) {
if (typeof P === "symbol") {
return Reflect.getOwnPropertyDescriptor(target, P);
}
let ignoreNamedProps = false;
`;
if (this.supportsIndexedProperties) {
this.str += `
if (utils.isArrayIndexPropName(P)) {
const index = P >>> 0;
`;
const func = this.indexedGetter.name ? `.${this.indexedGetter.name}` : "[utils.indexedGet]";
let preamble = "";
let condition;
if (utils.getExtAttr(this.indexedGetter.extAttrs, "WebIDL2JSValueAsUnsupported")) {
this.str += `const indexedValue = target[implSymbol]${func}(index);`;
condition = supportsPropertyIndex("target", "index", "indexedValue");
} else {
preamble = `const indexedValue = target[implSymbol]${func}(index);`;
condition = supportsPropertyIndex("target", "index");
}
this.str += `
if (${condition}) {
${preamble}
return {
writable: ${hasIndexedSetter},
enumerable: true,
configurable: true,
value: utils.tryWrapperForImpl(indexedValue)
};
}
ignoreNamedProps = true;
}
`;
}
if (this.supportsNamedProperties) {
const func = this.namedGetter.name ? `.${this.namedGetter.name}` : "[utils.namedGet]";
const enumerable = !utils.getExtAttr(this.idl.extAttrs, "LegacyUnenumerableNamedProperties");
let preamble = "";
const conditions = [];
if (utils.getExtAttr(this.namedGetter.extAttrs, "WebIDL2JSValueAsUnsupported")) {
this.str += `
const namedValue = target[implSymbol]${func}(P);
`;
conditions.push(supportsPropertyName("target", "index", "namedValue"));
conditions.push(namedPropertyVisible("P", "target", true));
} else {
preamble = `
const namedValue = target[implSymbol]${func}(P);
`;
conditions.push(namedPropertyVisible("P", "target", false));
}
conditions.push("!ignoreNamedProps");
this.str += `
if (${conditions.join(" && ")}) {
${preamble}
return {
writable: ${hasNamedSetter},
enumerable: ${enumerable},
configurable: true,
value: utils.tryWrapperForImpl(namedValue)
};
}
`;
}
this.str += `
return Reflect.getOwnPropertyDescriptor(target, P);
}
`;
// [[Set]]
this.str += `
set(target, P, V, receiver) {
if (typeof P === "symbol") {
return Reflect.set(target, P, V, receiver);
}
// The \`receiver\` argument refers to the Proxy exotic object or an object
// that inherits from it, whereas \`target\` refers to the Proxy target:
if (target[implSymbol][utils.wrapperSymbol] === receiver) {
`;
this.str += `
const globalObject = this._globalObject;
`;
if (this.supportsIndexedProperties && hasIndexedSetter) {
this.str += `
if (utils.isArrayIndexPropName(P)) {
${invokeIndexedSetter("target", "P", "V")}
return true;
}
`;
}
if (this.supportsNamedProperties && hasNamedSetter) {
this.str += `
if (typeof P === "string") {
${invokeNamedSetter("target", "P", "V")}
return true;
}
`;
}
this.str += `
}
let ownDesc;
`;
if (this.supportsIndexedProperties) {
this.str += `
if (utils.isArrayIndexPropName(P)) {
const index = P >>> 0;
`;
const func = this.indexedGetter.name ? `.${this.indexedGetter.name}` : "[utils.indexedGet]";
let preamble = "";
let condition;
if (utils.getExtAttr(this.indexedGetter.extAttrs, "WebIDL2JSValueAsUnsupported")) {
this.str += `const indexedValue = target[implSymbol]${func}(index);`;
condition = supportsPropertyIndex("target", "index", "indexedValue");
} else {
preamble = `const indexedValue = target[implSymbol]${func}(index);`;
condition = supportsPropertyIndex("target", "index");
}
this.str += `
if (${condition}) {
${preamble}
ownDesc = {
writable: ${hasIndexedSetter},
enumerable: true,
configurable: true,
value: utils.tryWrapperForImpl(indexedValue)
};
}
}
`;
}
this.str += `
if (ownDesc === undefined) {
ownDesc = Reflect.getOwnPropertyDescriptor(target, P);
}
return utils.ordinarySetWithOwnDescriptor(target, P, V, receiver, ownDesc);
}
`;
// [[DefineOwnProperty]]
this.str += `
defineProperty(target, P, desc) {
if (typeof P === "symbol") {
return Reflect.defineProperty(target, P, desc);
}
`;
this.str += `
const globalObject = this._globalObject;
`;
if (this.supportsIndexedProperties) {
this.str += `
if (utils.isArrayIndexPropName(P)) {
`;
if (hasIndexedSetter) {
this.str += `
if (desc.get || desc.set) {
return false;
}
${invokeIndexedSetter("target", "P", "desc.value")}
return true;
`;
} else {
this.str += `
return false;
`;
}
this.str += `
}
`;
}
let needFallback = false;
if (this.supportsNamedProperties && !this.isGlobal) {
const unforgeable = new Set();
for (const m of this.allMembers()) {
if ((m.type === "attribute" || m.type === "operation") && m.special !== "static" &&
utils.getExtAttr(m.extAttrs, "LegacyUnforgeable")) {
unforgeable.add(m.name);
}
}
if (unforgeable.size > 0) {
needFallback = true;
this.str += `if (!${JSON.stringify([...unforgeable])}.includes(P)) {`;
}
if (!overrideBuiltins) {
needFallback = true;
this.str += "if (!Object.hasOwn(target, P)) {";
}
if (!hasNamedSetter) {
needFallback = true;
this.str += `
const creating = !(${supportsPropertyName("target", "P")});
if (!creating) {
return false;
}
`;
} else {
this.str += `
if (desc.get || desc.set) {
return false;
}
${invokeNamedSetter("target", "P", "desc.value")}
return true;
`;
}
if (!overrideBuiltins) {
this.str += "}";
}
if (unforgeable.size > 0) {
this.str += "}";
}
} else {
needFallback = true;
}
if (needFallback) {
// Spec says to set configurable to true, but doing so will make Proxy's trap throw and also fail WPT.
// if (!this.isGlobal) {
// this.str += `
// desc.configurable = true;
// `;
// }
this.str += `
return Reflect.defineProperty(target, P, desc);
`;
}
this.str += `
}
`;
// [[Delete]]
this.str += `
deleteProperty(target, P) {
if (typeof P === "symbol") {
return Reflect.deleteProperty(target, P);
}
`;
this.str += `
const globalObject = this._globalObject;
`;
if (this.supportsIndexedProperties) {
this.str += `
if (utils.isArrayIndexPropName(P)) {
const index = P >>> 0;
return !(${supportsPropertyIndex("target", "index")});
}
`;
}
if (this.supportsNamedProperties && !this.isGlobal) {
this.str += `
if (${namedPropertyVisible("P", "target")}) {
`;
if (!hasNamedDeleter) {
this.str += `
return false;
`;
} else {
let invocation;
const func = this.namedDeleter.name ? `.${this.namedDeleter.name}` : "[utils.namedDelete]";
if (this.namedDeleter.idlType.idlType === "bool") {
invocation = `
return target[implSymbol]${func}(P);
`;
} else {
invocation = `
target[implSymbol]${func}(P);
return true;
`;
}
if (utils.hasCEReactions(this.namedDeleter)) {
invocation = this.ctx.invokeProcessCEReactions(invocation, {
requires: this.requires
});
}
this.str += invocation;
}
this.str += `
}
`;
}
this.str += `
return Reflect.deleteProperty(target, P);
}
`;
// [[PreventExtensions]]
this.str += `
preventExtensions() {
return false;
}
`;
this.str += `
};
`;
}
generateIface() {
const { _needsUnforgeablesObject } = this;
this.str += `
function makeWrapper(globalObject, newTarget) {
let proto;
if (newTarget !== undefined) {
proto = newTarget.prototype;
}
if (!utils.isObject(proto)) {
proto = globalObject[ctorRegistrySymbol]["${this.name}"].prototype;
}
return Object.create(proto);
}
`;
let setWrapperToProxy = ``;
if (this.isLegacyPlatformObj) {
this.str += `
function makeProxy(wrapper, globalObject) {
let proxyHandler = proxyHandlerCache.get(globalObject);
if (proxyHandler === undefined) {
proxyHandler = new ProxyHandler(globalObject);
proxyHandlerCache.set(globalObject, proxyHandler);
}
return new Proxy(wrapper, proxyHandler);
}
`;
setWrapperToProxy = `
wrapper = makeProxy(wrapper, globalObject);`;
}
this.str += `
exports.create = (globalObject, constructorArgs, privateData) => {
const wrapper = makeWrapper(globalObject);
return exports.setup(wrapper, globalObject, constructorArgs, privateData);
};
exports.createImpl = (globalObject, constructorArgs, privateData) => {
const wrapper = exports.create(globalObject, constructorArgs, privateData);
return utils.implForWrapper(wrapper);
};
`;
if (_needsUnforgeablesObject) {
this.generateUnforgeablesObject();
}
this.str += `
exports._internalSetup = (wrapper, globalObject) => {
`;
if (this.idl.inheritance) {
this.str += `
${this.idl.inheritance}._internalSetup(wrapper, globalObject);
`;
}
if (_needsUnforgeablesObject) {
this.str += `
utils.define(wrapper, getUnforgeables(globalObject));
`;
}
this.generateOnInstance();
this.str += `
};
exports.setup = (wrapper, globalObject, constructorArgs = [], privateData = {}) => {
privateData.wrapper = wrapper;
exports._internalSetup(wrapper, globalObject);
Object.defineProperty(wrapper, implSymbol, {
value: new Impl.implementation(globalObject, constructorArgs, privateData),
configurable: true
});
${setWrapperToProxy}
wrapper[implSymbol][utils.wrapperSymbol] = wrapper;
if (Impl.init) {
Impl.init(wrapper[implSymbol]);
}
return wrapper;
};
exports.new = (globalObject, newTarget) => {
${this.isLegacyPlatformObj ? "let" : "const"} wrapper = makeWrapper(globalObject, newTarget);
exports._internalSetup(wrapper, globalObject);
Object.defineProperty(wrapper, implSymbol, {
value: Object.create(Impl.implementation.prototype),
configurable: true
});
${setWrapperToProxy}
wrapper[implSymbol][utils.wrapperSymbol] = wrapper;
if (Impl.init) {
Impl.init(wrapper[implSymbol]);
}
return wrapper[implSymbol];
}
`;
}
addConstructor() {
const overloads = Overloads.getEffectiveOverloads("constructor", this.name, 0, this);
let body;
let argNames = [];
if (overloads.length !== 0) {
let minConstructor = overloads[0];
for (let i = 1; i < overloads.length; ++i) {
if (overloads[i].nameList.length < minConstructor.nameList.length) {
minConstructor = overloads[i];
}
}
argNames = minConstructor.nameList;
const conversions = Parameters.generateOverloadConversions(
this.ctx,
"constructor",
this.name,
this,
`Failed to construct '${this.name}': `
);
this.requires.merge(conversions.requires);
const setupArgs = [
"Object.create(new.target.prototype)",
"globalObject",
conversions.hasArgs ? "args" : "undefined"
];
body = `
${conversions.body}
return exports.setup(${utils.formatArgs(setupArgs)});
`;
} else {
body = `
throw new globalObject.TypeError("Illegal constructor");
`;
}
if (utils.getExtAttr(this.idl.extAttrs, "HTMLConstructor")) {
body = this.ctx.invokeProcessHTMLConstructor(body, {
requires: this.requires
});
}
this.addMethod("prototype", "constructor", argNames, body, "regular", { enumerable: false });
}
get defaultWhence() {
return this.isGlobal ? "instance" : "prototype";
}
addIteratorMethod() {
// TODO maplike setlike
// Don't bother checking "length" attribute as interfaces that support indexed properties must implement one.
// "Has value iterator" implies "supports indexed properties".
if (this.supportsIndexedProperties) {
this.addProperty(this.defaultWhence, Symbol.iterator, "globalObject.Array.prototype[Symbol.iterator]");
}
}
addAllMethodsProperties() {
this.addConstructor();
this.addProperty("prototype", Symbol.toStringTag, JSON.stringify(this.name), {
writable: false
});
const unscopables = Object.create(null);
for (const member of this.members()) {
if (utils.getExtAttr(member.extAttrs, "Unscopable")) {
unscopables[member.name] = true;
}
}
if (Object.keys(unscopables).length > 0) {
// eslint-disable-next-line no-proto
unscopables.__proto__ = null;
this.addProperty("prototype", Symbol.unscopables, JSON.stringify(unscopables), {
writable: false
});
}
for (const member of [...this.operations.values(), ...this.staticOperations.values()]) {
const data = member.generate();
this.requires.merge(data.requires);
}
this.addIteratorMethod();
if (this.iterable) {
const data = this.iterable.generate();
this.requires.merge(data.requires);
}
for (const member of [...this.attributes.values(), ...this.staticAttributes.values(), ...this.constants.values()]) {
const data = member.generate();
this.requires.merge(data.requires);
}
}
generateOffInstanceMethods() {
const addOne = (name, args, body) => {
this.str += `
${name}(${utils.formatArgs(args)}) {${body}}
`;
};
for (const [name, { whence, type, args, body }] of this._outputMethods) {
if (whence !== "prototype") {
continue;
}
const propName = utils.stringifyPropertyKey(name);
if (type === "regular") {
addOne(propName, args, body);
} else {
if (body[0] !== undefined) {
addOne(`get ${propName}`, [], body[0]);
}
if (body[1] !== undefined) {
addOne(`set ${propName}`, args, body[1]);
}
}
}
for (const [name, { type, args, body }] of this._outputStaticMethods) {
const propName = utils.stringifyPropertyKey(name);
if (type === "regular") {
addOne(`static ${propName}`, args, body);
} else {
if (body[0] !== undefined) {
addOne(`static get ${propName}`, [], body[0]);
}
if (body[1] !== undefined) {
addOne(`static set ${propName}`, args, body[1]);
}
}
}
}
generateOffInstanceAfterClass() {
// Inheritance is taken care of by "extends" clause in class declaration.
const protoProps = new Map();
const classProps = new Map();
for (const [name, { whence, type, descriptor }] of this._outputMethods) {
if (whence !== "prototype") {
continue;
}
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
protoProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}
for (const [name, { type, descriptor }] of this._outputStaticMethods) {
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultClassMethodDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
classProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}
for (const [name, { whence, body, descriptor }] of this._outputProperties) {
if (whence !== "prototype") {
continue;
}
const descriptorModifier =
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
protoProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}
for (const [name, { body, descriptor }] of this._outputStaticProperties) {
const descriptorModifier =
utils.getPropertyDescriptorModifier(utils.defaultDefinePropertyDescriptor, descriptor, "regular", body);
classProps.set(utils.stringifyPropertyKey(name), descriptorModifier);
}
if (protoProps.size > 0) {
const props = [...protoProps].map(([name, body]) => `${name}: ${body}`);
this.str += `Object.defineProperties(${this.name}.prototype, { ${props.join(", ")} });`;
}
if (classProps.size > 0) {
const props = [...classProps].map(([name, body]) => `${name}: ${body}`);
this.str += `Object.defineProperties(${this.name}, { ${props.join(", ")} });`;
}
}
generateOnInstance() {
const { isGlobal } = this;
const methods = [];
const props = new Map();
function addOne(name, args, body) {
methods.push(`
${name}(${utils.formatArgs(args)}) {${body}}
`);
}
for (const [name, { whence, type, args, body, descriptor }] of this._outputMethods) {
if (whence !== "instance" && (whence !== "unforgeables" || !isGlobal)) {
continue;
}
const propName = utils.stringifyPropertyKey(name);
if (type === "regular") {
addOne(propName, args, body);
} else {
if (body[0] !== undefined) {
addOne(`get ${propName}`, [], body[0]);
}
if (body[1] !== undefined) {
addOne(`set ${propName}`, args, body[1]);
}
}
const descriptorModifier = utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, type);
if (descriptorModifier === undefined) {
continue;
}
props.set(utils.stringifyPropertyKey(name), descriptorModifier);
}
for (const [name, { whence, body, descriptor }] of this._outputProperties) {
if (whence !== "instance") {
continue;
}
const propName = utils.stringifyPropertyKey(name);
methods.push(`${propName}: ${body}`);
const descriptorModifier =
utils.getPropertyDescriptorModifier(defaultObjectLiteralDescriptor, descriptor, "regular");
if (descriptorModifier === undefined) {
continue;
}
props.set(propName, descriptorModifier);
}
const propStrs = [...props].map(([name, body]) => `${name}: ${body}`);
if (methods.length > 0) {
this.str += `
utils.define(wrapper, {
${methods.join(", ")}
});
`;
}
if (propStrs.length > 0) {
this.str += `
Object.defineProperties(
wrapper,
{ ${propStrs.join(", ")} }
);
`;
}
}
generateUnforgeablesObject() {
const methods = [];
const props = new Map();
function addOne(name, args, body) {
methods.push(`
${name}(${utils.formatArgs(args)}) {${body}}
`);
}
for (const [name, { whence, type, args, body, descriptor }] of this._outputMethods) {
if (whence !== "unforgeables") {
continue;
}
const propName = utils.stringifyPropertyKey(name);
if (type === "regular") {
addOne(propName, args, body);
} else {
if (body[0] !== undefined) {