Repository: eslint/doctrine (js)

Issue:
Permitting numeric keys in parameter names
**What version are you using?**

2.0.0

**What JSDoc comment were you trying to parse?**

```js
/**
 * This tests our support for iterator rest inside an
 * iterator destructure (RestElement)
 *
 * @param {Array} input
 * @param {any} input.0 head of iterator
 * @param {any[]} ...input.xs body of iterator
 *
 * @returns {any[]} rotated such that the last element was the first
 */
```

**What Doctrine options were you using (sloppy, unwrap, etc.)?**

```js
var result = doctrine.parse(comment, {
    // have doctrine itself remove the comment asterisks from content
    unwrap: true,
    // enable parsing of optional parameters in brackets, JSDoc3 style
    sloppy: true,
    // `recoverable: true` is the only way to get error information out
    recoverable: true,
    // include line numbers
    lineNumbers: true
  });
```

**What happened? Include output if possible.**

The parsed tag for `input.0` is

```js
        {
            "title": "param",
            "description": "0 head of iterator",
            "type": {
                "type": "NameExpression",
                "name": "any"
            },
            "name": "input.null"
        },
```

Note that `input.0` is transformed to `input.null` because of [this clause in the parameter name parser](https://github.com/eslint/doctrine/blob/46c600f27f54b3ab6b0b8a9ac9f97c807ffa95ef/lib/doctrine.js#L304-L306) which ensures that parts of parameter names are either `[]` or valid JavaScript identifiers, and `0` is not a valid identifier.

**What did you expect to happen?**

I expected `input.0` to be parsed verbatim, with `0` serving as an array index. I can confirm that JSDoc's allows this syntax:

![image](https://cloud.githubusercontent.com/assets/32314/25157993/1482ff70-2473-11e7-9c13-8da7bfe98850.png)

---

Happy to provide a PR allowing for this syntax - I'm not sure if it'll be removing the `isIdentifierStartES5` code or if there's something else required to avoid regressions.

Pull request description:
fix: Allow array indexes in names
Fixes #192 by allowing numeric identifiers in `scanIdentifier`

Patch:
diff --git a/lib/doctrine.js b/lib/doctrine.js
index 095eeeb..263d7d6 100644
--- a/lib/doctrine.js
+++ b/lib/doctrine.js
@@ -10,13 +10,11 @@
 
     var typed,
         utility,
-        isArray,
         jsdoc,
         esutils,
         hasOwnProperty;
 
     esutils = require('esutils');
-    isArray = require('isarray');
     typed = require('./typed');
     utility = require('./utility');
 
@@ -268,7 +266,7 @@
 
         function scanIdentifier(last) {
             var identifier;
-            if (!esutils.code.isIdentifierStartES5(source.charCodeAt(index))) {
+            if (!esutils.code.isIdentifierStartES5(source.charCodeAt(index)) && !source[index].match(/[0-9]/)) {
                 return null;
             }
             identifier = advance();
@@ -296,13 +294,13 @@
                 return null;
             }
 
-            if (allowBrackets && source.charCodeAt(index) === 0x5B  /* '[' */) {
-                useBrackets = true;
-                name = advance();
-            }
-
-            if (!esutils.code.isIdentifierStartES5(source.charCodeAt(index))) {
-                return null;
+            if (source.charCodeAt(index) === 0x5B  /* '[' */) {
+                if (allowBrackets) {
+                    useBrackets = true;
+                    name = advance();
+                } else {
+                    return null;
+                }
             }
 
             name += scanIdentifier(last);
@@ -525,8 +523,8 @@
                         // extract the default value if there is one
                         // example: @param {string} [somebody=John Doe] description
                         assign = name.substring(1, name.length - 1).split('=');
-                        if (assign[1]) {
-                            this._tag['default'] = assign[1];
+                        if (assign.length > 1) {
+                            this._tag['default'] = assign.slice(1).join('=');
                         }
                         this._tag.name = assign[0];
 
@@ -839,7 +837,7 @@
 
             // array of relevant tags
             if (options.tags) {
-                if (isArray(options.tags)) {
+                if (Array.isArray(options.tags)) {
                     interestingTags = { };
                     for (i = 0, iz = options.tags.length; i < iz; i++) {
                         if (typeof options.tags[i] === 'string') {
diff --git a/package.json b/package.json
index 4616b44..71eec5c 100644
--- a/package.json
+++ b/package.json
@@ -54,7 +54,6 @@
     "betarelease": "eslint-prerelease beta"
   },
   "dependencies": {
-    "esutils": "^2.0.2",
-    "isarray": "^1.0.0"
+    "esutils": "^2.0.2"
   }
 }


Test patch:
diff --git a/test/parse.js b/test/parse.js
index abcde95..008acef 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -498,6 +498,41 @@ describe('parse', function () {
         });
     });
 
+    it('param with array indexes', function () {
+        var res = doctrine.parse(
+            [
+                "/**",
+                " * @param {String} user.0",
+                "*/"
+            ].join('\n'), { unwrap: true });
+        res.tags.should.have.length(1);
+        res.tags[0].should.have.property('title', 'param');
+        res.tags[0].should.have.property('name', 'user.0');
+        res.tags[0].should.have.property('type');
+        res.tags[0].type.should.eql({
+            type: 'NameExpression',
+            name: 'String'
+        });
+    });
+
+    it('param with array indexes and descriptions', function () {
+        var res = doctrine.parse(
+            [
+                "/**",
+                " * @param {String} user.0 The first element",
+                "*/"
+            ].join('\n'), { unwrap: true });
+        res.tags.should.have.length(1);
+        res.tags[0].should.have.property('title', 'param');
+        res.tags[0].should.have.property('name', 'user.0');
+        res.tags[0].should.have.property('type');
+        res.tags[0].should.have.property('description', 'The first element');
+        res.tags[0].type.should.eql({
+            type: 'NameExpression',
+            name: 'String'
+        });
+    });
+
     it('arg with properties', function () {
         var res = doctrine.parse(
             [
@@ -2271,6 +2306,28 @@ describe('optional params', function() {
         });
     });
 
+    it('default arrow functions', function() {
+        doctrine.parse(
+            ["/**", " * @param {Function} [fn=()=>{}] some description", " */"].join('\n'),
+            {unwrap: true, sloppy: true}
+        ).should.eql({
+            description: "",
+            tags: [{
+                "title": "param",
+                "description": "some description",
+                "type": {
+                    "type": "OptionalType",
+                    "expression": {
+                        "type": "NameExpression",
+                        "name": "Function"
+                    }
+                },
+                "name": "fn",
+                "default": "()=>{}"
+            }]
+        });
+    });
+
     it('line numbers', function() {
         var res = doctrine.parse(
             [