UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

185 lines (184 loc) 7.68 kB
import ts from "typescript"; import { Comment, ReflectionKind } from "../../models/index.js"; import { assertNever } from "../../utils/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"; 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, config, logger, checker, files) { 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, jsDoc, checker), config, file, logger, files); break; case ts.SyntaxKind.SingleLineCommentTrivia: comment = parseComment(lexLineComments(file.text, ranges), config, file, logger, files); 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, config, logger, moduleComment, checker, files) { const comment = getCommentWithCache(commentSource, config, logger, config.useTsLinkResolution ? checker : undefined, files); 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, config, logger, checker, files) { const declarations = symbol.declarations || []; if (declarations.length && declarations.every((d) => jsDocCommentKinds.includes(d.kind))) { return getJsDocComment(declarations[0], config, logger, checker, files); } const sf = declarations.find(ts.isSourceFile); if (sf) { return getFileComment(sf, config, logger, checker, files); } const isModule = declarations.some((decl) => { if (ts.isModuleDeclaration(decl) && ts.isStringLiteral(decl.name)) { return true; } return false; }); const comment = getCommentImpl(discoverComment(symbol, kind, logger, config.commentStyle, checker, !config.suppressCommentWarningsInDeclarationFiles), config, logger, isModule, checker, files); if (!comment && kind === ReflectionKind.Property) { return getConstructorParamPropertyComment(symbol, config, logger, checker, files); } return comment; } export function getNodeComment(node, moduleComment, config, logger, checker, files) { return getCommentImpl(discoverNodeComment(node, config.commentStyle), config, logger, moduleComment, checker, files); } export function getFileComment(file, config, logger, checker, files) { for (const commentSource of discoverFileComments(file, config.commentStyle)) { const comment = getCommentWithCache(commentSource, config, logger, config.useTsLinkResolution ? checker : undefined, files); if (comment?.getTag("@license") || comment?.getTag("@import")) { continue; } if (comment?.getTag("@module") || comment?.hasModifier("@packageDocumentation")) { return comment; } return; } } function getConstructorParamPropertyComment(symbol, config, logger, checker, files) { const decl = symbol.declarations?.find(ts.isParameter); if (!decl) return; const ctor = decl.parent; const comment = getSignatureComment(ctor, config, logger, checker, files); 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, config, logger, checker, files) { return getCommentImpl(discoverSignatureComment(declaration, checker, config.commentStyle), config, logger, false, checker, files); } export function getJsDocComment(declaration, config, logger, checker, files) { 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, }, config, logger, config.useTsLinkResolution ? checker : undefined, files); // 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. logger.warn(logger.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) { logger.error(logger.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; } }