Get JSDoc to correctly document nested closures
Asked Answered
K

1

6

I have a large, well-structured JavaScript object that is something like this:

/**
 * My 'class' begins here.  JSDoc barfs on this.
 */
var MyClass = (function(window, document, $) {

    return function(options) {

        "use strict";

        var
            x = 123,

            /**
             * This is a "public" function.
             */
            myFunc = function() { /*...*/ },

            /**
             * This is some other "private" function.
             */
            otherFunc = function() { /*...*/ };

        return {
            myFunc: myFunc
        };
    };

})(window, document, window.jQuery);

In effect, an outermost closure is used to control the global scope seen by an inner closure that implements a form of the module pattern: The inner function is effectively a constructor, and its nested functions are effectively methods, and the returned object is effectively a list of what parts of it are "public".

JSDoc hates this.

(For the record, I'm using JSDoc 3.4.)

I've tried several variants on @lend and @export and @namespace and @memberof in lots of places in this code, but no matter what I do, JSDoc refuses to notice any of the inner functions at all — it doesn't just "not associate them to the right things;" it doesn't include them at all in any of the output files.

This is an excellent pattern for tightly encapsulating classic JavaScript, not just encapsulating its internals but its dependencies too, and our real-world application uses this pattern in many many thousands of lines of code. So our JavaScript itself isn't going to change structure any time soon: Only the comments can reasonably be altered.

I'd like to be able to use JSDoc to document this application, but JSDoc fails badly on this no matter what I give it. Poking around on StackOverflow has identified ways to deal with various well-known module systems, but I haven't found solid answers for plain-jane JavaScript that uses multiply-nested closures.

So does anyone have a way to make JSDoc correctly generate MyClass as a "class-like" construct with the functions deeply nested inside it as "methods" of that "class"? I'm okay with having to tag otherFunc as @private or something to mark it as hidden, or having to tag myFunc as @public or something to mark it as visible, but no matter what I try, JSDoc doesn't seem to want to document any of those nested functions at all.

Kramlich answered 31/8, 2016 at 17:34 Comment(1)
I don't know JSDoc very well, but in cases like this you may want to consider typedef.Backbencher
P
2

I know this is a necro, but there really is not any good documentation on this, so here it goes. It took me a solid two days of reading other people's code and experimenting to figure this out. This is using 3.6.7

Basically:

  1. You have to use namespaces, but JSDoc does not like to have namespaces with the same names as functions, so you have to hide the fact that the namespaces are the same by preceding all the function namespaces with a tilde. ~ The tildes themselves will not show up in the documentation, but if you don't have them JSDoc will get confused. This is not really documented anywhere I could find, and I extrapolated it from the way JSDoc handles return values.

  2. You need to have a "root" namespace based on an actual attribute. const ClosureDoc = {version: "1.0.0"}; in the code below, underneath a /** @namespace */ callout. Attempting to drop the actual object and just have /** @namespace ClosureDoc */ does NOT work. No idea why, and this is not documented anywhere.

  3. You need to explicitly name all inner functions by hand. All return values that are functions also have to be explicitly defined by hand, but you don't actually need to @typedef if you use * @returns {outerScope~innerScope} (note the tilde)

All in all this seems to be a ridiculous amount of hand-holding to get a very simple result for very reasonable code, but it can be done.

`

'use strict';
/** @namespace */
const ClosureDoc = {version: "1.0.0"};

/**
 * Outer scope is used to enclose the inner scope
 * @function outerScope
 * @returns {outerScope~innerScope}
 * @returns {outerScope~logInnerMap}
 * @memberof ClosureDoc
 */
function outerScope () {
    /**@namespace ClosureDoc.~outerScope */
    const outerState = {
        innerMap: {}
    }

    const newInnerScope = (name) => (outerState.innerMap[name] = innerScope(name));
    const namedInnerScope = (name) => !(name in outerState.innerMap) ? newInnerScope(name) : outerState.innerMap[name];

    /**
     * Inner Scope is used inside the outer scope
     * @function innerScope
     * @param {string} name
     * @returns {innerScope~logScopeName}
     * @memberof ClosureDoc.~outerScope
     */
    function innerScope(name) {
        /**@namespace ClosureDoc.~outerScope.~innerScope */
        /**
         * Logs the name passed to the inner scope
         * @function logScopeName
         * @memberof ClosureDoc.~outerScope.~innerScope
         */
        function logScopeName() {
            console.log(name);
        }
        return {
            logScopeName
        }
    } 
    /**
     * Logs the map of all scope names
     * @function logInnerMap
     * @memberof ClosureDoc.~outerScope
     */
    function logInnerMap() {
        console.log(outerState.innerMap);
    }
   return {
       namedInnerScope,
       logInnerMap
    }
}

const outer = outerScope();
const inner_1 = outer.namedInnerScope("Bob");
const inner_2 = outer.namedInnerScope("Dole");
inner_1.logScopeName();
inner_2.logScopeName();
outer.logInnerMap();

`

Pelargonium answered 6/9, 2021 at 10:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.