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
JavaScript
;
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_,
};
}