import * as ts from 'typescript' ; import * as tss from 'typescript/lib/tsserverlibrary' ; import { Tree } from '@angular-devkit/schematics' ; import { MemberChange } from './schema' ; import { escapeRegExp } from './util' ; import { Logger } from './tsLogger' ; import { TSLanguageService } from './tsPlugin/TSLanguageService' ; export const IG_PACKAGE_NAME = 'igniteui-angular' ; export const NG_LANG_SERVICE_PACKAGE_NAME = '@angular/language-service' ; export const NG_CORE_PACKAGE_NAME = '@angular/core' ; export const CUSTOM_TS_PLUGIN_PATH = './tsPlugin' ; export const CUSTOM_TS_PLUGIN_NAME = 'igx-ts-plugin' ; enum SyntaxTokens { ClosingParenthesis = ')' , MemberAccess = '.' , Question = '?' } export class MemberInfo implements Pick < tss . DefinitionInfo , 'name' | 'fileName' > { public name : string ; public fileName : string ; } /** Returns a source file */ export const getIdentifierPositions = ( source , name ) : Array < { start : number ; end : number ; } > => { if ( typeof source === 'string' ) { source = ts . createSourceFile ( '' , source , ts . ScriptTarget . Latest , true ) ; } const positions = [ ] ; const checkIdentifier = ( node ) : boolean => { if ( ! ts . isIdentifier ( node ) || ! node . parent ) { return false ; } if ( node . parent . kind === ts . SyntaxKind . PropertyDeclaration ) { return false ; } if ( ts . isPropertyAssignment ( node . parent ) || ts . isPropertySignature ( node . parent ) ) { if ( node . parent . name . getText ( ) === name ) { return false ; } } const cleanName = name . match ( /\w+/g ) [ 0 ] || name ; return node . text === cleanName ; } ; const findIdentifiers = ( node ) => { if ( checkIdentifier ( node ) ) { positions . push ( { start : node . getStart ( ) , end : node . end } ) ; } ts . forEachChild ( node , findIdentifiers ) ; } ; source . forEachChild ( findIdentifiers ) ; return positions ; } ; /** Returns the positions of import from module string literals  */ export const getImportModulePositions = ( sourceText , startsWith ) : Array < { start : number ; end : number ; } > => { const source = ts . createSourceFile ( '' , sourceText , ts . ScriptTarget . Latest , true ) ; const positions = [ ] ; for ( const statement of source . statements ) { if ( statement . kind === ts . SyntaxKind . ImportDeclaration ) { const specifier = ( statement as ts . ImportDeclaration ) . moduleSpecifier as ts . StringLiteral ; if ( specifier . text . startsWith ( startsWith ) ) { positions . push ( { start : specifier . getStart ( ) + 1 , end : specifier . end - 1 } ) ; } } } return positions ; } ; /** Filters out statements to named imports (e.g. `import {x, y}`) from PACKAGE_IMPORT */ const namedImportFilter = ( statement ) => { if ( statement . kind === ts . SyntaxKind . ImportDeclaration && ( ( statement as ts . ImportDeclaration ) . moduleSpecifier as ts . StringLiteral ) . text . endsWith ( IG_PACKAGE_NAME ) ) { const clause = ( statement as ts . ImportDeclaration ) . importClause ; return clause && clause . namedBindings && clause . namedBindings . kind === ts . SyntaxKind . NamedImports ; } return false ; } ; export const getRenamePositions = ( sourcePath , name , service ) : Array < { start : number ; end : number ; } > => { const source = service . getProgram ( ) . getSourceFile ( sourcePath ) ; const positions = [ ] ; const imports = source . statements . filter ( < ( a : ts . Statement ) => a is ts . ImportDeclaration > namedImportFilter ) ; if ( ! imports . length ) { return positions ; } const elements = imports . map ( x => ( x . importClause . namedBindings as ts . NamedImports ) . elements ) . reduce ( ( prev , current ) => prev . concat ( current ) as unknown as ts . NodeArray < ts . ImportSpecifier > ) ; for ( const elem of elements ) { if ( elem . propertyName && elem . propertyName . text === name ) { positions . push ( { start : elem . propertyName . getStart ( ) , end : elem . propertyName . getEnd ( ) } ) ; break ; } if ( ! elem . propertyName && elem . name . text === name ) { const renames = service . findRenameLocations ( sourcePath , elem . name . getStart ( ) , false , false , false ) ; if ( renames ) { const renamesPos = renames . map ( x => ( { start : x . textSpan . start , end : x . textSpan . start + x . textSpan . length } ) ) ; positions . push ( ... renamesPos ) ; } } } return positions ; } ; export const findMatches = ( content , toFind ) : number [ ] => { let matches ; const regex = new RegExp ( escapeRegExp ( toFind ) , 'g' ) ; const matchesPositions = [ ] ; do { matches = regex . exec ( content ) ; if ( matches ) { matchesPositions . push ( matches . index ) ; } } while ( matches ) ; return matchesPositions ; } ; export const replaceMatch = ( content , toReplace , replaceWith , index ) : string => content . substring ( 0 , index ) + replaceWith + content . substring ( index + toReplace . length , content . length ) ; param serviceHost export const getLanguageService = ( filePaths , host , options = { } ) : ts . LanguageService => { const fileVersions = new Map < string , number > ( ) ; patchHostOverwrite ( host , fileVersions ) ; const servicesHost = { getCompilationSettings : ( ) => options , getScriptFileNames : ( ) => filePaths , getScriptVersion : fileName => { const version = fileVersions . get ( fileName ) || 0 ; return version . toString ( ) ; } , getScriptSnapshot : fileName => { if ( ! host . exists ( fileName ) ) { return undefined ; } return ts . ScriptSnapshot . fromString ( host . read ( fileName ) . toString ( ) ) ; } , getCurrentDirectory : ( ) => process . cwd ( ) , getDefaultLibFileName : opts => ts . getDefaultLibFilePath ( opts ) , fileExists : fileName => filePaths . indexOf ( fileName ) !== - 1 } ; return ts . createLanguageService ( servicesHost , ts . createDocumentRegistry ( ) ) ; } ; const patchHostOverwrite = ( host , fileVersions ) => { const original = host . overwrite ; host . overwrite = ( path , content ) => { const version = fileVersions . get ( path ) || 0 ; fileVersions . set ( path , version + 1 ) ; original . call ( host , path , content ) ; } ; } ; param serverHost export const createProjectService = ( serverHost ) : tss . server . ProjectService => { const logger = new Logger ( false , tss . server . LogLevel . verbose ) ; const projectService = new tss . server . ProjectService ( { host : serverHost , logger , cancellationToken : tss . server . nullCancellationToken , useSingleInferredProject : true , useInferredProjectPerProjectRoot : true , globalPlugins : [ CUSTOM_TS_PLUGIN_NAME , NG_LANG_SERVICE_PACKAGE_NAME ] , allowLocalPluginLoads : false , typingsInstaller : tss . server . nullTypingsInstaller , session : undefined } ) ; projectService . setHostConfiguration ( { formatOptions : projectService . getHostFormatCodeOptions ( ) , extraFileExtensions : [ { extension : '.html' , isMixedContent : false , scriptKind : tss . ScriptKind . External , } ] } ) ; projectService . configurePlugin ( { pluginName : CUSTOM_TS_PLUGIN_NAME , configuration : { } } ) ; projectService . configurePlugin ( { pluginName : NG_LANG_SERVICE_PACKAGE_NAME , configuration : { ivy : true , angularOnly : false , } , } ) ; return projectService ; } ; /**
 * Attempts to get type definitions using the TypeScript Language Service.
 * Can fall back to a cached version of the TSLS.
 */ const getTypeDefinitions = ( langServ , entryPath , position ) : any => langServ . getTypeDefinitionAtPosition ( entryPath , position ) || ( langServ as TSLanguageService ) . getTypeScriptLanguageService ( ) . getTypeDefinitionAtPosition ( entryPath , position ) ; param langServ param entryPath param position export const getTypeDefinitionAtPosition = ( langServ , entryPath , position ) : MemberInfo | null => { const definition = langServ . getDefinitionAndBoundSpan ( entryPath , position ) ?. definitions [ 0 ] ; if ( ! definition ) { return null ; } if ( definition . kind . toString ( ) === 'reference' ) { return langServ . getDefinitionAndBoundSpan ( entryPath , definition . textSpan . start ) . definitions [ 0 ] ; } if ( definition . kind . toString ( ) === 'method' ) { return getMethodTypeDefinition ( langServ , definition ) ; } if ( entryPath . endsWith ( '.ts' ) ) { const sourceFile = langServ . getProgram ( ) . getSourceFile ( entryPath ) ; const node = findNodeAtPosition ( sourceFile , position ) ; if ( node ) { const memberInfo = resolveMemberInfo ( langServ , node ) ; if ( memberInfo ) { return memberInfo ; } } } let typeDefs = getTypeDefinitions ( langServ , definition . fileName || entryPath , definition . textSpan . start ) ; if ( ! typeDefs ) { if ( ! definition . fileName . endsWith ( '.ts' ) ) { return null ; } const sourceFile = langServ . getProgram ( ) . getSourceFile ( definition . fileName ) ; if ( ! sourceFile ) { return null ; } const classDeclaration = sourceFile . statements . filter ( ( m ) : m is tss . ClassDeclaration => m . kind === tss . SyntaxKind . ClassDeclaration ) . find ( m => m . name ?. getText ( ) === definition . containerName ) ; if ( ! classDeclaration ) { return null ; } const member = classDeclaration . members . find ( m => m . name . getText ( ) === definition . name ) ; if ( ! member ?. name ) { return null ; } typeDefs = getTypeDefinitions ( langServ , definition . fileName , member . name . getStart ( ) + 1 ) ; } if ( typeDefs ?. length ) { return typeDefs [ 0 ] ; } return null ; } ; param change param langServ param entryPath param matchPosition export const isMemberIgniteUI = ( change , langServ , entryPath , matchPosition ) : boolean => { const content = langServ . getProgram ( ) . getSourceFile ( entryPath ) . getText ( ) ; matchPosition = shiftMatchPosition ( matchPosition , content ) ; const prevChar = content . substr ( matchPosition - 1 , 1 ) ; if ( prevChar === SyntaxTokens . ClosingParenthesis ) { matchPosition = langServ . getBraceMatchingAtPosition ( entryPath , matchPosition - 1 ) [ 0 ] ?. start ?? matchPosition ; } const typeDef = getTypeDefinitionAtPosition ( langServ , entryPath , matchPosition ) ; if ( ! typeDef ) { return false ; } return typeDef . fileName . includes ( IG_PACKAGE_NAME ) && change . definedIn . indexOf ( typeDef . name ) !== - 1 ; } ; const resolveMemberInfo = ( langServ , node ) : MemberInfo | null => { const typeChecker = langServ . getProgram ( ) . getTypeChecker ( ) ; const nodeType = typeChecker . getTypeAtLocation ( node ) ; const typeArguments = typeChecker . getTypeArguments ( nodeType as tss . TypeReference ) ; if ( typeArguments && typeArguments . length < 1 ) { const name = nodeType . getSymbol ( ) ?. getName ( ) ; const declarations = nodeType . getSymbol ( ) ?. getDeclarations ( ) ; if ( declarations && declarations . length > 0 ) { const fileName = declarations [ 0 ] . getSourceFile ( ) . fileName ; if ( name && fileName ) { return { name , fileName } ; } } } return null ; } ; /**
 * Looks up a node which end property matches the specified position.
 * Can go to the next node if the currently found one is invalid (comment for example)
 */ const findNodeAtPosition = ( sourceFile , position ) : tss . Node | null => { if ( ! sourceFile ) { return null ; } return findInnerNode ( sourceFile , position ) ; } ; const findInnerNode = ( node , position ) : tss . Node | null => { if ( position <= node . getEnd ( ) ) { return node . forEachChild ( cn => findInnerNode ( cn , position ) ) || node ; } return null ; } ; /**
 * Shifts the match position of the identifier to the left
 * until any character other than an empty string or a '.' is reached. #9347
 */ const shiftMatchPosition = ( matchPosition , content ) : number => { do { matchPosition -- ; } while ( matchPosition > 0 && ! content [ matchPosition - 1 ] . trim ( ) || content [ matchPosition - 1 ] === SyntaxTokens . MemberAccess || content [ matchPosition - 1 ] === SyntaxTokens . Question ) ; return matchPosition ; } ; param langServ param definition const getMethodTypeDefinition = ( langServ , definition ) : MemberInfo | null => { const sourceFile = langServ . getProgram ( ) . getSourceFile ( definition . fileName ) ; const classDeclaration = sourceFile . statements . filter ( ( m ) : m is tss . ClassDeclaration => m . kind === tss . SyntaxKind . ClassDeclaration ) . find ( m => m . name ?. getText ( ) === definition . containerName ) ; const methodDeclaration = classDeclaration ?. members . filter ( ( m ) : m is tss . MethodDeclaration => m . kind === tss . SyntaxKind . MethodDeclaration ) . find ( m => m . name . getText ( ) === definition . name ) ; if ( ! methodDeclaration ) { return null ; } const typeChecker = langServ . getProgram ( ) . getTypeChecker ( ) ; const signature = typeChecker . getSignatureFromDeclaration ( methodDeclaration ) ; if ( ! signature ) { return null ; } const returnType = typeChecker . getReturnTypeOfSignature ( signature ) ; const name = returnType ?. symbol ?. escapedName ?. toString ( ) ; if ( name && returnType . symbol ?. declarations ?. length > 0 ) { /**
         * For union return types like T | null | undefined
         * and intersection return types like T & null & undefined
         * the TypeChecker ignores null and undefined and returns only T which is not
         * marked as a union or intersection type.
         *
         * For union and intersection types like T | R | C
         * the TypeChecker returns a TypeObject which is marked as union or intersection type.
         */ const fileName = returnType . symbol . declarations [ 0 ] . getSourceFile ( ) . fileName ; return { name , fileName } ; } return null ; } ;