Skip to content

Commit

Permalink
refactor(resolve): decouple TypeResolver with abstract Transformer class
Browse files Browse the repository at this point in the history
  • Loading branch information
jackey8616 committed Oct 17, 2024
1 parent 8d86fdb commit c011c02
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 78 deletions.
19 changes: 10 additions & 9 deletions packages/cli/src/metadataGeneration/transformer/enumTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Tsoa } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { isExistJSDocTag } from '../../utils/jsDocUtils';
import { TypeResolver } from '../typeResolver';

export class EnumTransformer extends Transformer {
public static mergeMany(many: Tsoa.RefEnumType[]): Tsoa.RefEnumType {
Expand Down Expand Up @@ -40,36 +41,36 @@ export class EnumTransformer extends Transformer {
return isEnumDeclaration(declaration) || isEnumMember(declaration);
}

public transform(declaration: EnumDeclaration | EnumMember, enumName: string): Tsoa.RefEnumType {
public transform(resolver: TypeResolver, declaration: EnumDeclaration | EnumMember, enumName: string): Tsoa.RefEnumType {
if (isEnumDeclaration(declaration)) {
return this.transformDeclaration(declaration, enumName);
return this.transformDeclaration(resolver, declaration, enumName);
}
return this.transformMember(declaration, enumName);
return this.transformMember(resolver, declaration, enumName);
}

private transformDeclaration(declaration: EnumDeclaration, enumName: string): Tsoa.RefEnumType {
private transformDeclaration(resolver: TypeResolver, declaration: EnumDeclaration, enumName: string): Tsoa.RefEnumType {
const isNotUndefined = <T>(item: T): item is Exclude<T, undefined> => {
return item === undefined ? false : true;
};
const enums = declaration.members.map(e => this.resolver.current.typeChecker.getConstantValue(e)).filter(isNotUndefined);
const enums = declaration.members.map(e => resolver.current.typeChecker.getConstantValue(e)).filter(isNotUndefined);
const enumVarnames = declaration.members.map(e => e.name.getText()).filter(isNotUndefined);

return {
dataType: 'refEnum',
description: this.resolver.getNodeDescription(declaration),
example: this.resolver.getNodeExample(declaration),
description: resolver.getNodeDescription(declaration),
example: resolver.getNodeExample(declaration),
enums,
enumVarnames,
refName: enumName,
deprecated: isExistJSDocTag(declaration, tag => tag.tagName.text === 'deprecated'),
};
}

private transformMember(declaration: EnumMember, enumName: string): Tsoa.RefEnumType {
private transformMember(resolver: TypeResolver, declaration: EnumMember, enumName: string): Tsoa.RefEnumType {
return {
dataType: 'refEnum',
refName: enumName,
enums: [this.resolver.current.typeChecker.getConstantValue(declaration)!],
enums: [resolver.current.typeChecker.getConstantValue(declaration)!],
enumVarnames: [declaration.name.getText()],
deprecated: isExistJSDocTag(declaration, tag => tag.tagName.text === 'deprecated'),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import type { TypeNode, Node } from 'typescript';
import { SyntaxKind } from 'typescript';
import { type TypeNode, SyntaxKind } from 'typescript';
import { Tsoa, assertNever } from '@tsoa/runtime';

import { Transformer } from './transformer';
import { getJSDocTagNames } from '../../utils/jsDocUtils';

export class PrimitiveTransformer extends Transformer {
public static resolveKindToPrimitive(syntaxKind: SyntaxKind): ResolvesToPrimitive {
Expand All @@ -25,17 +23,15 @@ export class PrimitiveTransformer extends Transformer {
}
}

public transform(typeNode: TypeNode, parentNode?: Node): Tsoa.Type | undefined {
public transform(defaultNumberType: DefaultNumberType, typeNode: TypeNode, partentJsDocTagNames?: string[]): Tsoa.Type | undefined {
const resolvedType = PrimitiveTransformer.resolveKindToPrimitive(typeNode.kind);
if (!resolvedType) {
return;
}

const defaultNumberType = this.resolver.current.defaultNumberType;

switch (resolvedType) {
case 'number':
return this.transformNumber(defaultNumberType, parentNode);
return this.transformNumber(defaultNumberType, partentJsDocTagNames);
case 'string':
case 'boolean':
case 'void':
Expand All @@ -51,17 +47,14 @@ export class PrimitiveTransformer extends Transformer {
}
}

private transformNumber(defaultNumberType: NonNullable<'double' | 'float' | 'integer' | 'long' | undefined>, parentNode?: Node): Tsoa.PrimitiveType {
if (!parentNode) {
private transformNumber(defaultNumberType: DefaultNumberType, partentJsDocTagNames?: string[]): Tsoa.PrimitiveType {
if (!partentJsDocTagNames || partentJsDocTagNames.length === 0) {
return { dataType: defaultNumberType };
}

const tags = getJSDocTagNames(parentNode).filter(name => {
const tags = partentJsDocTagNames.filter(name => {
return ['isInt', 'isLong', 'isFloat', 'isDouble'].some(m => m === name);
});
if (tags.length === 0) {
return { dataType: defaultNumberType };
}

switch (tags[0]) {
case 'isInt':
Expand All @@ -78,4 +71,5 @@ export class PrimitiveTransformer extends Transformer {
}
}

type DefaultNumberType = NonNullable<'double' | 'float' | 'integer' | 'long' | undefined>;
type ResolvesToPrimitive = 'number' | 'string' | 'boolean' | 'void' | 'undefined' | 'null' | undefined;
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
import type {
Token,
InterfaceDeclaration,
ClassDeclaration,
PropertyDeclaration,
ParameterDeclaration,
ConstructorDeclaration,
TypeElement,
ClassElement,
PropertySignature,
} from 'typescript';
import {
NodeFlags,
NodeBuilderFlags,
SyntaxKind,
isInterfaceDeclaration,
isPropertyDeclaration,
isConstructorDeclaration,
isPropertySignature,
} from 'typescript';
import type { Token, InterfaceDeclaration, ClassDeclaration, PropertyDeclaration, ParameterDeclaration, ConstructorDeclaration, TypeElement, ClassElement, PropertySignature } from 'typescript';
import { NodeFlags, NodeBuilderFlags, SyntaxKind, isInterfaceDeclaration, isPropertyDeclaration, isConstructorDeclaration, isPropertySignature } from 'typescript';
import { Tsoa } from '@tsoa/runtime';

import { Transformer } from './transformer';
Expand All @@ -32,7 +14,7 @@ import { throwUnless } from '../../utils/flowUtils';
type OverrideToken = Token<SyntaxKind.QuestionToken> | Token<SyntaxKind.PlusToken> | Token<SyntaxKind.MinusToken> | undefined;

export class PropertyTransformer extends Transformer {
public transform(node: InterfaceDeclaration | ClassDeclaration, overrideToken?: OverrideToken): Tsoa.Property[] {
public transform(resolver: TypeResolver, node: InterfaceDeclaration | ClassDeclaration, overrideToken?: OverrideToken): Tsoa.Property[] {
const isIgnored = (e: TypeElement | ClassElement) => {
let ignore = isExistJSDocTag(e, tag => tag.tagName.text === 'ignore');
ignore = ignore || (e.flags & NodeFlags.ThisNodeHasError) > 0;
Expand All @@ -43,7 +25,7 @@ export class PropertyTransformer extends Transformer {
if (isInterfaceDeclaration(node)) {
return node.members
.filter((member): member is PropertySignature => !isIgnored(member) && isPropertySignature(member))
.map((member: PropertySignature) => this.propertyFromSignature(member, overrideToken));
.map((member: PropertySignature) => this.propertyFromSignature(resolver, member, overrideToken));
}

const properties: Array<PropertyDeclaration | ParameterDeclaration> = [];
Expand All @@ -61,10 +43,10 @@ export class PropertyTransformer extends Transformer {
properties.push(...constructorProperties);
}

return properties.map(property => this.propertyFromDeclaration(property, overrideToken));
return properties.map(property => this.propertyFromDeclaration(resolver, property, overrideToken));
}

private propertyFromSignature(propertySignature: PropertySignature, overrideToken?: OverrideToken): Tsoa.Property {
private propertyFromSignature(resolver: TypeResolver, propertySignature: PropertySignature, overrideToken?: OverrideToken): Tsoa.Property {
throwUnless(propertySignature.type, new GenerateMetadataError(`No valid type found for property declaration.`));

let required = !propertySignature.questionToken;
Expand All @@ -78,54 +60,54 @@ export class PropertyTransformer extends Transformer {

const property: Tsoa.Property = {
default: def,
description: this.resolver.getNodeDescription(propertySignature),
example: this.resolver.getNodeExample(propertySignature),
format: this.resolver.getNodeFormat(propertySignature),
name: this.resolver.getPropertyName(propertySignature),
description: resolver.getNodeDescription(propertySignature),
example: resolver.getNodeExample(propertySignature),
format: resolver.getNodeFormat(propertySignature),
name: resolver.getPropertyName(propertySignature),
required,
type: new TypeResolver(propertySignature.type, this.resolver.current, propertySignature.type.parent, this.resolver.context).resolve(),
type: new TypeResolver(propertySignature.type, resolver.current, propertySignature.type.parent, resolver.context).resolve(),
validators: getPropertyValidators(propertySignature) || {},
deprecated: isExistJSDocTag(propertySignature, tag => tag.tagName.text === 'deprecated'),
extensions: this.resolver.getNodeExtension(propertySignature),
extensions: resolver.getNodeExtension(propertySignature),
};
return property;
}

private propertyFromDeclaration(propertyDeclaration: PropertyDeclaration | ParameterDeclaration, overrideToken?: OverrideToken): Tsoa.Property {
private propertyFromDeclaration(resolver: TypeResolver, propertyDeclaration: PropertyDeclaration | ParameterDeclaration, overrideToken?: OverrideToken): Tsoa.Property {
let typeNode = propertyDeclaration.type;

const tsType = this.resolver.current.typeChecker.getTypeAtLocation(propertyDeclaration);
const tsType = resolver.current.typeChecker.getTypeAtLocation(propertyDeclaration);

if (!typeNode) {
// Type is from initializer
typeNode = this.resolver.current.typeChecker.typeToTypeNode(tsType, undefined, NodeBuilderFlags.NoTruncation)!;
typeNode = resolver.current.typeChecker.typeToTypeNode(tsType, undefined, NodeBuilderFlags.NoTruncation)!;
}

const type = new TypeResolver(typeNode, this.resolver.current, propertyDeclaration, this.resolver.context, tsType).resolve();
const type = new TypeResolver(typeNode, resolver.current, propertyDeclaration, resolver.context, tsType).resolve();

let required = !propertyDeclaration.questionToken && !propertyDeclaration.initializer;
if (overrideToken && overrideToken.kind === SyntaxKind.MinusToken) {
required = true;
} else if (overrideToken && overrideToken.kind === SyntaxKind.QuestionToken) {
required = false;
}
let def = getInitializerValue(propertyDeclaration.initializer, this.resolver.current.typeChecker);
let def = getInitializerValue(propertyDeclaration.initializer, resolver.current.typeChecker);
if (def === undefined) {
def = TypeResolver.getDefault(propertyDeclaration);
}

const property: Tsoa.Property = {
default: def,
description: this.resolver.getNodeDescription(propertyDeclaration),
example: this.resolver.getNodeExample(propertyDeclaration),
format: this.resolver.getNodeFormat(propertyDeclaration),
name: this.resolver.getPropertyName(propertyDeclaration),
description: resolver.getNodeDescription(propertyDeclaration),
example: resolver.getNodeExample(propertyDeclaration),
format: resolver.getNodeFormat(propertyDeclaration),
name: resolver.getPropertyName(propertyDeclaration),
required,
type,
validators: getPropertyValidators(propertyDeclaration) || {},
// class properties and constructor parameters may be deprecated either via jsdoc annotation or decorator
deprecated: isExistJSDocTag(propertyDeclaration, tag => tag.tagName.text === 'deprecated') || isDecorator(propertyDeclaration, identifier => identifier.text === 'Deprecated'),
extensions: this.resolver.getNodeExtension(propertyDeclaration),
extensions: resolver.getNodeExtension(propertyDeclaration),
};
return property;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,16 @@ export class ReferenceTransformer extends Transformer {
return result;
}

public transform(declaration: TypeAliasDeclaration, refTypeName: string, referencer?: Type): Tsoa.ReferenceType {
const example = this.resolver.getNodeExample(declaration);
public transform(declaration: TypeAliasDeclaration, refTypeName: string, resolver: TypeResolver, referencer?: Type): Tsoa.ReferenceType {
const example = resolver.getNodeExample(declaration);

const referenceType: Tsoa.ReferenceType = {
dataType: 'refAlias',
default: TypeResolver.getDefault(declaration),
description: this.resolver.getNodeDescription(declaration),
description: resolver.getNodeDescription(declaration),
refName: refTypeName,
format: this.resolver.getNodeFormat(declaration),
type: new TypeResolver(declaration.type, this.resolver.current, declaration, this.resolver.context, this.resolver.referencer || referencer).resolve(),
format: resolver.getNodeFormat(declaration),
type: new TypeResolver(declaration.type, resolver.current, declaration, resolver.context, resolver.referencer || referencer).resolve(),
validators: getPropertyValidators(declaration) || {},
...(example && { example }),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { SyntaxKind, getModifiers, type HasModifiers } from 'typescript';

import { TypeResolver } from '../typeResolver';

/**
* Transformer responsible to transforming native ts node into tsoa type.
*/
export abstract class Transformer {
constructor(
protected readonly resolver: TypeResolver,
) {}

protected hasPublicModifier(node: HasModifiers): boolean {
return (
!node.modifiers ||
Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/metadataGeneration/typeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export class TypeResolver {
}

public resolve(): Tsoa.Type {
const primitiveType = new PrimitiveTransformer(this).transform(this.typeNode, this.parentNode);
const partentJsDocTagNames = this.parentNode ? getJSDocTagNames(this.parentNode) : undefined;
const primitiveType = new PrimitiveTransformer().transform(this.current.defaultNumberType, this.typeNode, partentJsDocTagNames);
if (primitiveType) {
return primitiveType;
}
Expand Down Expand Up @@ -469,7 +470,7 @@ export class TypeResolver {

switch (typeName.text) {
case 'Date':
return new DateTransformer(this).transform(parentNode);
return new DateTransformer().transform(parentNode);
case 'Buffer':
case 'Readable':
return { dataType: 'buffer' };
Expand Down Expand Up @@ -782,9 +783,9 @@ export class TypeResolver {
for (const declaration of declarations) {
if (ts.isTypeAliasDeclaration(declaration)) {
const referencer = node.pos !== -1 ? this.current.typeChecker.getTypeFromTypeNode(node) : undefined;
referenceTypes.push(new ReferenceTransformer(this).transform(declaration, refTypeName, referencer));
referenceTypes.push(new ReferenceTransformer().transform(declaration, refTypeName, this, referencer));
} else if (EnumTransformer.transformable(declaration)) {
referenceTypes.push(new EnumTransformer(this).transform(declaration, refTypeName));
referenceTypes.push(new EnumTransformer().transform(this, declaration, refTypeName));
} else {
referenceTypes.push(this.getModelReference(declaration, refTypeName));
}
Expand Down Expand Up @@ -846,7 +847,7 @@ export class TypeResolver {
return referenceType;
}

const properties = new PropertyTransformer(this).transform(modelType);
const properties = new PropertyTransformer().transform(this, modelType);
const additionalProperties = this.getModelAdditionalProperties(modelType);
const inheritedProperties = this.getModelInheritedProperties(modelType) || [];

Expand Down

0 comments on commit c011c02

Please sign in to comment.