diff --git a/lib/plugin/utils/ast-utils.ts b/lib/plugin/utils/ast-utils.ts index 162be3653..5eeea754b 100644 --- a/lib/plugin/utils/ast-utils.ts +++ b/lib/plugin/utils/ast-utils.ts @@ -21,6 +21,27 @@ import { UnionTypeNode } from 'typescript'; import { isDynamicallyAdded } from './plugin-utils'; +import { + DocNode, + DocExcerpt, + TSDocParser, + ParserContext, + DocComment, + DocBlock +} from '@microsoft/tsdoc'; + +export function renderDocNode(docNode: DocNode) { + let result: string = ''; + if (docNode) { + if (docNode instanceof DocExcerpt) { + result += docNode.content.toString(); + } + for (const childNode of docNode.getChildNodes()) { + result += renderDocNode(childNode); + } + } + return result; +} export function isArray(type: Type) { const symbol = type.getSymbol(); @@ -121,114 +142,89 @@ export function getMainCommentOfNode( node: Node, sourceFile: SourceFile ): string { - const sourceText = sourceFile.getFullText(); - // in case we decide to include "// comments" - const replaceRegex = - /^\s*\** *@.*$|^\s*\/\*+ *|^\s*\/\/+.*|^\s*\/+ *|^\s*\*+ *| +$| *\**\/ *$/gim; - //const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim; - - const commentResult = []; - const introspectComments = (comments?: CommentRange[]) => - comments?.forEach((comment) => { - const commentSource = sourceText.substring(comment.pos, comment.end); - const oneComment = commentSource.replace(replaceRegex, '').trim(); - if (oneComment) { - commentResult.push(oneComment); - } - }); - - const leadingCommentRanges = getLeadingCommentRanges( - sourceText, - node.getFullStart() + const tsdocParser: TSDocParser = new TSDocParser(); + const parserContext: ParserContext = tsdocParser.parseString( + node.getFullText() ); - introspectComments(leadingCommentRanges); - if (!commentResult.length) { - const trailingCommentRanges = getTrailingCommentRanges( - sourceText, - node.getFullStart() - ); - introspectComments(trailingCommentRanges); + const docComment: DocComment = parserContext.docComment; + return renderDocNode(docComment.summarySection).trim(); +} + +export function parseCommentDocValue(docValue: string, type: ts.Type) { + let value = docValue.replace(/'/g, '"').trim(); + + if (!type || !isString(type)) { + try { + value = JSON.parse(value); + } catch {} + } else if (isString(type)) { + if (value.split(' ').length !== 1 && !value.startsWith('"')) { + value = null; + } else { + value = value.replace(/"/g, ''); + } } - return commentResult.join('\n'); + return value; } -export function getTsDocTagsOfNode( - node: Node, - sourceFile: SourceFile, - typeChecker: TypeChecker -) { - const sourceText = sourceFile.getFullText(); +export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) { + const tsdocParser: TSDocParser = new TSDocParser(); + const parserContext: ParserContext = tsdocParser.parseString( + node.getFullText() + ); + const docComment: DocComment = parserContext.docComment; const tagDefinitions: { [key: string]: { - regex: RegExp; hasProperties: boolean; repeatable: boolean; }; } = { example: { - regex: - /@example *((['"](?.+?)['"])|(?[^ ]+?)|(?(\[.+?\]))) *$/gim, hasProperties: true, repeatable: true - }, - deprecated: { - regex: /@deprecated */gim, - hasProperties: false, - repeatable: false } }; const tagResults: any = {}; - const introspectTsDocTags = (comments?: CommentRange[]) => - comments?.forEach((comment) => { - const commentSource = sourceText.substring(comment.pos, comment.end); - - for (const tag in tagDefinitions) { - const { regex, hasProperties, repeatable } = tagDefinitions[tag]; - - let value: any; - - let execResult: RegExpExecArray; - while ( - (execResult = regex.exec(commentSource)) && - (!hasProperties || execResult.length > 1) - ) { - if (repeatable && !tagResults[tag]) tagResults[tag] = []; - - if (hasProperties) { - const docValue = - execResult.groups?.string ?? - execResult.groups?.booleanOrNumber ?? - (execResult.groups?.array && - execResult.groups.array.replace(/'/g, '"')); - - const type = typeChecker.getTypeAtLocation(node); - - value = docValue; - if (!type || !isString(type)) { - try { - value = JSON.parse(value); - } catch {} - } - } else { - value = true; - } - if (repeatable) { - tagResults[tag].push(value); - } else { - tagResults[tag] = value; + const introspectTsDocTags = (docComment: DocComment) => { + for (const tag in tagDefinitions) { + const { hasProperties, repeatable } = tagDefinitions[tag]; + const blocks = docComment.customBlocks.filter( + (block) => block.blockTag.tagName === `@${tag}` + ); + if (blocks.length === 0) continue; + if (repeatable && !tagResults[tag]) tagResults[tag] = []; + const type = typeChecker.getTypeAtLocation(node); + if (hasProperties) { + blocks.forEach((block) => { + const docValue = renderDocNode(block.content).split('\n')[0]; + const value = parseCommentDocValue(docValue, type); + + if (value !== null) { + if (repeatable) { + tagResults[tag].push(value); + } else { + tagResults[tag] = value; + } } - } + }); + } else { + tagResults[tag] = true; } - }); + } + if (docComment.remarksBlock) { + tagResults['remarks'] = renderDocNode( + docComment.remarksBlock.content + ).trim(); + } + if (docComment.deprecatedBlock) { + tagResults['deprecated'] = true; + } + }; + introspectTsDocTags(docComment); - const leadingCommentRanges = getLeadingCommentRanges( - sourceText, - node.getFullStart() - ); - introspectTsDocTags(leadingCommentRanges); return tagResults; } diff --git a/lib/plugin/visitors/controller-class.visitor.ts b/lib/plugin/visitors/controller-class.visitor.ts index 19131d049..68162c6e5 100644 --- a/lib/plugin/visitors/controller-class.visitor.ts +++ b/lib/plugin/visitors/controller-class.visitor.ts @@ -223,7 +223,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor { if (!extractedComments) { return []; } - const tags = getTsDocTagsOfNode(node, sourceFile, typeChecker); + const tags = getTsDocTagsOfNode(node, typeChecker); const properties = [ factory.createPropertyAssignment( @@ -233,6 +233,18 @@ export class ControllerClassVisitor extends AbstractFileVisitor { ...(apiOperationExistingProps ?? factory.createNodeArray()) ]; + const hasRemarksKey = hasPropertyKey( + 'description', + factory.createNodeArray(apiOperationExistingProps) + ); + if (!hasRemarksKey && tags.remarks) { + const remarksPropertyAssignment = factory.createPropertyAssignment( + 'description', + createLiteralFromAnyValue(factory, tags.remarks) + ); + properties.push(remarksPropertyAssignment); + } + const hasDeprecatedKey = hasPropertyKey( 'deprecated', factory.createNodeArray(apiOperationExistingProps) diff --git a/lib/plugin/visitors/model-class.visitor.ts b/lib/plugin/visitors/model-class.visitor.ts index d1399ea06..35e50d2ce 100644 --- a/lib/plugin/visitors/model-class.visitor.ts +++ b/lib/plugin/visitors/model-class.visitor.ts @@ -695,7 +695,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { return result; } - const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength) ?? minLength; + const clonedMinLength = + this.clonePrimitiveLiteral(factory, minLength) ?? minLength; if (clonedMinLength) { result.push( factory.createPropertyAssignment('minLength', clonedMinLength) @@ -707,10 +708,8 @@ export class ModelClassVisitor extends AbstractFileVisitor { if (!canReferenceNode(maxLength, options)) { return result; } - const clonedMaxLength = this.clonePrimitiveLiteral( - factory, - maxLength - ) ?? maxLength; + const clonedMaxLength = + this.clonePrimitiveLiteral(factory, maxLength) ?? maxLength; if (clonedMaxLength) { result.push( factory.createPropertyAssignment('maxLength', clonedMaxLength) @@ -822,7 +821,7 @@ export class ModelClassVisitor extends AbstractFileVisitor { } const propertyAssignments = []; const comments = getMainCommentOfNode(node, sourceFile); - const tags = getTsDocTagsOfNode(node, sourceFile, typeChecker); + const tags = getTsDocTagsOfNode(node, typeChecker); const keyOfComment = options.dtoKeyOfComment; if (!hasPropertyKey(keyOfComment, existingProperties) && comments) { diff --git a/package-lock.json b/package-lock.json index 2f204ffc5..69ce4fc70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "7.2.0", "license": "MIT", "dependencies": { + "@microsoft/tsdoc": "^0.14.2", "@nestjs/mapped-types": "2.0.4", "js-yaml": "4.1.0", "lodash": "4.17.21", @@ -2146,6 +2147,11 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "node_modules/@nestjs/common": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.1.tgz", @@ -14687,6 +14693,11 @@ "integrity": "sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==", "dev": true }, + "@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" + }, "@nestjs/common": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.1.tgz", diff --git a/package.json b/package.json index a81aa68d9..510ebd396 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "start:debug": "nest start --watch --debug" }, "dependencies": { + "@microsoft/tsdoc": "^0.14.2", "@nestjs/mapped-types": "2.0.4", "js-yaml": "4.1.0", "lodash": "4.17.21", diff --git a/test/plugin/fixtures/app.controller.ts b/test/plugin/fixtures/app.controller.ts index 385a21c5c..e407a9db7 100644 --- a/test/plugin/fixtures/app.controller.ts +++ b/test/plugin/fixtures/app.controller.ts @@ -9,6 +9,8 @@ export class AppController { /** * create a Cat + * + * @remarks Creating a test cat * * @returns {Promise} * @memberof AppController @@ -71,6 +73,8 @@ let AppController = exports.AppController = class AppController { /** * create a Cat * + * @remarks Creating a test cat + * * @returns {Promise} * @memberof AppController */ @@ -104,7 +108,7 @@ let AppController = exports.AppController = class AppController { async findAll() { } }; __decorate([ - openapi.ApiOperation({ summary: \"create a Cat\" }), + openapi.ApiOperation({ summary: \"create a Cat\", description: \"Creating a test cat\" }), (0, common_1.Post)(), openapi.ApiResponse({ status: 201, type: Cat }) ], AppController.prototype, \"create\", null);