Repository: w3c/webidl2.js (js)

Issue:
Spaced three-dot shouldn't recognized as a variadic mark
```webidl
interface Interface {
  void valid(object... args); // okay
  void invalid(object. . . args); // shouldn't pass
};
```

This is a regression from #154. 😱

Pull request:
fix: prevent incorrect spaced negative infinity and variadic marks
Closes #162 

This is similar to #154 but for punctuations.

Patch:
diff --git a/lib/webidl2.js b/lib/webidl2.js
index 3c13c2b..bd92d7c 100644
--- a/lib/webidl2.js
+++ b/lib/webidl2.js
@@ -37,7 +37,7 @@
     "stringifier",
     "typedef",
     "unrestricted"
-  ]
+  ];
 
   const nonRegexTerminals = [
     "Infinity",
@@ -62,6 +62,24 @@
     "void"
   ].concat(argumentNameKeywords);
 
+  const punctuations = [
+    "(",
+    ")",
+    ",",
+    "-Infinity",
+    "...",
+    ":",
+    ";",
+    "<",
+    "=",
+    ">",
+    "?",
+    "[",
+    "]",
+    "{",
+    "}"
+  ];
+
   function tokenise(str) {
     const tokens = [];
     let lastIndex = 0;
@@ -83,10 +101,6 @@
         if (result === -1) {
           result = attemptTokenMatch("integer");
         }
-        if (result === -1) {
-          // '-' and '.' can also match "other".
-          result = attemptTokenMatch("other");
-        }
       } else if (/[A-Z_a-z]/.test(nextChar)) {
         result = attemptTokenMatch("identifier");
         const token = tokens[tokens.length - 1];
@@ -96,7 +110,17 @@
       } else if (nextChar === '"') {
         result = attemptTokenMatch("string");
       }
