Repository: w3c/webidl2.js (js)

Issue:
Add mixin support
Seems we don't support [mixins](https://heycam.github.io/webidl/#idl-interface-mixins) yet, right?

```
interface mixin Observable {
  void addEventListener(DOMString type,
                        EventListener listener,
                        boolean useCapture);
  // ...
};
```

Pull request:
Support mixins + includes statements
(This includes #103)

Fixes #92.

[MixinMember](https://heycam.github.io/webidl/#prod-MixinMember) only includes stringifiers but not static members, while current `operation()`/`attribute()` always include both. This PR pulls `static_member()` and `stringifier()` out of `operation()`/`attribute()` to support mixins + better match the parser structure to the spec.

```
MixinMember ::
    Const
    RegularOperation
    Stringifier
    ReadOnly AttributeRest
```

Bonus: This renames nonspecial_operation to regular_operation to match the spec naming.


Patch:
diff --git a/README.md b/README.md
index 2e0f35f..8fcdbc8 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,7 @@ Where the fields are as follows:
 * `union`: Boolean indicating whether this is a union type or not.
 
 ### Interface
+
 Interfaces look like this:
 
 ```JS
@@ -167,7 +168,36 @@ The fields are as follows:
   sense.
 * `extAttrs`: A list of [extended attributes](#extended-attributes).
 
+### Interface mixins
+
+Interfaces mixins look like this:
+
+```JS
+{
+  "type": "interface mixin",
+  "name": "Animal",
+  "partial": false,
+  "members": [...],
+  "extAttrs": [...]
+}, {
+  "type": "interface mixin",
+  "name": "Human",
+  "partial": false,
+  "members": [...],
+  "extAttrs": [...]
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "interface mixin".
+* `name`: The name of the interface mixin.
+* `partial`: A boolean indicating whether it's a partial interface mixin.
+* `members`: An array of interface members (attributes, operations, etc.). Empty if there are none.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
 ### Namespace
+
 Namespaces look like this:
 
 ```JS
@@ -291,7 +321,7 @@ The fields are as follows:
 
 * `type`: Always "enum".
 * `name`: The enum's name.
-* `value`: An array of values.
+* `values`: An array of values.
 * `extAttrs`: A list of [extended attributes](#extended-attributes).
 
 ### Typedef
@@ -350,6 +380,26 @@ The fields are as follows:
 * `implements`: The interface that is being implemented by the target.
 * `extAttrs`: A list of [extended attributes](#extended-attributes).
 
+### Includes
+
+An includes definition looks like this:
+
+```JS
+{
+  "type": "includes",
+  "target": "Node",
+  "includes": "EventTarget",
+  "extAttrs": []
+}
+```
+
+The fields are as follows:
+
+* `type`: Always "includes".
+* `target`: The interface that includes an interface mixin.
+* `includes`: The interface mixin that is being included by the target.
+* `extAttrs`: A list of [extended attributes](#extended-attributes).
+
 ### Operation Member
 
 An operation looks like this:
diff --git a/lib/webidl2.js b/lib/webidl2.js
index 2fd2671..a0d46d2 100644
--- a/lib/webidl2.js
+++ b/lib/webidl2.js
@@ -1,8 +1,6 @@
 "use strict";
 
-// Foo
-
-(function() {
+(() => {
   function tokenise(str) {
     const tokens = [];
     const re = {
@@ -54,6 +52,15 @@
     const STR = "string";
     const OTHER = "other";
 
+    const EMPTY_OPERATION = Object.freeze({
+      type: "operation",
+      getter: false,
+      setter: false,
+      deleter: false,
+      "static": false,
+      stringifier: false
+    });
+
     function error(str) {
       let tok = "";
       let numTokens = 0;
@@ -481,8 +488,7 @@
       all_ws();
       const tok = consume(ID, "interface");
       if (tok) {
-        tokens.unshift(tok);
-        ret = interface_();
+        ret = interface_rest();
         ret.type = "callback interface";
         return ret;
       }
@@ -512,13 +518,6 @@
         inherit: false,
         readonly: false
       };
-      if (consume(ID, "static")) {
-        ret["static"] = true;
-        grabbed.push(last_token);
-      } else if (consume(ID, "stringifier")) {
-        ret.stringifier = true;
-        grabbed.push(last_token);
-      }
       const w = all_ws();
       if (w) grabbed.push(w);
       if (consume(ID, "inherit")) {
@@ -569,14 +568,7 @@
 
     function operation(store) {
       all_ws(store, "pea");
-      const ret = {
-        type: "operation",
-        getter: false,
-        setter: false,
-        deleter: false,
-        "static": false,
-        stringifier: false
-      };
+      const ret = Object.assign({}, EMPTY_OPERATION);
       while (true) {
         all_ws();
         if (consume(ID, "getter")) ret.getter = true;
@@ -590,19 +582,6 @@
         operation_rest(ret, store);
         return ret;
       }
-      if (consume(ID, "static")) {
-        ret["static"] = true;
-        ret.idlType = return_type();
-        operation_rest(ret, store);
-        return ret;
-      } else if (consume(ID, "stringifier")) {
-        ret.stringifier = true; -
-        all_ws();
-        if (consume(OTHER, ";")) return ret;
-        ret.idlType = return_type();
-        operation_rest(ret, store);
-        return ret;
-      }
       ret.idlType = return_type();
       all_ws();
       if (consume(ID, "iterator")) {
@@ -624,6 +603,27 @@
       }
     };
 
+    function static_member(store) {
+      all_ws(store, "pea");
+      if (!consume(ID, "static")) return;
+      all_ws();
+      return noninherited_attribute(store, "static") ||
+        regular_operation(store, "static") ||
+        error("No body in static member");
+    }
+
+    function stringifier(store) {
+      all_ws(store, "pea");
+      if (!consume(ID, "stringifier")) return;
+      all_ws();
+      if (consume(OTHER, ";")) {
+        return Object.assign({}, EMPTY_OPERATION, { stringifier: true });
+      }
+      return noninherited_attribute(store, "stringifier") ||
+        regular_operation(store, "stringifier") ||
+        error("Unterminated stringifier");
+    }
+
     function identifiers(arr) {
       while (true) {
         all_ws();
@@ -697,9 +697,7 @@
       return ret;
     };
 
-    function interface_(isPartial, store) {
-      all_ws(isPartial ? null : store, "pea");
-      if (!consume(ID, "interface")) return;
+    function interface_rest(isPartial, store) {
       all_ws();
       const name = consume(ID) || error("No name for interface");
       const mems = [];
@@ -728,6 +726,8 @@
           continue;
         }
         const mem = (opt.allowNestedTypedefs && typedef(store ? mems : null)) ||
+          static_member(store ? mems : null) ||
+          stringifier(store ? mems : null) ||
           iterable(store ? mems : null) ||
           attribute(store ? mems : null) ||
           operation(store ? mems : null) ||
@@ -737,6 +737,52 @@
       }
     };
 
+    function mixin_rest(isPartial, store) {
+      all_ws();
+      if (!consume(ID, "mixin")) return;
+      all_ws();
+      const name = consume(ID) || error("No name for interface mixin");
+      const mems = [];
+      const ret = {
+        type: "interface mixin",
+        name: isPartial ? name.value : sanitize_name(name.value, "interface mixin"),
+        partial: false,
+        members: mems
+      };
+      all_ws();
+      consume(OTHER, "{") || error("Bodyless interface mixin");
+      while (true) {
+        all_ws(store ? mems : null);
+        if (consume(OTHER, "}")) {
+          all_ws();
+          consume(OTHER, ";") || error("Missing semicolon after interface mixin");
+          return ret;
+        }
+        const ea = extended_attrs(store ? mems : null);
+        all_ws();
+        const cnt = const_(store ? mems : null);
+        if (cnt) {
+          cnt.extAttrs = ea;
+          ret.members.push(cnt);
+          continue;
+        }
+        const mem = stringifier(store ? mems : null) ||
+          noninherited_attribute(store ? mems : null) ||
+          regular_operation(store ? mems : null) ||
+          error("Unknown member");
+        mem.extAttrs = ea;
+        ret.members.push(mem);
+      }
+    }
+
+    function interface_(isPartial, store) {
+      all_ws(isPartial ? null : store, "pea");
+      if (!consume(ID, "interface")) return;
+      return mixin_rest(isPartial, store) ||
+        interface_rest(isPartial, store) ||
+        error("Interface has no proper body");
+    }
+
     function namespace(isPartial, store) {
       all_ws(isPartial ? null : store, "pea");
       if (!consume(ID, "namespace")) return;
@@ -761,14 +807,14 @@
         const ea = extended_attrs(store ? mems : null);
         all_ws();
         const mem = noninherited_attribute(store ? mems : null) ||
-          nonspecial_operation(store ? mems : null) ||
+          regular_operation(store ? mems : null) ||
           error("Unknown member");
         mem.extAttrs = ea;
         ret.members.push(mem);
       }
     }
 
-    function noninherited_attribute(store) {
+    function noninherited_attribute(store, prefix) {
       const w = all_ws(store, "pea");
       const grabbed = [];
       const ret = {
@@ -778,6 +824,9 @@
         inherit: false,
         readonly: false
       };
+      if (prefix) {
+        ret[prefix] = true;
+      }
       if (w) grabbed.push(w);
       if (consume(ID, "readonly")) {
         ret.readonly = true;
@@ -792,16 +841,12 @@
       return rest;
     }
 
-    function nonspecial_operation(store) {
+    function regular_operation(store, prefix) {
       all_ws(store, "pea");
-      const ret = {
-        type: "operation",
-        getter: false,
-        setter: false,
-        deleter: false,
-        "static": false,
-        stringifier: false
-      };
+      const ret = Object.assign({}, EMPTY_OPERATION);
+      if (prefix) {
+        ret[prefix] = true;
+      }
       ret.idlType = return_type();
       return operation_rest(ret, store);
     }
@@ -863,7 +908,7 @@
       }
     };
 
-    const enum_ = function(store) {
+    function enum_(store) {
       all_ws(store, "pea");
       if (!consume(ID, "enum")) return;
       all_ws();
@@ -886,7 +931,7 @@
         }
         const val = consume(STR) || error("Unexpected value in enum");
         val.value = val.value.replace(/"/g, "");
-        ret.values.push( val );
+        ret.values.push(val);
         all_ws(store ? vals : null);
         if (consume(OTHER, ",")) {
           if (store) vals.push({ type: "," });
@@ -937,6 +982,29 @@
       }
     };
 
+    function includes(store) {
+      all_ws(store, "pea");
+      const target = consume(ID);
+      if (!target) return;
+      const w = all_ws();
+      if (consume(ID, "includes")) {
+        const ret = {
+          type: "includes",
+          target: target.value
+        };
+        all_ws();
+        const imp = consume(ID) || error("Incomplete includes statement");
+        ret["includes"] = imp.value;
+        all_ws();
+        consume(OTHER, ";") || error("No terminating ; for includes statement");
+        return ret;
+      } else {
+        // rollback
+        tokens.unshift(w);
+        tokens.unshift(target);
+      }
+    };
+
     function definition(store) {
       return callback(store) ||
         interface_(false, store) ||
@@ -945,6 +1013,7 @@
         enum_(store) ||
         typedef(store) ||
         implements_(store) ||
+        includes(store) ||
         namespace(false, store);
     };
 
@@ -983,4 +1052,4 @@
   } else {
     (self || window).WebIDL2 = obj;
   }
-}());
+})();


Test patch:
diff --git a/test/syntax/idl/mixin.widl b/test/syntax/idl/mixin.widl
new file mode 100644
index 0000000..7c37a6e
--- /dev/null
+++ b/test/syntax/idl/mixin.widl
@@ -0,0 +1,12 @@
+// Extracted from https://heycam.github.io/webidl/#using-mixins-and-partials on 2017-11-02
+
+interface mixin GlobalCrypto {
+  readonly attribute Crypto crypto;
+};
+
+Window includes GlobalCrypto;
+WorkerGlobalScope includes GlobalCrypto;
+
+partial interface mixin WindowOrWorkerGlobalScope {
+  readonly attribute Crypto crypto;
+};
diff --git a/test/syntax/json/mixin.json b/test/syntax/json/mixin.json
new file mode 100644
index 0000000..a2afae3
--- /dev/null
+++ b/test/syntax/json/mixin.json
@@ -0,0 +1,62 @@
+[
+    {
+        "type": "interface mixin",
+        "name": "GlobalCrypto",
+        "partial": false,
+        "members": [
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": true,
+                "idlType": {
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "Crypto"
+                },
+                "name": "crypto",
+                "extAttrs": []
+            }
+        ],
+        "extAttrs": []
+    },
+    {
+        "type": "includes",
+        "target": "Window",
+        "includes": "GlobalCrypto",
+        "extAttrs": []
+    },
+    {
+        "type": "includes",
+        "target": "WorkerGlobalScope",
+        "includes": "GlobalCrypto",
+        "extAttrs": []
+    },
+    {
+        "type": "interface mixin",
+        "name": "WindowOrWorkerGlobalScope",
+        "partial": true,
+        "members": [
+            {
+                "type": "attribute",
+                "static": false,
+                "stringifier": false,
+                "inherit": false,
+                "readonly": true,
+                "idlType": {
+                    "sequence": false,
+                    "generic": null,
+                    "nullable": false,
+                    "union": false,
+                    "idlType": "Crypto"
+                },
+                "name": "crypto",
+                "extAttrs": []
+            }
+        ],
+        "extAttrs": []
+    }
+]