UNPKG

prettier-plugin-jsdoc

Version:

Prettier plugin for format comment blocks and convert to standard Match with Visual studio and other IDE which support jsdoc and comments as markdown.

407 lines (406 loc) 15.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getParser = void 0; const comment_parser_1 = require("comment-parser"); const utils_1 = require("./utils"); const tags_1 = require("./tags"); const roles_1 = require("./roles"); const stringify_1 = require("./stringify"); const tags_2 = require("./tags"); /** @link https://prettier.io/docs/en/api.html#custom-parser-api} */ const getParser = (originalParse, parserName) => function jsdocParser(text, parsers, options) { var _a, _b; const prettierParse = ((_a = utils_1.findPluginByParser(parserName, options)) === null || _a === void 0 ? void 0 : _a.parse) || originalParse; const ast = prettierParse(text, parsers, options); // jsdocParser is deprecated,this is backward compatible will be remove // eslint-disable-next-line @typescript-eslint/ban-ts-comment //@ts-ignore if (options.jsdocParser === false) { return ast; } options = { ...options, printWidth: (_b = options.jsdocPrintWidth) !== null && _b !== void 0 ? _b : options.printWidth, }; const eol = options.endOfLine === "auto" ? utils_1.detectEndOfLine(text) : options.endOfLine; options = { ...options, endOfLine: "lf" }; ast.comments.forEach((comment) => { if (!isBlockComment(comment)) return; const tokenIndex = utils_1.findTokenIndex(ast.tokens, comment); const paramsOrder = getParamsOrders(ast, tokenIndex); /** Issue: https://github.com/hosseinmd/prettier-plugin-jsdoc/issues/18 */ comment.value = comment.value.replace(/^([*]+)/g, "*"); // Create the full comment string with line ends normalized to \n // This means that all following code can assume \n and should only use // \n. const commentString = `/*${comment.value.replace(/\r\n?/g, "\n")}*/`; /** * Check if this comment block is a JSDoc. Based on: * https://github.com/jsdoc/jsdoc/blob/master/packages/jsdoc/plugins/commentsOnly.js */ if (!/^\/\*\*[\s\S]+?\*\/$/.test(commentString)) return; const parsed = comment_parser_1.parse(commentString, { spacing: "preserve", })[0]; comment.value = ""; if (!parsed) { // Error on commentParser return; } normalizeTags(parsed); convertCommentDescToDescTag(parsed); const commentContentPrintWidth = getIndentationWidth(comment, text, options); let maxTagTitleLength = 0; let maxTagTypeLength = 0; let maxTagNameLength = 0; let tags = parsed.tags // Prepare tags data .map(({ type, optional, ...rest }) => { if (type) { /** * Convert optional to standard * https://jsdoc.app/tags-type.html#:~:text=Optional%20parameter */ type = type.replace(/[=]$/, () => { optional = true; return ""; }); type = utils_1.convertToModernType(type); type = utils_1.formatType(type, { ...options, printWidth: commentContentPrintWidth, }); } return { ...rest, type, optional, }; }); // Group tags tags = sortTags(tags, paramsOrder, options); if (options.jsdocSeparateReturnsFromParam) { tags = tags.flatMap((tag, index) => { var _a; if (tag.tag === tags_1.RETURNS && ((_a = tags[index - 1]) === null || _a === void 0 ? void 0 : _a.tag) === tags_1.PARAM) { return [tags_2.SPACE_TAG_DATA, tag]; } return [tag]; }); } tags .map(addDefaultValueToDescription) .map(assignOptionalAndDefaultToName) .map(({ type, name, description, tag, ...rest }) => { const isVerticallyAlignAbleTags = roles_1.TAGS_VERTICALLY_ALIGN_ABLE.includes(tag); if (isVerticallyAlignAbleTags) { maxTagTitleLength = Math.max(maxTagTitleLength, tag.length); maxTagTypeLength = Math.max(maxTagTypeLength, type.length); maxTagNameLength = Math.max(maxTagNameLength, name.length); } return { type, name, description: description.trim(), tag, ...rest, }; }) .filter(({ description, tag }) => { if (!description && roles_1.TAGS_DESCRIPTION_NEEDED.includes(tag)) { return false; } return true; }) // Create final jsDoc string .forEach((tagData, tagIndex, finalTagsArray) => { comment.value += stringify_1.stringify(tagData, tagIndex, finalTagsArray, { ...options, printWidth: commentContentPrintWidth }, maxTagTitleLength, maxTagTypeLength, maxTagNameLength); }); comment.value = comment.value.trimEnd(); if (comment.value) { comment.value = utils_1.addStarsToTheBeginningOfTheLines(comment.value, options); } if (eol === "cr") { comment.value = comment.value.replace(/\n/g, "\r"); } else if (eol === "crlf") { comment.value = comment.value.replace(/\n/g, "\r\n"); } }); ast.comments = ast.comments.filter((comment) => !(isBlockComment(comment) && !comment.value)); return ast; }; exports.getParser = getParser; function sortTags(tags, paramsOrder, options) { let canGroupNextTags = false; let shouldSortAgain = false; tags = tags .reduce((tagGroups, cur) => { if (tagGroups.length === 0 || (roles_1.TAGS_GROUP_HEAD.includes(cur.tag) && canGroupNextTags)) { canGroupNextTags = false; tagGroups.push([]); } if (roles_1.TAGS_GROUP_CONDITION.includes(cur.tag)) { canGroupNextTags = true; } tagGroups[tagGroups.length - 1].push(cur); return tagGroups; }, []) .flatMap((tagGroup, index, array) => { var _a; // sort tags within groups tagGroup.sort((a, b) => { if (paramsOrder && paramsOrder.length > 1 && a.tag === tags_1.PARAM && b.tag === tags_1.PARAM) { const aIndex = paramsOrder.indexOf(a.name); const bIndex = paramsOrder.indexOf(b.name); if (aIndex > -1 && bIndex > -1) { //sort params return aIndex - bIndex; } return 0; } return (getTagOrderWeight(a.tag, options) - getTagOrderWeight(b.tag, options)); }); // add an empty line between groups if (array.length - 1 !== index) { tagGroup.push(tags_2.SPACE_TAG_DATA); } if (index > 0 && ((_a = tagGroup[0]) === null || _a === void 0 ? void 0 : _a.tag) && !roles_1.TAGS_GROUP_HEAD.includes(tagGroup[0].tag)) { shouldSortAgain = true; } return tagGroup; }); return shouldSortAgain ? sortTags(tags, paramsOrder, options) : tags; } /** * Control order of tags by weights. Smaller value brings tag higher. */ function getTagOrderWeight(tag, options) { if (tag === tags_1.DESCRIPTION && !options.jsdocDescriptionTag) { return -1; } const index = roles_1.TAGS_ORDER.indexOf(tag); return index === -1 ? roles_1.TAGS_ORDER.indexOf("other") : index; } function isBlockComment(comment) { return comment.type === "CommentBlock" || comment.type === "Block"; } function getIndentationWidth(comment, text, options) { const line = text.split(/\r\n?|\n/g)[comment.loc.start.line - 1]; let spaces = 0; let tabs = 0; for (let i = comment.loc.start.column - 1; i >= 0; i--) { const c = line[i]; if (c === " ") { spaces++; } else if (c === "\t") { tabs++; } else { break; } } return options.printWidth - (spaces + tabs * options.tabWidth) - " * ".length; } const TAGS_ORDER_LOWER = roles_1.TAGS_ORDER.map((tagOrder) => tagOrder.toLowerCase()); /** * This will adjust the casing of tag titles, resolve synonyms, fix * incorrectly parsed tags, correct incorrectly assigned names and types, and * trim spaces. * * @param parsed */ function normalizeTags(parsed) { parsed.tags = parsed.tags.map(({ tag, type, name, description, default: _default, ...rest }) => { tag = tag || ""; type = type || ""; name = name || ""; description = description || ""; _default = _default === null || _default === void 0 ? void 0 : _default.trim(); /** When the space between tag and type is missing */ const tagSticksToType = tag.indexOf("{"); if (tagSticksToType !== -1 && tag[tag.length - 1] === "}") { type = tag.slice(tagSticksToType + 1, -1) + " " + type; tag = tag.slice(0, tagSticksToType); } tag = tag.trim(); const lower = tag.toLowerCase(); const tagIndex = TAGS_ORDER_LOWER.indexOf(lower); if (tagIndex >= 0) { tag = roles_1.TAGS_ORDER[tagIndex]; } else if (lower in roles_1.TAGS_SYNONYMS) { // resolve synonyms tag = roles_1.TAGS_SYNONYMS[lower]; } type = type.trim(); name = name.trim(); if (name && roles_1.TAGS_NAMELESS.includes(tag)) { description = `${name} ${description}`; name = ""; } if (type && roles_1.TAGS_TYPELESS.includes(tag)) { description = `{${type}} ${description}`; type = ""; } description = description.trim(); return { tag, type, name, description, default: _default, ...rest, }; }); } /** * This will merge the comment description and all `@description` tags into one * `@description` tag. * * @param parsed */ function convertCommentDescToDescTag(parsed) { let description = parsed.description || ""; parsed.description = ""; parsed.tags = parsed.tags.filter(({ description: _description, tag }) => { if (tag.toLowerCase() === tags_1.DESCRIPTION) { if (_description.trim()) { description += "\n\n" + _description; } return false; } else { return true; } }); if (description) { parsed.tags.unshift({ tag: tags_1.DESCRIPTION, description, name: undefined, type: undefined, source: [], optional: false, problems: [], }); } } /** * This is for find params of function name in code as strings of array */ function getParamsOrders(ast, tokenIndex) { var _a; let paramsOrder; let params; try { // next token const nextTokenType = (_a = ast.tokens[tokenIndex + 1]) === null || _a === void 0 ? void 0 : _a.type; if (typeof nextTokenType !== "object") { return undefined; } // Recognize function if (nextTokenType.label === "function") { let openedParenthesesCount = 1; const tokensAfterComment = ast.tokens.slice(tokenIndex + 4); const endIndex = tokensAfterComment.findIndex(({ type }) => { if (typeof type === "string") { return false; } else if (type.label === "(") { openedParenthesesCount++; } else if (type.label === ")") { openedParenthesesCount--; } return openedParenthesesCount === 0; }); params = tokensAfterComment.slice(0, endIndex + 1); } // Recognize arrow function if (nextTokenType.label === "const") { let openedParenthesesCount = 1; let tokensAfterComment = ast.tokens.slice(tokenIndex + 1); const firstParenthesesIndex = tokensAfterComment.findIndex(({ type }) => typeof type === "object" && type.label === "("); tokensAfterComment = tokensAfterComment.slice(firstParenthesesIndex + 1); const endIndex = tokensAfterComment.findIndex(({ type }) => { if (typeof type === "string") { return false; } else if (type.label === "(") { openedParenthesesCount++; } else if (type.label === ")") { openedParenthesesCount--; } return openedParenthesesCount === 0; }); const arrowItem = tokensAfterComment[endIndex + 1]; if (typeof (arrowItem === null || arrowItem === void 0 ? void 0 : arrowItem.type) === "object" && arrowItem.type.label === "=>") { params = tokensAfterComment.slice(0, endIndex + 1); } } paramsOrder = params === null || params === void 0 ? void 0 : params.filter(({ type }) => typeof type === "object" && type.label === "name").map(({ value }) => value); } catch (_b) { // } return paramsOrder; } /** * If the given tag has a default value, then this will add a note to the * description with that default value. This is done because TypeScript does * not display the documented JSDoc default value (e.g. `@param [name="John"]`). * * If the description already contains such a note, it will be updated. */ function addDefaultValueToDescription(tag) { if (tag.optional && tag.default) { let { description } = tag; // remove old note description = description.replace(/[ \t]*Default is `.*`\.?$/, ""); // add a `.` at the end of previous sentences if (description && !/[.\n]$/.test(description)) { description += "."; } description += ` Default is \`${tag.default}\``; return { ...tag, description: description.trim(), }; } else { return tag; } } /** * This will combine the `name`, `optional`, and `default` properties into name * setting the other two to `false` and `undefined` respectively. */ function assignOptionalAndDefaultToName({ name, optional, default: default_, ...rest }) { if (name && optional) { // Figure out if tag type have default value if (default_) { name = `[${name}=${default_}]`; } else { name = `[${name}]`; } } return { ...rest, name: name, optional: optional, default: default_, }; }