-      
+
+      for (const punctuation of punctuations) {
+        if (str.startsWith(punctuation, lastIndex)) {
+          tokens.push({ type: punctuation, value: punctuation, trivia });
+          trivia = "";
+          lastIndex += punctuation.length;
+          result = lastIndex;
+          break;
+        }
+      }
+
       // other as the last try
       if (result === -1) {
         result = attemptTokenMatch("other");
@@ -198,12 +222,15 @@
 
     let last_token = null;
 
-    function consume(type, value) {
-      if (!tokens.length || tokens[0].type !== type) return;
-      if (typeof value === "undefined" || tokens[0].value === value) {
-        last_token = tokens.shift();
-        line += count(last_token.trivia, "\n");
-        return last_token;
+    function consume(...candidates) {
+      // TODO: use const when Servo updates its JS engine
+      for (let type of candidates) {
+        if (!tokens.length || tokens[0].type !== type) continue;
+        if (typeof value === "undefined" || tokens[0].value === value) {
+          last_token = tokens.shift();
+          line += count(last_token.trivia, "\n");
+          return last_token;
+        }
       }
     }
 
@@ -261,19 +288,15 @@
       if (consume("false")) return { type: "boolean", value: false };
       if (consume("null")) return { type: "null" };
       if (consume("Infinity")) return { type: "Infinity", negative: false };
+      if (consume("-Infinity")) return { type: "Infinity", negative: true };
       if (consume("NaN")) return { type: "NaN" };
-      const ret = consume(FLOAT) || consume(INT);
+      const ret = consume(FLOAT, INT);
       if (ret) return { type: "number", value: ret.value };
-      const tok = consume(OTHER, "-");
-      if (tok) {
-        if (consume("Infinity")) return { type: "Infinity", negative: true };
-        else unconsume(tok);
-      }
     }
 
     function type_suffix(obj) {
       while (true) {
-        if (consume(OTHER, "?")) {
+        if (consume("?")) {
           if (obj.nullable) error("Can't nullable more than once");
           obj.nullable = true;
         } else return;
@@ -290,7 +313,7 @@
       } else if (name = consume(ID)) {
         value = name.value;
         // Generic types
-        if (consume(OTHER, "<")) {
+        if (consume("<")) {
           // backwards compat
           if (value === "sequence") {
             ret.sequence = true;
@@ -300,7 +323,7 @@
           do {
             types.push(type_with_extended_attributes(typeName) || error("Error parsing generic type " + value));
           }
-          while (consume(OTHER, ","));
+          while (consume(","));
           if (value === "sequence") {
             if (types.length !== 1) error("A sequence must have exactly one subtype");
           } else if (value === "record") {
@@ -313,7 +336,7 @@
             if (types[0].extAttrs.length) error("Promise type cannot have extended attribute");
           }
           ret.idlType = types.length === 1 ? types[0] : types;
-          if (!consume(OTHER, ">")) error("Unterminated generic type " + value);
+          if (!consume(">")) error("Unterminated generic type " + value);
           type_suffix(ret);
           return ret;
         } else {
@@ -328,16 +351,16 @@
     }
 
     function union_type(typeName) {
-      if (!consume(OTHER, "(")) return;
+      if (!consume("(")) return;
       const ret = Object.assign({ type: typeName || null }, EMPTY_IDLTYPE, { union: true, idlType: [] });
       do {
         const typ = type_with_extended_attributes() || error("No type after open parenthesis or 'or' in union type");
         ret.idlType.push(typ);
-      } while(consume("or"));
+      } while (consume("or"));
       if (ret.idlType.length < 2) {
         error("At least two types are expected in a union type but found less");
       }
-      if (!consume(OTHER, ")")) error("Unterminated union type");
+      if (!consume(")")) error("Unterminated union type");
       type_suffix(ret);
       return ret;
     }
@@ -366,17 +389,8 @@
         return;
       }
       const type_token = last_token;
-      if (!ret.optional) {
-        if (tokens.length >= 3 &&
-          tokens[0].type === "other" && tokens[0].value === "." &&
-          tokens[1].type === "other" && tokens[1].value === "." &&
-          tokens[2].type === "other" && tokens[2].value === "."
-        ) {
-          tokens.shift();
-          tokens.shift();
-          tokens.shift();
-          ret.variadic = true;
-        }
+      if (!ret.optional && consume("...")) {
+        ret.variadic = true;
       }
       const name = argument_name(ID);
       if (!name) {
@@ -394,7 +408,7 @@
       }
       return ret;
     }
-    
+
     function argument_name() {
       let name = consume(ID);
       if (name) {
@@ -414,7 +428,7 @@
       if (!arg) return ret;
       ret.push(arg);
       while (true) {
-        if (!consume(OTHER, ",")) return ret;
+        if (!consume(",")) return ret;
         const nxt = argument() || error("Trailing comma in arguments list");
         ret.push(nxt);
       }
@@ -429,7 +443,7 @@
         type: "extended-attribute",
         rhs: null
       };
-      const eq = consume(OTHER, "=");
+      const eq = consume("=");
       if (eq) {
         ret.rhs = consume(ID) ||
           consume(FLOAT) ||
@@ -440,7 +454,7 @@
           ret.rhs.trivia = undefined;
         }
       }
-      if (consume(OTHER, "(")) {
+      if (consume("(")) {
         if (eq && !ret.rhs) {
           // [Exposed=(Window,Worker)]
           ret.rhs = {
@@ -452,7 +466,7 @@
           // [NamedConstructor=Audio(DOMString src)] or [Constructor(DOMString str)]
           ret.arguments = argument_list();
         }
-        consume(OTHER, ")") || error("Unexpected token in extended attribute argument list");
+        consume(")") || error("Unexpected token in extended attribute argument list");
       }
       if (eq && !ret.rhs) error("No right hand side to extended attribute assignment");
       return ret;
@@ -462,22 +476,22 @@
     // seems to be used
     function extended_attrs() {
       const eas = [];
-      if (!consume(OTHER, "[")) return eas;
+      if (!consume("[")) return eas;
       eas[0] = simple_extended_attr() || error("Extended attribute with not content");
-      while (consume(OTHER, ",")) {
+      while (consume(",")) {
         eas.push(simple_extended_attr() || error("Trailing comma in extended attribute"));
       }
-      consume(OTHER, "]") || error("No end of extended attribute");
+      consume("]") || error("No end of extended attribute");
       return eas;
     }
 
     function default_() {
-      if (consume(OTHER, "=")) {
+      if (consume("=")) {
         const def = const_value();
         if (def) {
           return def;
-        } else if (consume(OTHER, "[")) {
-          if (!consume(OTHER, "]")) error("Default sequence value must be empty");
+        } else if (consume("[")) {
+          if (!consume("]")) error("Default sequence value must be empty");
           return { type: "sequence", value: [] };
         } else {
           const str = consume(STR) || error("No value for default");
@@ -498,21 +512,21 @@
         typ = typ.value;
       }
       ret.idlType = Object.assign({ type: "const-type" }, EMPTY_IDLTYPE, { idlType: typ });
-      if (consume(OTHER, "?")) {
+      if (consume("?")) {
         ret.nullable = true;
       }
       const name = consume(ID) || error("No name for const");
       ret.name = name.value;
-      consume(OTHER, "=") || error("No value assignment for const");
+      consume("=") || error("No value assignment for const");
       const cnt = const_value();
       if (cnt) ret.value = cnt;
       else error("No value for const");
-      consume(OTHER, ";") || error("Unterminated const");
+      consume(";") || error("Unterminated const");
       return ret;
     }
 
     function inheritance() {
-      if (consume(OTHER, ":")) {
+      if (consume(":")) {
         const inh = consume(ID) || error("No type in inheritance");
         return inh.value;
       }
@@ -523,10 +537,10 @@
       const name = consume(ID);
       ret.name = name ? unescape(name.value) : null;
       ret.escapedName = name ? name.value : null;
-      consume(OTHER, "(") || error("Invalid operation");
+      consume("(") || error("Invalid operation");
       ret.arguments = argument_list();
-      consume(OTHER, ")") || error("Unterminated operation");
-      consume(OTHER, ";") || error("Unterminated operation");
+      consume(")") || error("Unterminated operation");
+      consume(";") || error("Unterminated operation");
       return ret;
     }
 
@@ -540,12 +554,12 @@
       }
       const name = consume(ID) || error("No name for callback");
       ret = current = { type: "callback", name: sanitize_name(name.value, "callback") };
-      consume(OTHER, "=") || error("No assignment in callback");
+      consume("=") || error("No assignment in callback");
       ret.idlType = return_type();
-      consume(OTHER, "(") || error("No arguments in callback");
+      consume("(") || error("No arguments in callback");
       ret.arguments = argument_list();
-      consume(OTHER, ")") || error("Unterminated callback");
-      consume(OTHER, ";") || error("Unterminated callback");
+      consume(")") || error("Unterminated callback");
+      consume(";") || error("Unterminated callback");
       return ret;
     }
 
@@ -585,7 +599,7 @@
       const name = consume(ID) || error("No name in attribute");
       ret.name = unescape(name.value);
       ret.escapedName = name.value;
-      consume(OTHER, ";") || error("Unterminated attribute");
+      consume(";") || error("Unterminated attribute");
       return ret;
     }
 
@@ -623,7 +637,7 @@
 
     function stringifier() {
       if (!consume("stringifier")) return;
-      if (consume(OTHER, ";")) {
+      if (consume(";")) {
         return Object.assign({}, EMPTY_OPERATION, { stringifier: true });
       }
       const member = attribute({ noInherit: true }) ||
@@ -641,7 +655,7 @@
       }
       else error("Expected identifiers but not found");
       while (true) {
-        if (consume(OTHER, ",")) {
+        if (consume(",")) {
           const name = consume(ID) || error("Trailing comma in identifiers list");
           arr.push(name.value);
         } else break;
@@ -683,17 +697,17 @@
       ret.type = ittype;
       if (ret.type !== 'maplike' && ret.type !== 'setlike')
         delete ret.readonly;
-      if (consume(OTHER, "<")) {
+      if (consume("<")) {
         ret.idlType = [type_with_extended_attributes()] || error(`Error parsing ${ittype} declaration`);
         if (secondTypeAllowed) {
-          if (consume(OTHER, ",")) {
+          if (consume(",")) {
             ret.idlType.push(type_with_extended_attributes());
           }
           else if (secondTypeRequired)
             error(`Missing second type argument in ${ittype} declaration`);
         }
-        if (!consume(OTHER, ">")) error(`Unterminated ${ittype} declaration`);
-        if (!consume(OTHER, ";")) error(`Missing semicolon after ${ittype} declaration`);
+        if (!consume(">")) error(`Unterminated ${ittype} declaration`);
+        if (!consume(";")) error(`Missing semicolon after ${ittype} declaration`);
       } else
         error(`Error parsing ${ittype} declaration`);
 
@@ -710,10 +724,10 @@
         members: mems
       };
       if (!isPartial) ret.inheritance = inheritance() || null;
-      consume(OTHER, "{") || error("Bodyless interface");
+      consume("{") || error("Bodyless interface");
       while (true) {
-        if (consume(OTHER, "}")) {
-          consume(OTHER, ";") || error("Missing semicolon after interface");
+        if (consume("}")) {
+          consume(";") || error("Missing semicolon after interface");
           return ret;
         }
         const ea = extended_attrs();
@@ -744,10 +758,10 @@
         partial: isPartial,
         members: mems
       };
-      consume(OTHER, "{") || error("Bodyless interface mixin");
+      consume("{") || error("Bodyless interface mixin");
       while (true) {
-        if (consume(OTHER, "}")) {
-          consume(OTHER, ";") || error("Missing semicolon after interface mixin");
+        if (consume("}")) {
+          consume(";") || error("Missing semicolon after interface mixin");
           return ret;
         }
         const ea = extended_attrs();
@@ -783,10 +797,10 @@
         partial: isPartial,
         members: mems
       };
-      consume(OTHER, "{") || error("Bodyless namespace");
+      consume("{") || error("Bodyless namespace");
       while (true) {
-        if (consume(OTHER, "}")) {
-          consume(OTHER, ";") || error("Missing semicolon after namespace");
+        if (consume("}")) {
+          consume(";") || error("Missing semicolon after namespace");
           return ret;
         }
         const ea = extended_attrs();
@@ -818,10 +832,10 @@
         members: mems
       };
       if (!isPartial) ret.inheritance = inheritance() || null;
-      consume(OTHER, "{") || error("Bodyless dictionary");
+      consume("{") || error("Bodyless dictionary");
       while (true) {
-        if (consume(OTHER, "}")) {
-          consume(OTHER, ";") || error("Missing semicolon after dictionary");
+        if (consume("}")) {
+          consume(";") || error("Missing semicolon after dictionary");
           return ret;
         }
         const ea = extended_attrs();
@@ -842,7 +856,7 @@
           member["default"] = dflt;
         }
         ret.members.push(member);
-        consume(OTHER, ";") || error("Unterminated dictionary member");
+        consume(";") || error("Unterminated dictionary member");
       }
     }
 
@@ -855,12 +869,12 @@
         name: sanitize_name(name.value, "enum"),
         values: vals
       };
-      consume(OTHER, "{") || error("No curly for enum");
+      consume("{") || error("No curly for enum");
       let value_expected = true;
       while (true) {
-        if (consume(OTHER, "}")) {
+        if (consume("}")) {
           if (!ret.values.length) error("No value in enum");
-          consume(OTHER, ";") || error("No semicolon after enum");
+          consume(";") || error("No semicolon after enum");
           return ret;
         }
         else if (!value_expected) {
@@ -871,7 +885,7 @@
         // No trivia exposure yet
         val.trivia = undefined;
         ret.values.push(val);
-        value_expected = !!consume(OTHER, ",");
+        value_expected = !!consume(",");
       }
     }
 
@@ -884,7 +898,7 @@
       const name = consume(ID) || error("No name in typedef");
       ret.name = sanitize_name(name.value, "typedef");
       current = ret;
-      consume(OTHER, ";") || error("Unterminated typedef");
+      consume(";") || error("Unterminated typedef");
       return ret;
     }
 
@@ -898,7 +912,7 @@
         };
         const imp = consume(ID) || error("Incomplete implements statement");
         ret["implements"] = imp.value;
-        consume(OTHER, ";") || error("No terminating ; for implements statement");
+        consume(";") || error("No terminating ; for implements statement");
         return ret;
       } else {
         // rollback
@@ -916,7 +930,7 @@
         };
         const imp = consume(ID) || error("Incomplete includes statement");
         ret["includes"] = imp.value;
-        consume(OTHER, ";") || error("No terminating ; for includes statement");
+        consume(";") || error("No terminating ; for includes statement");
         return ret;
       } else {
         // rollback


Test patch:
diff --git a/test/invalid/idl/spaced-negative-infinity.widl b/test/invalid/idl/spaced-negative-infinity.widl
new file mode 100644
index 0000000..3d71222
--- /dev/null
+++ b/test/invalid/idl/spaced-negative-infinity.widl
@@ -0,0 +1,3 @@
+interface X {
+    const float infinity = - Infinity;
+};
diff --git a/test/invalid/idl/spaced-variadic.widl b/test/invalid/idl/spaced-variadic.widl
new file mode 100644
index 0000000..6d77e18
--- /dev/null
+++ b/test/invalid/idl/spaced-variadic.widl
@@ -0,0 +1,3 @@
+interface X {
+    void operation(object . . . args);
+};
diff --git a/test/invalid/json/spaced-negative-infinity.json b/test/invalid/json/spaced-negative-infinity.json
new file mode 100644
index 0000000..9e5d618
--- /dev/null
+++ b/test/invalid/json/spaced-negative-infinity.json
@@ -0,0 +1,4 @@
+{
+    "message": "Got an error during or right after parsing `interface X`: No value for const",
+    "line": 2
+}
diff --git a/test/invalid/json/spaced-variadic.json b/test/invalid/json/spaced-variadic.json
new file mode 100644
index 0000000..0090abe
--- /dev/null
+++ b/test/invalid/json/spaced-variadic.json
@@ -0,0 +1,4 @@
+{
+    "message": "Got an error during or right after parsing `interface X`: Unterminated operation",
+    "line": 2
+}