From df5ad7691e0970f08fada8df99a474dd9b517f8d Mon Sep 17 00:00:00 2001 From: Timothy Gu Date: Thu, 26 Mar 2020 14:04:19 -0400 Subject: [PATCH] Preliminary support for callback interfaces Implemented JS-to-IDL type conversion but not: - Saving callback context [1] - Call a user object's operation [2] - IDL-to-JS type conversion (#71) - Legacy callback interface object [3] Fixes https://github.com/jsdom/jsdom/issues/2869. [1]: https://heycam.github.io/webidl/#dfn-callback-context [2]: https://heycam.github.io/webidl/#call-a-user-objects-operation [3]: https://heycam.github.io/webidl/#dfn-legacy-callback-interface-object --- lib/context.js | 4 + lib/transformer.js | 6 +- lib/types.js | 57 ++++++-- test/__snapshots__/test.js.snap | 244 ++++++++++++++++++++++++++++++++ test/cases/EventTarget.webidl | 14 ++ 5 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 test/cases/EventTarget.webidl diff --git a/lib/context.js b/lib/context.js index 05f7ae7f..e03a7eda 100644 --- a/lib/context.js +++ b/lib/context.js @@ -35,6 +35,7 @@ class Context { this.typedefs = new Map(); this.interfaces = new Map(); this.interfaceMixins = new Map(); + this.callbackInterfaces = new Map(); this.dictionaries = new Map(); this.enumerations = new Map(); @@ -50,6 +51,9 @@ class Context { if (this.interfaces.has(name)) { return "interface"; } + if (this.callbackInterfaces.has(name)) { + return "callback interface"; + } if (this.dictionaries.has(name)) { return "dictionary"; } diff --git a/lib/transformer.js b/lib/transformer.js index ec480b31..58c3f1d1 100644 --- a/lib/transformer.js +++ b/lib/transformer.js @@ -83,7 +83,7 @@ class Transformer { })); this.ctx.initialize(); - const { interfaces, interfaceMixins, dictionaries, enumerations, typedefs } = this.ctx; + const { interfaces, interfaceMixins, callbackInterfaces, dictionaries, enumerations, typedefs } = this.ctx; // first we're gathering all full interfaces and ignore partial ones for (const file of parsed) { @@ -108,6 +108,10 @@ class Transformer { obj = new InterfaceMixin(this.ctx, instruction); interfaceMixins.set(obj.name, obj); break; + case "callback interface": + obj = { name: instruction.name }; // Not fully implemented yet. + callbackInterfaces.set(obj.name, obj); + break; case "includes": break; // handled later case "dictionary": diff --git a/lib/types.js b/lib/types.js index 596979ad..4ad217bb 100644 --- a/lib/types.js +++ b/lib/types.js @@ -127,6 +127,13 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e fn = `exports.convert`; } generateGeneric(fn); + } else if (ctx.typeOf(idlType.idlType) === "callback interface") { + // We do not save the callback context yet. + str += ` + if (!utils.isObject(${name})) { + throw new TypeError(${errPrefix} + " is not an object"); + } + `; } else { // unknown // Try to get the impl anyway. @@ -205,7 +212,7 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e output.push(`if (typeof ${name} === "function") {}`); } - if (union.sequenceLike || union.dictionary || union.record || union.object) { + if (union.sequenceLike || union.dictionary || union.record || union.object || union.callbackInterface) { let code = `if (utils.isObject(${name})) {`; if (union.sequenceLike) { @@ -217,10 +224,19 @@ function generateTypeConversion(ctx, name, idlType, argAttrs = [], parentName, e code += `} else {`; } - if (union.dictionary || union.record) { - const prop = union.dictionary ? "dictionary" : "record"; - const conv = generateTypeConversion(ctx, name, union[prop], [], parentName, - `${errPrefix} + " ${prop}"`); + if (union.dictionary) { + const conv = generateTypeConversion(ctx, name, union.dictionary, [], parentName, + `${errPrefix} + " dictionary"`); + requires.merge(conv.requires); + code += conv.body; + } else if (union.record) { + const conv = generateTypeConversion(ctx, name, union.record, [], parentName, + `${errPrefix} + " record"`); + requires.merge(conv.requires); + code += conv.body; + } else if (union.callbackInterface) { + const conv = generateTypeConversion(ctx, name, union.callbackInterface, [], parentName, + `${errPrefix} + " callback interface"`); requires.merge(conv.requires); code += conv.body; } else if (union.object) { @@ -381,7 +397,7 @@ function extractUnionInfo(ctx, idlType, errPrefix) { sequenceLike: null, record: null, get dictionaryLike() { - return this.dictionary !== null || this.record !== null; + return this.dictionary !== null || this.record !== null || this.callbackInterface !== null; }, ArrayBuffer: false, ArrayBufferViews: new Set(), @@ -395,6 +411,7 @@ function extractUnionInfo(ctx, idlType, errPrefix) { // Callback function, not interface callback: false, dictionary: null, + callbackInterface: null, interfaces: new Set(), get interfaceLike() { return this.interfaces.size > 0 || this.BufferSource; @@ -408,7 +425,13 @@ function extractUnionInfo(ctx, idlType, errPrefix) { } seen.sequenceLike = item; } else if (item.generic === "record") { - if (seen.record || seen.dictionary) { + if (seen.object) { + error("Dictionary-like types are not distinguishable with object type"); + } + if (seen.callback) { + error("Dictionary-like types are not distinguishable with callback functions"); + } + if (seen.dictionaryLike) { error("There can only be one dictionary-like type in a union type"); } seen.record = item; @@ -474,6 +497,17 @@ function extractUnionInfo(ctx, idlType, errPrefix) { error("There can only be one dictionary-like type in a union type"); } seen.dictionary = item; + } else if (ctx.callbackInterfaces.has(item.idlType)) { + if (seen.object) { + error("Dictionary-like types are not distinguishable with object type"); + } + if (seen.callback) { + error("Dictionary-like types are not distinguishable with callback functions"); + } + if (seen.dictionaryLike) { + error("There can only be one dictionary-like type in a union type"); + } + seen.callbackInterface = item.idlType; } else if (ctx.interfaces.has(item.idlType)) { if (seen.object) { error("Interface types are not distinguishable with object type"); @@ -570,6 +604,7 @@ function sameType(ctx, type1, type2) { sameType(ctx, extracted1.dictionary, extracted2.dictionary) && JSON.stringify([...extracted1.interfaces].sort()) === JSON.stringify([...extracted2.interfaces].sort()) && + extracted1.callbackInterface === extracted2.callbackInterface && extracted1.unknown === extracted2.unknown; } @@ -621,8 +656,12 @@ function areDistinguishable(ctx, type1, type2) { bufferSourceTypes.has(inner1.idlType); const isInterfaceLike2 = ctx.interfaces.has(inner2.idlType) || bufferSourceTypes.has(inner2.idlType); - const isDictionaryLike1 = ctx.dictionaries.has(inner1.idlType) || inner1.generic === "record"; - const isDictionaryLike2 = ctx.dictionaries.has(inner2.idlType) || inner2.generic === "record"; + const isDictionaryLike1 = ctx.dictionaries.has(inner1.idlType) || + ctx.callbackInterfaces.has(inner1.idlType) || + inner1.generic === "record"; + const isDictionaryLike2 = ctx.dictionaries.has(inner2.idlType) || + ctx.callbackInterfaces.has(inner2.idlType) || + inner2.generic === "record"; const isSequenceLike1 = inner1.generic === "sequence" || inner1.generic === "FrozenArray"; const isSequenceLike2 = inner2.generic === "sequence" || inner2.generic === "FrozenArray"; diff --git a/test/__snapshots__/test.js.snap b/test/__snapshots__/test.js.snap index 537dc9cb..3d96d4ed 100644 --- a/test/__snapshots__/test.js.snap +++ b/test/__snapshots__/test.js.snap @@ -1112,6 +1112,128 @@ const Impl = require(\\"../implementations/Enum.js\\"); " `; +exports[`with processors EventTarget.webidl 1`] = ` +"\\"use strict\\"; + +const conversions = require(\\"webidl-conversions\\"); +const utils = require(\\"./utils.js\\"); + +const implSymbol = utils.implSymbol; +const ctorRegistrySymbol = utils.ctorRegistrySymbol; + +const interfaceName = \\"EventTarget\\"; + +exports.is = function is(obj) { + return utils.isObject(obj) && utils.hasOwn(obj, implSymbol) && obj[implSymbol] instanceof Impl.implementation; +}; +exports.isImpl = function isImpl(obj) { + return utils.isObject(obj) && obj instanceof Impl.implementation; +}; +exports.convert = function convert(obj, { context = \\"The provided value\\" } = {}) { + if (exports.is(obj)) { + return utils.implForWrapper(obj); + } + throw new TypeError(\`\${context} is not of type 'EventTarget'.\`); +}; + +exports.create = function create(globalObject, constructorArgs, privateData) { + if (globalObject[ctorRegistrySymbol] === undefined) { + throw new Error(\\"Internal error: invalid global object\\"); + } + + const ctor = globalObject[ctorRegistrySymbol][\\"EventTarget\\"]; + if (ctor === undefined) { + throw new Error(\\"Internal error: constructor EventTarget is not installed on the passed global object\\"); + } + + let obj = Object.create(ctor.prototype); + obj = exports.setup(obj, globalObject, constructorArgs, privateData); + return obj; +}; +exports.createImpl = function createImpl(globalObject, constructorArgs, privateData) { + const obj = exports.create(globalObject, constructorArgs, privateData); + return utils.implForWrapper(obj); +}; +exports._internalSetup = function _internalSetup(obj, globalObject) {}; +exports.setup = function setup(obj, globalObject, constructorArgs = [], privateData = {}) { + privateData.wrapper = obj; + + exports._internalSetup(obj, globalObject); + Object.defineProperty(obj, implSymbol, { + value: new Impl.implementation(globalObject, constructorArgs, privateData), + configurable: true + }); + + obj[implSymbol][utils.wrapperSymbol] = obj; + if (Impl.init) { + Impl.init(obj[implSymbol], privateData); + } + return obj; +}; + +exports.install = function install(globalObject) { + class EventTarget { + constructor() { + return exports.setup(Object.create(new.target.prototype), globalObject, undefined); + } + + addEventListener(type, callback) { + const esValue = this !== null && this !== undefined ? this : globalObject; + if (!exports.is(esValue)) { + throw new TypeError(\\"Illegal invocation\\"); + } + + if (arguments.length < 2) { + throw new TypeError( + \\"Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only \\" + + arguments.length + + \\" present.\\" + ); + } + const args = []; + { + let curArg = arguments[0]; + curArg = conversions[\\"DOMString\\"](curArg, { + context: \\"Failed to execute 'addEventListener' on 'EventTarget': parameter 1\\" + }); + args.push(curArg); + } + { + let curArg = arguments[1]; + if (curArg === null || curArg === undefined) { + curArg = null; + } else { + if (!utils.isObject(curArg)) { + throw new TypeError( + \\"Failed to execute 'addEventListener' on 'EventTarget': parameter 2\\" + \\" is not an object\\" + ); + } + } + args.push(curArg); + } + return esValue[implSymbol].addEventListener(...args); + } + } + Object.defineProperties(EventTarget.prototype, { + addEventListener: { enumerable: true }, + [Symbol.toStringTag]: { value: \\"EventTarget\\", configurable: true } + }); + if (globalObject[ctorRegistrySymbol] === undefined) { + globalObject[ctorRegistrySymbol] = Object.create(null); + } + globalObject[ctorRegistrySymbol][interfaceName] = EventTarget; + + Object.defineProperty(globalObject, interfaceName, { + configurable: true, + writable: true, + value: EventTarget + }); +}; + +const Impl = require(\\"../implementations/EventTarget.js\\"); +" +`; + exports[`with processors Global.webidl 1`] = ` "\\"use strict\\"; @@ -8138,6 +8260,128 @@ const Impl = require(\\"../implementations/Enum.js\\"); " `; +exports[`without processors EventTarget.webidl 1`] = ` +"\\"use strict\\"; + +const conversions = require(\\"webidl-conversions\\"); +const utils = require(\\"./utils.js\\"); + +const implSymbol = utils.implSymbol; +const ctorRegistrySymbol = utils.ctorRegistrySymbol; + +const interfaceName = \\"EventTarget\\"; + +exports.is = function is(obj) { + return utils.isObject(obj) && utils.hasOwn(obj, implSymbol) && obj[implSymbol] instanceof Impl.implementation; +}; +exports.isImpl = function isImpl(obj) { + return utils.isObject(obj) && obj instanceof Impl.implementation; +}; +exports.convert = function convert(obj, { context = \\"The provided value\\" } = {}) { + if (exports.is(obj)) { + return utils.implForWrapper(obj); + } + throw new TypeError(\`\${context} is not of type 'EventTarget'.\`); +}; + +exports.create = function create(globalObject, constructorArgs, privateData) { + if (globalObject[ctorRegistrySymbol] === undefined) { + throw new Error(\\"Internal error: invalid global object\\"); + } + + const ctor = globalObject[ctorRegistrySymbol][\\"EventTarget\\"]; + if (ctor === undefined) { + throw new Error(\\"Internal error: constructor EventTarget is not installed on the passed global object\\"); + } + + let obj = Object.create(ctor.prototype); + obj = exports.setup(obj, globalObject, constructorArgs, privateData); + return obj; +}; +exports.createImpl = function createImpl(globalObject, constructorArgs, privateData) { + const obj = exports.create(globalObject, constructorArgs, privateData); + return utils.implForWrapper(obj); +}; +exports._internalSetup = function _internalSetup(obj, globalObject) {}; +exports.setup = function setup(obj, globalObject, constructorArgs = [], privateData = {}) { + privateData.wrapper = obj; + + exports._internalSetup(obj, globalObject); + Object.defineProperty(obj, implSymbol, { + value: new Impl.implementation(globalObject, constructorArgs, privateData), + configurable: true + }); + + obj[implSymbol][utils.wrapperSymbol] = obj; + if (Impl.init) { + Impl.init(obj[implSymbol], privateData); + } + return obj; +}; + +exports.install = function install(globalObject) { + class EventTarget { + constructor() { + return exports.setup(Object.create(new.target.prototype), globalObject, undefined); + } + + addEventListener(type, callback) { + const esValue = this !== null && this !== undefined ? this : globalObject; + if (!exports.is(esValue)) { + throw new TypeError(\\"Illegal invocation\\"); + } + + if (arguments.length < 2) { + throw new TypeError( + \\"Failed to execute 'addEventListener' on 'EventTarget': 2 arguments required, but only \\" + + arguments.length + + \\" present.\\" + ); + } + const args = []; + { + let curArg = arguments[0]; + curArg = conversions[\\"DOMString\\"](curArg, { + context: \\"Failed to execute 'addEventListener' on 'EventTarget': parameter 1\\" + }); + args.push(curArg); + } + { + let curArg = arguments[1]; + if (curArg === null || curArg === undefined) { + curArg = null; + } else { + if (!utils.isObject(curArg)) { + throw new TypeError( + \\"Failed to execute 'addEventListener' on 'EventTarget': parameter 2\\" + \\" is not an object\\" + ); + } + } + args.push(curArg); + } + return esValue[implSymbol].addEventListener(...args); + } + } + Object.defineProperties(EventTarget.prototype, { + addEventListener: { enumerable: true }, + [Symbol.toStringTag]: { value: \\"EventTarget\\", configurable: true } + }); + if (globalObject[ctorRegistrySymbol] === undefined) { + globalObject[ctorRegistrySymbol] = Object.create(null); + } + globalObject[ctorRegistrySymbol][interfaceName] = EventTarget; + + Object.defineProperty(globalObject, interfaceName, { + configurable: true, + writable: true, + value: EventTarget + }); +}; + +const Impl = require(\\"../implementations/EventTarget.js\\"); +" +`; + exports[`without processors Global.webidl 1`] = ` "\\"use strict\\"; diff --git a/test/cases/EventTarget.webidl b/test/cases/EventTarget.webidl new file mode 100644 index 00000000..6d327d91 --- /dev/null +++ b/test/cases/EventTarget.webidl @@ -0,0 +1,14 @@ +// Simplified from https://dom.spec.whatwg.org/#eventtarget + +[Exposed=(Window,Worker,AudioWorklet)] +interface EventTarget { + constructor(); + + void addEventListener(DOMString type, EventListener? callback); + // void removeEventListener(DOMString type, EventListener? callback); + // boolean dispatchEvent(Event event); +}; + +callback interface EventListener { + void handleEvent(/* Event event */); +};