@ts-ast-parser/core
Version:
Reflects a simplified version of the TypeScript AST for generating documentation
128 lines • 4.72 kB
JavaScript
import { parse } from '@ts-ast-parser/comment';
import ts from 'typescript';
/**
* Reflected node that represents a documentation comment
*
* @see {@link https://tsdoc.org/}
* @see {@link https://jsdoc.app/}
*/
export class CommentNode {
constructor(node) {
Object.defineProperty(this, "_parts", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
this._parseComments(node);
}
/**
* Whether the comment has the specified tag
*
* @param name - The name of the documentation tag to check
* @returns True if the block tag exist, otherwise false
*/
hasTag(name) {
return this._parts.some(p => p.kind === name);
}
/**
* Returns the first tag with the given name.
*
* @param name - The name of the tag.
* @returns The first tag with the given name or `undefined` if no such tag exists.
*/
getTag(name) {
return this._parts.find(p => p.kind === name);
}
/**
* Returns all the matches found for the given tag.
* A tag may appear more than once in a documentation comment.
* For example `@param` can appear multiple times. This method
* will return all the appearances of a given tag.
*
* @param name - The name of the tag to search
* @returns All the available block tags instances
*/
getAllTags(name) {
return this._parts.filter(p => p.kind === name);
}
/**
* Whether the documentation comment has tags that make the
* associated declaration ignored for documentation purposes.
*
* @returns True if the symbol should be ignored based on the JSDoc, otherwise false
*/
isIgnored() {
return this._parts.some(p => {
return p.kind === 'ignore' || p.kind === 'internal' || p.kind === 'private';
});
}
/**
* Serializes the reflected node
*
* @returns The reflected node as a serializable object
*/
serialize() {
return this._parts;
}
_parseComments(node) {
const isSourceFile = ts.isSourceFile(node);
let sourceCode;
let jsDocNode;
if (isSourceFile) {
// TS does not have a concept of module-level JSDoc.
// If it exists, it will always be attached to the first statement in the module.
jsDocNode = ts.forEachChild(node, n => n);
sourceCode = node.text;
}
else {
jsDocNode = node;
sourceCode = node.getSourceFile()?.text;
}
if (!sourceCode || !jsDocNode) {
return;
}
let ranges = ts.getLeadingCommentRanges(sourceCode, jsDocNode.pos) ?? [];
if (!ranges.length) {
return;
}
if (isSourceFile) {
// If the first statement has more than one JSDoc block, we collect all
// but the last and use those, regardless of whether they contain one
// of the module-designating tags (the last one is assumed to belong
// to the first statement)
ranges = ranges.slice(0, ranges.length > 1 ? -1 : 1);
}
else {
// For declarations, we only care about one JSDoc
ranges = [ranges[ranges.length - 1]];
}
for (const range of ranges) {
let comment = sourceCode.substring(range.pos, range.end);
comment = comment.split(/\r\n|\n/).map(p => p.replace(/^\s+/, ' ')).join('\n');
let parserResult;
try {
parserResult = parse(comment);
}
catch (_) {
// TODO(Jordi M.): Handle the error accordingly
}
if (!parserResult || parserResult.error) {
continue;
}
// For the module-level JSDoc if the first statement only has one
// JSDoc block, it is only treated as module documentation
// if it contains a `@module`, `@fileoverview`, or `@packageDocumentation` tag.
// This is required to disambiguate a module description (with an
// undocumented first statement) from documentation for the first statement.
const isModuleJSDoc = parserResult.parts.some(p => {
return p.kind === 'module' || p.kind === 'fileoverview' || p.kind === 'packageDocumentation';
});
if ((!isSourceFile && isModuleJSDoc) || (ranges.length === 1 && isSourceFile && !isModuleJSDoc)) {
continue;
}
this._parts = this._parts.concat(parserResult.parts);
}
}
}
//# sourceMappingURL=comment-node.js.map