"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.parseComment = void 0; const assert_1 = require("assert"); const models_1 = require("../../models"); const utils_1 = require("../../utils"); const paths_1 = require("../../utils/paths"); const lexer_1 = require("./lexer"); const tagName_1 = require("./tagName"); function makeLookaheadGenerator(gen) { let trackHistory = false; const history = []; const next = [gen.next()]; return { done() { return !!next[0].done; }, peek() { (0, assert_1.ok)(!next[0].done); return next[0].value; }, take() { const thisItem = next.shift(); if (trackHistory) { history.push(thisItem); } (0, assert_1.ok)(!thisItem.done); next.push(gen.next()); return thisItem.value; }, mark() { (0, assert_1.ok)(!trackHistory, "Can only mark one location for backtracking at a time"); trackHistory = true; }, release() { trackHistory = false; next.unshift(...history); history.length = 0; }, }; } function parseComment(tokens, config, file, logger) { const lexer = makeLookaheadGenerator(tokens); const tok = lexer.done() || lexer.peek(); const comment = new models_1.Comment(); comment.summary = blockContent(comment, lexer, config, warningImpl); while (!lexer.done()) { comment.blockTags.push(blockTag(comment, lexer, config, warningImpl)); } postProcessComment(comment, (message) => { (0, assert_1.ok)(typeof tok === "object"); logger.warn(`${message} in comment at ${(0, paths_1.nicePath)(file.fileName)}:${file.getLineAndCharacterOfPosition(tok.pos).line + 1}`); }); return comment; function warningImpl(message, token) { logger.warn(message, token.pos, file); } } exports.parseComment = parseComment; const HAS_USER_IDENTIFIER = [ "@callback", "@param", "@prop", "@property", "@template", "@typedef", "@typeParam", "@inheritDoc", ]; function makeCodeBlock(text) { return "```ts\n" + text + "\n```"; } /** * Loop over comment, produce lint warnings, and set tag names for tags * which have them. */ function postProcessComment(comment, warning) { for (const tag of comment.blockTags) { if (HAS_USER_IDENTIFIER.includes(tag.tag) && tag.content.length) { const first = tag.content[0]; if (first.kind === "text") { const { name, newText } = (0, tagName_1.extractTagName)(first.text); tag.name = name; if (newText) { first.text = newText; } else { // Remove this token, no real text in it. tag.content.shift(); } } } if (tag.content.some((part) => part.kind === "inline-tag" && part.tag === "@inheritDoc")) { warning("An inline @inheritDoc tag should not appear within a block tag as it will not be processed"); } } const remarks = comment.blockTags.filter((tag) => tag.tag === "@remarks"); if (remarks.length > 1) { warning("At most one @remarks tag is expected in a comment, ignoring all but the first"); (0, utils_1.removeIf)(comment.blockTags, (tag) => remarks.indexOf(tag) > 0); } const returns = comment.blockTags.filter((tag) => tag.tag === "@returns"); if (remarks.length > 1) { warning("At most one @returns tag is expected in a comment, ignoring all but the first"); (0, utils_1.removeIf)(comment.blockTags, (tag) => returns.indexOf(tag) > 0); } const inheritDoc = comment.blockTags.filter((tag) => tag.tag === "@inheritDoc"); const inlineInheritDoc = comment.summary.filter((part) => part.kind === "inline-tag" && part.tag === "@inheritDoc"); if (inlineInheritDoc.length + inheritDoc.length > 1) { warning("At most one @inheritDoc tag is expected in a comment, ignoring all but the first"); const allInheritTags = [...inlineInheritDoc, ...inheritDoc]; (0, utils_1.removeIf)(comment.summary, (part) => allInheritTags.indexOf(part) > 0); (0, utils_1.removeIf)(comment.blockTags, (tag) => allInheritTags.indexOf(tag) > 0); } if ((inlineInheritDoc.length || inheritDoc.length) && comment.summary.some((part) => part.kind !== "inline-tag" && /\S/.test(part.text))) { warning("Content in the summary section will be overwritten by the @inheritDoc tag"); } if ((inlineInheritDoc.length || inheritDoc.length) && remarks.length) { warning("Content in the @remarks block will be overwritten by the @inheritDoc tag"); } } const aliasedTags = new Map([["@return", "@returns"]]); function blockTag(comment, lexer, config, warning) { const blockTag = lexer.take(); (0, assert_1.ok)(blockTag.kind === lexer_1.TokenSyntaxKind.Tag, "blockTag called not at the start of a block tag."); // blockContent is broken if this fails. const tagName = aliasedTags.get(blockTag.text) || blockTag.text; let content; if (tagName === "@example") { return exampleBlock(comment, lexer, config, warning); } else if (["@default", "@defaultValue"].includes(tagName) && config.jsDocCompatibility.defaultTag) { content = defaultBlockContent(comment, lexer, config, warning); } else { content = blockContent(comment, lexer, config, warning); } return new models_1.CommentTag(tagName, content); } /** * The `@default` tag gets a special case because otherwise we will produce many warnings * about unescaped/mismatched/missing braces in legacy JSDoc comments */ function defaultBlockContent(comment, lexer, config, warning) { lexer.mark(); const content = blockContent(comment, lexer, config, () => { }); const end = lexer.done() || lexer.peek(); lexer.release(); if (content.some((part) => part.kind === "code")) { return blockContent(comment, lexer, config, warning); } const tokens = []; while ((lexer.done() || lexer.peek()) !== end) { tokens.push(lexer.take()); } const blockText = tokens .map((tok) => tok.text) .join("") .trim(); return [ { kind: "code", text: makeCodeBlock(blockText), }, ]; } /** * The `@example` tag gets a special case because otherwise we will produce many warnings * about unescaped/mismatched/missing braces in legacy JSDoc comments. * * In TSDoc, we also want to treat the first line of the block as the example name. */ function exampleBlock(comment, lexer, config, warning) { lexer.mark(); const content = blockContent(comment, lexer, config, () => { }); const end = lexer.done() || lexer.peek(); lexer.release(); if (!config.jsDocCompatibility.exampleTag || content.some((part) => part.kind === "code" && part.text.startsWith("```"))) { let exampleName = ""; // First line of @example block is the example name. let warnedAboutRichNameContent = false; outer: while ((lexer.done() || lexer.peek()) !== end) { const next = lexer.peek(); switch (next.kind) { case lexer_1.TokenSyntaxKind.NewLine: lexer.take(); break outer; case lexer_1.TokenSyntaxKind.Text: { const newline = next.text.indexOf("\n"); if (newline !== -1) { exampleName += next.text.substring(0, newline); next.pos += newline + 1; break outer; } else { exampleName += lexer.take().text; } break; } case lexer_1.TokenSyntaxKind.Code: case lexer_1.TokenSyntaxKind.Tag: case lexer_1.TokenSyntaxKind.TypeAnnotation: case lexer_1.TokenSyntaxKind.CloseBrace: case lexer_1.TokenSyntaxKind.OpenBrace: if (!warnedAboutRichNameContent) { warning("The first line of an example tag will be taken literally as" + " the example name, and should only contain text.", lexer.peek()); warnedAboutRichNameContent = true; } exampleName += lexer.take().text; break; default: (0, utils_1.assertNever)(next.kind); } } const content = blockContent(comment, lexer, config, warning); const tag = new models_1.CommentTag("@example", content); if (exampleName.trim()) { tag.name = exampleName.trim(); } return tag; } const tokens = []; while ((lexer.done() || lexer.peek()) !== end) { tokens.push(lexer.take()); } const blockText = tokens .map((tok) => tok.text) .join("") .trim(); const caption = blockText.match(/^\s*