typedoc
Version:
Create api documentation for TypeScript projects.
191 lines (190 loc) • 7.58 kB
JavaScript
import ts from "typescript";
import { Comment, ReflectionKind } from "../../models/index.js";
import { lexBlockComment } from "./blockLexer.js";
import { discoverComment, discoverFileComments, discoverNodeComment, discoverSignatureComment, } from "./discovery.js";
import { lexLineComments } from "./lineLexer.js";
import { parseComment } from "./parser.js";
import { assertNever, i18n } from "#utils";
const jsDocCommentKinds = [
ts.SyntaxKind.JSDocPropertyTag,
ts.SyntaxKind.JSDocCallbackTag,
ts.SyntaxKind.JSDocTypedefTag,
ts.SyntaxKind.JSDocTemplateTag,
ts.SyntaxKind.JSDocEnumTag,
];
let commentDiscoveryId = 0;
let commentCache = new WeakMap();
// We need to do this for tests so that changing the tsLinkResolution option
// actually works. Without it, we'd get the old parsed comment which doesn't
// have the TS symbols attached.
export function clearCommentCache() {
commentCache = new WeakMap();
commentDiscoveryId = 0;
}
function getCommentWithCache(discovered, context) {
if (!discovered)
return;
const { file, ranges, jsDoc } = discovered;
const cache = commentCache.get(file) || new Map();
if (cache.has(ranges[0].pos)) {
const clone = cache.get(ranges[0].pos).clone();
clone.inheritedFromParentDeclaration = discovered.inheritedFromParentDeclaration;
return clone;
}
let comment;
switch (ranges[0].kind) {
case ts.SyntaxKind.MultiLineCommentTrivia:
comment = parseComment(lexBlockComment(file.text, ranges[0].pos, ranges[0].end, context.createSymbolId, jsDoc, context.checker), file, context);
break;
case ts.SyntaxKind.SingleLineCommentTrivia:
comment = parseComment(lexLineComments(file.text, ranges), file, context);
break;
default:
assertNever(ranges[0].kind);
}
comment.discoveryId = ++commentDiscoveryId;
comment.inheritedFromParentDeclaration = discovered.inheritedFromParentDeclaration;
cache.set(ranges[0].pos, comment);
commentCache.set(file, cache);
return comment.clone();
}
function getCommentImpl(commentSource, moduleComment, context) {
const comment = getCommentWithCache(commentSource, {
...context,
checker: context.config.useTsLinkResolution ? context.checker : undefined,
});
if (comment?.getTag("@import") || comment?.getTag("@license")) {
return;
}
if (moduleComment && comment) {
// Module comment, make sure it is tagged with @packageDocumentation or @module.
// If it isn't then the comment applies to the first statement in the file, so throw it away.
if (!comment.hasModifier("@packageDocumentation") &&
!comment.getTag("@module")) {
return;
}
}
if (!moduleComment && comment) {
// Ensure module comments are not attached to non-module reflections.
if (comment.hasModifier("@packageDocumentation") ||
comment.getTag("@module")) {
return;
}
}
return comment;
}
export function getComment(symbol, kind, context) {
const declarations = symbol.declarations || [];
if (declarations.length &&
declarations.every((d) => jsDocCommentKinds.includes(d.kind))) {
return getJsDocComment(declarations[0], context);
}
const sf = declarations.find(ts.isSourceFile);
if (sf) {
return getFileComment(sf, context);
}
const isModule = declarations.some((decl) => {
if (ts.isModuleDeclaration(decl) && ts.isStringLiteral(decl.name)) {
return true;
}
return false;
});
const comment = getCommentImpl(discoverComment(symbol, kind, context.logger, context.config.commentStyle, context.checker, !context.config.suppressCommentWarningsInDeclarationFiles), isModule, context);
if (!comment && kind === ReflectionKind.Property) {
return getConstructorParamPropertyComment(symbol, context);
}
return comment;
}
export function getNodeComment(node, moduleComment, context) {
return getCommentImpl(discoverNodeComment(node, context.config.commentStyle), moduleComment, context);
}
export function getFileComment(file, context) {
for (const commentSource of discoverFileComments(file, context.config.commentStyle)) {
const comment = getCommentWithCache(commentSource, context);
if (comment?.getTag("@license") || comment?.getTag("@import")) {
continue;
}
if (comment?.getTag("@module") ||
comment?.hasModifier("@packageDocumentation")) {
return comment;
}
return;
}
}
function getConstructorParamPropertyComment(symbol, context) {
const decl = symbol.declarations?.find(ts.isParameter);
if (!decl)
return;
const ctor = decl.parent;
const comment = getSignatureComment(ctor, context);
const paramTag = comment?.getIdentifiedTag(symbol.name, "@param");
if (paramTag) {
const result = new Comment(paramTag.content);
result.sourcePath = comment.sourcePath;
return result;
}
}
export function getSignatureComment(declaration, context) {
return getCommentImpl(discoverSignatureComment(declaration, context.checker, context.config.commentStyle), false, context);
}
export function getJsDocComment(declaration, context) {
const file = declaration.getSourceFile();
// First, get the whole comment. We know we'll need all of it.
let parent = declaration.parent;
while (!ts.isJSDoc(parent)) {
parent = parent.parent;
}
// Then parse it.
const comment = getCommentWithCache({
file,
ranges: [
{
kind: ts.SyntaxKind.MultiLineCommentTrivia,
pos: parent.pos,
end: parent.end,
},
],
jsDoc: parent,
inheritedFromParentDeclaration: false,
}, context);
// And pull out the tag we actually care about.
if (ts.isJSDocEnumTag(declaration)) {
const result = new Comment(comment.getTag("@enum")?.content);
result.sourcePath = comment.sourcePath;
return result;
}
if (ts.isJSDocTemplateTag(declaration) &&
declaration.comment &&
declaration.typeParameters.length > 1) {
// We could just put the same comment on everything, but due to how comment parsing works,
// we'd have to search for any @template with a name starting with the first type parameter's name
// which feels horribly hacky.
context.logger.warn(i18n.multiple_type_parameters_on_template_tag_unsupported(), declaration);
return;
}
let name;
if (ts.isJSDocTemplateTag(declaration)) {
// This isn't really ideal.
name = declaration.typeParameters[0].name.text;
}
else {
name = declaration.name?.getText();
}
if (!name) {
return;
}
const tag = comment.getIdentifiedTag(name, `@${declaration.tagName.text}`);
if (!tag) {
// If this is a template tag with multiple declarations, we warned already if there
// was a comment attached. If there wasn't, then don't error about failing to find
// a tag because this is unsupported.
if (!ts.isJSDocTemplateTag(declaration)) {
context.logger.error(i18n.failed_to_find_jsdoc_tag_for_name_0(name), declaration);
}
}
else {
const result = new Comment(Comment.cloneDisplayParts(tag.content));
result.sourcePath = comment.sourcePath;
return result;
}
}