From 25244133d1aa23483b6cae2b0319f36d4223da59 Mon Sep 17 00:00:00 2001 From: Hossein Mohammadi Date: Mon, 8 Feb 2021 00:35:20 +0330 Subject: [PATCH] feat: fix function params order issue: https://github.com/hosseinmd/prettier-plugin-jsdoc/issues/60 --- src/parser.ts | 162 +++++++++++++++++--------- src/types.ts | 23 +++- tests/__snapshots__/main.test.js.snap | 35 ++++++ tests/main.test.js | 41 +++++++ 4 files changed, 207 insertions(+), 54 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 423382a..0d56121 100755 --- a/src/parser.ts +++ b/src/parser.ts @@ -5,7 +5,7 @@ import { formatType, detectEndOfLine, } from "./utils"; -import { DESCRIPTION } from "./tags"; +import { DESCRIPTION, PARAM } from "./tags"; import { TAGS_DESCRIPTION_NEEDED, TAGS_GROUP, @@ -54,6 +54,7 @@ export const getParser = (parser: Parser["parse"]) => ast.comments.forEach((comment) => { if (!isBlockComment(comment)) return; + const tokenIndex = ast.tokens.findIndex(({ loc }) => loc === comment.loc); /** Issue: https://github.com/hosseinmd/prettier-plugin-jsdoc/issues/18 */ comment.value = comment.value.replace(/^([*]+)/g, "*"); @@ -95,37 +96,119 @@ export const getParser = (parser: Parser["parse"]) => 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 = convertToModernType(type); + type = formatType(type, { + ...options, + printWidth: commentContentPrintWidth, + }); + } + + return { + ...rest, + type, + optional, + } as commentParser.Tag; + }) + + // Group tags + .reduce((tagGroups, cur, index, array) => { + if ( + (tagGroups.length === 0 || TAGS_GROUP.includes(cur.tag)) && + array[index - 1]?.tag !== DESCRIPTION + ) { + tagGroups.push([]); + } + tagGroups[tagGroups.length - 1].push(cur); + + return tagGroups; + }, []) + .flatMap((tagGroup, index, tags) => { + let paramsOrder: string[] | undefined; + // next token + const nextTokenType = ast.tokens[tokenIndex + 1]?.type; + if ( + typeof nextTokenType === "object" && + nextTokenType.label === "function" + ) { + try { + let openedParenthesesCount = 1; + const endIndex = ast.tokens + .slice(tokenIndex + 4) + .findIndex(({ type }) => { + if (typeof type === "string") { + return false; + } else if (type.label === "(") { + openedParenthesesCount++; + } else if (type.label === ")") { + openedParenthesesCount--; + } + + return openedParenthesesCount === 0; + }); + + const params = ast.tokens.slice( + tokenIndex + 4, + tokenIndex + 4 + endIndex + 1, + ); + + paramsOrder = params + .filter( + ({ type }) => + typeof type === "object" && type.label === "name", + ) + .map(({ value }) => value); + } catch { + // + } + } + + // sort tags within groups + tagGroup.sort((a, b) => { + if ( + paramsOrder && + paramsOrder.length > 1 && + a.tag === PARAM && + b.tag === PARAM + ) { + //sort params + return paramsOrder.indexOf(a.name) - paramsOrder.indexOf(b.name); + } + return getTagOrderWeight(a.tag) - getTagOrderWeight(b.tag); + }); + + // add an empty line between groups + if (tags.length - 1 !== index) { + tagGroup.push(SPACE_TAG_DATA); + } + + return tagGroup; + }) .map( ({ - name, - description, type, - tag, - source, + name, optional, default: _default, - ...restInfo + description, + tag, + ...rest }) => { const isVerticallyAlignAbleTags = TAGS_VERTICALLY_ALIGN_ABLE.includes( tag, ); if (type) { - /** - * Convert optional to standard - * https://jsdoc.app/tags-type.html#:~:text=Optional%20parameter - */ - type = type.replace(/[=]$/, () => { - optional = true; - return ""; - }); - - type = convertToModernType(type); - type = formatType(type, { - ...options, - printWidth: commentContentPrintWidth, - }); - // Additional operations on name if (name) { // Optional tag name @@ -157,43 +240,16 @@ export const getParser = (parser: Parser["parse"]) => description = description.trim(); return { - ...restInfo, + type, name, + optional, + default: _default, description, - type, tag, - source, - default: _default, - optional, - } as commentParser.Tag; + ...rest, + }; }, ) - - // Group tags - .reduce((tagGroups, cur, index, array) => { - if ( - (tagGroups.length === 0 || TAGS_GROUP.includes(cur.tag)) && - array[index - 1]?.tag !== DESCRIPTION - ) { - tagGroups.push([]); - } - tagGroups[tagGroups.length - 1].push(cur); - - return tagGroups; - }, []) - .flatMap((tagGroup, index, tags) => { - // sort tags within groups - tagGroup.sort((a, b) => { - return getTagOrderWeight(a.tag) - getTagOrderWeight(b.tag); - }); - - // add an empty line between groups - if (tags.length - 1 !== index) { - tagGroup.push(SPACE_TAG_DATA); - } - - return tagGroup; - }) .filter(({ description, tag }) => { if (!description && TAGS_DESCRIPTION_NEEDED.includes(tag)) { return false; diff --git a/src/types.ts b/src/types.ts index 8400ed5..6ad8a80 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,7 +13,7 @@ type LocationDetails = { line: number; column: number }; type Location = { start: LocationDetails; end: LocationDetails }; export type PrettierComment = { - type: "CommentBlock"; + type: "CommentBlock" | "Block"; value: string; start: number; end: number; @@ -36,4 +36,25 @@ export type AST = { directives: []; }; comments: PrettierComment[]; + tokens: { + type: + | "CommentBlock" + | "Block" + | { + label: string; // "function" | "name"; + keyword?: string; + beforeExpr: boolean; + startsExpr: boolean; + rightAssociative: boolean; + isLoop: boolean; + isAssign: boolean; + prefix: boolean; + postfix: boolean; + binop: null; + }; + value: string; + start: number; + end: number; + loc: Location; + }[]; }; diff --git a/tests/__snapshots__/main.test.js.snap b/tests/__snapshots__/main.test.js.snap index e714e88..56b7a8f 100644 --- a/tests/__snapshots__/main.test.js.snap +++ b/tests/__snapshots__/main.test.js.snap @@ -330,6 +330,41 @@ exports[`Sould keep params ordering when more than 10 tags are present 1`] = ` " `; +exports[`param order 1`] = ` +"/** + * @param {string} param0 Description + * @param {object} param1 Description + * @param {number} param2 Description + */ +function fun(param0, param1, param2) {} + +export const SubDomain = { + /** + * @param subDomainAddress2 + * @param {any} subDomainAddress + * @returns {import(\\"axios\\").AxiosResponse} + */ + async subDomain(subDomainAddress2, subDomainAddress) {}, +}; + +/** + * @param {string} param0 Description + * @param {object} param1 Description + * @param {number} param2 Description + */ +function fun(param0: string, param1: {}, param2: () => {}) {} + +/** + * @param {string} param0 Description + * @param {number} param2 Description + * @param {object} param1 Description + */ +const fun = (param0, param1, param2) => { + console.log(\\"\\"); +}; +" +`; + exports[`since 1`] = ` "/** @since 3.16.0 */ " diff --git a/tests/main.test.js b/tests/main.test.js index 2d9c673..e94041e 100755 --- a/tests/main.test.js +++ b/tests/main.test.js @@ -475,3 +475,44 @@ function a() {} expect(subject(text_lf, { endOfLine: "auto" })).toEqual(formatted_lf); expect(subject(text_crlf, { endOfLine: "auto" })).toEqual(formatted_crlf); }); + +test("param order", () => { + const result = subject(` + /** +* @param { string } param0 description +* @param { number } param2 description +* @param { object } param1 description + */ +function fun(param0, param1, param2){} + +export const SubDomain = { +/** + * @param {} subDomainAddress2 + * @param {any} subDomainAddress +* @returns {import('axios').AxiosResponse} +*/ +async subDomain(subDomainAddress2,subDomainAddress) { +}, +}; + +/** + * @param { string } param0 description + * @param { number } param2 description + * @param { object } param1 description + */ + function fun(param0:string, param1:{}, param2:()=>{}){} + + +/** + * @param { string } param0 description + * @param { number } param2 description + * @param { object } param1 description + */ + const fun=(param0, param1, param2)=>{ + console.log('') + } + +`); + + expect(result).toMatchSnapshot(); +});