jsdoc2md / dmd

The default output template for jsdoc2md
MIT License
39 stars 49 forks source link

Maximum call stack size exceeded #89

Closed jasonk closed 2 months ago

jasonk commented 4 years ago

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... ```sh $ 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 commented 4 years ago

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 commented 4 years ago

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..

75lb commented 4 years ago

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() {}
}
kodmunki commented 4 years ago

@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)

brianjacobs-natgeo commented 4 years ago

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 jsdoc-to-markdown@6.0.1

Squareys commented 3 years ago

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.

75lb commented 2 months ago

Reproduced - thanks for the reproduction case @jasonk.. It's due to this doclet's id being equal to its memberof value in the jsdoc2md --json output (which comes from jsdoc via jsdoc-parse)..

  {
    "id": "Foo",
    ...
    "memberof": "Foo",
    ...
  }

It's at least 10 years since I wrote this app but from memory, the raw jsdoc output is flat, just an array of doclet metadata objects which have no structure.. this was not useful for feeding into a template engine like Handlebars where the output documentation was required to have structure (e.g. module -> exported class -> public/private properties/methods etc).. So the jsdoc-parse module was created to transform jsdoc output into something with structure, where the tree structure is represented using generated, unique doclet id values and memberof properties pointing to the unique doclet id they are a member of..

So, in this case the code encounters doclet id: Foo then sets about searching for its children (doclets that are memberof: Foo).. A child is found (id: Foo), so the code checks whether this child has children (doclets with memberof: Foo) - it does.. itself..

You get the idea - recursive.. Anyway, I'll implement a fix then post again later.

75lb commented 2 months ago

A fix for this is implmemented and will be in the next version.. will post again once that goes live.. In the meantime, you can test the prerelease:

npm install jsdoc-to-markdown@next