jsdoc-to-markdown icon indicating copy to clipboard operation
jsdoc-to-markdown copied to clipboard

Maximum call stack size exceeded

Open jasonk opened this issue 5 years ago • 6 comments

I had a whole bunch of source files that caused jsdoc2md to throw RangeError: Maximum call stack size exceeded. when I attempted to render them. I managed to reduce the issue down to this minimal reproduction (with v 5.0.3):

Foo.js:

/** */
export class Foo {
  constructor() { }
}

template.hbs:

{{#class name="Foo"}}{{>docs}}{{/class}}

Result:

$ jsdoc2md --files ./Foo.js --template ./template.hbs
RangeError: Maximum call stack size exceeded

It doesn't appear to matter whether or not there is content in the jsdoc block, it seems like the only things required to trigger it are a doc block before the class and the class must have a constructor.

More info, if it's helpful...
$ jsdoc2md --version
5.0.3

$ jsdoc2md --files ./Foo.js --template ./template.hbs --config
{
  "files": [
    "./Foo.js"
  ],
  "template": "./template.hbs"
}

$ jsdoc2md --files ./Foo.js --template ./template.hbs --json
[
  {
    "id": "Foo",
    "longname": "Foo",
    "name": "Foo",
    "kind": "class",
    "scope": "instance",
    "memberof": "Foo",
    "meta": {
      "lineno": 1,
      "filename": "Foo.js",
      "path": "/Users/jasonk/jsdoc2md-repro"
    },
    "order": 0
  }
]

$ jsdoc2md --files ./Foo.js --template ./template.hbs --jsdoc
[
  {
    "comment": "/** */",
    "meta": {
      "range": [
        7,
        44
      ],
      "filename": "Foo.js",
      "lineno": 1,
      "columnno": 7,
      "path": "/Users/jasonk/jsdoc2md-repro",
      "code": {
        "id": "astnode100000002",
        "name": "exports.Foo",
        "type": "ClassDeclaration"
      }
    },
    "name": "Foo",
    "longname": "Foo",
    "kind": "class",
    "scope": "global",
    "undocumented": true
  },
  {
    "comment": "",
    "meta": {
      "range": [
        14,
        44
      ],
      "filename": "Foo.js",
      "lineno": 1,
      "columnno": 14,
      "path": "/Users/jasonk/jsdoc2md-repro",
      "code": {
        "id": "astnode100000003",
        "name": "Foo",
        "type": "ClassDeclaration",
        "paramnames": []
      }
    },
    "undocumented": true,
    "name": "Foo",
    "longname": "Foo",
    "kind": "class",
    "scope": "global"
  },
  {
    "comment": "",
    "meta": {
      "range": [
        26,
        42
      ],
      "filename": "Foo.js",
      "lineno": 1,
      "columnno": 26,
      "path": "/Users/jasonk/jsdoc2md-repro",
      "code": {
        "id": "astnode100000006",
        "name": "exports.Foo",
        "type": "MethodDefinition",
        "paramnames": []
      },
      "vars": {
        "": null
      }
    },
    "undocumented": true,
    "name": "Foo",
    "longname": "Foo#Foo",
    "kind": "class",
    "memberof": "Foo",
    "scope": "instance",
    "params": []
  },
  {
    "comment": "",
    "meta": {
      "range": [
        26,
        42
      ],
      "filename": "Foo.js",
      "lineno": 1,
      "columnno": 26,
      "path": "/Users/jasonk/jsdoc2md-repro",
      "code": {
        "id": "astnode100000006",
        "name": "exports.Foo",
        "type": "MethodDefinition",
        "paramnames": []
      }
    },
    "name": "Foo",
    "longname": "Foo",
    "kind": "class",
    "memberof": "Foo",
    "scope": "instance"
  },
  {
    "kind": "package",
    "longname": "package:undefined",
    "files": [
      "/Users/jasonk/jsdoc2md-repro/Foo.js"
    ]
  }
]

jasonk avatar Dec 31 '19 01:12 jasonk

After some more debugging I managed to get a stack trace. Looks like it might actually be a dmd issue:

RangeError: Maximum call stack size exceeded
    at Function.keys (<anonymous>)
    at testValue (/Users/jasonk/jsdoc2md-repro/node_modules/test-value/index.js:23:19)
    at /Users/jasonk/jsdoc2md-repro/node_modules/test-value/index.js:81:12
    at Array.filter (<anonymous>)
    at _identifiers (/Users/jasonk/jsdoc2md-repro/node_modules/dmd/helpers/ddata.js:460:38)
    at Object._children (/Users/jasonk/jsdoc2md-repro/node_modules/dmd/helpers/ddata.js:478:16)
    at /Users/jasonk/jsdoc2md-repro/node_modules/dmd/helpers/ddata.js:506:27
    at Array.forEach (<anonymous>)
    at iterate (/Users/jasonk/jsdoc2md-repro/node_modules/dmd/helpers/ddata.js:504:20)
    at /Users/jasonk/jsdoc2md-repro/node_modules/dmd/helpers/ddata.js:506:9

jasonk avatar Dec 31 '19 01:12 jasonk

I've tracked this down as far as the descendants function in dmd/helpers/ddata.js. Maybe it's because it's late and I'll figure it out tomorrow after sleeping on it, but at the moment I'm not really following what this function is trying to do. The function itself looks like this:

function descendants (options) {
  var min = typeof options.hash.min !== 'undefined' ? options.hash.min : 2
  delete options.hash.min
  options.hash.memberof = this.id
  var output = []
  function iterate (childrenList) {
    if (childrenList.length) {
      childrenList.forEach(function (child) {
        output.push(child)
        iterate(_children.call(child, options))
      })
    }
  }
  iterate(_children.call(this, options))
  if (output.length >= (min || 0)) return output
}

The problem is that the call to _children.call that is inside the iterate function is returning exactly the same thing that the one outside the iterate function is returning, so once it enters this function that iterate gets called recursively on the same object a few thousand times until it blows the stack..

jasonk avatar Dec 31 '19 03:12 jasonk

I'm not yet sure what is causing the stack issue (i haven't traced it) but as is stressed in the wiki, an ESM module must include a @module declaration at the top.

This should work.

/**
 * @module something
 */

/**
 * A description.
 */
export class Foo {
  constructor() {}
}

75lb avatar Dec 31 '19 19:12 75lb

@jasonk did @75lb suggestion work for you? I have gone through my library (that incidentally has been building jsdoc2md successfully many months) and added the suggested @module but it is still not building for me. (Throws Max Stack exception)

kodmunki avatar Mar 26 '20 18:03 kodmunki

i get the "maximum call stack size exceeded" error when i do this

/**
 * @module something
 */

/**
 * A description.
*  @alias module:something
 * @typicalname othersomething
 */
export class Foo {
  constructor() {}
}

if change the export syntax, no more error

class Foo {
  constructor() {}
}

export {Foo}

using [email protected]

brianjacobs-natgeo avatar Aug 25 '20 15:08 brianjacobs-natgeo

I hastily refactored the function yesterday, avoiding the recursion, which fixes the issue here:

/**
return a flat list containing all decendants
@param [sortBy] {string} - "kind"
@param [min] {number} - only returns if there are `min` children
@this {identifier}
@returns {identifier[]}
@static
*/
function descendants (options) {
    var min = typeof options.hash.min !== 'undefined' ? options.hash.min : 2
    delete options.hash.min
    options.hash.memberof = this.id
    var output = []
    var newList = [this];
    while(newList.length) {
        const oldList = newList;
        newList = [];
        for(let el of oldList) {
            for(let c of _children.call(el, options)) {
                if(c.id === this.id) continue;
                output.push(c);
                newList.push(c)
            }
        }
    }
    if (output.length >= (min || 0)) return output
}

Only to hit the same issue at another point in code, though -- probably sig() in ddata.js

In case it helps: when removing export, the issue is avoided.

Squareys avatar Jun 08 '21 16:06 Squareys