Repository: w3c/webidl2.js
Pull request #176 (2018-05-25 06:30:39)

Add probe() function
This PR introduces `probe()` function which checks whether the next unconsumed token has the target type.

Currently checking excessive tokens requires using `consume()`:

```js
if (consume("?")) error("Can't nullable more than once");
```

Our `error()` method assumes that the error is from the first unconsumed token so it potentially throws with incorrect line number. The error message from the following example says it's from line 3 because the first unconsumed token is there.

```webidl
interface X {
  attribute object?? // error is from line 2, but webidl2.js throws with line 3
    attributeName;
};
```

`probe()` will fix this by checking the next token without consuming it:

```js
if (probe("?")) error("Can't nullable more than once");
```


Patch:
diff --git a/lib/webidl2.js b/lib/webidl2.js
index 81f9fc1..0f91ee6 100644
--- a/lib/webidl2.js
+++ b/lib/webidl2.js
@@ -221,10 +221,14 @@
 
     let last_token = null;
 
+    function probe(type) {
+      return tokens.length > 0 && tokens[0].type === type;
+    }
+
     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 (!probe(type)) continue;
         if (typeof value === "undefined" || tokens[0].value === value) {
           last_token = tokens.shift();
           line += count(last_token.trivia, "\n");
@@ -294,12 +298,8 @@
     }
 
     function type_suffix(obj) {
-      while (true) {
-        if (consume("?")) {
-          if (obj.nullable) error("Can't nullable more than once");
-          obj.nullable = true;
-        } else return;
-      }
+      obj.nullable = !!consume("?");
+      if (probe("?")) error("Can't nullable more than once");
     }
 
     function single_type(typeName) {
@@ -440,10 +440,7 @@
       };
       const eq = consume("=");
       if (eq) {
-        ret.rhs = consume(ID) ||
-          consume(FLOAT) ||
-          consume(INT) ||
-          consume(STR);
+        ret.rhs = consume(ID, FLOAT, INT, STR);
         if (ret.rhs) {
           // No trivia exposure yet
           ret.rhs.trivia = undefined;
@@ -507,9 +504,7 @@
         typ = typ.value;
       }
       ret.idlType = Object.assign({ type: "const-type" }, EMPTY_IDLTYPE, { idlType: typ });
-      if (consume("?")) {
-        ret.nullable = true;
-      }
+      type_suffix(ret);
       const name = consume(ID) || error("No name for const");
       ret.name = name.value;
       consume("=") || error("No value assignment for const");
@@ -574,7 +569,7 @@
       if (consume("readonly")) {
         ret.readonly = true;
         grabbed.push(last_token);
-      } else if (readonly && tokens[0] && tokens[0].type === "attribute") {
+      } else if (readonly && probe("attribute")) {
         error("Attributes must be readonly in this context");
       }
       const rest = attribute_rest(ret);


Test patch:
diff --git a/test/invalid/idl/nonnullableobjects.widl b/test/invalid/idl/nonnullableobjects.widl
index 1deac5a..6c875ff 100644
--- a/test/invalid/idl/nonnullableobjects.widl
+++ b/test/invalid/idl/nonnullableobjects.widl
@@ -1,5 +1,6 @@
 interface Foo {};
 
 interface NonNullable {
-  attribute Foo?? foo;
-};
\ No newline at end of file
+  attribute Foo??
+    foo;
+};
diff --git a/test/invalid/json/nonnullableobjects.json b/test/invalid/json/nonnullableobjects.json
index 52bd8da..d470ec9 100644
--- a/test/invalid/json/nonnullableobjects.json
+++ b/test/invalid/json/nonnullableobjects.json
@@ -1,4 +1,4 @@
 {
-    "message":  "Got an error during or right after parsing `interface NonNullable`: Can't nullable more than once"
-,   "line":     4
+    "message": "Got an error during or right after parsing `interface NonNullable`: Can't nullable more than once",
+    "line": 4
 }