Open VividVisions opened 9 months ago
Can you provide a minimal example code? It would help to check how the parser processes it.
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 ...
*/
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):
*Doc
entries (ClassDoc
, MemberDoc
, MethodDoc
, FunctionDoc
), taking the detected AST node, extracting data and read comments; once all entries are detected;*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.
@
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 export
ed, what type
is it, does it have author
, version
, since
, does it have description
etc.
*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;*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?);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
createdleadingComment
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:
@external
, @typedef
and other, so it doesn't have to be attach to any code AST node@name
tag if name is not specified directly in function
@memberof
if scope is not specified with nameIf 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 MethodDoc
s 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.
html-publish-plugin
MUST be decoupled from code and put into templates. (being worked on);*Doc
entries should be accessible to all *Doc
entries to query for parents, siblings, externals and other useful information;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.
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.
Since there is no
@function
tag, there's currently no way to document dynamically added methods. Or have I missed something?