Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to document dynamically generated methods? #31

Open
VividVisions opened this issue Dec 28, 2023 · 5 comments
Open

How to document dynamically generated methods? #31

VividVisions opened this issue Dec 28, 2023 · 5 comments
Assignees
Labels
future Will be fixed/implemented in future version of ESDoc

Comments

@VividVisions
Copy link

VividVisions commented Dec 28, 2023

Since there is no @function tag, there's currently no way to document dynamically added methods. Or have I missed something?

@EnterTheNameHere
Copy link
Owner

Can you provide a minimal example code? It would help to check how the parser processes it.

@VividVisions
Copy link
Author

Here you are:

/**
 * Test class
 */
class Test {
   /**
    * Foo function.
    */
   foo(str) {
      return str + '!';
   }
}

['bar', 'baz'].forEach(name => {
   Test.prototype[name] = function()  {
      return this.foo(name);
   }
});

// With JSDoc or other tools, I would've documented the functions like this:
// (Maybe with a @memberof, when the comments are outside of the class.)

/**
 * @name #bar
 * @function
 * @description ...
 * @returns ...
 */

/**
 * @name #baz
 * @function
 * @description ...
 * @returns ...
 */

@EnterTheNameHere
Copy link
Owner

So I did some research into what can be done, and to not let you wait for too long, this is what I found:

@function tag functionality is not at all implemented. Simple hacking it in quickly is not easy task as there is no posibility to change code here and there and it would fit in as a piece of puzzle called ESDoc.

Unfortunately ESDoc is old, from ES5- era, and it shows. My fork updates dependencies, replaces unsupported packages, puts all in single monorepo and allows use of modern JS, If you have project, which used ESDoc, you can replace old ESDoc with my fork and it "should work"™.

If you are looking for proper documentation generator, I recommend checking other tools and not waste your precious time with ESDoc.

I have plans how to make ESDoc better, but I won't lie, I'm slow and few features I work on are in various states of completion and I'm not ready to trust them for retail use...

That's TL;DR, I'm sorry I don't have a solution for you.

I'm including some more info on why it's not quick hacking to implement @function - you don't need to continue reading, I'm leaving it here as notes which should be left somewhere...


ESDoc is abandoned, so I forked it, updated dependencies, replaced unsupported dependencies, added some new functionality here and there... The whole process of how ESDoc works can be divided into three places (phases):

  • (phase 1) taking a file, parsing it to create AST, traversing AST, and for detected known patterns:
  • (phase 2) create individual *Doc entries (ClassDoc, MemberDoc, MethodDoc, FunctionDoc), taking the detected AST node, extracting data and read comments; once all entries are detected;
  • (phase 3) send those *Docs to HTML renderer, which will, based on individual *Docs, create the presentation;

(phase 1) and (phase 2) are done by core ESDoc, (phase 3) is done by publish-html-plugin

So we take file, get AST tree from it, and we traverse this tree, trying to recognize known patters - this is class declaration, this is member declaration, this is function declaration, this is exported etc.

For each recognized pattern a *Doc entry (ClassDoc, MemberDoc, FunctionDoc) is created. It will receive AST node, AST tree and extracted tags as parameters to it.

This is where first obstacle is found, "extracted tags":
Obviously JSDoc is in comments. Comments are attached as leadingComment or trailingComment to each AST node. ESDoc then in (phase 1) splits comment's text by @ character and array of slightly edited text (sanitization, trimming) is passed to *Doc.

There is no JSDoc parsing/lexing done. ESDoc just looks if there is @export or @external or typedef in comments, otherwise it cuts AST nodes and let *Doc's do their own work. That's all what (phase 1) does.

  • no JSDoc parsing/lexing;
  • comments are split by @ to array *Doc gets as a parameter;

So *Doc is created, having AST node, AST tree and extracted tags to work with. This is where (phase 2) is done - *Doc decides what is the name of entry, what longname should be, access privacy (private, protected, public), is symbol it exported, what type is it, does it have author, version, since, does it have description etc.

  • there is no reference to parent *Doc entry in case we are method or member; the *Doc knows only what it can extract from AST node or AST tree, instead of asking parent for that data; context is lost;
  • there is no reference to other *Doc entries in database; we cannot query for more interesting data, we cannot know if parent exists or we should create it (can we even create it?);
  • tags are in array which is simple text; no lexing or further analysis can be easily done; if we need anything, we have to again go back to AST node/tree and look ourselves, probably do work which was already done;

Let's talk more about comments. AST node has comments attached as leadingComments and trailingComments:

/**
  * Leading comment
  */
{
    // Block AST node
}
/**
  * Trailing comment
  */

In (phase 1) comments are checked for @external, @export and typedef, as these can stand alone - not connected to any code AST node. They are treated as to be "virtual", because they do not exist - but not for documentation entry - they are used for type recognition. You can do @external {WebWorker} put-mdn-link-here and in presentation where type WebWorker is used, it will be link to mdn website where you can find info on WebWorker.

