From 074d05db1dbc1439dc89c17a74aa9c5ebb0c7b8b Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 6 Apr 2023 12:44:18 -0400 Subject: [PATCH 01/18] feat: allow adding additional members to production parse methods --- lib/productions/callback-interface.js | 9 +++++++-- lib/productions/container.js | 17 ++++++++++++++++- lib/productions/dictionary.js | 5 +++-- lib/productions/interface.js | 7 ++++++- lib/productions/mixin.js | 6 ++++-- lib/productions/namespace.js | 4 +++- 6 files changed, 39 insertions(+), 9 deletions(-) diff --git a/lib/productions/callback-interface.js b/lib/productions/callback-interface.js index da9dc96f..293b4a83 100644 --- a/lib/productions/callback-interface.js +++ b/lib/productions/callback-interface.js @@ -5,8 +5,12 @@ import { Constant } from "./constant.js"; export class CallbackInterface extends Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {*} callback + * @param {object} [options] + * @param {boolean} [options.inheritable] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] */ - static parse(tokeniser, callback, { partial = null } = {}) { + static parse(tokeniser, callback, { inheritable, extMembers = [] } = {}) { const tokens = { callback }; tokens.base = tokeniser.consume("interface"); if (!tokens.base) { @@ -16,8 +20,9 @@ export class CallbackInterface extends Container { tokeniser, new CallbackInterface({ source: tokeniser.source, tokens }), { - inheritable: !partial, + inheritable: !!inheritable, allowedMembers: [ + ...extMembers, [Constant.parse], [Operation.parse, { regular: true }], ], diff --git a/lib/productions/container.js b/lib/productions/container.js index d52dc509..ed628aae 100644 --- a/lib/productions/container.js +++ b/lib/productions/container.js @@ -16,11 +16,26 @@ function inheritance(tokeniser) { return { colon, inheritance }; } +/** + * Parser callback. + * @callback parserCallback + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {...*} args + */ + +/** + * A parser callback and optional option object. + * @typedef AllowedMember + * @type {[parserCallback, object?]} + */ + export class Container extends Base { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {*} instance TODO: This should be {T extends Container}, but see https://github.com/microsoft/TypeScript/issues/4628 - * @param {*} args + * @param {object} args + * @param {boolean} args.inheritable + * @param {AllowedMember[]} args.allowedMembers */ static parse(tokeniser, instance, { inheritable, allowedMembers }) { const { tokens, type } = instance; diff --git a/lib/productions/dictionary.js b/lib/productions/dictionary.js index fa55d979..57462c4c 100644 --- a/lib/productions/dictionary.js +++ b/lib/productions/dictionary.js @@ -5,9 +5,10 @@ export class Dictionary extends Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] * @param {import("../tokeniser.js").Token} [options.partial] */ - static parse(tokeniser, { partial } = {}) { + static parse(tokeniser, { extMembers = [], partial } = {}) { const tokens = { partial }; tokens.base = tokeniser.consume("dictionary"); if (!tokens.base) { @@ -18,7 +19,7 @@ export class Dictionary extends Container { new Dictionary({ source: tokeniser.source, tokens }), { inheritable: !partial, - allowedMembers: [[Field.parse]], + allowedMembers: [...extMembers, [Field.parse]], } ); } diff --git a/lib/productions/interface.js b/lib/productions/interface.js index e22b54a2..f361e527 100644 --- a/lib/productions/interface.js +++ b/lib/productions/interface.js @@ -34,8 +34,12 @@ function static_member(tokeniser) { export class Interface extends Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {Token} base + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token|null} [options.partial] */ - static parse(tokeniser, base, { partial = null } = {}) { + static parse(tokeniser, base, { extMembers = [], partial = null } = {}) { const tokens = { partial, base }; return Container.parse( tokeniser, @@ -43,6 +47,7 @@ export class Interface extends Container { { inheritable: !partial, allowedMembers: [ + ...extMembers, [Constant.parse], [Constructor.parse], [static_member], diff --git a/lib/productions/mixin.js b/lib/productions/mixin.js index c3094e7c..5108efdb 100644 --- a/lib/productions/mixin.js +++ b/lib/productions/mixin.js @@ -11,9 +11,10 @@ export class Mixin extends Container { * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {Token} base * @param {object} [options] - * @param {Token} [options.partial] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token} [options.partial] */ - static parse(tokeniser, base, { partial } = {}) { + static parse(tokeniser, base, { extMembers = [], partial } = {}) { const tokens = { partial, base }; tokens.mixin = tokeniser.consume("mixin"); if (!tokens.mixin) { @@ -24,6 +25,7 @@ export class Mixin extends Container { new Mixin({ source: tokeniser.source, tokens }), { allowedMembers: [ + ...extMembers, [Constant.parse], [stringifier], [Attribute.parse, { noInherit: true }], diff --git a/lib/productions/namespace.js b/lib/productions/namespace.js index ef7c35f5..34fa6e49 100644 --- a/lib/productions/namespace.js +++ b/lib/productions/namespace.js @@ -9,9 +9,10 @@ export class Namespace extends Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] * @param {import("../tokeniser.js").Token} [options.partial] */ - static parse(tokeniser, { partial } = {}) { + static parse(tokeniser, { extMembers = [], partial } = {}) { const tokens = { partial }; tokens.base = tokeniser.consume("namespace"); if (!tokens.base) { @@ -22,6 +23,7 @@ export class Namespace extends Container { new Namespace({ source: tokeniser.source, tokens }), { allowedMembers: [ + ...extMembers, [Attribute.parse, { noInherit: true, readonly: true }], [Constant.parse], [Operation.parse, { regular: true }], From bb9f3b42dc356ad6e0e85ee02fcfdd7c899e70c6 Mon Sep 17 00:00:00 2001 From: pyoor Date: Wed, 12 Apr 2023 11:33:47 -0400 Subject: [PATCH 02/18] feat: add 'extensions' option for extending existing productions --- lib/webidl2.js | 54 +++++++++++++------ test/extensions.js | 20 +++++++ .../baseline/attr-on-callback-interface.json | 30 +++++++++++ .../baseline/namespace-attribute.json | 30 +++++++++++ .../idl/attr-on-callback-interface.webidl | 3 ++ .../extensions/idl/namespace-attribute.webidl | 3 ++ test/util/collect.js | 3 +- 7 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 test/extensions.js create mode 100644 test/extensions/baseline/attr-on-callback-interface.json create mode 100644 test/extensions/baseline/namespace-attribute.json create mode 100644 test/extensions/idl/attr-on-callback-interface.webidl create mode 100644 test/extensions/idl/namespace-attribute.webidl diff --git a/lib/webidl2.js b/lib/webidl2.js index 6129879b..c4841d45 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -12,11 +12,22 @@ import { CallbackInterface } from "./productions/callback-interface.js"; import { autoParenter } from "./productions/helpers.js"; import { Eof } from "./productions/token.js"; +/** @typedef {'callback-interface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ +/** @typedef {Object.<'extMembers', import("./container.js").AllowedMember[]>} Extension */ + /** - * @param {Tokeniser} tokeniser + * Parser options. + * @typedef ParserOptions * @param {object} options + * @param {string} [options.sourceName] * @param {boolean} [options.concrete] * @param {Function[]} [options.productions] + * @param {Object.} [options.extensions] + */ + +/** + * @param {Tokeniser} tokeniser + * @param {ParserOptions} options */ function parseByTokens(tokeniser, options) { const source = tokeniser.source; @@ -33,7 +44,9 @@ function parseByTokens(tokeniser, options) { const callback = consume("callback"); if (!callback) return; if (tokeniser.probe("interface")) { - return CallbackInterface.parse(tokeniser, callback); + return CallbackInterface.parse(tokeniser, callback, { + ...options?.extensions?.["callback-interface"], + }); } return CallbackFunction.parse(tokeniser, callback); } @@ -41,20 +54,32 @@ function parseByTokens(tokeniser, options) { function interface_(opts) { const base = consume("interface"); if (!base) return; - const ret = - Mixin.parse(tokeniser, base, opts) || - Interface.parse(tokeniser, base, opts) || - error("Interface has no proper body"); - return ret; + return ( + Mixin.parse(tokeniser, base, { + ...opts, + ...options?.extensions?.mixin, + }) || + Interface.parse(tokeniser, base, { + ...opts, + ...options?.extensions?.interface, + }) || + error("Interface has no proper body") + ); } function partial() { const partial = consume("partial"); if (!partial) return; return ( - Dictionary.parse(tokeniser, { partial }) || + Dictionary.parse(tokeniser, { + partial, + ...options?.extensions?.dictionary, + }) || interface_({ partial }) || - Namespace.parse(tokeniser, { partial }) || + Namespace.parse(tokeniser, { + partial, + ...options?.extensions?.namespace, + }) || error("Partial doesn't apply to anything") ); } @@ -73,11 +98,11 @@ function parseByTokens(tokeniser, options) { callback() || interface_() || partial() || - Dictionary.parse(tokeniser) || + Dictionary.parse(tokeniser, { ...options?.extensions?.dictionary }) || Enum.parse(tokeniser) || Typedef.parse(tokeniser) || Includes.parse(tokeniser) || - Namespace.parse(tokeniser) + Namespace.parse(tokeniser, { ...options?.extensions?.namespace }) ); } @@ -100,6 +125,7 @@ function parseByTokens(tokeniser, options) { } return defs; } + const res = definitions(); if (tokeniser.position < source.length) error("Unrecognised tokens"); return res; @@ -107,11 +133,7 @@ function parseByTokens(tokeniser, options) { /** * @param {string} str - * @param {object} [options] - * @param {*} [options.sourceName] - * @param {boolean} [options.concrete] - * @param {Function[]} [options.productions] - * @return {import("./productions/base.js").Base[]} + * @param {ParserOptions} [options] */ export function parse(str, options = {}) { const tokeniser = new Tokeniser(str); diff --git a/test/extensions.js b/test/extensions.js new file mode 100644 index 00000000..5033b8da --- /dev/null +++ b/test/extensions.js @@ -0,0 +1,20 @@ +import { collect } from "./util/collect.js"; +import expect from "expect"; +import { Attribute } from "../lib/productions/attribute.js"; + +const extensions = { + "callback-interface": { + extMembers: [[Attribute.parse]], + }, + namespace: { + extMembers: [[Attribute.parse, { readonly: false }]], + }, +}; + +describe("Parses all of the IDLs that require extensions", () => { + for (const test of collect("extensions", { extensions })) { + it(`should produce the same AST for ${test.path}`, () => { + expect(test.diff()).toBeFalsy(); + }); + } +}); diff --git a/test/extensions/baseline/attr-on-callback-interface.json b/test/extensions/baseline/attr-on-callback-interface.json new file mode 100644 index 00000000..25e73251 --- /dev/null +++ b/test/extensions/baseline/attr-on-callback-interface.json @@ -0,0 +1,30 @@ +[ + { + "type": "callback interface", + "name": "TestCallbackInterface", + "inheritance": null, + "members": [ + { + "type": "attribute", + "name": "foo", + "idlType": { + "type": "attribute-type", + "extAttrs": [], + "generic": "", + "nullable": false, + "union": false, + "idlType": "long" + }, + "extAttrs": [], + "special": "", + "readonly": true + } + ], + "extAttrs": [], + "partial": false + }, + { + "type": "eof", + "value": "" + } +] diff --git a/test/extensions/baseline/namespace-attribute.json b/test/extensions/baseline/namespace-attribute.json new file mode 100644 index 00000000..cb829b8f --- /dev/null +++ b/test/extensions/baseline/namespace-attribute.json @@ -0,0 +1,30 @@ +[ + { + "type": "namespace", + "name": "WebrtcGlobalInformation", + "inheritance": null, + "members": [ + { + "type": "attribute", + "name": "debugLevel", + "idlType": { + "type": "attribute-type", + "extAttrs": [], + "generic": "", + "nullable": false, + "union": false, + "idlType": "long" + }, + "extAttrs": [], + "special": "", + "readonly": false + } + ], + "extAttrs": [], + "partial": false + }, + { + "type": "eof", + "value": "" + } +] diff --git a/test/extensions/idl/attr-on-callback-interface.webidl b/test/extensions/idl/attr-on-callback-interface.webidl new file mode 100644 index 00000000..112fa0c3 --- /dev/null +++ b/test/extensions/idl/attr-on-callback-interface.webidl @@ -0,0 +1,3 @@ +callback interface TestCallbackInterface { + readonly attribute long foo; +}; diff --git a/test/extensions/idl/namespace-attribute.webidl b/test/extensions/idl/namespace-attribute.webidl new file mode 100644 index 00000000..c7d09504 --- /dev/null +++ b/test/extensions/idl/namespace-attribute.webidl @@ -0,0 +1,3 @@ +namespace WebrtcGlobalInformation { + attribute long debugLevel; +}; diff --git a/test/util/collect.js b/test/util/collect.js index 7909f810..ef82ebbf 100644 --- a/test/util/collect.js +++ b/test/util/collect.js @@ -7,7 +7,7 @@ import { diff } from "jsondiffpatch"; * Collects test items from the specified directory * @param {string} base */ -export function* collect(base, { expectError, raw } = {}) { +export function* collect(base, { expectError, raw, extensions = {} } = {}) { const dir = new URL(join("..", base, "idl/"), import.meta.url); const idls = readdirSync(dir) .filter((it) => it.endsWith(".webidl")) @@ -19,6 +19,7 @@ export function* collect(base, { expectError, raw } = {}) { const ast = parse(text, { concrete: true, sourceName: basename(path.pathname), + extensions, }); const validation = validate(ast); if (validation) { From 0a2b8600887062e163fb987c38e7ae96a97cecb8 Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 10:32:51 -0400 Subject: [PATCH 03/18] fix: remove unnecessary spread operator Co-authored-by: Kagami Sascha Rosylight --- lib/webidl2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webidl2.js b/lib/webidl2.js index c4841d45..1bfc2762 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -98,7 +98,7 @@ function parseByTokens(tokeniser, options) { callback() || interface_() || partial() || - Dictionary.parse(tokeniser, { ...options?.extensions?.dictionary }) || + Dictionary.parse(tokeniser, options?.extensions?.dictionary) || Enum.parse(tokeniser) || Typedef.parse(tokeniser) || Includes.parse(tokeniser) || From 491c5cf1d742f9642199d71191850423c9d84e3f Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 10:30:04 -0400 Subject: [PATCH 04/18] refactor: rename extension 'callback-interface' to callbackInterface --- lib/webidl2.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/webidl2.js b/lib/webidl2.js index 1bfc2762..515133fc 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -12,7 +12,7 @@ import { CallbackInterface } from "./productions/callback-interface.js"; import { autoParenter } from "./productions/helpers.js"; import { Eof } from "./productions/token.js"; -/** @typedef {'callback-interface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ +/** @typedef {'callbackInterface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ /** @typedef {Object.<'extMembers', import("./container.js").AllowedMember[]>} Extension */ /** @@ -45,7 +45,7 @@ function parseByTokens(tokeniser, options) { if (!callback) return; if (tokeniser.probe("interface")) { return CallbackInterface.parse(tokeniser, callback, { - ...options?.extensions?.["callback-interface"], + ...options?.extensions?.callbackInterface, }); } return CallbackFunction.parse(tokeniser, callback); From 883ca3c33863745d75b2c202e1490266a39d5416 Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 11:57:59 -0400 Subject: [PATCH 05/18] test: improve extension parsing tests --- test/extensions.js | 58 ++++++++++++++----- .../baseline/attr-on-callback-interface.json | 30 ---------- .../baseline/namespace-attribute.json | 30 ---------- .../idl/attr-on-callback-interface.webidl | 3 - .../extensions/idl/namespace-attribute.webidl | 3 - 5 files changed, 44 insertions(+), 80 deletions(-) delete mode 100644 test/extensions/baseline/attr-on-callback-interface.json delete mode 100644 test/extensions/baseline/namespace-attribute.json delete mode 100644 test/extensions/idl/attr-on-callback-interface.webidl delete mode 100644 test/extensions/idl/namespace-attribute.webidl diff --git a/test/extensions.js b/test/extensions.js index 5033b8da..ef017ba5 100644 --- a/test/extensions.js +++ b/test/extensions.js @@ -1,20 +1,50 @@ -import { collect } from "./util/collect.js"; +"use strict"; import expect from "expect"; + import { Attribute } from "../lib/productions/attribute.js"; +import { parse } from "../lib/webidl2.js"; +import { IterableLike } from "../lib/productions/iterable.js"; + +describe("Parse IDLs using custom extensions", () => { + ["callback interface", "dictionary"].forEach((type) => { + it(`Attribute on ${type}`, () => { + const customIdl = `${type} Foo { + attribute long bar; + };`; -const extensions = { - "callback-interface": { - extMembers: [[Attribute.parse]], - }, - namespace: { - extMembers: [[Attribute.parse, { readonly: false }]], - }, -}; + // Convert to camel case + const key = type.replace(/ (.)/g, (m, c) => c.toUpperCase()); + const result = parse(customIdl, { + concrete: true, + extensions: { [key]: { extMembers: [[Attribute.parse]] } }, + }); + expect(result[0].type).toBe(type); + expect(result[0].members[0].type).toBe("attribute"); + }); + }); + + it("Attribute (writable) on namespace", () => { + const customIdl = `namespace Foo { + attribute long bar; + };`; + const result = parse(customIdl, { + concrete: true, + extensions: { namespace: { extMembers: [[Attribute.parse]] } }, + }); + expect(result[0].type).toBe("namespace"); + expect(result[0].members[0].type).toBe("attribute"); + expect(result[0].members[0].readonly).toBe(false); + }); -describe("Parses all of the IDLs that require extensions", () => { - for (const test of collect("extensions", { extensions })) { - it(`should produce the same AST for ${test.path}`, () => { - expect(test.diff()).toBeFalsy(); + it("Map-like on mixin", () => { + const customIdl = `interface mixin Foo { + readonly maplike; + };`; + const result = parse(customIdl, { + concrete: true, + extensions: { mixin: { extMembers: [[IterableLike.parse]] } }, }); - } + expect(result[0].type).toBe("interface mixin"); + expect(result[0].members[0].type).toBe("maplike"); + }); }); diff --git a/test/extensions/baseline/attr-on-callback-interface.json b/test/extensions/baseline/attr-on-callback-interface.json deleted file mode 100644 index 25e73251..00000000 --- a/test/extensions/baseline/attr-on-callback-interface.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "type": "callback interface", - "name": "TestCallbackInterface", - "inheritance": null, - "members": [ - { - "type": "attribute", - "name": "foo", - "idlType": { - "type": "attribute-type", - "extAttrs": [], - "generic": "", - "nullable": false, - "union": false, - "idlType": "long" - }, - "extAttrs": [], - "special": "", - "readonly": true - } - ], - "extAttrs": [], - "partial": false - }, - { - "type": "eof", - "value": "" - } -] diff --git a/test/extensions/baseline/namespace-attribute.json b/test/extensions/baseline/namespace-attribute.json deleted file mode 100644 index cb829b8f..00000000 --- a/test/extensions/baseline/namespace-attribute.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "type": "namespace", - "name": "WebrtcGlobalInformation", - "inheritance": null, - "members": [ - { - "type": "attribute", - "name": "debugLevel", - "idlType": { - "type": "attribute-type", - "extAttrs": [], - "generic": "", - "nullable": false, - "union": false, - "idlType": "long" - }, - "extAttrs": [], - "special": "", - "readonly": false - } - ], - "extAttrs": [], - "partial": false - }, - { - "type": "eof", - "value": "" - } -] diff --git a/test/extensions/idl/attr-on-callback-interface.webidl b/test/extensions/idl/attr-on-callback-interface.webidl deleted file mode 100644 index 112fa0c3..00000000 --- a/test/extensions/idl/attr-on-callback-interface.webidl +++ /dev/null @@ -1,3 +0,0 @@ -callback interface TestCallbackInterface { - readonly attribute long foo; -}; diff --git a/test/extensions/idl/namespace-attribute.webidl b/test/extensions/idl/namespace-attribute.webidl deleted file mode 100644 index c7d09504..00000000 --- a/test/extensions/idl/namespace-attribute.webidl +++ /dev/null @@ -1,3 +0,0 @@ -namespace WebrtcGlobalInformation { - attribute long debugLevel; -}; From aabbb9ed4baaec1c9a5d4459a0f15d80bc51960d Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 11:58:13 -0400 Subject: [PATCH 06/18] docs: fix up jsdoc definition for ParserOptions --- lib/webidl2.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/webidl2.js b/lib/webidl2.js index 515133fc..aa532426 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -13,16 +13,16 @@ import { autoParenter } from "./productions/helpers.js"; import { Eof } from "./productions/token.js"; /** @typedef {'callbackInterface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ -/** @typedef {Object.<'extMembers', import("./container.js").AllowedMember[]>} Extension */ +/** @typedef {Object.<'extMembers', import("./container.js").AllowedMember[] >} Extension */ +/** @typedef {Object.<[ExtendableInterfaces], Extension>} Extensions */ /** * Parser options. - * @typedef ParserOptions - * @param {object} options - * @param {string} [options.sourceName] - * @param {boolean} [options.concrete] - * @param {Function[]} [options.productions] - * @param {Object.} [options.extensions] + * @typedef {Object} ParserOptions + * @property {string} [sourceName] + * @property {boolean} [concrete] + * @property {Function[]} [productions] + * @property {Extensions} [extensions] */ /** From e20190ec319038782ce4eb8325888e21518de274 Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 14:19:33 -0400 Subject: [PATCH 07/18] test: remove use strict --- test/extensions.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/extensions.js b/test/extensions.js index ef017ba5..06cd3e8a 100644 --- a/test/extensions.js +++ b/test/extensions.js @@ -1,4 +1,3 @@ -"use strict"; import expect from "expect"; import { Attribute } from "../lib/productions/attribute.js"; From 48c4dc2e1b221b80113cd1e1cfebb3d92088e731 Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 15:08:19 -0400 Subject: [PATCH 08/18] test: merge extension test into custom-production --- test/custom-production.js | 59 +++++++++++++++++++++++++++++++++++++-- test/extensions.js | 49 -------------------------------- 2 files changed, 57 insertions(+), 51 deletions(-) delete mode 100644 test/extensions.js diff --git a/test/custom-production.js b/test/custom-production.js index 245fdec6..1f96a57e 100644 --- a/test/custom-production.js +++ b/test/custom-production.js @@ -1,7 +1,40 @@ -"use strict"; +import expect from "expect"; -import { expect } from "expect"; import { parse, write } from "webidl2"; +import { Base } from "../lib/productions/base.js"; +import { + autoParenter, + type_with_extended_attributes, +} from "../lib/productions/helpers.js"; + +class CustomAttribute extends Base { + static parse(tokeniser) { + const start_position = tokeniser.position; + const tokens = {}; + const ret = autoParenter( + new CustomAttribute({ source: tokeniser.source, tokens }) + ); + tokens.base = tokeniser.consumeIdentifier("custom"); + if (!tokens.base) { + tokeniser.unconsume(start_position); + return; + } + ret.idlType = + type_with_extended_attributes(tokeniser, "attribute-type") || + tokeniser.error("Attribute lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Attribute lacks a name"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated attribute, expected `;`"); + return ret.this; + } + + get type() { + return "custom attribute"; + } +} describe("Writer template functions", () => { const customIdl = ` @@ -49,3 +82,25 @@ describe("Writer template functions", () => { const rewritten = write(result); expect(rewritten).toBe(customIdl); }); + +describe("Parse IDLs using custom extensions", () => { + [ + ["callback interface", "callbackInterface"], + ["dictionary", "dictionary"], + ["interface", "interface"], + ["interface mixin", "mixin"], + ["namespace", "namespace"], + ].forEach(([type, key]) => { + it(`Attribute on ${type}`, () => { + const customIdl = `${type} Foo { + custom long bar; + };`; + const result = parse(customIdl, { + concrete: true, + extensions: { [key]: { extMembers: [[CustomAttribute.parse]] } }, + }); + expect(result[0].type).toBe(type); + expect(result[0].members[0].type).toBe("custom attribute"); + }); + }); +}); diff --git a/test/extensions.js b/test/extensions.js deleted file mode 100644 index 06cd3e8a..00000000 --- a/test/extensions.js +++ /dev/null @@ -1,49 +0,0 @@ -import expect from "expect"; - -import { Attribute } from "../lib/productions/attribute.js"; -import { parse } from "../lib/webidl2.js"; -import { IterableLike } from "../lib/productions/iterable.js"; - -describe("Parse IDLs using custom extensions", () => { - ["callback interface", "dictionary"].forEach((type) => { - it(`Attribute on ${type}`, () => { - const customIdl = `${type} Foo { - attribute long bar; - };`; - - // Convert to camel case - const key = type.replace(/ (.)/g, (m, c) => c.toUpperCase()); - const result = parse(customIdl, { - concrete: true, - extensions: { [key]: { extMembers: [[Attribute.parse]] } }, - }); - expect(result[0].type).toBe(type); - expect(result[0].members[0].type).toBe("attribute"); - }); - }); - - it("Attribute (writable) on namespace", () => { - const customIdl = `namespace Foo { - attribute long bar; - };`; - const result = parse(customIdl, { - concrete: true, - extensions: { namespace: { extMembers: [[Attribute.parse]] } }, - }); - expect(result[0].type).toBe("namespace"); - expect(result[0].members[0].type).toBe("attribute"); - expect(result[0].members[0].readonly).toBe(false); - }); - - it("Map-like on mixin", () => { - const customIdl = `interface mixin Foo { - readonly maplike; - };`; - const result = parse(customIdl, { - concrete: true, - extensions: { mixin: { extMembers: [[IterableLike.parse]] } }, - }); - expect(result[0].type).toBe("interface mixin"); - expect(result[0].members[0].type).toBe("maplike"); - }); -}); From ca97d20114340bc316cc5588d8ce45ffe6587c2d Mon Sep 17 00:00:00 2001 From: pyoor Date: Thu, 13 Apr 2023 16:18:55 -0400 Subject: [PATCH 09/18] test: replace customProduction with top-level CustomAttribute --- test/custom-production.js | 67 ++++++++++++++------------------------- 1 file changed, 24 insertions(+), 43 deletions(-) diff --git a/test/custom-production.js b/test/custom-production.js index 1f96a57e..5ada1068 100644 --- a/test/custom-production.js +++ b/test/custom-production.js @@ -34,53 +34,34 @@ class CustomAttribute extends Base { get type() { return "custom attribute"; } -} -describe("Writer template functions", () => { - const customIdl = ` - interface X {}; - custom Y; - `; + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.termination), + ]), + { data: this, parent } + ); + } +} - /** - * @param {import("../lib/tokeniser").Tokeniser} tokeniser - */ - const customProduction = (tokeniser) => { - const { position } = tokeniser; - const base = tokeniser.consumeIdentifier("custom"); - if (!base) { - return; - } - const tokens = { base }; - tokens.name = tokeniser.consumeKind("identifier"); - tokens.termination = tokeniser.consume(";"); - if (!tokens.name || !tokens.termination) { - tokeniser.unconsume(position); - return; - } - return { - type: "custom", - tokens, - /** @param {import("../lib/writer.js").Writer} w */ - write(w) { - return w.ts.wrap([ - w.token(this.tokens.base), - w.token(this.tokens.name), - w.token(this.tokens.termination), - ]); - }, - }; - }; +describe("Parse IDLs using custom productions", () => { + it("Parse and rewrite top-level custom attribute", () => { + const customIdl = "custom long bar;"; + const result = parse(customIdl, { + productions: [CustomAttribute.parse], + concrete: true, + }); + expect(result[0].type).toBe("custom attribute"); - const result = parse(customIdl, { - productions: [customProduction], - concrete: true, + const rewritten = write(result); + expect(rewritten).toBe(customIdl); }); - expect(result[0].type).toBe("interface"); - expect(result[1].type).toBe("custom"); - - const rewritten = write(result); - expect(rewritten).toBe(customIdl); }); describe("Parse IDLs using custom extensions", () => { From 66e3862e2cca9ad30a3fe7cf655b5011023cdb34 Mon Sep 17 00:00:00 2001 From: pyoor Date: Mon, 17 Apr 2023 09:11:51 -0400 Subject: [PATCH 10/18] test: remove extension argument from collection utility --- test/util/collect.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/util/collect.js b/test/util/collect.js index ef82ebbf..7909f810 100644 --- a/test/util/collect.js +++ b/test/util/collect.js @@ -7,7 +7,7 @@ import { diff } from "jsondiffpatch"; * Collects test items from the specified directory * @param {string} base */ -export function* collect(base, { expectError, raw, extensions = {} } = {}) { +export function* collect(base, { expectError, raw } = {}) { const dir = new URL(join("..", base, "idl/"), import.meta.url); const idls = readdirSync(dir) .filter((it) => it.endsWith(".webidl")) @@ -19,7 +19,6 @@ export function* collect(base, { expectError, raw, extensions = {} } = {}) { const ast = parse(text, { concrete: true, sourceName: basename(path.pathname), - extensions, }); const validation = validate(ast); if (validation) { From 38c949eb10eb0fea6d5c240202672035c5e98365 Mon Sep 17 00:00:00 2001 From: pyoor Date: Mon, 17 Apr 2023 12:22:56 -0400 Subject: [PATCH 11/18] docs: normalize use of Token import --- lib/productions/interface.js | 2 +- lib/productions/mixin.js | 4 +--- lib/productions/operation.js | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/productions/interface.js b/lib/productions/interface.js index f361e527..0f88e77c 100644 --- a/lib/productions/interface.js +++ b/lib/productions/interface.js @@ -34,7 +34,7 @@ function static_member(tokeniser) { export class Interface extends Container { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser - * @param {Token} base + * @param {import("../tokeniser.js").Token} base * @param {object} [options] * @param {import("./container.js").AllowedMember[]} [options.extMembers] * @param {import("../tokeniser.js").Token|null} [options.partial] diff --git a/lib/productions/mixin.js b/lib/productions/mixin.js index 5108efdb..19cce9f8 100644 --- a/lib/productions/mixin.js +++ b/lib/productions/mixin.js @@ -6,10 +6,8 @@ import { stringifier } from "./helpers.js"; export class Mixin extends Container { /** - * @typedef {import("../tokeniser.js").Token} Token - * * @param {import("../tokeniser.js").Tokeniser} tokeniser - * @param {Token} base + * @param {import("../tokeniser.js").Token} base * @param {object} [options] * @param {import("./container.js").AllowedMember[]} [options.extMembers] * @param {import("../tokeniser.js").Token} [options.partial] diff --git a/lib/productions/operation.js b/lib/productions/operation.js index b92c10aa..380ada5e 100644 --- a/lib/productions/operation.js +++ b/lib/productions/operation.js @@ -9,12 +9,10 @@ import { validationError } from "../error.js"; export class Operation extends Base { /** - * @typedef {import("../tokeniser.js").Token} Token - * * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {object} [options] - * @param {Token} [options.special] - * @param {Token} [options.regular] + * @param {import("../tokeniser.js").Token} [options.special] + * @param {import("../tokeniser.js").Token} [options.regular] */ static parse(tokeniser, { special, regular } = {}) { const tokens = { special }; From 12e19d06c2a55e88fa704d7e77a411f42b4965b1 Mon Sep 17 00:00:00 2001 From: pyoor Date: Mon, 17 Apr 2023 12:23:11 -0400 Subject: [PATCH 12/18] test: fix import of expect function --- test/custom-production.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/custom-production.js b/test/custom-production.js index 5ada1068..7c11dce7 100644 --- a/test/custom-production.js +++ b/test/custom-production.js @@ -1,4 +1,4 @@ -import expect from "expect"; +import { expect } from "expect"; import { parse, write } from "webidl2"; import { Base } from "../lib/productions/base.js"; From 364de96a049203a61e2db78318c40119b2335085 Mon Sep 17 00:00:00 2001 From: pyoor Date: Mon, 17 Apr 2023 12:26:37 -0400 Subject: [PATCH 13/18] docs: mark args as any This is also due to https://github.com/microsoft/TypeScript/issues/4628 which prevents changing the signature of static methods on inherited classes. --- lib/productions/container.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/productions/container.js b/lib/productions/container.js index ed628aae..ef078af7 100644 --- a/lib/productions/container.js +++ b/lib/productions/container.js @@ -33,9 +33,7 @@ export class Container extends Base { /** * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {*} instance TODO: This should be {T extends Container}, but see https://github.com/microsoft/TypeScript/issues/4628 - * @param {object} args - * @param {boolean} args.inheritable - * @param {AllowedMember[]} args.allowedMembers + * @param {*} args */ static parse(tokeniser, instance, { inheritable, allowedMembers }) { const { tokens, type } = instance; From 59b4172902a4c831d023ac94b4f942dc6d1fa059 Mon Sep 17 00:00:00 2001 From: pyoor Date: Mon, 17 Apr 2023 12:27:12 -0400 Subject: [PATCH 14/18] docs: fix path to container.js --- lib/webidl2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webidl2.js b/lib/webidl2.js index aa532426..b02d659a 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -13,7 +13,7 @@ import { autoParenter } from "./productions/helpers.js"; import { Eof } from "./productions/token.js"; /** @typedef {'callbackInterface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ -/** @typedef {Object.<'extMembers', import("./container.js").AllowedMember[] >} Extension */ +/** @typedef {Object.<'extMembers', import("./productions/container.js").AllowedMember[] >} Extension */ /** @typedef {Object.<[ExtendableInterfaces], Extension>} Extensions */ /** From 38ffe973444610d4475d88a667cc26ae3ba111e0 Mon Sep 17 00:00:00 2001 From: pyoor Date: Wed, 19 Apr 2023 10:48:16 -0400 Subject: [PATCH 15/18] refactor: remove unnecessary spread operator --- lib/webidl2.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/webidl2.js b/lib/webidl2.js index b02d659a..6c3bc08d 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -102,7 +102,7 @@ function parseByTokens(tokeniser, options) { Enum.parse(tokeniser) || Typedef.parse(tokeniser) || Includes.parse(tokeniser) || - Namespace.parse(tokeniser, { ...options?.extensions?.namespace }) + Namespace.parse(tokeniser, options?.extensions?.namespace) ); } From 688adf2d1838cf62c4e53a340197ba4bcff55cb1 Mon Sep 17 00:00:00 2001 From: pyoor Date: Tue, 20 Jun 2023 12:01:54 -0400 Subject: [PATCH 16/18] docs: fix jsdoc types Co-authored-by: Kagami Sascha Rosylight --- lib/webidl2.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/webidl2.js b/lib/webidl2.js index 6c3bc08d..26a020ad 100644 --- a/lib/webidl2.js +++ b/lib/webidl2.js @@ -13,8 +13,8 @@ import { autoParenter } from "./productions/helpers.js"; import { Eof } from "./productions/token.js"; /** @typedef {'callbackInterface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ -/** @typedef {Object.<'extMembers', import("./productions/container.js").AllowedMember[] >} Extension */ -/** @typedef {Object.<[ExtendableInterfaces], Extension>} Extensions */ +/** @typedef {{ extMembers?: import("./productions/container.js").AllowedMember[]}} Extension */ +/** @typedef {Partial>} Extensions */ /** * Parser options. From bc54d0eadef41c732fd5c232ffbb1f79f0f3bcf5 Mon Sep 17 00:00:00 2001 From: pyoor Date: Tue, 20 Jun 2023 12:02:01 -0400 Subject: [PATCH 17/18] docs: fix jsdoc types Co-authored-by: Kagami Sascha Rosylight --- lib/productions/container.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/productions/container.js b/lib/productions/container.js index ef078af7..8f1f22d5 100644 --- a/lib/productions/container.js +++ b/lib/productions/container.js @@ -18,7 +18,7 @@ function inheritance(tokeniser) { /** * Parser callback. - * @callback parserCallback + * @callback ParserCallback * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {...*} args */ @@ -26,7 +26,7 @@ function inheritance(tokeniser) { /** * A parser callback and optional option object. * @typedef AllowedMember - * @type {[parserCallback, object?]} + * @type {[ParserCallback, object?]} */ export class Container extends Base { From bdf5ee4d7e7741a0a8705f496175aad1d6cf9721 Mon Sep 17 00:00:00 2001 From: pyoor Date: Wed, 21 Jun 2023 09:39:21 -0400 Subject: [PATCH 18/18] fix: remove iheritance attribute from CallbackInterface --- lib/productions/callback-interface.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/productions/callback-interface.js b/lib/productions/callback-interface.js index 293b4a83..a22b0098 100644 --- a/lib/productions/callback-interface.js +++ b/lib/productions/callback-interface.js @@ -7,10 +7,9 @@ export class CallbackInterface extends Container { * @param {import("../tokeniser.js").Tokeniser} tokeniser * @param {*} callback * @param {object} [options] - * @param {boolean} [options.inheritable] * @param {import("./container.js").AllowedMember[]} [options.extMembers] */ - static parse(tokeniser, callback, { inheritable, extMembers = [] } = {}) { + static parse(tokeniser, callback, { extMembers = [] } = {}) { const tokens = { callback }; tokens.base = tokeniser.consume("interface"); if (!tokens.base) { @@ -20,7 +19,6 @@ export class CallbackInterface extends Container { tokeniser, new CallbackInterface({ source: tokeniser.source, tokens }), { - inheritable: !!inheritable, allowedMembers: [ ...extMembers, [Constant.parse],