Skip to content

Commit

Permalink
auto generate description from comments
Browse files Browse the repository at this point in the history
  • Loading branch information
hydrant committed Aug 5, 2020
1 parent 6686e48 commit ecab1d9
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 21 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/.idea
/.awcache
/.vscode
.history

# misc
npm-debug.log
Expand Down
48 changes: 47 additions & 1 deletion lib/plugin/utils/ast-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
Type,
TypeChecker,
TypeFlags,
TypeFormatFlags
TypeFormatFlags,
SourceFile,
getTrailingCommentRanges,
getLeadingCommentRanges,
CommentRange
} from 'typescript';
import { isDynamicallyAdded } from './plugin-utils';

Expand Down Expand Up @@ -74,6 +78,8 @@ export function hasObjectFlag(type: Type, flag: ObjectFlags) {
return ((type as ObjectType).objectFlags & flag) === flag;
}

// exprot function getDescription()

export function getText(
type: Type,
typeChecker: TypeChecker,
Expand All @@ -98,6 +104,46 @@ export function getDefaultTypeFormatFlags(enclosingNode: Node) {
return formatFlags;
}

export function getMainCommentAnExamplesOfNode(
node: Node,
sourceFile: SourceFile,
needExamples?: boolean
): [string, string[]] {
const sourceText = sourceFile.getText();
const replaceRegex = /^ *\** *@.*$|^ *\/\*+ *|^ *\/+ *|^ *\*+ *| +$| *\**\/ *$/gim;

const commentResult = [];
const examplesResult = [];
const extractCommentsAndExamples = (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) {
const regexOfExample = /@example *['"]?([^ ]+?)['"]? *$/gim;
let execResult: RegExpExecArray;
while (
(execResult = regexOfExample.exec(commentSource)) &&
execResult.length > 1
) {
examplesResult.push(execResult[1]);
}
}
});
extractCommentsAndExamples(
getLeadingCommentRanges(sourceText, node.getFullStart())
);
if (!commentResult.length) {
extractCommentsAndExamples(
getTrailingCommentRanges(sourceText, node.getFullStart())
);
}

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

export function getDecoratorArguments(decorator: Decorator) {
const callExpression = decorator.expression;
return (callExpression && (callExpression as CallExpression).arguments) || [];
Expand Down
76 changes: 67 additions & 9 deletions lib/plugin/visitors/controller-class.visitor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { compact, head } from 'lodash';
import * as ts from 'typescript';
import { ApiResponse } from '../../decorators';
import { ApiResponse, ApiOperation } from '../../decorators';
import { OPENAPI_NAMESPACE } from '../plugin-constants';
import { getDecoratorArguments } from '../utils/ast-utils';
import {
getDecoratorArguments,
getMainCommentAnExamplesOfNode
} from '../utils/ast-utils';
import {
getDecoratorOrUndefinedByNames,
getTypeReferenceAsString,
Expand All @@ -22,7 +25,12 @@ export class ControllerClassVisitor extends AbstractFileVisitor {

const visitNode = (node: ts.Node): ts.Node => {
if (ts.isMethodDeclaration(node)) {
return this.addDecoratorToNode(node, typeChecker, sourceFile.fileName);
return this.addDecoratorToNode(
node,
typeChecker,
sourceFile.fileName,
sourceFile
);
}
return ts.visitEachChild(node, visitNode, ctx);
};
Expand All @@ -32,17 +40,17 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
addDecoratorToNode(
compilerNode: ts.MethodDeclaration,
typeChecker: ts.TypeChecker,
hostFilename: string
hostFilename: string,
sourceFile: ts.SourceFile
): ts.MethodDeclaration {
const node = ts.getMutableClone(compilerNode);
if (!node.decorators) {
return compilerNode;
}
const { pos, end } = node.decorators;
const nodeArray = node.decorators || ts.createNodeArray();
const { pos, end } = nodeArray;

node.decorators = Object.assign(
[
...node.decorators,
...this.createApiOperationOrEmptyInArray(node, nodeArray, sourceFile),
...nodeArray,
ts.createDecorator(
ts.createCall(
ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiResponse.name}`),
Expand All @@ -63,6 +71,56 @@ export class ControllerClassVisitor extends AbstractFileVisitor {
return node;
}

createApiOperationOrEmptyInArray(
node: ts.MethodDeclaration,
nodeArray: ts.NodeArray<ts.Decorator>,
sourceFile: ts.SourceFile
) {
const descriptionKey = 'description';
const apiOperationDecorator = getDecoratorOrUndefinedByNames(
[ApiOperation.name],
nodeArray
);
let apiOperationOptions: ts.ObjectLiteralExpression;
let apiOperationOptionsProperties: ts.NodeArray<ts.PropertyAssignment>;
let comments;
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(descriptionKey, apiOperationOptionsProperties)) &&
// Has comments
([comments] = getMainCommentAnExamplesOfNode(node, sourceFile))[0]
) {
const properties = [
ts.createPropertyAssignment(descriptionKey, ts.createLiteral(comments)),
...(apiOperationOptionsProperties ?? ts.createNodeArray())
];
const apiOperationDecoratorArguments: ts.NodeArray<ts.Expression> = ts.createNodeArray(
[ts.createObjectLiteral(compact(properties))]
);
if (apiOperationDecorator) {
(apiOperationDecorator.expression as ts.CallExpression).arguments = apiOperationDecoratorArguments;
} else {
return [
ts.createDecorator(
ts.createCall(
ts.createIdentifier(`${OPENAPI_NAMESPACE}.${ApiOperation.name}`),
undefined,
apiOperationDecoratorArguments
)
)
];
}
}
return [];
}

createDecoratorObjectLiteralExpr(
node: ts.MethodDeclaration,
typeChecker: ts.TypeChecker,
Expand Down
85 changes: 80 additions & 5 deletions lib/plugin/visitors/model-class.visitor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { compact, flatten, head } from 'lodash';
import * as ts from 'typescript';
import { ApiHideProperty } from '../../decorators';
import {
ApiHideProperty,
ApiProperty,
ApiPropertyOptional
} from '../../decorators';
import { PluginOptions } from '../merge-options';
import { METADATA_FACTORY_NAME } from '../plugin-constants';
import { getDecoratorArguments, getText, isEnum } from '../utils/ast-utils';
import {
getDecoratorArguments,
getText,
isEnum,
getMainCommentAnExamplesOfNode
} from '../utils/ast-utils';
import {
extractTypeArgumentIfArray,
getDecoratorOrUndefinedByNames,
Expand Down Expand Up @@ -40,6 +49,23 @@ export class ModelClassVisitor extends AbstractFileVisitor {
if (hidePropertyDecorator) {
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
);
Expand All @@ -51,6 +77,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
node,
typeChecker,
options,
apiOperationOptionsProperties ?? ts.createNodeArray(),
sourceFile.fileName,
sourceFile
);
Expand Down Expand Up @@ -100,15 +127,17 @@ export class ModelClassVisitor extends AbstractFileVisitor {
compilerNode: ts.PropertyDeclaration,
typeChecker: ts.TypeChecker,
options: PluginOptions,
existingProperties: ts.NodeArray<ts.PropertyAssignment>,
hostFilename: string,
sourceFile: ts.SourceFile
) {
const objectLiteralExpr = this.createDecoratorObjectLiteralExpr(
compilerNode,
typeChecker,
ts.createNodeArray(),
existingProperties,
options,
hostFilename
hostFilename,
sourceFile
);
this.addClassMetadata(compilerNode, objectLiteralExpr, sourceFile);
}
Expand All @@ -120,12 +149,57 @@ export class ModelClassVisitor extends AbstractFileVisitor {
ts.PropertyAssignment
> = ts.createNodeArray(),
options: PluginOptions = {},
hostFilename = ''
hostFilename = '',
sourceFile?: ts.SourceFile
): ts.ObjectLiteralExpression {
const isRequired = !node.questionToken;

const descriptionPropertyWapper = [];
const examplesPropertyWapper = [];
if (sourceFile) {
const [comments, examples] = getMainCommentAnExamplesOfNode(
node,
sourceFile,
true
);
if (!hasPropertyKey('description', existingProperties) && comments) {
descriptionPropertyWapper.push(
ts.createPropertyAssignment('description', ts.createLiteral(comments))
);
}
if (
!(
hasPropertyKey('example', existingProperties) ||
hasPropertyKey('examples', existingProperties)
) &&
examples.length
) {
console.log(
examples,
hasPropertyKey('example', existingProperties),
hasPropertyKey('examples', existingProperties),
'==============================================='
);
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.createTypePropertyAssignment(
Expand All @@ -134,6 +208,7 @@ export class ModelClassVisitor extends AbstractFileVisitor {
existingProperties,
hostFilename
),
...examplesPropertyWapper,
this.createDefaultPropertyAssignment(node, existingProperties),
this.createEnumPropertyAssignment(
node,
Expand Down
Loading

0 comments on commit ecab1d9

Please sign in to comment.