import * as fs from 'fs' ; namespace pfs { export async function isFile ( path ) { const stat = await fs . promises . stat ( path ) ; return stat . isFile ( ) ; } export async function isDirectory ( path ) { const stat = await fs . promises . stat ( path ) ; return stat . isDirectory ( ) ; } } import * as path from 'path' ; import * as uuid from 'uuid' ; import * as crypto from 'crypto' ; import * as os from 'os' ; import * as yargs from 'yargs' ; import { URI } from 'vscode-uri' ; import * as ts from 'typescript' ; import { Id , Version , Vertex , Edge , Source , RepositoryInfo } from 'lsif-protocol' ; import { Writer , StdoutWriter , FileWriter } from './common/writer' ; import { Builder , EmitterContext } from './common/graph' ; import { Emitter , EmitterModule } from './emitters/emitter' ; import { TypingsInstaller } from './typings' ; import { lsif , ProjectInfo , Options as LSIFOptions , DataManager , DataMode , Logger , NullLogger } from './lsif' ; import * as tss from './typescripts' ; import { Options , builder , ConfigOptions } from './args' ; import { ImportMonikers } from './npm/importMonikers' ; import { ExportMonikers } from './npm/exportMonikers' ; import { PackageJson } from './npm/package' ; function loadConfigFile ( file ) { let absolute = path . resolve ( file ) ; let readResult = ts . readConfigFile ( absolute , ts . sys . readFile ) ; if ( readResult . error ) { throw new Error ( ts . formatDiagnostics ( [ readResult . error ] , ts . createCompilerHost ( { } ) ) ) ; } let config = readResult . config ; if ( config . compilerOptions !== undefined ) { config . compilerOptions = Object . assign ( config . compilerOptions , tss . getDefaultCompilerOptions ( file ) ) ; } let result = ts . parseJsonConfigFileContent ( config , ts . sys , path . dirname ( absolute ) ) ; if ( result . errors . length > 0 ) { throw new Error ( ts . formatDiagnostics ( result . errors , ts . createCompilerHost ( { } ) ) ) ; } return result ; } function parseConfigFileContent ( options , basePath ) { const configFileName = options . kind === 'ts' ? 'tsconfig.json' : 'jsconfig.json' ; const config = Object . assign ( { } , options ) ; delete config . __brand ; delete config . configFilePath ; delete config . kind ; if ( config . compilerOptions !== undefined ) { config . compilerOptions = Object . assign ( config . compilerOptions , tss . getDefaultCompilerOptions ( configFileName ) ) ; } let result = ts . parseJsonConfigFileContent ( config , ts . sys , basePath ) ; if ( result . errors . length > 0 ) { throw new Error ( ts . formatDiagnostics ( result . errors , ts . createCompilerHost ( { } ) ) ) ; } return result ; } function createEmitter ( options , writer , idGenerator ) { let emitterModule ; switch ( options . outputFormat ) { case 'json' : emitterModule = require ( './emitters/json' ) ; break ; case 'line' : emitterModule = require ( './emitters/line' ) ; break ; case 'vis' : emitterModule = require ( './emitters/vis' ) ; break ; case 'graphSON' : emitterModule = require ( './emitters/graphSON' ) ; break ; default : emitterModule = require ( './emitters/json' ) ; } return emitterModule . create ( writer , idGenerator ) ; } function createIdGenerator ( options ) { switch ( options . id ) { case 'uuid' : return ( ) => { return uuid . v4 ( ) ; } ; default : let counter = 1 ; return ( ) => { return counter ++ ; } ; } } function makeKey ( config ) { let hash = crypto . createHash ( 'md5' ) ; hash . update ( JSON . stringify ( ConfigOptions . is ( config ) ? config : config . options , undefined , 0 ) ) ; return hash . digest ( 'base64' ) ; } interface InternalLogger extends Logger { begin ( ) : void ; end ( ) : void ; } class InternalNullLogger extends NullLogger implements InternalLogger { constructor ( ) { super ( ) ; } public begin ( ) { } public end ( ) { } } class AbstractLogger extends InternalNullLogger implements InternalLogger { private reported : Set < string > ; constructor ( ) { super ( ) ; this . reported = new Set ( ) ; } protected internalSymbolMessage ( symbol , symbolId , location ) { if ( this . reported . has ( symbolId ) ) { return undefined ; } this . reported . add ( symbolId ) ; const buffer = [ ] ; buffer . push ( os . EOL ) ; buffer . push ( `The symbol ${ symbol . name } with id ${ symbolId } is treated as internal although it is referenced outside` ) ; buffer . push ( os . EOL ) ; const declarations = symbol . getDeclarations ( ) ; if ( declarations === undefined ) { buffer . push ( `  The symbol has no visible declarations.` ) ; buffer . push ( os . EOL ) ; } else { buffer . push ( `  The symbol is declared in the following files:` ) ; buffer . push ( os . EOL ) ; for ( const declaration of declarations ) { const sourceFile = declaration . getSourceFile ( ) ; const lc = ts . getLineAndCharacterOfPosition ( sourceFile , declaration . getStart ( ) ) ; buffer . push ( `    ${ sourceFile . fileName } at location [${ lc . line },${ lc . character }]` ) ; buffer . push ( os . EOL ) ; } } if ( location !== undefined ) { const sourceFile = location . getSourceFile ( ) ; const lc = ts . getLineAndCharacterOfPosition ( sourceFile , location . getStart ( ) ) ; buffer . push ( `  Problem got detected while parsing the following file:` ) ; buffer . push ( os . EOL ) ; buffer . push ( `    ${ sourceFile . fileName } at location [${ lc . line },${ lc . character }]` ) ; buffer . push ( os . EOL ) ; } return buffer . join ( '' ) ; } } class StreamLogger extends AbstractLogger implements InternalLogger { constructor ( private stream ) { super ( ) ; } public doneIndexFile ( _fileName ) { this . stream . write ( '.' ) ; } public projectStatus ( projectName , numberOfSymbols , numberOfDocuments , time ) { this . stream . write ( os . EOL ) ; this . stream . write ( `Processed ${ numberOfSymbols } symbols in ${ numberOfDocuments } files for project ${ projectName }` ) ; if ( time !== undefined ) { this . stream . write ( ` in ${ time }ms.` ) ; } else { this . stream . write ( '.' ) ; } this . stream . write ( os . EOL ) ; this . stream . write ( os . EOL ) ; } public internalSymbol ( symbol , symbolId , location ) { const message = this . internalSymbolMessage ( symbol , symbolId , location ) ; if ( message === undefined ) { return ; } this . stream . write ( message ) ; } } class FileLogger extends AbstractLogger { private fileHandle : number ; private reportProgressOnStdout : boolean ; constructor ( file , reportProgressOnStdout ) { super ( ) ; this . fileHandle = fs . openSync ( file , 'w' ) ; this . reportProgressOnStdout = reportProgressOnStdout ; } public startIndexFile ( fileName ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Start indexing file: ${ fileName }${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public internalSymbol ( symbol , symbolId , location ) { const message = this . internalSymbolMessage ( symbol , symbolId , location ) ; if ( message === undefined ) { return ; } fs . writeSync ( this . fileHandle , message ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public doneIndexFile ( fileName ) { if ( this . reportProgressOnStdout ) { process . stdout . write ( '.' ) ; } this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Done indexing file: ${ fileName }${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public beginProject ( name ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Begin project ${ name }${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public startEndProject ( name ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Start ending project ${ name }${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public projectStatus ( projectName , numberOfSymbols , numberOfDocuments , time ) { this . writeTime ( ) ; const buffer = [ `Processed ${ numberOfSymbols } symbols in ${ numberOfDocuments } files for project ${ projectName }` ] ; if ( time !== undefined ) { buffer . push ( ` in ${ time }ms.` ) ; } else { buffer . push ( '.' ) ; } buffer . push ( os . EOL ) ; fs . writeSync ( this . fileHandle , buffer . join ( '' ) ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public doneEndProject ( name ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Done ending project ${ name }${ os . EOL }${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public beginDataManager ( ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Begin global data manager${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public startEndDataManager ( ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Start ending global data manager${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public doneEndDataManager ( ) { this . writeTime ( ) ; fs . writeSync ( this . fileHandle , `Done ending global data manager${ os . EOL }` ) ; fs . fdatasyncSync ( this . fileHandle ) ; } public end ( ) { fs . closeSync ( this . fileHandle ) ; if ( this . reportProgressOnStdout ) { process . stdout . write ( os . EOL ) ; } } private writeTime ( ) { const date = new Date ( ) ; fs . writeSync ( this . fileHandle , `[${ date . getFullYear ( ) }-${ this . pad ( date . getMonth ( ) , 2 ) }-${ this . pad ( date . getDay ( ) , 2 ) } ${ this . pad ( date . getHours ( ) , 2 ) }:${ this . pad ( date . getMinutes ( ) , 2 ) }:${ this . pad ( date . getSeconds ( ) , 2 ) }:${ this . pad ( date . getMilliseconds ( ) , 3 ) }] ` ) ; } private pad ( value , digits ) { return ( '00' + value ) . slice ( - 1 * digits ) ; } } interface ProcessProjectOptions { workspaceRoot : string ; projectName ? : string ; typeAcquisition : boolean ; noProjectReferences : boolean ; packageInfo ? : Map < string , string > ; stdout : boolean ; dataMode : DataMode ; reporter : Logger ; processed : Map < String , ProjectInfo > ; files ? : Map < string , string > ; } async function processProject ( pclOrOptions , emitter , typingsInstaller , dataManager , importMonikers , exportMonikers , options ) { let config ; let configFilePath ; let key ; if ( ConfigOptions . is ( pclOrOptions ) ) { if ( pclOrOptions . configFilePath === undefined ) { console . error ( `No config file path available although --config is used.` ) ; return - 1 ; } configFilePath = pclOrOptions . configFilePath ; key = configFilePath ?? makeKey ( pclOrOptions ) ; if ( options . processed . has ( key ) ) { return options . processed . get ( key ) ! ; } config = parseConfigFileContent ( pclOrOptions , path . dirname ( configFilePath ) ) ; } else { config = pclOrOptions ; configFilePath = tss . CompileOptions . getConfigFilePath ( config . options ) ; key = configFilePath ?? makeKey ( config ) ; if ( options . processed . has ( key ) ) { return options . processed . get ( key ) ! ; } if ( configFilePath && ! ts . sys . fileExists ( configFilePath ) ) { console . error ( `Project configuration file ${ configFilePath } does not exist` ) ; return 1 ; } if ( configFilePath && config . options . project ) { config = loadConfigFile ( configFilePath ) ; } } if ( options . typeAcquisition && ( config . typeAcquisition === undefined || ! ! config . typeAcquisition . enable ) ) { const projectRoot = options . workspaceRoot ; if ( config . options . types !== undefined ) { const start = configFilePath !== undefined ? configFilePath : process . cwd ( ) ; await typingsInstaller . installTypings ( projectRoot , start , config . options . types ) ; } else { await typingsInstaller . guessTypings ( projectRoot , configFilePath !== undefined ? path . dirname ( configFilePath ) : process . cwd ( ) ) ; } } if ( configFilePath !== undefined && options . packageInfo !== undefined ) { const packageFile = options . packageInfo . get ( configFilePath ) ; if ( packageFile !== undefined ) { const packageJson = PackageJson . read ( packageFile ) ; if ( packageJson !== undefined ) { exportMonikers = new ExportMonikers ( emitter , options . workspaceRoot , packageJson ) ; } } } let scriptSnapshots = new Map ( ) ; const host = { getScriptFileNames : ( ) => { return config . fileNames ; } , getCompilationSettings : ( ) => { return config . options ; } , getProjectReferences : ( ) => { return config . projectReferences ; } , getScriptVersion : ( _fileName ) : string => { return '0' ; } , getProjectVersion : ( ) => '0' , getScriptSnapshot : ( fileName ) : ts . IScriptSnapshot | undefined => { let result = scriptSnapshots . get ( fileName ) ; if ( result === undefined ) { let content = options . files !== undefined ? options . files . get ( fileName ) : undefined ; if ( content === undefined && ts . sys . fileExists ( fileName ) ) { content = ts . sys . readFile ( fileName ) ; } if ( content === undefined ) { return undefined ; } result = ts . ScriptSnapshot . fromString ( content ) ; scriptSnapshots . set ( fileName , result ) ; } return result ; } , getCurrentDirectory : ( ) => { if ( configFilePath !== undefined ) { return path . dirname ( configFilePath ) ; } else { return process . cwd ( ) ; } } , getDefaultLibFileName : ( options ) => { return ts . getDefaultLibFilePath ( options ) ; } , directoryExists : ts . sys . directoryExists , getDirectories : ts . sys . getDirectories , fileExists : ts . sys . fileExists , readFile : ts . sys . readFile , readDirectory : ts . sys . readDirectory , realpath : ts . sys . realpath } ; tss . LanguageServiceHost . useSourceOfProjectReferenceRedirect ( host , ( ) => { return ! config . options . disableSourceOfProjectReferenceRedirect ; } ) ; const languageService = ts . createLanguageService ( host ) ; let program = languageService . getProgram ( ) ; if ( program === undefined ) { console . error ( 'Couldn\'t create language service with underlying program.' ) ; process . exitCode = - 1 ; return - 1 ; } const dependsOn = [ ] ; const references = options . noProjectReferences ? undefined : program . getResolvedProjectReferences ( ) ; if ( references ) { for ( let reference of references ) { if ( reference ) { const result = await processProject ( reference . commandLine , emitter , typingsInstaller , dataManager , importMonikers , exportMonikers , options ) ; if ( typeof result === 'number' ) { return result ; } dependsOn . push ( result ) ; } } } if ( ( ! references || references . length === 0 ) && config . fileNames . length === 0 ) { console . error ( `No input files specified.` ) ; return 1 ; } program = languageService . getProgram ( ) ! ; program . getTypeChecker ( ) ; const level = options . processed . size ; let projectName ; if ( options . projectName !== undefined && level === 0 && ( ! references || references . length === 0 ) ) { projectName = options . projectName ; } if ( projectName === undefined && configFilePath !== undefined ) { projectName = path . basename ( path . dirname ( configFilePath ) ) ; } if ( projectName === undefined ) { if ( options . projectName !== undefined ) { projectName = `${ options . projectName }/${ level + 1 }` ; } else { projectName = `${ path . basename ( options . workspaceRoot ) }/${ level + 1 }` ; } } if ( projectName === undefined ) { console . error ( `No project name provided.` ) ; return 1 ; } const packageJsonFile = options . packageInfo === undefined ? undefined : typeof options . packageInfo === 'string' ? options . packageInfo : configFilePath !== undefined ? options . packageInfo . get ( configFilePath ) : undefined ; const lsifOptions = { workspaceRoot : options . workspaceRoot , projectName : projectName , tsConfigFile : configFilePath , packageJsonFile : packageJsonFile , stdout : options . stdout , logger : options . reporter , dataMode : options . dataMode , } ; const result = await lsif ( emitter , languageService , dataManager , importMonikers , exportMonikers , dependsOn , lsifOptions ) ; if ( typeof result !== 'number' ) { options . processed . set ( key , result ) ; } return result ; } export async function run ( this , options ) { if ( options . help ) { return ; } if ( options . version ) { console . log ( require ( '../package.json' ) . version ) ; return ; } if ( options . package !== undefined && options . publishedPackages !== undefined ) { console . log ( `Only package or publishedPackages can be set but not both.` ) ; process . exitCode = - 1 ; return ; } options = Options . resolvePathToConfig ( options ) ; if ( options . package && ! await pfs . isFile ( options . package ) ) { console . error ( `The package.json file referenced by the package option doesn't exist. The value is ${ options . package }` ) ; process . exitCode = - 1 ; return ; } else if ( options . publishedPackages !== undefined ) { let failed = false ; for ( const item of options . publishedPackages ) { if ( ! await pfs . isFile ( item . package ) ) { console . error ( `The package.json file referenced by the 'publishedPackages' option doesn't exist. The value is ${ JSON . stringify ( item , undefined , 0 ) }` ) ; failed = true ; } if ( ! await pfs . isFile ( item . project ) ) { console . error ( `The project file referenced by the 'publishedPackages' options doesn't exist. The value is ${ JSON . stringify ( item , undefined , 0 ) }` ) ; failed = true ; } } if ( failed ) { process . exitCode = - 1 ; return ; } } let writer = options . outputWriter ; if ( writer === undefined ) { if ( options . stdout ) { writer = new StdoutWriter ( ) ; } else if ( options . out ) { writer = new FileWriter ( options . out ) ; } } if ( writer === undefined ) { console . log ( `Either a output file using --out or --stdout must be specified.` ) ; process . exitCode = - 1 ; return ; } const workspaceRoot = tss . normalizePath ( options . workspaceRoot ?? process . cwd ( ) ) ; if ( ! await pfs . isDirectory ( workspaceRoot ) ) { console . error ( `The workspace root doesn't denote a folder on disk. The value is ${ workspaceRoot }` ) ; process . exitCode = - 1 ; return ; } if ( typeof options . source === 'string' ) { if ( ! await pfs . isFile ( options . source ) ) { console . error ( `The source option doesn't denote a valid file on disk. The value is ${ options . source }` ) ; process . exitCode = - 1 ; return ; } if ( path . basename ( options . source ) !== 'package.json' ) { console . error ( `The source option can only point to a package.json file. The value is ${ options . source }` ) ; process . exitCode = - 1 ; return ; } } const args = [ ] ; let needsProject = true ; for ( let i = 0 ; i < ts . sys . args . length ; i ++ ) { const arg = ts . sys . args [ i ] ; if ( arg === '-p' || arg === '--project' || arg . startsWith ( '-p=' ) || arg . startsWith ( '--project=' ) ) { needsProject = false ; } if ( arg === '--config' ) { i ++ ; continue ; } if ( arg . startsWith ( '--config=' ) ) { continue ; } args . push ( arg ) ; } if ( needsProject && typeof options . p === 'string' ) { args . push ( '-p' , options . p ) ; } if ( options . files !== undefined ) { for ( const filename of options . files . keys ( ) ) { args . push ( filename ) ; } } const config = ConfigOptions . is ( options . p ) ? options . p : ts . parseCommandLine ( args ) ; const idGenerator = createIdGenerator ( options ) ; const emitter = createEmitter ( options , writer , idGenerator ) ; emitter . start ( ) ; const builder = new Builder ( { idGenerator , emitSource : ! options . noContents } ) ; const emitterContext = { get edge ( ) { return builder . edge ; } , get vertex ( ) { return builder . vertex ; } , emit ( element ) { emitter . emit ( element ) ; } , flush ( ) { return emitter . flush ( ) ; } } ; const metaData = builder . vertex . metaData ( Version ) ; const source = await async function ( ) { const result = builder . vertex . source ( URI . file ( workspaceRoot ) . toString ( true ) ) ; if ( typeof options . source === 'string' ) { try { const pjc = JSON . parse ( await fs . promises . readFile ( options . source , { encoding : 'utf8' } ) ) as { repository ? : RepositoryInfo ; } ; if ( pjc . repository !== undefined && typeof pjc . repository . type === 'string' && typeof pjc . repository . url === 'string' ) { result . repository = Object . assign ( { } , pjc . repository ) ; } } catch ( error ) { console . error ( `Reading package.json file to obtain repository source failed.` ) ; return - 1 ; } } else if ( options . source !== undefined && options . source . repository !== undefined ) { result . repository = { url : options . source . repository . url , type : options . source . repository . type } ; } return result ; } ( ) ; if ( typeof source === 'number' ) { process . exitCode = source ; return ; } emitter . emit ( metaData ) ; emitter . emit ( source ) ; const capabilities = builder . vertex . capabilities ( true ) ; capabilities . declarationProvider = false ; emitter . emit ( capabilities ) ; let logger ; if ( options . log === '' ) { if ( options . stdout ) { logger = new InternalNullLogger ( ) ; } else { logger = new StreamLogger ( process . stdout ) ; } } else if ( options . log === true ) { if ( options . stdout === undefined && options . out !== undefined ) { logger = new FileLogger ( `${ options . out }.log` , true ) ; } else if ( options . stdout ) { logger = new InternalNullLogger ( ) ; } else { logger = new StreamLogger ( process . stdout ) ; } } else if ( ( typeof options . log === 'string' ) && options . log . length > 0 ) { logger = new FileLogger ( options . log , ! options . stdout ) ; } else { logger = new InternalNullLogger ( ) ; } logger . begin ( ) ; let packageInfo ; if ( options . package !== undefined ) { packageInfo = options . package ; } else { packageInfo = new Map ( ) ; if ( options . publishedPackages !== undefined ) { for ( const item of options . publishedPackages ) { const packagePath = tss . normalizePath ( item . package ) ; const projectPath = tss . normalizePath ( item . project ) ; packageInfo . set ( projectPath , packagePath ) ; } } } let exportMonikers ; if ( typeof packageInfo === 'string' ) { const packageJson = PackageJson . read ( packageInfo ) ; if ( packageJson !== undefined ) { exportMonikers = new ExportMonikers ( emitterContext , workspaceRoot , packageJson ) ; } packageInfo = undefined ; } const processProjectOptions = { workspaceRoot : workspaceRoot , projectName : options . projectName , typeAcquisition : options . typeAcquisition , noProjectReferences : options . noProjectReferences , stdout : options . stdout , dataMode : options . moniker === 'strict' ? DataMode . free : DataMode . keep , packageInfo : packageInfo , reporter : logger , processed : new Map ( ) , files : options . files } ; const dataManager = new DataManager ( emitterContext , processProjectOptions . workspaceRoot , processProjectOptions . reporter , processProjectOptions . dataMode ) ; const importMonikers = new ImportMonikers ( emitterContext , processProjectOptions . workspaceRoot ) ; dataManager . begin ( ) ; await processProject ( config , emitterContext , new TypingsInstaller ( ) , dataManager , importMonikers , exportMonikers , processProjectOptions ) ; dataManager . end ( ) ; await emitter . end ( ) ; logger . end ( ) ; } export async function main ( this ) { yargs . parserConfiguration ( { 'camel-case-expansion' : false } ) . exitProcess ( false ) . usage ( `Language Server Index Format tool for TypeScript\nVersion: ${ require ( '../package.json' ) . version }\nUsage: lsif-tsc [options][tsc options]` ) . example ( `lsif-tsc -p tsconfig.json --stdout` , `Create a LSIF dump for the tsconfig.json file and print it to stdout.` ) . version ( false ) . wrap ( Math . min ( 100 , yargs . terminalWidth ( ) ) ) ; const parsed = builder ( yargs ) . argv ; const options = Object . assign ( { } , Options . defaults , parsed ) ; return run ( Options . sanitize ( options ) ) ; } if ( require . main === module ) { main ( ) . then ( undefined , ( error ) => { console . error ( error ) ; process . exitCode = 1 ; } ) ; }