Repository: eslint/doctrine
Pull request #173 (2016-09-02 01:39:21)

Update: Add support for BooleanLiteralType
Both Flow and TypeScript (2.0) support boolean literals as types.

These are mostly used with unions as in:

```js
type Result = {
  ok: true,
  value: string,
} | {
  ok: false,
}
```

jquerybot: Thank you for your pull request. It looks like this may be your first contribution to a jQuery Foundation project, if so we need you to sign our Contributor License Agreement (CLA).

__:memo: Please visit http://contribute.jquery.org/CLA/ to sign.__

After you signed, the PR is checked again automatically after a minute. If there's still an issue, please reply here to let us know.

---

If you've already signed our CLA, it's possible your git author information doesn't match your CLA signature (both your name and email have to match), for more information, [check the status of your CLA check](http://contribute.jquery.org/CLA/status/?owner=eslint&repo=doctrine&sha=d07115f4cac51f720fab5b9c4e2f24371dd4f2d1).

arv: I signed the CLA yesterday...
nzakas: I'm sorry, I don't understand. How does this relate to JSDoc syntax?
arv: It is related to the type expression language that jsdoc uses. For example:

```js
/**
 * @return {true}
 */
function alwaysTrue() {
  return true;
}
```

Silly example but when you combine it with `@typedef` and more complex types it is useful.

For some background: 
https://flowtype.org/docs/dynamic-type-tests.html#tagged-unions
https://blogs.msdn.microsoft.com/typescript/2016/08/30/announcing-typescript-2-0-rc/

nzakas: I'm sorry, I'm having a hard time understanding. Why wouldn't you use `boolean` there? I don't see anything in those links that talks about JSDoc supporting this.

This utility is used by a lot of people, and I'm afraid can't stray from JSDoc expectations without some good justification or evidence that JSDoc supports what you're proposing here. 
arv: Let me try to justify this:

- The type system for JSDoc should be able to express the type systems of Closure Compiler, TypeScript and Flow.
- Both TypeScript and Flow supports boolean literal types.
- Doctrine already support NumericLiteralType and StringLiteralType... it is just missing the boolean equivalent. #172 added support for numeric and string literal types. You (@nzakas) then closed #156 without boolean support. This PR adds the missing boolean support.
nzakas: @arv https://github.com/eslint/doctrine/issues/156 explained the use case and how JSDoc supports the syntax they were proposing. In my last comment, I asked for information showing how JSDoc works with this change. This is a JSDoc parsing tool, so the justification needs to be about JSDoc, not Flow or TypeScript. 

If you'd like to provide an example JSDoc comment that makes use of this change, and explain how JSDoc handles using boolean literals instead of `boolean`, I'll be happy to evaluate this further. 
arv: Ok. Here is is a less contrived example:

```js
/**
 * @param {{ok: true, data: string} | {ok: false, error: Error}} result
 */
function handleResult(result) {
  if (result.ok) {
    print(result.data);
  } else {
    throw result.error;
  }
}
```
nzakas: And does that work with JSDoc without error?

If yes, please add that code in a unit test.
arv: @nzakas Thanks for getting back to me on this. I've updated the PR with a unit test that uses that complex type.
arv: What is the next step here?
nzakas: Sorry, lost track of this. It looks good. I'll merge it and cut a new release.

Patch:
diff --git a/lib/typed.js b/lib/typed.js
index a4c282f..f0d759b 100644
--- a/lib/typed.js
+++ b/lib/typed.js
@@ -43,7 +43,8 @@
         NameExpression: 'NameExpression',
         TypeApplication: 'TypeApplication',
         StringLiteralType: 'StringLiteralType',
-        NumericLiteralType: 'NumericLiteralType'
+        NumericLiteralType: 'NumericLiteralType',
+        BooleanLiteralType: 'BooleanLiteralType'
     };
 
     Token = {
@@ -883,6 +884,14 @@
                 };
             }
 
