UNPKG

typescript-swagger

Version:

Generate Swagger files from a decorator library like typescript-rest or a @decorators/express.

1,089 lines (924 loc) 56.5 kB
import * as _ from 'lodash'; import * as ts from 'typescript'; import {getDecoratorName} from "../../decorator/utils"; import { getJSDocTagComment, getJSDocTagNames, isExistJSDocTag } from '../../utils/jsDocUtils'; import {MetadataGenerator, Property} from '../metadataGenerator'; import {ResolverError} from "./error"; import { Resolver } from "./type"; import {getInitializerValue, hasOwnProperty} from "./utils"; const localReferenceTypeCache: { [typeName: string]: Resolver.ReferenceType } = {}; const inProgressTypes: { [typeName: string]: boolean } = {}; type OverrideToken = ts.Token<ts.SyntaxKind.QuestionToken> | ts.Token<ts.SyntaxKind.PlusToken> | ts.Token<ts.SyntaxKind.MinusToken> | undefined; type UsableDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.PropertySignature | ts.TypeAliasDeclaration | ts.EnumMember; interface TypeNodeResolverContext { [name: string]: ts.TypeReferenceNode | ts.TypeNode; } function getPropertyValidators(property: ts.PropertyDeclaration | ts.TypeAliasDeclaration | ts.PropertySignature | ts.ParameterDeclaration) { return {}; } type UtilityType = 'NonNullable' | 'Omit' | 'Partial' | 'Readonly' | 'Record' | 'Required' | 'Pick'; interface UtilityOptions { keys: Array<string | number | boolean>; } export class TypeNodeResolver { constructor( private readonly typeNode: ts.TypeNode, private readonly current: MetadataGenerator, private readonly parentNode?: ts.Node, private context: TypeNodeResolverContext = {}, private readonly referencer?: ts.TypeNode, ) {} public static clearCache() { Object.keys(localReferenceTypeCache).forEach(key => { delete localReferenceTypeCache[key]; }); Object.keys(inProgressTypes).forEach(key => { delete inProgressTypes[key]; }); } public resolve(): Resolver.Type { const primitiveType = this.getPrimitiveType(this.typeNode, this.parentNode); if (primitiveType) { return primitiveType; } if (this.typeNode.kind === ts.SyntaxKind.NullKeyword) { return { typeName: 'enum', members: [null], } as Resolver.EnumType; } if (this.typeNode.kind === ts.SyntaxKind.ArrayType) { return { typeName: 'array', elementType: new TypeNodeResolver((this.typeNode as ts.ArrayTypeNode).elementType, this.current, this.parentNode, this.context).resolve(), } as Resolver.ArrayType; } if (ts.isUnionTypeNode(this.typeNode)) { const types = this.typeNode.types.map(type => { return new TypeNodeResolver(type, this.current, this.parentNode, this.context).resolve(); }); return { typeName: 'union', members: types, } as Resolver.UnionType; } if (ts.isIntersectionTypeNode(this.typeNode)) { const types = this.typeNode.types.map(type => { return new TypeNodeResolver(type, this.current, this.parentNode, this.context).resolve(); }); return { typeName: 'intersection', members: types, } as Resolver.IntersectionType; } if (this.typeNode.kind === ts.SyntaxKind.AnyKeyword || this.typeNode.kind === ts.SyntaxKind.UnknownKeyword || this.typeNode.kind === ts.SyntaxKind.UndefinedKeyword) { return { typeName: 'any', } as Resolver.AnyType; } if (ts.isLiteralTypeNode(this.typeNode)) { return { typeName: 'enum', members: [TypeNodeResolver.getLiteralValue(this.typeNode)], } as Resolver.EnumType; } if (ts.isTypeLiteralNode(this.typeNode)) { const properties : Array<Property> = this.typeNode.members .filter(member => ts.isPropertySignature(member)) .reduce((res, propertySignature: ts.PropertySignature) => { const type = new TypeNodeResolver(propertySignature.type as ts.TypeNode, this.current, propertySignature, this.context).resolve(); const property: Property = { example: TypeNodeResolver.getNodeExample(propertySignature), default: getJSDocTagComment(propertySignature, 'default'), description: this.getNodeDescription(propertySignature), format: TypeNodeResolver.getNodeFormat(propertySignature), name: (propertySignature.name as ts.Identifier).text, required: !propertySignature.questionToken, type: type, validators: getPropertyValidators(propertySignature) || {}, }; return [property, ...res]; }, []); const indexMember = this.typeNode.members.find(member => ts.isIndexSignatureDeclaration(member)); let additionalType: Resolver.Type | undefined; if (indexMember) { const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration; const indexType = new TypeNodeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.context).resolve(); if (indexType.typeName !== 'string') { throw new ResolverError(`Only string indexes are supported.`, this.typeNode); } additionalType = new TypeNodeResolver(indexSignatureDeclaration.type, this.current, this.parentNode, this.context).resolve(); } return { additionalProperties: indexMember && additionalType, typeName: 'nestedObjectLiteral', properties: properties, } as Resolver.NestedObjectLiteralType; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword || ts.isFunctionTypeNode(this.typeNode)) { return { typeName: 'object' }; } if (ts.isMappedTypeNode(this.typeNode) && this.referencer) { const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer); const mappedTypeNode = this.typeNode; const typeChecker = this.current.typeChecker; const getDeclaration = (prop: ts.Symbol) => prop.declarations && (prop.declarations[0] as ts.Declaration | undefined); const isIgnored = (prop: ts.Symbol) => { const declaration = getDeclaration(prop); return ( prop.getJsDocTags().find(tag => tag.name === 'ignore') !== undefined || (declaration !== undefined && !ts.isPropertyDeclaration(declaration) && !ts.isPropertySignature(declaration) && !ts.isParameter(declaration)) ); }; const properties: Array<Property> = type .getProperties() // Ignore methods, getter, setter and @ignored props .filter(property => isIgnored(property) === false) // Transform to property .map(property => { const propertyType = typeChecker.getTypeOfSymbolAtLocation(property, this.typeNode); const declaration = getDeclaration(property) as ts.PropertySignature | ts.PropertyDeclaration | ts.ParameterDeclaration | undefined; if (declaration && ts.isPropertySignature(declaration)) { return { ...this.propertyFromSignature(declaration, mappedTypeNode.questionToken), name: property.getName() }; } else if (declaration && (ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration))) { return { ...this.propertyFromDeclaration(declaration, mappedTypeNode.questionToken), name: property.getName() }; } // Resolve default value, required and typeNode let required = false; const typeNode = this.current.typeChecker.typeToTypeNode(propertyType, undefined, ts.NodeBuilderFlags.NoTruncation)!; if (mappedTypeNode.questionToken && mappedTypeNode.questionToken.kind === ts.SyntaxKind.MinusToken) { required = true; } else if (mappedTypeNode.questionToken && mappedTypeNode.questionToken.kind === ts.SyntaxKind.QuestionToken) { required = false; } // Push property return { name: property.getName(), required: required, type: new TypeNodeResolver(typeNode, this.current, this.typeNode, this.context, this.referencer).resolve(), validators: {}, }; }); return { typeName: 'nestedObjectLiteral', properties: properties, } as Resolver.NestedObjectLiteralType; } if (ts.isConditionalTypeNode(this.typeNode) && this.referencer && ts.isTypeReferenceNode(this.referencer)) { const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer); if (type.aliasSymbol) { let declaration = type.aliasSymbol.declarations[0] as ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.DeclarationStatement; if (declaration.name) { declaration = this.getModelTypeDeclaration(declaration.name as ts.EntityName) as ts.TypeAliasDeclaration | ts.EnumDeclaration | ts.DeclarationStatement; } const name = TypeNodeResolver.getRefTypeName(this.referencer.getText()); return this.handleCachingAndCircularReferences(name, () => { if (ts.isTypeAliasDeclaration(declaration)) { // Note: I don't understand why typescript lose type for `this.referencer` (from above with isTypeReferenceNode()) return this.getTypeAliasReference(declaration, this.current.typeChecker.typeToString(type), this.referencer as ts.TypeReferenceNode); } else if (ts.isEnumDeclaration(declaration)) { return this.getEnumerateType(declaration.name) as Resolver.RefEnumType; } else { throw new ResolverError( `Couldn't resolve Conditional to TypeNode. If you think this should be resolvable, please file an Issue. We found an aliasSymbol and it's declaration was of kind ${declaration.kind}`, this.typeNode, ); } }); } else if (type.isClassOrInterface()) { let declaration = type.symbol.declarations[0] as ts.InterfaceDeclaration | ts.ClassDeclaration; if (declaration.name) { declaration = this.getModelTypeDeclaration(declaration.name) as ts.InterfaceDeclaration | ts.ClassDeclaration; } const name = TypeNodeResolver.getRefTypeName(this.referencer.getText()); return this.handleCachingAndCircularReferences(name, () => this.getModelReference(declaration, this.current.typeChecker.typeToString(type))); } else { try { return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation)!, this.current, this.typeNode, this.context, this.referencer).resolve(); } catch { throw new ResolverError( `Couldn't resolve Conditional to TypeNode. If you think this should be resolvable, please file an Issue. The flags on the result of the ConditionalType was ${type.flags}`, this.typeNode, ); } } } if (ts.isTypeOperatorNode(this.typeNode) && this.typeNode.operator === ts.SyntaxKind.KeyOfKeyword) { const type = this.current.typeChecker.getTypeFromTypeNode(this.typeNode); try { return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.NoTruncation)!, this.current, this.typeNode, this.context, this.referencer).resolve(); } catch (err) { const indexedTypeName = this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.type)); throw new ResolverError(`Could not determine the keys on ${indexedTypeName}`, this.typeNode); } } if (ts.isIndexedAccessTypeNode(this.typeNode) && (this.typeNode.indexType.kind === ts.SyntaxKind.NumberKeyword || this.typeNode.indexType.kind === ts.SyntaxKind.StringKeyword)) { const numberIndexType = this.typeNode.indexType.kind === ts.SyntaxKind.NumberKeyword; const objectType = this.current.typeChecker.getTypeFromTypeNode(this.typeNode.objectType); const type = numberIndexType ? objectType.getNumberIndexType() : objectType.getStringIndexType(); if (type === undefined) { throw new ResolverError(`Could not determine ${numberIndexType ? 'number' : 'string'} index on ${this.current.typeChecker.typeToString(objectType)}`, this.typeNode); } return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(type, undefined, undefined)!, this.current, this.typeNode, this.context, this.referencer).resolve(); } if ( ts.isIndexedAccessTypeNode(this.typeNode) && ts.isLiteralTypeNode(this.typeNode.indexType) && (ts.isStringLiteral(this.typeNode.indexType.literal) || ts.isNumericLiteral(this.typeNode.indexType.literal)) ) { const hasType = (node: ts.Node | undefined): node is ts.HasType => node !== undefined && node.hasOwnProperty('type'); const symbol = this.current.typeChecker.getPropertyOfType(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.objectType), this.typeNode.indexType.literal.text); if (symbol === undefined) { throw new ResolverError( `Could not determine the keys on ${this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode.objectType))}`, this.typeNode, ); } if (hasType(symbol.valueDeclaration) && symbol.valueDeclaration.type) { return new TypeNodeResolver(symbol.valueDeclaration.type, this.current, this.typeNode, this.context, this.referencer).resolve(); } const declaration = this.current.typeChecker.getTypeOfSymbolAtLocation(symbol, this.typeNode.objectType); try { return new TypeNodeResolver(this.current.typeChecker.typeToTypeNode(declaration, undefined, undefined)!, this.current, this.typeNode, this.context, this.referencer).resolve(); } catch { throw new ResolverError( `Could not determine the keys on ${this.current.typeChecker.typeToString( this.current.typeChecker.getTypeFromTypeNode(this.current.typeChecker.typeToTypeNode(declaration, undefined, undefined)!), )}`, this.typeNode, ); } } // @ts-ignore if (this.typeNode.kind === ts.SyntaxKind.TemplateLiteralType) { const type = this.current.typeChecker.getTypeFromTypeNode(this.referencer || this.typeNode); if (type.isUnion() && type.types.every(unionElementType => unionElementType.isStringLiteral())) { return { typeName: 'enum', members: type.types.map((stringLiteralType: ts.StringLiteralType) => stringLiteralType.value), } as Resolver.EnumType; } else { throw new ResolverError(`Could not the type of ${this.current.typeChecker.typeToString(this.current.typeChecker.getTypeFromTypeNode(this.typeNode), this.typeNode)}`, this.typeNode); } } if (ts.isParenthesizedTypeNode(this.typeNode)) { return new TypeNodeResolver(this.typeNode.type, this.current, this.typeNode, this.context, this.referencer).resolve(); } if (this.typeNode.kind !== ts.SyntaxKind.TypeReference) { throw new ResolverError(`Unknown type: ${ts.SyntaxKind[this.typeNode.kind]}`, this.typeNode); } const typeReference = this.typeNode as ts.TypeReferenceNode; if (typeReference.typeName.kind === ts.SyntaxKind.Identifier) { // Special Utility Type if(typeReference.typeName.text === 'Record') { return { additionalProperties: new TypeNodeResolver(typeReference.typeArguments[1], this.current, this.parentNode, this.context).resolve(), typeName: 'nestedObjectLiteral', properties: [], } as Resolver.NestedObjectLiteralType; } const specialReference = TypeNodeResolver.resolveSpecialReference(typeReference.typeName); if(typeof specialReference !== 'undefined') { return specialReference; } if (typeReference.typeName.text === 'Date') { return this.getDateType(this.parentNode); } if (typeReference.typeName.text === 'Buffer' || typeReference.typeName.text === 'Readable') { return { typeName: 'buffer' } as Resolver.BufferType; } if (typeReference.typeName.text === 'Array' && typeReference.typeArguments && typeReference.typeArguments.length >= 1) { return { typeName: 'array', elementType: new TypeNodeResolver(typeReference.typeArguments[0], this.current, this.parentNode, this.context).resolve(), } as Resolver.ArrayType; } if (typeReference.typeName.text === 'Promise' && typeReference.typeArguments && typeReference.typeArguments.length === 1) { return new TypeNodeResolver(typeReference.typeArguments[0], this.current, this.parentNode, this.context).resolve(); } if (typeReference.typeName.text === 'String') { return {typeName: 'string'} as Resolver.StringType; } if (this.context[typeReference.typeName.text]) { return new TypeNodeResolver(this.context[typeReference.typeName.text], this.current, this.parentNode, this.context).resolve(); } } const referenceType = this.getReferenceType(typeReference); this.current.addReferenceType(referenceType); return referenceType; } // ------------------------------------------------------------------------ // Utility Type(s) // ------------------------------------------------------------------------ private static isSupportedUtilityType(typeName: string | ts.Identifier | undefined) : typeName is UtilityType { if(typeof typeName === 'undefined') { return false; } return ['NonNullable', 'Pick', 'Omit', 'Partial', 'Readonly', 'Record', 'Required'].indexOf(typeof typeName !== 'string' ? typeName.text : typeName) !== -1; } private static getUtilityTypeOptions(typeArguments: ts.NodeArray<ts.TypeNode>) { const utilityOptions : UtilityOptions = { keys: [] }; if(typeArguments.length >= 2) { if (ts.isUnionTypeNode(typeArguments[1])) { const args : ts.NodeArray<ts.TypeNode> = (typeArguments[1] as ts.UnionTypeNode).types; for(let i=0; i<args.length; i++) { if (ts.isLiteralTypeNode(args[i])) { utilityOptions['keys'].push(TypeNodeResolver.getLiteralValue(args[i] as ts.LiteralTypeNode)); } } } if (ts.isLiteralTypeNode(typeArguments[1])) { utilityOptions['keys'].push(TypeNodeResolver.getLiteralValue(typeArguments[1] as ts.LiteralTypeNode)); } } return utilityOptions; } private filterUtilityProperties<T extends Record<'name' | string, any>>(properties: Array<T>, utilityType?: UtilityType, utilityOptions?: UtilityOptions) : Array<T> { if(typeof utilityType === 'undefined' || typeof utilityOptions === 'undefined') { return properties; } return properties .filter(property => { const name : string = typeof property.name !== 'string' ? (property.name as ts.Identifier).text : property.name; switch (utilityType) { case 'Pick': return utilityOptions.keys.indexOf(name) !== -1; case 'Omit': return utilityOptions.keys.indexOf(name) === -1; } return true; }) .map(property => { if(hasOwnProperty(property, 'required')) { switch (utilityType) { case 'Partial': property.required = false; break; case 'Required': case 'NonNullable': property.required = true; break; } } return property; }); } private static resolveSpecialReference(node: ts.Identifier) : Resolver.Type | undefined { switch (node.text) { case 'Buffer': case 'DownloadBinaryData': case 'DownloadResource': return { typeName: 'buffer' } as Resolver.BufferType; default: return undefined; } } private static getLiteralValue(typeNode: ts.LiteralTypeNode): string | number | boolean | null { let value: boolean | number | string | null; switch (typeNode.literal.kind) { case ts.SyntaxKind.TrueKeyword: value = true; break; case ts.SyntaxKind.FalseKeyword: value = false; break; case ts.SyntaxKind.StringLiteral: value = typeNode.literal.text; break; case ts.SyntaxKind.NumericLiteral: value = parseFloat(typeNode.literal.text); break; case ts.SyntaxKind.NullKeyword: value = null; break; default: if (typeNode.literal.hasOwnProperty('text')) { value = (typeNode.literal as ts.LiteralExpression).text; } else { throw new ResolverError(`Couldn't resolve literal node: ${typeNode.literal.getText()}`); } } return value; } private getPrimitiveType(typeNode: ts.TypeNode, parentNode?: ts.Node): Resolver.PrimitiveType | undefined { if(!typeNode) { return { typeName: 'void', }; } const resolvedType = this.attemptToResolveKindToPrimitive(typeNode.kind); if (typeof resolvedType === 'undefined') { return undefined; } if (resolvedType === 'number') { if (!parentNode) { return { typeName: 'double' }; } const tags = getJSDocTagNames(parentNode) .filter(name => { return [ 'isInt', 'isLong', 'isFloat', 'isDouble' ].some(m => m.toLowerCase() === name.toLowerCase()); }) .map(name => name.toLowerCase()); let decorator : string | undefined = getDecoratorName(parentNode, identifier => [ 'isInt', 'isLong', 'isFloat', 'isDouble' ].some(m => m.toLowerCase() === identifier.text.toLowerCase())); if(typeof decorator !== 'undefined') { decorator = decorator.toLowerCase(); } if (!decorator && tags.length === 0) { return { typeName: 'double' }; } switch (decorator || tags[0]) { case 'isint': return { typeName: 'integer' }; case 'islong': return { typeName: 'long' }; case 'isfloat': return { typeName: 'float' }; case 'isdouble': return { typeName: 'double' }; default: return { typeName: 'double' }; } } else if (resolvedType === 'string') { return { typeName: 'string', }; } else if (resolvedType === 'boolean') { return { typeName: 'boolean', }; } else if (resolvedType === 'void') { return { typeName: 'void', }; } else { // todo: should not occur return resolvedType; } } private getDateType(parentNode?: ts.Node): Resolver.DateType | Resolver.DateTimeType { if (!parentNode) { return { typeName: 'datetime' }; } const tags = getJSDocTagNames(parentNode).filter(name => { return ['isDate', 'isDateTime'].some(m => m === name); }); if (tags.length === 0) { return { typeName: 'datetime' }; } switch (tags[0]) { case 'isDate': return { typeName: 'date' }; case 'isDateTime': return { typeName: 'datetime' }; default: return { typeName: 'datetime' }; } } private static getDesignatedModels(nodes: Array<ts.Node>, typeName: string): Array<ts.Node> { return nodes; } private getEnumerateType(typeName: ts.EntityName): Resolver.RefEnumType | undefined { const enumName = (typeName as ts.Identifier).text; let enumNodes = this.current.nodes.filter(node => node.kind === ts.SyntaxKind.EnumDeclaration).filter(node => (node as any).name.text === enumName); if (!enumNodes.length) { return undefined; } enumNodes = TypeNodeResolver.getDesignatedModels(enumNodes, enumName); if (enumNodes.length > 1) { throw new ResolverError(`Multiple matching enum found for enum ${enumName}; please make enum names unique.`); } const enumDeclaration = enumNodes[0] as ts.EnumDeclaration; const isNotUndefined = <T>(item: T): item is Exclude<T, undefined> => { return item !== undefined; }; const enums = enumDeclaration.members.map(this.current.typeChecker.getConstantValue.bind(this.current.typeChecker)).filter(isNotUndefined); const enumNames = enumDeclaration.members.map(e => e.name.getText()).filter(isNotUndefined); return { typeName: 'refEnum', description: this.getNodeDescription(enumDeclaration), members: enums as Array<string>, memberNames: enumNames, refName: enumName, }; } private getReferenceType(node: ts.TypeReferenceType): Resolver.ReferenceType { let type: ts.EntityName; if (ts.isTypeReferenceNode(node)) { type = node.typeName; } else if (ts.isExpressionWithTypeArguments(node)) { type = node.expression as ts.EntityName; } else { throw new ResolverError(`Can't resolve Reference type.`); } // Can't invoke getText on Synthetic Nodes let resolvableName = node.pos !== -1 ? node.getText() : (type as ts.Identifier).text; if (node.pos === -1 && 'typeArguments' in node && Array.isArray(node.typeArguments)) { // Add typeArguments for Synthetic nodes (e.g. Record<> in TestClassModel.indexedResponse) const argumentsString = node.typeArguments .map(arg => { if (ts.isLiteralTypeNode(arg)) { return `'${String(TypeNodeResolver.getLiteralValue(arg))}'`; } const resolvedType = this.attemptToResolveKindToPrimitive(arg.kind); if (typeof resolvedType === 'undefined') { return 'any'; } return resolvedType; }); resolvableName += `<${argumentsString.join(', ')}>`; } const name = this.contextualizedName(resolvableName); // Handle Utility Types const identifierName = (type as ts.Identifier).text; const utilityTypeSupported : boolean = TypeNodeResolver.isSupportedUtilityType(identifierName); const utilityType : UtilityType | undefined = utilityTypeSupported ? identifierName as UtilityType : undefined; let utilityOptions : UtilityOptions = { keys: [] }; if(utilityTypeSupported) { const typeArguments : ts.NodeArray<ts.TypeNode> = (type.parent as ts.TypeReferenceNode).typeArguments; if (ts.isTypeReferenceNode(typeArguments[0])) { type = (typeArguments[0] as ts.TypeReferenceNode).typeName; } else if (ts.isExpressionWithTypeArguments(typeArguments[0])) { type = (typeArguments[0] as ts.ExpressionWithTypeArguments).expression as ts.EntityName; } else { throw new ResolverError(`Can't resolve Reference type.`); } utilityOptions = TypeNodeResolver.getUtilityTypeOptions(typeArguments); } else { this.typeArgumentsToContext(node, type, this.context); } try { const existingType = localReferenceTypeCache[name]; if (existingType) { return existingType; } const refEnumType = this.getEnumerateType(type); if (refEnumType) { localReferenceTypeCache[name] = refEnumType; return refEnumType; } if (inProgressTypes[name]) { return this.createCircularDependencyResolver(name); } inProgressTypes[name] = true; const declaration : UsableDeclaration = this.getModelTypeDeclaration(type); let referenceType: Resolver.ReferenceType; if (ts.isTypeAliasDeclaration(declaration)) { referenceType = this.getTypeAliasReference(declaration, name, node); } else if (ts.isEnumMember(declaration)) { referenceType = { typeName: 'refEnum', refName: TypeNodeResolver.getRefTypeName(name, utilityType), members: [this.current.typeChecker.getConstantValue(declaration)!], // @ts-ignore memberNames: [declaration.name.getText()], }; } else { referenceType = this.getModelReference(declaration, name, utilityType, utilityOptions); } localReferenceTypeCache[name] = referenceType; return referenceType; } catch (err) { // eslint-disable-next-line no-console console.error(`There was a problem resolving type of '${name}'.`); throw err; } } private getTypeAliasReference(declaration: ts.TypeAliasDeclaration, name: string, referencer: ts.TypeReferenceType): Resolver.ReferenceType { const refName = TypeNodeResolver.getRefTypeName(name); let type : Resolver.Type | undefined; if(declaration.type.kind === ts.SyntaxKind.TypeReference) { const referenceType = this.getReferenceType(declaration.type as ts.TypeReferenceNode); if(referenceType.refName === refName) { return referenceType; } } if(typeof type === 'undefined') { type = new TypeNodeResolver(declaration.type, this.current, declaration, this.context, this.referencer || referencer).resolve(); } const example = TypeNodeResolver.getNodeExample(declaration); return { typeName: 'refAlias', default: getJSDocTagComment(declaration, 'default'), description: this.getNodeDescription(declaration), refName: refName, format: TypeNodeResolver.getNodeFormat(declaration), type: type, validators: getPropertyValidators (declaration) || {}, ...(example && { example: example }), }; } private getModelReference(modelType: ts.InterfaceDeclaration | ts.ClassDeclaration, name: string, utilityType?: UtilityType, utilityOptions?: UtilityOptions) { const example = TypeNodeResolver.getNodeExample(modelType); const description = this.getNodeDescription(modelType); // Handle toJSON methods if (!modelType.name) { throw new ResolverError("Can't get Symbol from anonymous class", modelType); } const type = this.current.typeChecker.getTypeAtLocation(modelType.name); const toJSON = this.current.typeChecker.getPropertyOfType(type, 'toJSON'); if (toJSON && toJSON.valueDeclaration && (ts.isMethodDeclaration(toJSON.valueDeclaration) || ts.isMethodSignature(toJSON.valueDeclaration))) { let nodeType = toJSON.valueDeclaration.type; if (!nodeType) { const signature = this.current.typeChecker.getSignatureFromDeclaration(toJSON.valueDeclaration); const implicitType = this.current.typeChecker.getReturnTypeOfSignature(signature!); nodeType = this.current.typeChecker.typeToTypeNode(implicitType, undefined, ts.NodeBuilderFlags.NoTruncation) as ts.TypeNode; } return { refName: TypeNodeResolver.getRefTypeName(name, utilityType)+'Alias', typeName: 'refAlias', description: description, type: new TypeNodeResolver(nodeType, this.current).resolve(), validators: {}, ...(example && { example: example }), } as Resolver.ReferenceType; } const properties = this.getModelProperties(modelType, undefined, utilityType, utilityOptions); const additionalProperties = this.getModelAdditionalProperties(modelType); const inheritedProperties = this.getModelInheritedProperties(modelType) || []; const referenceType: Resolver.ReferenceType & { properties: Array<Property> } = { additionalProperties: additionalProperties, typeName: 'refObject', description: description, properties: this.filterUtilityProperties(inheritedProperties, utilityType, utilityOptions), refName: TypeNodeResolver.getRefTypeName(name, utilityType), ...(example && { example: example }), }; referenceType.properties = referenceType.properties.concat(properties); return referenceType; } private static getRefTypeName(name: string, utilityType?: UtilityType): string { return encodeURIComponent( name .replace(/<|>/g, '_') .replace(/\s+/g, '') .replace(/,/g, '.') .replace(/\'([^']*)\'/g, '$1') .replace(/\"([^"]*)\"/g, '$1') .replace(/&/g, typeof utilityType !== 'undefined' ? '--' : '-and-') .replace(/\|/g, typeof utilityType !== 'undefined' ? '--' : '-or-') .replace(/\[\]/g, '-array') .replace(/{|}/g, '_') // SuccessResponse_{indexesCreated-number}_ -> SuccessResponse__indexesCreated-number__ .replace(/([a-z]+):([a-z]+)/gi, '$1-$2') // SuccessResponse_indexesCreated:number_ -> SuccessResponse_indexesCreated-number_ .replace(/;/g, '--') .replace(/([a-z]+)\[([a-z]+)\]/gi, '$1-at-$2') // Partial_SerializedDatasourceWithVersion[format]_ -> Partial_SerializedDatasourceWithVersion~format~_, .replace(/_/g, '') .replace(/-/g, '') ); } private attemptToResolveKindToPrimitive = (syntaxKind: ts.SyntaxKind) : 'number' | 'string' | 'boolean' | 'void' | undefined => { if (syntaxKind === ts.SyntaxKind.NumberKeyword) { return 'number'; } else if (syntaxKind === ts.SyntaxKind.StringKeyword) { return 'string'; } else if (syntaxKind === ts.SyntaxKind.BooleanKeyword) { return 'boolean'; } else if (syntaxKind === ts.SyntaxKind.VoidKeyword) { return 'void'; } else { return undefined; } } private contextualizedName(name: string): string { return Object.entries(this.context).reduce((acc, [key, entry]) => { return acc .replace(new RegExp(`<\\s*([^>]*\\s)*\\s*(${key})(\\s[^>]*)*\\s*>`, 'g'), `<$1${entry.getText()}$3>`) .replace(new RegExp(`<\\s*([^,]*\\s)*\\s*(${key})(\\s[^,]*)*\\s*,`, 'g'), `<$1${entry.getText()}$3,`) .replace(new RegExp(`,\\s*([^>]*\\s)*\\s*(${key})(\\s[^>]*)*\\s*>`, 'g'), `,$1${entry.getText()}$3>`) .replace(new RegExp(`<\\s*([^<]*\\s)*\\s*(${key})(\\s[^<]*)*\\s*<`, 'g'), `<$1${entry.getText()}$3<`); }, name); } private handleCachingAndCircularReferences(name: string, declarationResolver: () => Resolver.ReferenceType): Resolver.ReferenceType { try { const existingType = localReferenceTypeCache[name]; if (existingType) { return existingType; } if (inProgressTypes[name]) { return this.createCircularDependencyResolver(name); } inProgressTypes[name] = true; const reference = declarationResolver(); localReferenceTypeCache[name] = reference; this.current.addReferenceType(reference); return reference; } catch (err) { // eslint-disable-next-line no-console console.error(`There was a problem resolving type of '${name}'.`); throw err; } } private createCircularDependencyResolver(refName: string) { const referenceType = { typeName: 'refObject', refName: refName, } as Resolver.ReferenceType; this.current.onFinish(referenceTypes => { const realReferenceType = referenceTypes[refName]; if (!realReferenceType) { return; } referenceType.description = realReferenceType.description; if (realReferenceType.typeName === 'refObject' && referenceType.typeName === 'refObject') { referenceType.properties = realReferenceType.properties; } referenceType.typeName = realReferenceType.typeName; referenceType.refName = realReferenceType.refName; }); return referenceType; } private static nodeIsUsable(node: ts.Node) { switch (node.kind) { case ts.SyntaxKind.InterfaceDeclaration: case ts.SyntaxKind.ClassDeclaration: case ts.SyntaxKind.TypeAliasDeclaration: case ts.SyntaxKind.EnumDeclaration: case ts.SyntaxKind.EnumMember: return true; default: return false; } } // @ts-ignore private static resolveLeftmostIdentifier(type: ts.EntityName): ts.Identifier { while (type.kind !== ts.SyntaxKind.Identifier) { type = type.left; } return type; } // @ts-ignore private static resolveRightMostIdentifier(type: ts.EntityName) : ts.Identifier { while(type.kind !== ts.SyntaxKind.Identifier) { type = type.right; } return type; } private resolveModelTypeScope(leftmost: ts.EntityName, statements: any): Array<any> { /* while (leftmost.parent && leftmost.parent.kind === ts.SyntaxKind.QualifiedName) { const leftmostName = leftmost.kind === ts.SyntaxKind.Identifier ? leftmost.text : leftmost.right.text; const moduleDeclarations = statements.filter((node: ts.Node) => { if ((node.kind !== ts.SyntaxKind.ModuleDeclaration || !this.current.isExportedNode(node)) && !ts.isEnumDeclaration(node)) { return false; } const moduleDeclaration = node as ts.ModuleDeclaration | ts.EnumDeclaration; return (moduleDeclaration.name as ts.Identifier).text.toLowerCase() === leftmostName.toLowerCase(); }) as Array<ts.ModuleDeclaration | ts.EnumDeclaration>; if (!moduleDeclarations.length) { throw new ResolverError(`No matching module declarations found for ${leftmostName}.`); } statements = Array.prototype.concat( ...moduleDeclarations.map(declaration => { if (ts.isEnumDeclaration(declaration)) { return declaration.members; } else { if (!declaration.body || !ts.isModuleBlock(declaration.body)) { throw new ResolverError(`Module declaration found for ${leftmostName} has no body.`); } return declaration.body.statements; } }), ); leftmost = leftmost.parent as ts.EntityName; } */ return statements; } private getModelTypeDeclaration(type: ts.EntityName) { type UsableDeclarationWithoutPropertySignature = Exclude<UsableDeclaration, ts.PropertySignature>; const leftmostIdentifier = TypeNodeResolver.resolveLeftmostIdentifier(type); const statements : Array<ts.Node> = this.resolveModelTypeScope(leftmostIdentifier, this.current.nodes); const typeName = type.kind === ts.SyntaxKind.Identifier ? type.text : type.right.text; let modelTypes = statements.filter(node => { if (!TypeNodeResolver.nodeIsUsable(node) || !this.current.isExportedNode(node)) { return false; } const modelTypeDeclaration = node as UsableDeclaration; return (modelTypeDeclaration.name as ts.Identifier)?.text === typeName; }) as Array<UsableDeclarationWithoutPropertySignature>; if (!modelTypes.length) { throw new ResolverError( `No matching model found for referenced type ${typeName}. If ${typeName} comes from a dependency, please create an interface in your own code that has the same structure. The compiler can not utilize interfaces from external dependencies.`, ); } if (modelTypes.length > 1) { // remove types that are from typescript e.g. 'Account' modelTypes = modelTypes.filter(modelType => { return modelType.getSourceFile().fileName.replace(/\\/g, '/').toLowerCase().indexOf('node_modules') <= -1; }); modelTypes = TypeNodeResolver.getDesignatedModels(modelTypes, typeName) as Array<UsableDeclarationWithoutPropertySignature>; } if (modelTypes.length > 1) { const conflicts = modelTypes.map(modelType => modelType.getSourceFile().fileName).join('"; "'); throw new ResolverError(`Multiple matching models found for referenced type ${typeName}; please make model names unique. Conflicts found: "${conflicts}".`); } return modelTypes[0]; } private getModelProperties(node: ts.InterfaceDeclaration | ts.ClassDeclaration, overrideToken?: OverrideToken, utilityType?: UtilityType, utilityOptions?: UtilityOptions): Array<Property> { const isIgnored = (e: ts.TypeElement | ts.ClassElement) => { return isExistJSDocTag(e, tag => tag.tagName.text === 'ignore'); }; // Interface model if (ts.isInterfaceDeclaration(node)) { return node.members.filter(member => !isIgnored(member) && ts.isPropertySignature(member)).map((member: ts.PropertySignature) => this.propertyFromSignature(member, overrideToken)); } // Class model let properties = node.members .filter(member => !isIgnored(member)) .filter(member => member.kind === ts.SyntaxKind.PropertyDeclaration) .filter(member => !this.hasStaticModifier(member)) .filter(member => this.hasPublicModifier(member)) as Array<ts.PropertyDeclaration | ts.ParameterDeclaration>; const classConstructor = node.members.find(member => ts.isConstructorDeclaration(member)) as ts.ConstructorDeclaration; if (classConstructor && classConstructor.parameters) { const constructorProperties = classConstructor.parameters.filter(parameter => this.isAccessibleParameter(parameter)); properties.push(...constructorProperties); } properties = this.filterUtilityProperties(properties, utilityType, utilityOptions); return properties.map(property => this.propertyFromDeclaration(property, overrideToken, utilityType)); } private propertyFromSignature(propertySignature: ts.PropertySignature, overrideToken?: OverrideToken) { const identifier = propertySignature.name as ts.Identifier; if (!propertySignature.type) { throw new ResolverError(`No valid type found for property declaration.`); } let required = !propertySignature.questionToken; if (overrideToken && overrideToken.kind === ts.SyntaxKind.MinusToken) { required = true; } else if (overrideToken && overrideToken.kind === ts.SyntaxKind.QuestionToken) { required = false; } const property: Property = { default: getJSDocTagComment(propertySignature, 'default'), description: this.getNodeDescription(propertySignature), example: TypeNodeResolver.getNodeExample(propertySignature), format: TypeNodeResolver.getNodeFormat(propertySignature), name: identifier.text, required: required, type: new TypeNodeResolver(propertySignature.type, this.current, propertySignature.type.parent, this.context, propertySignature.type).resolve(), validators: getPropertyValidators(propertySignature) || {}, }; return property; } private propertyFromDeclaration(propertyDeclaration: ts.PropertyDeclaration | ts.ParameterDeclaration, overrideToken?: OverrideToken, utilityType?: string) { const identifier = propertyDeclaration.name as ts.Identifier; let typeNode = propertyDeclaration.type; if (!typeNode) { const tsType = this.current.typeChecker.getTypeAtLocation(propertyDeclaration); typeNode = this.current.typeChecker.typeToTypeNode(tsType, undefined, ts.NodeBuilderFlags.NoTruncation); } if (!typeNode) { throw new ResolverError(`No valid type found for property declaration.`); } const type = new TypeNodeResolver(typeNode, this.current, propertyDeclaration, this.context, typeNode).resolve(); let required = !propertyDeclaration.questionToken && !propertyDeclaration.initializer; if (overrideToken && overrideToken.kind === ts.SyntaxKind.MinusToken) { required = true; } else if (overrideToken && overrideToken.kind === ts.SyntaxKind.QuestionToken) { required = false; } if(typeof utilityType !== 'undefined') { if(utilityType === 'Partial') { required = false; } if(utilityType === 'Required') { required = true; } } const property: Property = { default: getInitializerValue(propertyDeclaration.initializer, this.current.typeChecker), description: this.getNodeDescription(propertyDeclaration), example: TypeNodeResolver.getNodeExample(propertyDeclaration), format: TypeNodeResolver.getNodeFormat(propertyDeclaration), name: identifier.text, required: required, type: type, validators: getPropertyValidators(propertyDeclaration) || {}, }; return property; } private getModelAdditionalProperties(node: UsableDeclaration) { if (node.kind === ts.SyntaxKind.InterfaceDeclaration) { const indexMember = node.members.find(member => member.kind === ts.SyntaxKind.IndexSignature); if (!indexMember) { return undefined; } const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration; const indexType = new TypeNodeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current, this.parentNode, this.context).resolve(); if (indexType.typeName !== 'string') { throw new ResolverError(`Only string indexers are suppo