Skip to content

Commit

Permalink
feat(): add comments introspection
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilmysliwiec committed Aug 20, 2020
1 parent 7b96d7f commit b2daad6
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 119 deletions.
4 changes: 3 additions & 1 deletion lib/plugin/merge-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ export interface PluginOptions {
classValidatorShim?: boolean;
dtoKeyOfComment?: string;
controllerKeyOfComment?: string;
introspectComments?: boolean;
}

const defaultOptions: PluginOptions = {
dtoFileNameSuffix: ['.dto.ts', '.entity.ts'],
controllerFileNameSuffix: ['.controller.ts'],
classValidatorShim: true,
dtoKeyOfComment: 'description',
controllerKeyOfComment: 'description'
controllerKeyOfComment: 'description',
introspectComments: false
};

export const mergePluginOptions = (
Expand Down
31 changes: 17 additions & 14 deletions lib/plugin/utils/ast-utils.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import {
CallExpression,
CommentRange,
Decorator,
getLeadingCommentRanges,
getTrailingCommentRanges,
Identifier,
LeftHandSideExpression,
Node,
ObjectFlags,
ObjectType,
PropertyAccessExpression,
SourceFile,
SyntaxKind,
Type,
TypeChecker,
TypeFlags,
TypeFormatFlags,
SourceFile,
getTrailingCommentRanges,
getLeadingCommentRanges,
CommentRange
TypeFormatFlags
} from 'typescript';
import { isDynamicallyAdded } from './plugin-utils';

Expand Down Expand Up @@ -105,22 +105,21 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) {
export function getMainCommentAndExamplesOfNode(
node: Node,
sourceFile: SourceFile,
needExamples?: boolean
includeExamples?: boolean
): [string, string[]] {
const sourceText = sourceFile.getFullText();

const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/\/+.*|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;

const commentResult = [];
const examplesResult = [];
const extractCommentsAndExamples = (comments?: CommentRange[]) =>
const introspectCommentsAndExamples = (comments?: CommentRange[]) =>
comments?.forEach((comment) => {
const commentSource = sourceText.substring(comment.pos, comment.end);
const oneComment = commentSource.replace(replaceRegex, '').trim();
if (oneComment) {
commentResult.push(oneComment);
}
if (needExamples) {
if (includeExamples) {
const regexOfExample = /@example *['"]?([^ ]+?)['"]? *$/gim;
let execResult: RegExpExecArray;
while (
Expand All @@ -131,15 +130,19 @@ export function getMainCommentAndExamplesOfNode(
}
}
});
extractCommentsAndExamples(
getLeadingCommentRanges(sourceText, node.getFullStart())

const leadingCommentRanges = getLeadingCommentRanges(
sourceText,
node.getFullStart()
);
introspectCommentsAndExamples(leadingCommentRanges);
if (!commentResult.length) {
extractCommentsAndExamples(
getTrailingCommentRanges(sourceText, node.getFullStart())
const trailingCommentRanges = getTrailingCommentRanges(
sourceText,
node.getFullStart()
);
introspectCommentsAndExamples(trailingCommentRanges);
}

return [commentResult.join('\n'), examplesResult];
}

Expand Down
69 changes: 42 additions & 27 deletions lib/plugin/visitors/controller-class.visitor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { compact, head } from 'lodash';
import * as ts from 'typescript';
import { ApiResponse, ApiOperation } from '../../decorators';
import { ApiOperation, ApiResponse } from '../../decorators';
import { PluginOptions } from '../merge-options';
import { OPENAPI_NAMESPACE } from '../plugin-constants';
import {
getDecoratorArguments,
Expand All @@ -13,7 +14,6 @@ import {
replaceImportPath
} from '../utils/plugin-utils';
import { AbstractFileVisitor } from './abstract.visitor';
import { PluginOptions } from '../merge-options';

export class ControllerClassVisitor extends AbstractFileVisitor {
visit(
Expand All @@ -27,13 +27,17 @@ export class ControllerClassVisitor extends AbstractFileVisitor {

const visitNode = (node: ts.Node): ts.Node => {
if (ts.isMethodDeclaration(node)) {
return this.addDecoratorToNode(
node,
typeChecker,
options,
sourceFile.fileName,
sourceFile
);
try {
return this.addDecoratorToNode(
node,
typeChecker,
options,
sourceFile.fileName,
sourceFile
);
} catch {
return node;
}
}
return ts.visitEachChild(node, visitNode, ctx);
};
Expand All @@ -53,7 +57,7 @@ export class ControllerClassVisitor extends AbstractFileVisitor {

node.decorators = Object.assign(
[
...this.createApiOperationOrEmptyInArray(
...this.createApiOperationDecorator(
node,
nodeArray,
options,
Expand All @@ -80,36 +84,47 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
return node;
}

createApiOperationOrEmptyInArray(
createApiOperationDecorator(
node: ts.MethodDeclaration,
nodeArray: ts.NodeArray<ts.Decorator>,
options: PluginOptions,
sourceFile: ts.SourceFile
) {
if (!options.introspectComments) {
return [];
}
const keyToGenerate = options.controllerKeyOfComment;
const apiOperationDecorator = getDecoratorOrUndefinedByNames(
[ApiOperation.name],
nodeArray
);
let apiOperationOptions: ts.ObjectLiteralExpression;
let apiOperationOptionsProperties: ts.NodeArray<ts.PropertyAssignment>;
let comments;
const apiOperationExpr: ts.ObjectLiteralExpression | undefined =
apiOperationDecorator &&
head(getDecoratorArguments(apiOperationDecorator));
const apiOperationExprProperties =
apiOperationExpr &&
(apiOperationExpr.properties as ts.NodeArray<ts.PropertyAssignment>);

if (
// No ApiOperation or No ApiOperationOptions or ApiOperationOptions is empty or No description in ApiOperationOptions
(!apiOperationDecorator ||
!(apiOperationOptions = head(
getDecoratorArguments(apiOperationDecorator)
)) ||
!(apiOperationOptionsProperties = apiOperationOptions.properties as ts.NodeArray<
ts.PropertyAssignment
>) ||
!hasPropertyKey(keyToGenerate, apiOperationOptionsProperties)) &&
// Has comments
([comments] = getMainCommentAndExamplesOfNode(node, sourceFile))[0]
!apiOperationDecorator ||
!apiOperationExpr ||
!apiOperationExprProperties ||
!hasPropertyKey(keyToGenerate, apiOperationExprProperties)
) {
const [extractedComments] = getMainCommentAndExamplesOfNode(
node,
sourceFile
);
if (!extractedComments) {
// Node does not have any comments
return [];
}
const properties = [
ts.createPropertyAssignment(keyToGenerate, ts.createLiteral(comments)),
...(apiOperationOptionsProperties ?? ts.createNodeArray())
ts.createPropertyAssignment(
keyToGenerate,
ts.createLiteral(extractedComments)
),
...(apiOperationExprProperties ?? ts.createNodeArray())
];
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> = ts.createNodeArray(
[ts.createObjectLiteral(compact(properties))]
Expand Down
124 changes: 59 additions & 65 deletions lib/plugin/visitors/model-class.visitor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { compact, flatten, head } from 'lodash';
import * as ts from 'typescript';
import {
ApiHideProperty,
ApiProperty,
ApiPropertyOptional
} from '../../decorators';
import { ApiHideProperty } from '../../decorators';
import { PluginOptions } from '../merge-options';
import { METADATA_FACTORY_NAME } from '../plugin-constants';
import {
Expand Down Expand Up @@ -49,24 +45,9 @@ export class ModelClassVisitor extends AbstractFileVisitor {
return node;
}

let apiOperationOptionsProperties: ts.NodeArray<ts.PropertyAssignment>;
const apiPropertyDecorator = getDecoratorOrUndefinedByNames(
[ApiProperty.name, ApiPropertyOptional.name],
decorators
);
if (apiPropertyDecorator) {
apiOperationOptionsProperties = head(
getDecoratorArguments(apiPropertyDecorator)
)?.properties;
node.decorators = ts.createNodeArray([
...node.decorators.filter(
(decorator) => decorator != apiPropertyDecorator
)
]);
}

const isPropertyStatic = (node.modifiers || []).some(
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword
(modifier: ts.Modifier) =>
modifier.kind === ts.SyntaxKind.StaticKeyword
);
if (isPropertyStatic) {
return node;
Expand All @@ -76,7 +57,6 @@ export class ModelClassVisitor extends AbstractFileVisitor {
node,
typeChecker,
options,
apiOperationOptionsProperties ?? ts.createNodeArray(),
sourceFile.fileName,
sourceFile,
metadata
Expand Down Expand Up @@ -135,15 +115,14 @@ export class ModelClassVisitor extends AbstractFileVisitor {
compilerNode: ts.PropertyDeclaration,
typeChecker: ts.TypeChecker,
options: PluginOptions,
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
hostFilename: string,
sourceFile: ts.SourceFile,
metadata: ClassMetadata
) {
const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
compilerNode,
typeChecker,
existingProperties,
ts.createNodeArray(),
options,
hostFilename,
sourceFile
Expand All @@ -168,47 +147,8 @@ export class ModelClassVisitor extends AbstractFileVisitor {
): ts.ObjectLiteralExpression {
const isRequired = !node.questionToken;

const descriptionPropertyWapper = [];
const examplesPropertyWapper = [];
if (sourceFile) {
const [comments, examples] = getMainCommentAndExamplesOfNode(
node,
sourceFile,
true
);
const keyOfComment = options?.dtoKeyOfComment ?? 'description';
if (!hasPropertyKey(keyOfComment, existingProperties) && comments) {
descriptionPropertyWapper.push(
ts.createPropertyAssignment(keyOfComment, ts.createLiteral(comments))
);
}
if (
!(
hasPropertyKey('example', existingProperties) ||
hasPropertyKey('examples', existingProperties)
) &&
examples.length
) {
if (examples.length == 1) {
examplesPropertyWapper.push(
ts.createPropertyAssignment(
'example',
ts.createLiteral(examples[0])
)
);
} else {
examplesPropertyWapper.push(
ts.createPropertyAssignment(
'examples',
ts.createArrayLiteral(examples.map((e) => ts.createLiteral(e)))
)
);
}
}
}
let properties = [
...existingProperties,
...descriptionPropertyWapper,
!hasPropertyKey('required', existingProperties) &&
ts.createPropertyAssignment('required', ts.createLiteral(isRequired)),
...this.createTypePropertyAssignments(
Expand All @@ -217,7 +157,12 @@ export class ModelClassVisitor extends AbstractFileVisitor {
existingProperties,
hostFilename
),
...examplesPropertyWapper,
...this.createDescriptionAndExamplePropertyAssigments(
node,
existingProperties,
options,
sourceFile
),
this.createDefaultPropertyAssignment(node, existingProperties),
this.createEnumPropertyAssignment(
node,
Expand Down Expand Up @@ -488,4 +433,53 @@ export class ModelClassVisitor extends AbstractFileVisitor {
}
metadata[propertyName] = objectLiteral;
}

createDescriptionAndExamplePropertyAssigments(
node: ts.PropertyDeclaration | ts.PropertySignature,
existingProperties: ts.NodeArray<
ts.PropertyAssignment
> = ts.createNodeArray(),
options: PluginOptions = {},
sourceFile?: ts.SourceFile
): ts.PropertyAssignment[] {
if (!options.introspectComments || !sourceFile) {
return [];
}
const propertyAssignments = [];
const [comments, examples] = getMainCommentAndExamplesOfNode(
node,
sourceFile,
true
);

const keyOfComment = options.dtoKeyOfComment;
if (!hasPropertyKey(keyOfComment, existingProperties) && comments) {
const descriptionPropertyAssignment = ts.createPropertyAssignment(
keyOfComment,
ts.createLiteral(comments)
);
propertyAssignments.push(descriptionPropertyAssignment);
}

const hasExampleOrExamplesKey =
hasPropertyKey('example', existingProperties) ||
hasPropertyKey('examples', existingProperties);

if (!hasExampleOrExamplesKey && examples.length) {
if (examples.length === 1) {
const examplePropertyAssignment = ts.createPropertyAssignment(
'example',
ts.createLiteral(examples[0])
);
propertyAssignments.push(examplePropertyAssignment);
} else {
const examplesPropertyAssignment = ts.createPropertyAssignment(
'examples',
ts.createArrayLiteral(examples.map((e) => ts.createLiteral(e)))
);
propertyAssignments.push(examplesPropertyAssignment);
}
}
return propertyAssignments;
}
}

0 comments on commit b2daad6

Please sign in to comment.