Skip to content

Commit 979f256

Browse files
authoredFeb 7, 2024
Merge pull request #2822 from lit26/features/enhanced-comments
feat(introspectComments): Enhanced comment parsing
2 parents 500a718 + 7e6b64f commit 979f256

File tree

6 files changed

+117
-94
lines changed

6 files changed

+117
-94
lines changed
 

‎lib/plugin/utils/ast-utils.ts

+82-86
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@ import {
2121
UnionTypeNode
2222
} from 'typescript';
2323
import { isDynamicallyAdded } from './plugin-utils';
24+
import {
25+
DocNode,
26+
DocExcerpt,
27+
TSDocParser,
28+
ParserContext,
29+
DocComment,
30+
DocBlock
31+
} from '@microsoft/tsdoc';
32+
33+
export function renderDocNode(docNode: DocNode) {
34+
let result: string = '';
35+
if (docNode) {
36+
if (docNode instanceof DocExcerpt) {
37+
result += docNode.content.toString();
38+
}
39+
for (const childNode of docNode.getChildNodes()) {
40+
result += renderDocNode(childNode);
41+
}
42+
}
43+
return result;
44+
}
2445

2546
export function isArray(type: Type) {
2647
const symbol = type.getSymbol();
@@ -121,114 +142,89 @@ export function getMainCommentOfNode(
121142
node: Node,
122143
sourceFile: SourceFile
123144
): string {
124-
const sourceText = sourceFile.getFullText();
125-
// in case we decide to include "// comments"
126-
const replaceRegex =
127-
/^\s*\** *@.*$|^\s*\/\*+ *|^\s*\/\/+.*|^\s*\/+ *|^\s*\*+ *| +$| *\**\/ *$/gim;
128-
//const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;
129-
130-
const commentResult = [];
131-
const introspectComments = (comments?: CommentRange[]) =>
132-
comments?.forEach((comment) => {
133-
const commentSource = sourceText.substring(comment.pos, comment.end);
134-
const oneComment = commentSource.replace(replaceRegex, '').trim();
135-
if (oneComment) {
136-
commentResult.push(oneComment);
137-
}
138-
});
139-
140-
const leadingCommentRanges = getLeadingCommentRanges(
141-
sourceText,
142-
node.getFullStart()
145+
const tsdocParser: TSDocParser = new TSDocParser();
146+
const parserContext: ParserContext = tsdocParser.parseString(
147+
node.getFullText()
143148
);
144-
introspectComments(leadingCommentRanges);
145-
if (!commentResult.length) {
146-
const trailingCommentRanges = getTrailingCommentRanges(
147-
sourceText,
148-
node.getFullStart()
149-
);
150-
introspectComments(trailingCommentRanges);
149+
const docComment: DocComment = parserContext.docComment;
150+
return renderDocNode(docComment.summarySection).trim();
151+
}
152+
153+
export function parseCommentDocValue(docValue: string, type: ts.Type) {
154+
let value = docValue.replace(/'/g, '"').trim();
155+
156+
if (!type || !isString(type)) {
157+
try {
158+
value = JSON.parse(value);
159+
} catch {}
160+
} else if (isString(type)) {
161+
if (value.split(' ').length !== 1 && !value.startsWith('"')) {
162+
value = null;
163+
} else {
164+
value = value.replace(/"/g, '');
165+
}
151166
}
152-
return commentResult.join('\n');
167+
return value;
153168
}
154169

155-
export function getTsDocTagsOfNode(
156-
node: Node,
157-
sourceFile: SourceFile,
158-
typeChecker: TypeChecker
159-
) {
160-
const sourceText = sourceFile.getFullText();
170+
export function getTsDocTagsOfNode(node: Node, typeChecker: TypeChecker) {
171+
const tsdocParser: TSDocParser = new TSDocParser();
172+
const parserContext: ParserContext = tsdocParser.parseString(
173+
node.getFullText()
174+
);
175+
const docComment: DocComment = parserContext.docComment;
161176

162177
const tagDefinitions: {
163178
[key: string]: {
164-
regex: RegExp;
165179
hasProperties: boolean;
166180
repeatable: boolean;
167181
};
168182
} = {
169183
example: {
170-
regex:
171-
/@example *((['"](?<string>.+?)['"])|(?<booleanOrNumber>[^ ]+?)|(?<array>(\[.+?\]))) *$/gim,
172184
hasProperties: true,
173185
repeatable: true
174-
},
175-
deprecated: {
176-
regex: /@deprecated */gim,
177-
hasProperties: false,
178-
repeatable: false
179186
}
180187
};
181188

182189
const tagResults: any = {};
183-
const introspectTsDocTags = (comments?: CommentRange[]) =>
184-
comments?.forEach((comment) => {
185-
const commentSource = sourceText.substring(comment.pos, comment.end);
186-
187-
for (const tag in tagDefinitions) {
188-
const { regex, hasProperties, repeatable } = tagDefinitions[tag];
189-
190-
let value: any;
191-
192-
let execResult: RegExpExecArray;
193-
while (
194-
(execResult = regex.exec(commentSource)) &&
195-
(!hasProperties || execResult.length > 1)
196-
) {
197-
if (repeatable && !tagResults[tag]) tagResults[tag] = [];
198-
199-
if (hasProperties) {
200-
const docValue =
201-
execResult.groups?.string ??
202-
execResult.groups?.booleanOrNumber ??
203-
(execResult.groups?.array &&
204-
execResult.groups.array.replace(/'/g, '"'));
205-
206-
const type = typeChecker.getTypeAtLocation(node);
207-
208-
value = docValue;
209-
if (!type || !isString(type)) {
210-
try {
211-
value = JSON.parse(value);
212-
} catch {}
213-
}
214-
} else {
215-
value = true;
216-
}
217190

218-
if (repeatable) {
219-
tagResults[tag].push(value);
220-
} else {
221-
tagResults[tag] = value;
191+
const introspectTsDocTags = (docComment: DocComment) => {
192+
for (const tag in tagDefinitions) {
193+
const { hasProperties, repeatable } = tagDefinitions[tag];
194+
const blocks = docComment.customBlocks.filter(
195+
(block) => block.blockTag.tagName === `@${tag}`
196+
);
197+
if (blocks.length === 0) continue;
198+
if (repeatable && !tagResults[tag]) tagResults[tag] = [];
199+
const type = typeChecker.getTypeAtLocation(node);
200+
if (hasProperties) {
201+
blocks.forEach((block) => {
202+
const docValue = renderDocNode(block.content).split('\n')[0];
203+
const value = parseCommentDocValue(docValue, type);
204+
205+
if (value !== null) {
206+
if (repeatable) {
207+
tagResults[tag].push(value);
208+
} else {
209+
tagResults[tag] = value;
210+
}
222211
}
223-
}
212+
});
213+
} else {
214+
tagResults[tag] = true;
224215
}
225-
});
216+
}
217+
if (docComment.remarksBlock) {
218+
tagResults['remarks'] = renderDocNode(
219+
docComment.remarksBlock.content
220+
).trim();
221+
}
222+
if (docComment.deprecatedBlock) {
223+
tagResults['deprecated'] = true;
224+
}
225+
};
226+
introspectTsDocTags(docComment);
226227

227-
const leadingCommentRanges = getLeadingCommentRanges(
228-
sourceText,
229-
node.getFullStart()
230-
);
231-
introspectTsDocTags(leadingCommentRanges);
232228
return tagResults;
233229
}
234230

‎lib/plugin/visitors/controller-class.visitor.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
223223
if (!extractedComments) {
224224
return [];
225225
}
226-
const tags = getTsDocTagsOfNode(node, sourceFile, typeChecker);
226+
const tags = getTsDocTagsOfNode(node, typeChecker);
227227

228228
const properties = [
229229
factory.createPropertyAssignment(
@@ -233,6 +233,18 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
233233
...(apiOperationExistingProps ?? factory.createNodeArray())
234234
];
235235

236+
const hasRemarksKey = hasPropertyKey(
237+
'description',
238+
factory.createNodeArray(apiOperationExistingProps)
239+
);
240+
if (!hasRemarksKey && tags.remarks) {
241+
const remarksPropertyAssignment = factory.createPropertyAssignment(
242+
'description',
243+
createLiteralFromAnyValue(factory, tags.remarks)
244+
);
245+
properties.push(remarksPropertyAssignment);
246+
}
247+
Has a comment. Original line has a comment.
236248
const hasDeprecatedKey = hasPropertyKey(
237249
'deprecated',
238250
factory.createNodeArray(apiOperationExistingProps)

‎lib/plugin/visitors/model-class.visitor.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
695695
return result;
696696
}
697697

698-
const clonedMinLength = this.clonePrimitiveLiteral(factory, minLength) ?? minLength;
698+
const clonedMinLength =
699+
this.clonePrimitiveLiteral(factory, minLength) ?? minLength;
699700
if (clonedMinLength) {
700701
result.push(
701702
factory.createPropertyAssignment('minLength', clonedMinLength)
@@ -707,10 +708,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
707708
if (!canReferenceNode(maxLength, options)) {
708709
return result;
709710
}
710-
const clonedMaxLength = this.clonePrimitiveLiteral(
711-
factory,
712-
maxLength
713-
) ?? maxLength;
711+
const clonedMaxLength =
712+
this.clonePrimitiveLiteral(factory, maxLength) ?? maxLength;
714713
if (clonedMaxLength) {
715714
result.push(
716715
factory.createPropertyAssignment('maxLength', clonedMaxLength)
@@ -822,7 +821,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
822821
}
823822
const propertyAssignments = [];
824823
const comments = getMainCommentOfNode(node, sourceFile);
825-
const tags = getTsDocTagsOfNode(node, sourceFile, typeChecker);
824+
const tags = getTsDocTagsOfNode(node, typeChecker);
826825

827826
const keyOfComment = options.dtoKeyOfComment;
828827
if (!hasPropertyKey(keyOfComment, existingProperties) && comments) {

‎package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"start:debug": "nest start --watch --debug"
2525
},
2626
"dependencies": {
27+
"@microsoft/tsdoc": "^0.14.2",
2728
"@nestjs/mapped-types": "2.0.4",
2829
"js-yaml": "4.1.0",
2930
"lodash": "4.17.21",

‎test/plugin/fixtures/app.controller.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export class AppController {
99
1010
/**
1111
* create a Cat
12+
*
13+
* @remarks Creating a test cat
1214
*
1315
* @returns {Promise<Cat>}
1416
* @memberof AppController
@@ -71,6 +73,8 @@ let AppController = exports.AppController = class AppController {
7173
/**
7274
* create a Cat
7375
*
76+
* @remarks Creating a test cat
77+
*
7478
* @returns {Promise<Cat>}
7579
* @memberof AppController
7680
*/
@@ -104,7 +108,7 @@ let AppController = exports.AppController = class AppController {
104108
async findAll() { }
105109
};
106110
__decorate([
107-
openapi.ApiOperation({ summary: \"create a Cat\" }),
111+
openapi.ApiOperation({ summary: \"create a Cat\", description: \"Creating a test cat\" }),
108112
(0, common_1.Post)(),
109113
openapi.ApiResponse({ status: 201, type: Cat })
110114
], AppController.prototype, \"create\", null);

0 commit comments

Comments
 (0)
Please sign in to comment.