import * as _ from 'lodash' ; import * as ts from 'typescript' ; import { getDecoratorName } from '../utils/decoratorUtils' ; import { getFirstMatchingJSDocTagName } from '../utils/jsDocUtils' ; import { keywords } from './keywordKinds' ; import { ArrayType , EnumerateType , MetadataGenerator , ObjectType , Property , ReferenceType , Type } from './metadataGenerator' ; const syntaxKindMap = { } ; syntaxKindMap [ ts . SyntaxKind . NumberKeyword ] = 'number' ; syntaxKindMap [ ts . SyntaxKind . StringKeyword ] = 'string' ; syntaxKindMap [ ts . SyntaxKind . BooleanKeyword ] = 'boolean' ; syntaxKindMap [ ts . SyntaxKind . VoidKeyword ] = 'void' ; const localReferenceTypeCache = { } ; const inProgressTypes = { } ; type UsableDeclaration = ts . InterfaceDeclaration | ts . ClassDeclaration | ts . TypeAliasDeclaration ; export function resolveType ( typeNode ? , genericTypeMap ? ) { if ( ! typeNode ) { return { typeName : 'void' } ; } const primitiveType = getPrimitiveType ( typeNode ) ; if ( primitiveType ) { return primitiveType ; } if ( typeNode . kind === ts . SyntaxKind . ArrayType ) { const arrayType = typeNode as ts . ArrayTypeNode ; return { elementType : resolveType ( arrayType . elementType , genericTypeMap ) , typeName : 'array' } as ArrayType ; } if ( ( typeNode . kind === ts . SyntaxKind . AnyKeyword ) || ( typeNode . kind === ts . SyntaxKind . ObjectKeyword ) ) { return { typeName : 'object' } ; } if ( typeNode . kind === ts . SyntaxKind . TypeLiteral ) { return getInlineObjectType ( typeNode ) ; } if ( typeNode . kind === ts . SyntaxKind . UnionType ) { return getUnionType ( typeNode ) ; } if ( typeNode . kind !== ts . SyntaxKind . TypeReference ) { throw new Error ( `Unknown type: ${ ts . SyntaxKind [ typeNode . kind ] }` ) ; } let typeReference = typeNode ; let typeName = resolveSimpleTypeName ( typeReference . typeName as ts . EntityName ) ; if ( typeName === 'Date' ) { return getDateType ( typeNode ) ; } if ( typeName === 'Buffer' ) { return { typeName : 'buffer' } ; } if ( typeName === 'DownloadBinaryData' ) { return { typeName : 'buffer' } ; } if ( typeName === 'DownloadResource' ) { return { typeName : 'buffer' } ; } if ( typeName === 'Promise' ) { typeReference = typeReference . typeArguments [ 0 ] ; return resolveType ( typeReference , genericTypeMap ) ; } if ( typeName === 'Array' ) { typeReference = typeReference . typeArguments [ 0 ] ; return { elementType : resolveType ( typeReference , genericTypeMap ) , typeName : 'array' } as ArrayType ; } const enumType = getEnumerateType ( typeNode ) ; if ( enumType ) { return enumType ; } const literalType = getLiteralType ( typeNode ) ; if ( literalType ) { return literalType ; } let referenceType ; if ( typeReference . typeArguments && typeReference . typeArguments . length === 1 ) { const typeT = typeReference . typeArguments as Array < ts . TypeNode > ; referenceType = getReferenceType ( typeReference . typeName as ts . EntityName , genericTypeMap , typeT ) ; typeName = resolveSimpleTypeName ( typeReference . typeName as ts . EntityName ) ; if ( [ 'NewResource' , 'RequestAccepted' , 'MovedPermanently' , 'MovedTemporarily' ] . indexOf ( typeName ) >= 0 ) { referenceType . typeName = typeName ; referenceType . typeArgument = resolveType ( typeT [ 0 ] , genericTypeMap ) ; } else { MetadataGenerator . current . addReferenceType ( referenceType ) ; } } else { referenceType = getReferenceType ( typeReference . typeName as ts . EntityName , genericTypeMap ) ; MetadataGenerator . current . addReferenceType ( referenceType ) ; } return referenceType ; } function getPrimitiveType ( typeNode ) { const primitiveType = syntaxKindMap [ typeNode . kind ] ; if ( ! primitiveType ) { return undefined ; } if ( primitiveType === 'number' ) { const parentNode = typeNode . parent as ts . Node ; if ( ! parentNode ) { return { typeName : 'double' } ; } const validDecorators = [ 'IsInt' , 'IsLong' , 'IsFloat' , 'IsDouble' ] ; const jsdocTagName = getFirstMatchingJSDocTagName ( parentNode , tag => { return validDecorators . some ( t => t === tag . tagName . text ) ; } ) ; const decoratorName = getDecoratorName ( parentNode , identifier => { return validDecorators . some ( m => m === identifier . text ) ; } ) ; switch ( decoratorName || jsdocTagName ) { case 'IsInt' : return { typeName : 'integer' } ; case 'IsLong' : return { typeName : 'long' } ; case 'IsFloat' : return { typeName : 'float' } ; case 'IsDouble' : return { typeName : 'double' } ; default : return { typeName : 'double' } ; } } return { typeName : primitiveType } ; } function getDateType ( typeNode ) { const parentNode = typeNode . parent as ts . Node ; if ( ! parentNode ) { return { typeName : 'datetime' } ; } const decoratorName = getDecoratorName ( parentNode , identifier => { return [ 'IsDate' , 'IsDateTime' ] . some ( m => m === identifier . text ) ; } ) ; switch ( decoratorName ) { case 'IsDate' : return { typeName : 'date' } ; case 'IsDateTime' : return { typeName : 'datetime' } ; default : return { typeName : 'datetime' } ; } } function getEnumerateType ( typeNode ) { const enumName = ( typeNode as any ) . typeName . text ; const enumTypes = MetadataGenerator . current . nodes . filter ( node => node . kind === ts . SyntaxKind . EnumDeclaration ) . filter ( node => ( node as any ) . name . text === enumName ) ; if ( ! enumTypes . length ) { return undefined ; } if ( enumTypes . length > 1 ) { throw new Error ( `Multiple matching enum found for enum ${ enumName }; please make enum names unique.` ) ; } const enumDeclaration = enumTypes [ 0 ] as ts . EnumDeclaration ; function getEnumValue ( member ) { const initializer = member . initializer ; if ( initializer ) { if ( initializer . expression ) { return parseEnumValueByKind ( initializer . expression . text , initializer . kind ) ; } return parseEnumValueByKind ( initializer . text , initializer . kind ) ; } return ; } return { enumMembers : enumDeclaration . members . map ( ( member , index ) => { return getEnumValue ( member ) || index ; } ) , typeName : 'enum' , } as EnumerateType ; } function parseEnumValueByKind ( value , kind ) { return kind === ts . SyntaxKind . NumericLiteral ? parseFloat ( value ) : value ; } function getUnionType ( typeNode ) { const union = typeNode as ts . UnionTypeNode ; let baseType = null ; let isObject = false ; union . types . forEach ( type => { if ( baseType === null ) { baseType = type ; } if ( baseType . kind !== type . kind ) { isObject = true ; } } ) ; if ( isObject ) { return { typeName : 'object' } ; } return { enumMembers : union . types . map ( ( type , index ) => { return type . getText ( ) ? removeQuotes ( type . getText ( ) ) : index ; } ) , typeName : 'enum' , } as EnumerateType ; } function removeQuotes ( str ) { return str . replace ( /^["']|["']$/g , '' ) ; } function getLiteralType ( typeNode ) { const literalName = ( typeNode as any ) . typeName . text ; const literalTypes = MetadataGenerator . current . nodes . filter ( node => node . kind === ts . SyntaxKind . TypeAliasDeclaration ) . filter ( node => { const innerType = ( node as any ) . type ; return innerType . kind === ts . SyntaxKind . UnionType && ( innerType as any ) . types ; } ) . filter ( node => ( node as any ) . name . text === literalName ) ; if ( ! literalTypes . length ) { return undefined ; } if ( literalTypes . length > 1 ) { throw new Error ( `Multiple matching enum found for enum ${ literalName }; please make enum names unique.` ) ; } const unionTypes = ( literalTypes [ 0 ] as any ) . type . types ; return { enumMembers : unionTypes . map ( ( unionNode ) => unionNode . literal . text as string ) , typeName : 'enum' , } as EnumerateType ; } function getInlineObjectType ( typeNode ) { const type = { properties : getModelTypeProperties ( typeNode ) , typeName : '' } ; return type ; } function getReferenceType ( type , genericTypeMap ? , genericTypes ? ) { let typeName = resolveFqTypeName ( type ) ; if ( genericTypeMap && genericTypeMap . has ( typeName ) ) { const refType = genericTypeMap . get ( typeName ) ; type = refType . typeName as ts . EntityName ; typeName = resolveFqTypeName ( type ) ; } const typeNameWithGenerics = getTypeName ( typeName , genericTypes ) ; try { const existingType = localReferenceTypeCache [ typeNameWithGenerics ] ; if ( existingType ) { return existingType ; } if ( inProgressTypes [ typeNameWithGenerics ] ) { return createCircularDependencyResolver ( typeNameWithGenerics ) ; } inProgressTypes [ typeNameWithGenerics ] = true ; const modelTypeDeclaration = getModelTypeDeclaration ( type ) ; const properties = getModelTypeProperties ( modelTypeDeclaration , genericTypes ) ; const additionalProperties = getModelTypeAdditionalProperties ( modelTypeDeclaration ) ; const referenceType = { description : getModelDescription ( modelTypeDeclaration ) , properties : properties , typeName : typeNameWithGenerics , } ; if ( additionalProperties && additionalProperties . length ) { referenceType . additionalProperties = additionalProperties ; } const extendedProperties = getInheritedProperties ( modelTypeDeclaration , genericTypes ) ; mergeReferenceTypeProperties ( referenceType . properties , extendedProperties ) ; localReferenceTypeCache [ typeNameWithGenerics ] = referenceType ; return referenceType ; } catch ( err ) { console . error ( `There was a problem resolving type of '${ getTypeName ( typeName , genericTypes ) }'.` ) ; throw err ; } } function mergeReferenceTypeProperties ( properties , extendedProperties ) { extendedProperties . forEach ( prop => { const existingProp = properties . find ( p => p . name === prop . name ) ; if ( existingProp ) { existingProp . description = existingProp . description || prop . description ; } else { properties . push ( prop ) ; } } ) ; } function resolveFqTypeName ( type ) { if ( type . kind === ts . SyntaxKind . Identifier ) { return ( type as ts . Identifier ) . text ; } const qualifiedType = type as ts . QualifiedName ; return resolveFqTypeName ( qualifiedType . left ) + '.' + ( qualifiedType . right as ts . Identifier ) . text ; } function resolveSimpleTypeName ( type ) { if ( type . kind === ts . SyntaxKind . Identifier ) { return ( type as ts . Identifier ) . text ; } const qualifiedType = type as ts . QualifiedName ; return ( qualifiedType . right as ts . Identifier ) . text ; } function getTypeName ( typeName , genericTypes ? ) { if ( ! genericTypes || ! genericTypes . length ) { return typeName ; } return typeName + genericTypes . map ( t => getAnyTypeName ( t ) ) . join ( '' ) ; } function getAnyTypeName ( typeNode ) { const primitiveType = syntaxKindMap [ typeNode . kind ] ; if ( primitiveType ) { return primitiveType ; } if ( typeNode . kind === ts . SyntaxKind . ArrayType ) { const arrayType = typeNode as ts . ArrayTypeNode ; return getAnyTypeName ( arrayType . elementType ) + 'Array' ; } if ( typeNode . kind === ts . SyntaxKind . UnionType || typeNode . kind === ts . SyntaxKind . AnyKeyword ) { return 'object' ; } if ( typeNode . kind !== ts . SyntaxKind . TypeReference ) { throw new Error ( `Unknown type: ${ ts . SyntaxKind [ typeNode . kind ] }` ) ; } const typeReference = typeNode as ts . TypeReferenceNode ; try { const typeName = ( typeReference . typeName as ts . Identifier ) . text ; if ( typeName === 'Array' ) { return getAnyTypeName ( typeReference . typeArguments [ 0 ] ) + 'Array' ; } return typeName ; } catch ( e ) { console . error ( e ) ; return typeNode . toString ( ) ; } } function createCircularDependencyResolver ( typeName ) { const referenceType = { description : '' , properties : new Array < Property > ( ) , typeName : typeName , } ; MetadataGenerator . current . onFinish ( referenceTypes => { const realReferenceType = referenceTypes [ typeName ] ; if ( ! realReferenceType ) { return ; } referenceType . description = realReferenceType . description ; referenceType . properties = realReferenceType . properties ; referenceType . typeName = realReferenceType . typeName ; } ) ; return referenceType ; } function nodeIsUsable ( node ) { switch ( node . kind ) { case ts . SyntaxKind . InterfaceDeclaration : case ts . SyntaxKind . ClassDeclaration : case ts . SyntaxKind . TypeAliasDeclaration : return true ; default : return false ; } } function resolveLeftmostIdentifier ( type ) { while ( type . kind !== ts . SyntaxKind . Identifier ) { type = ( type as ts . QualifiedName ) . left ; } return type as ts . Identifier ; } function resolveModelTypeScope ( leftmost , statements ) { return statements ; } function getModelTypeDeclaration ( type ) { const leftmostIdentifier = resolveLeftmostIdentifier ( type ) ; const statements = resolveModelTypeScope ( leftmostIdentifier , MetadataGenerator . current . nodes ) ; const typeName = type . kind === ts . SyntaxKind . Identifier ? ( type as ts . Identifier ) . text : ( type as ts . QualifiedName ) . right . text ; const modelTypes = statements . filter ( node => { if ( ! nodeIsUsable ( node ) ) { return false ; } const modelTypeDeclaration = node as UsableDeclaration ; return ( modelTypeDeclaration . name as ts . Identifier ) . text === typeName ; } ) as Array < UsableDeclaration > ; if ( ! modelTypes . length ) { throw new Error ( `No matching model found for referenced type ${ typeName }` ) ; } return modelTypes [ 0 ] ; } function getModelTypeProperties ( node , genericTypes ? ) { if ( node . kind === ts . SyntaxKind . TypeLiteral || node . kind === ts . SyntaxKind . InterfaceDeclaration ) { const interfaceDeclaration = node as ts . InterfaceDeclaration ; return interfaceDeclaration . members . filter ( member => { if ( ( member as any ) . type && ( member as any ) . type . kind === ts . SyntaxKind . FunctionType ) { return false ; } return member . kind === ts . SyntaxKind . PropertySignature ; } ) . map ( ( member ) => { const propertyDeclaration = member as ts . PropertyDeclaration ; const identifier = propertyDeclaration . name as ts . Identifier ; if ( ! propertyDeclaration . type ) { throw new Error ( 'No valid type found for property declaration.' ) ; } let aType = propertyDeclaration . type ; if ( aType . kind === ts . SyntaxKind . TypeReference && genericTypes && genericTypes . length && node . typeParameters ) { const typeParams = _ . map ( node . typeParameters , ( typeParam ) => { return typeParam . name . text ; } ) ; const typeIdentifier = ( aType as ts . TypeReferenceNode ) . typeName ; let typeIdentifierName ; if ( ( typeIdentifier as ts . Identifier ) . text ) { typeIdentifierName = ( typeIdentifier as ts . Identifier ) . text ; } else { typeIdentifierName = ( typeIdentifier as ts . QualifiedName ) . right . text ; } const indexOfType = _ . indexOf < string > ( typeParams , typeIdentifierName ) ; if ( indexOfType >= 0 ) { aType = genericTypes [ indexOfType ] as ts . TypeNode ; } } return { description : getNodeDescription ( propertyDeclaration ) , name : identifier . text , required : ! propertyDeclaration . questionToken , type : resolveType ( aType ) } ; } ) ; } if ( node . kind === ts . SyntaxKind . TypeAliasDeclaration ) { const typeAlias = node as ts . TypeAliasDeclaration ; return ! keywords . includes ( typeAlias . type . kind ) ? getModelTypeProperties ( typeAlias . type , genericTypes ) : [ ] ; } const classDeclaration = node as ts . ClassDeclaration ; let properties = classDeclaration . members . filter ( ( member ) => { if ( member . kind !== ts . SyntaxKind . PropertyDeclaration ) { return false ; } const propertySignature = member as ts . PropertySignature ; return propertySignature && hasPublicMemberModifier ( propertySignature ) ; } ) as Array < ts . PropertyDeclaration | ts . ParameterDeclaration > ; const classConstructor = classDeclaration . members . find ( ( member ) => member . kind === ts . SyntaxKind . Constructor ) as ts . ConstructorDeclaration ; if ( classConstructor && classConstructor . parameters ) { properties = properties . concat ( classConstructor . parameters . filter ( parameter => hasPublicConstructorModifier ( parameter ) ) as any ) ; } return properties . map ( declaration => { const identifier = declaration . name as ts . Identifier ; if ( ! declaration . type ) { throw new Error ( 'No valid type found for property declaration.' ) ; } return { description : getNodeDescription ( declaration ) , name : identifier . text , required : ! declaration . questionToken , type : resolveType ( resolveTypeParameter ( declaration . type , classDeclaration , genericTypes ) ) } ; } ) ; } function resolveTypeParameter ( type , classDeclaration , genericTypes ? ) { if ( genericTypes && classDeclaration . typeParameters && classDeclaration . typeParameters . length ) { for ( let i = 0 ; i < classDeclaration . typeParameters . length ; i ++ ) { if ( type . typeName && classDeclaration . typeParameters [ i ] . name . text === type . typeName . text ) { return genericTypes [ i ] ; } } } return type ; } function getModelTypeAdditionalProperties ( node ) { if ( node . kind === ts . SyntaxKind . InterfaceDeclaration ) { const interfaceDeclaration = node as ts . InterfaceDeclaration ; return interfaceDeclaration . members . filter ( member => member . kind === ts . SyntaxKind . IndexSignature ) . map ( ( member ) => { const indexSignatureDeclaration = member as ts . IndexSignatureDeclaration ; const indexType = resolveType ( indexSignatureDeclaration . parameters [ 0 ] . type as ts . TypeNode ) ; if ( indexType . typeName !== 'string' ) { throw new Error ( `Only string indexers are supported. Found ${ indexType . typeName }.` ) ; } return { description : '' , name : '' , required : true , type : resolveType ( indexSignatureDeclaration . type as ts . TypeNode ) } ; } ) ; } return undefined ; } function hasPublicMemberModifier ( node ) { return ! node . modifiers || node . modifiers . every ( modifier => { return modifier . kind !== ts . SyntaxKind . ProtectedKeyword && modifier . kind !== ts . SyntaxKind . PrivateKeyword ; } ) ; } function hasPublicConstructorModifier ( node ) { return node . modifiers && node . modifiers . some ( modifier => { return modifier . kind === ts . SyntaxKind . PublicKeyword ; } ) ; } function getInheritedProperties ( modelTypeDeclaration , genericTypes ? ) { const properties = new Array < Property > ( ) ; if ( modelTypeDeclaration . kind === ts . SyntaxKind . TypeAliasDeclaration ) { return [ ] ; } const heritageClauses = modelTypeDeclaration . heritageClauses ; if ( ! heritageClauses ) { return properties ; } heritageClauses . forEach ( clause => { if ( ! clause . types ) { return ; } clause . types . forEach ( ( t ) => { let type = MetadataGenerator . current . getClassDeclaration ( t . expression . getText ( ) ) ; if ( ! type ) { type = MetadataGenerator . current . getInterfaceDeclaration ( t . expression . getText ( ) ) ; } if ( ! type ) { throw new Error ( `No type found for ${ t . expression . getText ( ) }` ) ; } const baseEntityName = t . expression as ts . EntityName ; const parentGenerictypes = resolveTypeArguments ( modelTypeDeclaration as ts . ClassDeclaration , genericTypes ) ; const genericTypeMap = resolveTypeArguments ( type , t . typeArguments , parentGenerictypes ) ; const subClassGenericTypes = getSubClassGenericTypes ( genericTypeMap , t . typeArguments ) ; getReferenceType ( baseEntityName , genericTypeMap , subClassGenericTypes ) . properties . forEach ( property => properties . push ( property ) ) ; } ) ; } ) ; return properties ; } function getModelDescription ( modelTypeDeclaration ) { return getNodeDescription ( modelTypeDeclaration ) ; } function getNodeDescription ( node ) { const symbol = MetadataGenerator . current . typeChecker . getSymbolAtLocation ( node . name as ts . Node ) ; if ( symbol ) { /**
         * TODO: Workaround for what seems like a bug in the compiler
         * Warrants more investigation and possibly a PR against typescript
         */ if ( node . kind === ts . SyntaxKind . Parameter ) { symbol . flags = 0 ; } const comments = symbol . getDocumentationComment ( MetadataGenerator . current . typeChecker ) ; if ( comments . length ) { return ts . displayPartsToString ( comments ) ; } } return '' ; } function getSubClassGenericTypes ( genericTypeMap ? , typeArguments ? ) { if ( genericTypeMap && typeArguments ) { const result = [ ] ; typeArguments . forEach ( ( t ) => { const typeName = getAnyTypeName ( t ) ; if ( genericTypeMap . has ( typeName ) ) { result . push ( genericTypeMap . get ( typeName ) as ts . TypeNode ) ; } else { result . push ( t ) ; } } ) ; return result ; } return null ; } export function getSuperClass ( node , typeArguments ? ) { const clauses = node . heritageClauses ; if ( clauses ) { const filteredClauses = clauses . filter ( clause => clause . token === ts . SyntaxKind . ExtendsKeyword ) ; if ( filteredClauses . length > 0 ) { const clause = filteredClauses [ 0 ] ; if ( clause . types && clause . types . length ) { const type = MetadataGenerator . current . getClassDeclaration ( clause . types [ 0 ] . expression . getText ( ) ) ; return { type : type , typeArguments : resolveTypeArguments ( type , clause . types [ 0 ] . typeArguments , typeArguments ) } ; } } } return undefined ; } function buildGenericTypeMap ( node , typeArguments ? ) { const result = new Map < String , ts . TypeNode > ( ) ; if ( node . typeParameters && typeArguments ) { node . typeParameters . forEach ( ( typeParam , index ) => { const paramName = typeParam . name . text ; result . set ( paramName , typeArguments [ index ] ) ; } ) ; } return result ; } function resolveTypeArguments ( node , typeArguments ? , parentTypeArguments ? ) { const result = buildGenericTypeMap ( node , typeArguments ) ; if ( parentTypeArguments ) { result . forEach ( ( value , key ) => { const typeName = getAnyTypeName ( value ) ; if ( parentTypeArguments . has ( typeName ) ) { result . set ( key , parentTypeArguments . get ( typeName ) as ts . TypeNode ) ; } } ) ; } return result ; } /**
 * Used to identify union types of a primitive and array of the same primitive, e.g. `string | string[]`
 */ export function getCommonPrimitiveAndArrayUnionType ( typeNode ? ) { if ( typeNode && typeNode . kind === ts . SyntaxKind . UnionType ) { const union = typeNode as ts . UnionTypeNode ; const types = union . types . map ( t => resolveType ( t ) ) ; const arrType = types . find ( t => t . typeName === 'array' ) as ArrayType | undefined ; const primitiveType = types . find ( t => t . typeName !== 'array' ) ; if ( types . length === 2 && arrType && arrType . elementType && primitiveType && arrType . elementType . typeName === primitiveType . typeName ) { return arrType ; } } return null ; } export function getLiteralValue ( expression ) { if ( expression . kind === ts . SyntaxKind . StringLiteral ) { return ( expression as ts . StringLiteral ) . text ; } if ( expression . kind === ts . SyntaxKind . NumericLiteral ) { return parseFloat ( ( expression as ts . NumericLiteral ) . text ) ; } if ( expression . kind === ts . SyntaxKind . TrueKeyword ) { return true ; } if ( expression . kind === ts . SyntaxKind . FalseKeyword ) { return false ; } if ( expression . kind === ts . SyntaxKind . ArrayLiteralExpression ) { return ( expression as ts . ArrayLiteralExpression ) . elements . map ( e => getLiteralValue ( e ) ) ; } return ; }