+            if (value === 'true' || value === 'false') {
+                consume(Token.NAME);
+                return {
+                    type: Syntax.BooleanLiteralType,
+                    value: value === 'true'
+                };
+            }
+
             context = Context.save();
             if (value === 'function') {
                 try {
@@ -1248,6 +1257,10 @@
             result = String(node.value);
             break;
 
+        case Syntax.BooleanLiteralType:
+            result = String(node.value);
+            break;
+
         default:
             utility.throwError('Unknown type ' + node.type);
         }


Test patch:
diff --git a/test/parse.js b/test/parse.js
index 832af76..abcde95 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -1264,6 +1264,74 @@ describe('parse', function () {
         res.tags[1].type.elements.should.containEql({type: 'StringLiteralType', value: 'private'});
         res.tags[1].type.elements.should.containEql({type: 'StringLiteralType', value: 'protected'});
     });
+
+    it('numeric literal property', function () {
+        var res = doctrine.parse(
+            [
+                "/**",
+                " * @typedef {Object} comment",
+                " * @property {(-42|1.5|0)} access",
+                "*/"
+            ].join('\n'), { unwrap: true });
+
+        res.tags.should.have.length(2);
+        res.tags[1].should.have.property('title', 'property');
+        res.tags[1].should.have.property('name', 'access');
+        res.tags[1].type.should.have.property('type', 'UnionType');
+        res.tags[1].type.elements.should.have.length(3);
+        res.tags[1].type.elements.should.containEql({type: 'NumericLiteralType', value: -42});
+        res.tags[1].type.elements.should.containEql({type: 'NumericLiteralType', value: 1.5});
+        res.tags[1].type.elements.should.containEql({type: 'NumericLiteralType', value: 0});
+    });
+
+    it('boolean literal property', function () {
+        var res = doctrine.parse(
+            [
+                "/**",
+                " * @typedef {Object} comment",
+                " * @property {(true|false)} access",
+                "*/"
+            ].join('\n'), { unwrap: true });
+
+        res.tags.should.have.length(2);
+        res.tags[1].should.have.property('title', 'property');
+        res.tags[1].should.have.property('name', 'access');
+        res.tags[1].type.should.have.property('type', 'UnionType');
+        res.tags[1].type.elements.should.have.length(2);
+        res.tags[1].type.elements.should.containEql({type: 'BooleanLiteralType', value: true});
+        res.tags[1].type.elements.should.containEql({type: 'BooleanLiteralType', value: false});
+    });
+
+    it('complex union with literal types', function () {
+        var res = doctrine.parse(
+            [
+                "/**",
+                " * @typedef {({ok: true, data: string} | {ok: false, error: Error})} Result",
+                "*/"
+            ].join('\n'), { unwrap: true });
+
+        res.tags.should.have.length(1);
+        res.tags[0].should.have.property('title', 'typedef');
+        res.tags[0].should.have.property('name', 'Result');
+        res.tags[0].type.should.have.property('type', 'UnionType');
+        res.tags[0].type.elements.should.have.length(2);
+
+        var e0 = res.tags[0].type.elements[0];
+        e0.should.have.property('type', 'RecordType');
+        e0.fields.should.have.length(2);
+        e0.fields.should.containEql({type: 'FieldType', key: 'ok',
+          value: {type: 'BooleanLiteralType', value: true}});
+        e0.fields.should.containEql({type: 'FieldType', key: 'data',
+          value: {type: 'NameExpression', name: 'string'}});
+
+        var e1 = res.tags[0].type.elements[1];
+        e1.should.have.property('type', 'RecordType');
+        e1.fields.should.have.length(2);
+        e1.fields.should.containEql({type: 'FieldType', key: 'ok',
+          value: {type: 'BooleanLiteralType', value: false}});
+        e1.fields.should.containEql({type: 'FieldType', key: 'error',
+          value: {type: 'NameExpression', name: 'Error'}});
+    });
 });
 
 describe('parseType', function () {
@@ -1804,6 +1872,20 @@ describe('parseType', function () {
         });
     });
 
+    it('boolean literal type', function () {
+        var type;
+        type = doctrine.parseType('true');
+        type.should.eql({
+            type: 'BooleanLiteralType',
+            value: true
+        });
+        type = doctrine.parseType('false');
+        type.should.eql({
+            type: 'BooleanLiteralType',
+            value: false
+        });
+    });
+
     it('illegal tokens', function () {
         doctrine.parseType.bind(doctrine, ".").should.throw('unexpected token');
         doctrine.parseType.bind(doctrine, ".d").should.throw('unexpected token');
@@ -2678,6 +2760,7 @@ describe('exported Syntax', function() {
             VoidLiteral: 'VoidLiteral',
             UnionType: 'UnionType',
             ArrayType: 'ArrayType',
+            BooleanLiteralType: 'BooleanLiteralType',
             RecordType: 'RecordType',
             FieldType: 'FieldType',
             FunctionType: 'FunctionType',
diff --git a/test/stringify.js b/test/stringify.js
index 2531458..f59bb2b 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -117,9 +117,20 @@ describe('literals', function() {
     it('NumericLiteralType', function () {
         doctrine.type.stringify({
             type: doctrine.Syntax.NumericLiteralType,
-            value: '-142.35'
+            value: -142.35
         }).should.equal('-142.35');
     });
+
+    it('BooleanLiteralType', function () {
+        doctrine.type.stringify({
+            type: doctrine.Syntax.BooleanLiteralType,
+            value: true
+        }).should.equal('true');
+        doctrine.type.stringify({
+            type: doctrine.Syntax.BooleanLiteralType,
+            value: false
+        }).should.equal('false');
+    });
 });
 
 describe('Expression', function () {