Repository: eslint/doctrine
Pull request #174 (2016-09-07 22:37:42)

Update: add ability to parse optional properties in typedefs (refs #5)
This is rebased to master pull request https://github.com/eslint/doctrine/pull/162. Unrelated changes in `CHANGELOG.md` and `package.json` are removed.


nzakas: Overall looks good, just one question (left inline).

Patch:
diff --git a/lib/doctrine.js b/lib/doctrine.js
index d9a4081..095eeeb 100644
--- a/lib/doctrine.js
+++ b/lib/doctrine.js
@@ -72,6 +72,10 @@
         return isProperty(title) || isParamTitle(title);
     }
 
+    function isAllowedOptional(title) {
+        return isProperty(title) || isParamTitle(title);
+    }
+
     function isTypeParameterRequired(title) {
         return isParamTitle(title) || isReturnTitle(title) ||
             title === 'define' || title === 'enum' ||
@@ -255,9 +259,10 @@
                 return utility.throwError('Braces are not balanced');
             }
 
-            if (isParamTitle(title)) {
+            if (isAllowedOptional(title)) {
                 return typed.parseParamType(type);
             }
+
             return typed.parseType(type);
         }
 
@@ -284,6 +289,7 @@
                 useBrackets,
                 insideString;
 
+
             skipWhiteSpace(last);
 
             if (index >= last) {
@@ -469,7 +475,7 @@
 
         TagParser.prototype._parseNamePath = function (optional) {
             var name;
-            name = parseName(this._last, sloppy && isParamTitle(this._title), true);
+            name = parseName(this._last, sloppy && isAllowedOptional(this._title), true);
             if (!name) {
                 if (!optional) {
                     if (!this.addError('Missing or invalid tag name')) {
@@ -495,7 +501,7 @@
 
             // param, property requires name
             if (isAllowedName(this._title)) {
-                this._tag.name = parseName(this._last, sloppy && isParamTitle(this._title), isAllowedNested(this._title));
+                this._tag.name = parseName(this._last, sloppy && isAllowedOptional(this._title), isAllowedNested(this._title));
                 if (!this._tag.name) {
                     if (!isNameParameterRequired(this._title)) {
                         return true;
@@ -535,6 +541,7 @@
                 }
             }
 
+
             return true;
         };
 
@@ -647,7 +654,7 @@
 
             description = this._tag.description;
             // un-fix potentially sloppy declaration
-            if (isParamTitle(this._title) && !this._tag.type && description && description.charAt(0) === '[') {
+            if (isAllowedOptional(this._title) && !this._tag.type && description && description.charAt(0) === '[') {
                 this._tag.type = this._extra.name;
                 if (!this._tag.name) {
                     this._tag.name = undefined;
@@ -739,6 +746,7 @@
         TagParser.prototype.parse = function parse() {
             var i, iz, sequences, method;
 
+
             // empty title
             if (!this._title) {
                 if (!this.addError('Missing or invalid title')) {
@@ -785,6 +793,7 @@
             while (index < parser._last) {
                 advance();
             }
+
             return tag;
         }
 


Test patch:
diff --git a/test/parse.js b/test/parse.js
index 438ed6d..832af76 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -866,6 +866,24 @@ describe('parse', function () {
         res.tags.should.have.length(0);
     });
 
+
+    it('property with optional type', function() {
+        var res = doctrine.parse(
+            ["/**",
+                "* testtypedef",
+                "* @typedef {object} abc",
+                "* @property {String} [val] value description",
+                "*/"].join('\n'), {
+                unwrap: true,
+                sloppy: true
+            });
+
+        res.tags[1].should.have.property('title', 'property');
+        res.tags[1].should.have.property('type');
+        res.tags[1]['type'].should.have.property('type', "OptionalType");
+    });
+
+
     it('property with nested name', function () {
         var res = doctrine.parse(
             [
@@ -2227,6 +2245,228 @@ describe('optional params', function() {
     });
 });
 
+describe('optional properties', function() {
+
+    // should fail since sloppy option not set
+    it('failure 0', function() {
+        doctrine.parse(
+            [   "/**",
+                " * @property {String} [val] some description",
+                " */"].join('\n'), {
+                unwrap: true
+            }).should.eql({
+                "description": "",
+                "tags": []
+            });
+    });
+
+    it('failure 1', function() {
+        doctrine.parse(
+            ["/**", " * @property [val", " */"].join('\n'), {
+                unwrap: true, sloppy: true
+            }).should.eql({
+                "description": "",
+                "tags": []
+            });
+    });
+
+    it('success 1', function() {
+        doctrine.parse(
+            ["/**", " * @property {String} [val]", " */"].join('\n'), {
+                unwrap: true, sloppy: true
+            }).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": null,
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val"
+                }]
+            });
+    });
+
+    it('success 2', function() {
+        doctrine.parse(
+            ["/**", " * @property {String=} val", " */"].join('\n'), {
+                unwrap: true, sloppy: true
+            }).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": null,
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val"
+                }]
+            });
+    });
+
+    it('success 3', function() {
+        doctrine.parse(
+            ["/**", " * @property {String=} [val=abc] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "abc"
+                }]
+            });
+    });
+
+    it('success 4', function() {
+        doctrine.parse(
+            ["/**", " * @property {String=} [val = abc] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "abc"
+                }]
+            });
+    });
+
+    it('default string', function() {
+        doctrine.parse(
+            ["/**", " * @property {String} [val=\"foo\"] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "\"foo\""
+                }]
+            });
+    });
+
+    it('default string surrounded by whitespace', function() {
+        doctrine.parse(
+            ["/**", " * @property {String} [val=   'foo'  ] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "'foo'"
+                }]
+            });
+    });
+
+    it('should preserve whitespace in default string', function() {
+        doctrine.parse(
+            ["/**", " * @property {String} [val=   \"   foo\"  ] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "\"   foo\""
+                }]
+            });
+    });
+
+    it('default array', function() {
+        doctrine.parse(
+            ["/**", " * @property {String} [val=['foo']] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "['foo']"
+                }]
+            });
+    });
+
+    it('default array within white spaces', function() {
+        doctrine.parse(
+            ["/**", " * @property {String} [val = [ 'foo' ]] some description", " */"].join('\n'),
+            { unwrap: true, sloppy: true}
+        ).should.eql({
+                "description": "",
+                "tags": [{
+                    "title": "property",
+                    "description": "some description",
+                    "type": {
+                        "type": "OptionalType",
+                        "expression": {
+                            "type": "NameExpression",
+                            "name": "String"
+                        }
+                    },
+                    "name": "val",
+                    "default": "['foo']"
+                }]
+            });
+    });
+});
+
 describe('recovery tests', function() {
 	it ('params 2', function () {
 		var res = doctrine.parse(