In (phase 1) comments are chopped by @ character, so:

/**
  * An example function with `useless` parameter, returning **true**.
  * And look,
  * It's on multiple lines...
  * @param {boolean} [useless=true] - better not be false
  * @returns {true}
  * @see {link-to-something-more-interesting-here}
  */
function foo(useless = true) {
    returns true;
}

will create an array with:

[
    {"tagName": "@desc", "tagValue": "An example function with `useless` parameter, returning **true**.\nAnd look,\nit's on multiple lines..."},
    {"tagName": "@param", "tagValue": "{boolean} [useless=true] - better not be false"},
    {"tagName": "@returns", "tagValue": "{true}"},
    {"tagName": "@see", "tagValue": "{link-to-something-more-interesting-here}"}
]

*Doc can extract data like name of parameter, what is description etc. from these.
Notice the multiline text - the starting [space/tab]* is trimmed.

So ESDoc, in it's two phases, deals with comments like this:

/**
  * class node leading comment
  * - (phase 1) checks for @external, @extend needing special treatment
  * - all tags for class must be here, because ClassDoc will be looking here in (phase 2)
  */
class TestClass {
    /**
      * method node leading comment
      * - (phase 1) checks for @external, @extend needing special treatment
      * - all tags for foo must be here, because MethodDoc will be looking here in (phase 2)
      */
    foo(str) {
        return `${str}!`;
    }
    /**
      * Trailing comment
      * - is checked in (phase 1)
      */
}
/**
  * Trailing comment
  * - is checked in (phase 1)
  */

Now to our slightly expanded code example:

/**
 * Test class
 */
class Test {
   /**
    * Foo function.
    */
   foo(str) {
      return str + '!';
   }
}

/**
 * @name #bar
 * @function
 * @description ...
 * @returns ...
 */
['bar', 'baz'].forEach(name => {
   Test.prototype[name] = function()  {
      return this.foo(name);
   }
});

/**
  * @function #quax
  * @memberof TestClass
  */
TestClass['quax'] = function() {
    return this.foo('quax');
}

/**
  * @function
  */
const obviouslyNotAFunction = null;

/**
  * @function
  * @memberof ThisIsNotWhereIDidParkMyClass
  */
const thisIsNotTheFunctionYouAreLookingFor = 42;

// With JSDoc or other tools, I would've documented the functions like this:
// (Maybe with a @memberof, when the comments are outside of the class.)

/**
 * @name #baz
 * @function
 * @description ...
 * @returns ...
 */

Let's implement @function!

First, let's look at what ESDoc finds:
class TestClass
method foo
variable obviouslyNotAFunction
variable thisIsNotTheFunctionYouAreLookingFor

These are the 4 *Doc entries which will be created, ClassDoc, MethodDoc, VariableDoc and VariableDoc.

Where can @function tag be found?

@function #bar as:

  • trailingComment of ClassDeclaration (TestClass) - detected by ESDoc and ClassDoc created
  • leadingComment of ExpressionStatement (the forEach call) - not detected by ESDoc

@function #quax as:

  • trailingComment of that same ExpressionStatement (the forEach call)
  • leadingComment of ExpressionStatement (TestClass['quax'] assignment) - not detected by ESDoc

@function (obviouslyNotAFunction) as:

  • trailingComment of that same ExpressionStatement (TestClass['quax'] assignment)
  • leadingComment of VariableDeclaration (obviouslyNotAFunction) - detected by ESDoc and VariableDoc created

@function (thisIsNotTheFunctionYouAreLookingFor) as:

  • trailingComment of that same VariableDeclaration (obviouslyNotAFunction)
  • leadingComment of VariableDeclaration (thisIsNotTheFunctionYouAreLookingFor) - detected by ESDoc and VariableDoc created

@function #baz as:

  • trailingComment of said VariableDeclaration (thisIsNotTheFunctionYouAreLookingFor)

There's one more place we can find @function tags - the File AST node, which have comments attached.


Now more about @function tag:

  • it can be present anywhere - as some similar tags like @external, @typedef and other, so it doesn't have to be attach to any code AST node
  • usually together with @name tag if name is not specified directly in function
  • with @memberof if scope is not specified with name
  • if there is any code AST node following, it changes it's kind to function

If it's present in leadingComment of detected *Doc, it should be easy to detect it in (phase 2) and ...change type of *Doc? Ok, with only reference to itself and AST nodes, changing our type to other *Doc needs somehow telling that to database. Different *Doc also require different behavior, so basically it's throwing out detected *Doc and creating new FunctionDoc/MethodDoc based on the @function.

If @function is in undetected AST node comment, it's effectively lost and cannot be detected in (phase 2). This case is not shown in the example code, but can easily happen with multiple undetected AST nodes one after other.

If @function is in trailingComment, it will be visited at least once in (phase 1). This is true for @function #bar, @function #quax, @function (obviouslyNotAFunction), @function (thisIsNotTheFunctionYouAreLookingFor) and at last @function #baz.

Yes, I know, we're telling ESDoc that those two variables are functions, but that's our order, ESDoc might warn something is fishy, but ultimately document them as global functions...

I think we found our place - (phase 1)! It visits leading and trailing comments and we can make it to look at all comments, not only those of detected nodes! If we are clever we can also gather more info ahead of creating *Doc and ease *Doc's work by providing more tags with data we extract!

It kinda works - we can replace normal FunctionDoc and MethodDoc (phase 1) creation by creating them ourselves. Feed it all other data like @name, @memberof, @desc and others and rejoice at seeing those in presentation!

But the data we fed to FunctionDoc and MethodDoc we created is not all shown or is incorrect? Ah, some tags are ignored, and instead data is extracted from AST node? Nice... Ok, let's hack that.

Method not pointing to correct class? Let me take longname of that class, since it exist - wait, I don't have reference to it... Does it exists already? Should I create it if it doesn't exist? I probably need reference to database... What if (phase 1) detects the class if I create it, and rewrites it? All I have is AST node and tree

Hmm, this class, which doesn't exist, is not shown, let's try to simulate it and create virtual... What if it exist in other file, which is processed by ESDoc?

What do I use as location of the function if it's virtual? Let's point to the place @function #name is. Wait, I have only text without context, if it's (phase 2), I need to get the comment reference and somehow find text which was edited, if it's multiline, or if user didn't specify a name with @function, but instead uses @name and @memberof or combination of those. If only JSDoc had AST too, ready for me to easily lex it.

So this way we cannot "create" functions and methods which are not detected by ESDoc in the first place. Ok, then let's teach ESDoc to detect:
['bar', 'baz'].forEach( (name) => { TestClass[name] = function() { return this.foo(name); } });

That's easy, we have Member with property, Assignment of function, preceded by Call with forEach, then name as parameter and name as property name, so we know it assigns elements of array which have bar and baz. Easy, let's tell ESDoc we found two functions - wait, it allows only one function (as a return value)? But I have two. Here I have to return what I found as a single return value. I cannot return two. Should I create one MemberDoc myself, and return the other as return value? Should I create both MethodDocs myself, and return null? Should returning of multiple detected nodes be implemented?

Oh, if I create MemberDoc, it doesn't have correct longname of parent class, because there's no ClassDeclaration above our node in the AST tree... And I don't have reference to parent, nor a way to query if parent whether class with that name even exist.

Ok, this is not simple hacking, this is where multiple test cases, not just unit tests needs to be made, and breaking something in (phase 2) and (phase 3) not recognizing changes is highly possible.

We didn't even talked about (phase 3). The publish-html-plugin has... hardcoded HTML in it (facepalm). So any change needs to be fitted to fit the hardcoded HTML, or changed in that plugin too. Want virtual class and functions/method? Well, if existing ClassDoc, FunctionDoc and MethodDoc are not really useful in this situation, too bad, there is hardcoded HTML.
It's not that terrible... Whole plugin needs to be compiled with each smallest change and same code is repeated on multiple places. What is terrible is JSDoc parsing of types is done here... Not in ESDoc core, but in the plugin...


This short (pun intended) text is explanation why change needs to be done instead of hacking in the support for @function.

  • JSDoc needs to be parsed and have AST tree which can be integrated into (Babel/Typescript/Closure/ES)Tree, available well before (phase 1);
  • JSDoc type parsing MUST be moved from (phase 3) to before (phase 1). (being worked on);
  • HTML in html-publish-plugin MUST be decoupled from code and put into templates. (being worked on);
  • if possible, do not specifically use variable/function/class/member/method but have abstract parent-child visualization, the less concrete similar looking stuff with only small differences the better;
  • database with *Doc entries should be accessible to all *Doc entries to query for parents, siblings, externals and other useful information;
  • more clever AST patterns detection - if we find function declaration, no matter where, it's probably significant enough to at least check comments around it for JSDoc;

@EnterTheNameHere EnterTheNameHere self-assigned this Jan 11, 2024
@EnterTheNameHere EnterTheNameHere added the future Will be fixed/implemented in future version of ESDoc label Jan 11, 2024
@VividVisions
Copy link
Author

Thank you for this detailed response!

If you are looking for proper documentation generator, I recommend checking other tools and not waste your precious time with ESDoc.

Do you know any? I've looked at so many but none could handle modern code. Or Node.js' exports structure.

@EnterTheNameHere
Copy link
Owner

Sorry for taking two weeks,

well apart from JSDoc itself, which seems more like exporter than a documentation generator "suite", one interesting application seems to be documentation.js. That's all I know of.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
future Will be fixed/implemented in future version of ESDoc
Projects
None yet
Development

No branches or pull requests

2 participants