import { timingStats } from './common/timing' ; import chalk from 'chalk' ; import commandLineArgs from 'command-line-args' ; import { CommandLineOptions , OptionDefinition } from 'command-line-args' ; import { PackageTypeVerifier } from './analyzer/packageTypeVerifier' ; import { AnalyzerService } from './analyzer/service' ; import { CommandLineOptions as PyrightCommandLineOptions } from './common/commandLineOptions' ; import { LogLevel , StandardConsoleWithLevel , StderrConsoleWithLevel } from './common/console' ; import { Diagnostic , DiagnosticCategory } from './common/diagnostic' ; import { FileDiagnostics } from './common/diagnosticSink' ; import { combinePaths , normalizePath } from './common/pathUtils' ; import { createFromRealFileSystem } from './common/realFileSystem' ; import { isEmptyRange , Range } from './common/textRange' ; import { versionFromString } from './common/pythonVersion' ; import { PyrightFileSystem } from './pyrightFileSystem' ; import { PackageTypeReport , TypeKnownStatus } from './analyzer/packageTypeReport' ; import { createDeferred } from './common/deferred' ; import { FullAccessHost } from './common/fullAccessHost' ; import { ChokidarFileWatcherProvider } from './common/chokidarFileWatcherProvider' ; const toolName = 'pyright' ; enum ExitStatus { NoErrors = 0 , ErrorsReported = 1 , FatalError = 2 , ConfigFileParseError = 3 , ParameterError = 4 } interface PyrightJsonResults { version : string ; time : string ; generalDiagnostics : PyrightJsonDiagnostic [ ] ; summary : PyrightJsonSummary ; typeCompleteness ? : PyrightTypeCompletenessReport ; } interface PyrightSymbolCount { withKnownType : number ; withUnknownType : number ; } interface PyrightTypeCompletenessReport { packageName : string ; ignoreUnknownTypesFromImports : boolean ; packageRootDirectory ? : string | undefined ; pyTypedPath ? : string | undefined ; exportedSymbolCounts : PyrightSymbolCount ; otherSymbolCounts : PyrightSymbolCount ; missingFunctionDocStringCount : number ; missingClassDocStringCount : number ; missingDefaultParamCount : number ; completenessScore : number ; modules : PyrightPublicModuleReport [ ] ; symbols : PyrightPublicSymbolReport [ ] ; } interface PyrightPublicModuleReport { name : string ; } interface PyrightPublicSymbolReport { category : string ; name : string ; referenceCount : number ; isTypeKnown : boolean ; isExported : boolean ; diagnostics : PyrightJsonDiagnostic [ ] ; alternateNames ? : string [ ] | undefined ; } interface PyrightJsonDiagnostic { file : string ; severity : 'error' | 'warning' | 'information' ; message : string ; range ? : Range | undefined ; rule ? : string | undefined ; } interface PyrightJsonSummary { filesAnalyzed : number ; errorCount : number ; warningCount : number ; informationCount : number ; timeInSec : number ; } interface DiagnosticResult { errorCount : number ; warningCount : number ; informationCount : number ; diagnosticCount : number ; } const cancellationNone = Object . freeze ( { isCancellationRequested : false , onCancellationRequested : function ( ) { return { dispose ( ) { } , } ; } , } ) ; async function processArgs ( ) { const optionDefinitions = [ { name : 'createstub' , type : String } , { name : 'dependencies' , type : Boolean } , { name : 'files' , type : String , multiple : true , defaultOption : true } , { name : 'help' , alias : 'h' , type : Boolean } , { name : 'ignoreexternal' , type : Boolean } , { name : 'lib' , type : Boolean } , { name : 'outputjson' , type : Boolean } , { name : 'project' , alias : 'p' , type : String } , { name : 'pythonplatform' , type : String } , { name : 'pythonversion' , type : String } , { name : 'skipunannotated' , type : Boolean } , { name : 'stats' , type : Boolean } , { name : 'typeshed-path' , alias : 't' , type : String } , { name : 'venv-path' , alias : 'v' , type : String } , { name : 'verifytypes' , type : String } , { name : 'verbose' , type : Boolean } , { name : 'version' , type : Boolean } , { name : 'warnings' , type : Boolean } , { name : 'watch' , alias : 'w' , type : Boolean } , ] ; let args ; try { args = commandLineArgs ( optionDefinitions ) ; } catch ( e ) { const argErr = e ; if ( argErr && argErr . optionName ) { console . error ( `Unexpected option ${ argErr . optionName }.\n${ toolName } --help for usage` ) ; return ExitStatus . ParameterError ; } console . error ( `Unexpected error\n${ toolName } --help for usage` ) ; return ExitStatus . ParameterError ; } if ( args . help !== undefined ) { printUsage ( ) ; return ExitStatus . NoErrors ; } if ( args . version !== undefined ) { printVersion ( ) ; return ExitStatus . NoErrors ; } if ( args . outputjson ) { const incompatibleArgs = [ 'watch' , 'stats' , 'verbose' , 'createstub' , 'dependencies' ] ; for ( const arg of incompatibleArgs ) { if ( args [ arg ] !== undefined ) { console . error ( `'outputjson' option cannot be used with '${ arg }' option` ) ; return ExitStatus . ParameterError ; } } } if ( args [ 'verifytypes' ] !== undefined ) { const incompatibleArgs = [ 'watch' , 'stats' , 'createstub' , 'dependencies' , 'skipunannotated' ] ; for ( const arg of incompatibleArgs ) { if ( args [ arg ] !== undefined ) { console . error ( `'verifytypes' option cannot be used with '${ arg }' option` ) ; return ExitStatus . ParameterError ; } } } if ( args . createstub ) { const incompatibleArgs = [ 'watch' , 'stats' , 'verifytypes' , 'dependencies' , 'skipunannotated' ] ; for ( const arg of incompatibleArgs ) { if ( args [ arg ] !== undefined ) { console . error ( `'createstub' option cannot be used with '${ arg }' option` ) ; return ExitStatus . ParameterError ; } } } const options = new PyrightCommandLineOptions ( process . cwd ( ) , false ) ; if ( args . files && Array . isArray ( args . files ) ) { options . fileSpecs = args . files ; options . fileSpecs = options . fileSpecs . map ( ( f ) => combinePaths ( process . cwd ( ) , f ) ) ; } else { options . fileSpecs = [ ] ; } if ( args . project ) { options . configFilePath = combinePaths ( process . cwd ( ) , normalizePath ( args . project ) ) ; } if ( args . pythonplatform ) { if ( args . pythonplatform === 'Darwin' || args . pythonplatform === 'Linux' || args . pythonplatform === 'Windows' ) { options . pythonPlatform = args . pythonplatform ; } else { console . error ( `'${ args . pythonplatform }' is not a supported Python platform; specify Darwin, Linux, or Windows` ) ; return ExitStatus . ParameterError ; } } if ( args . pythonversion ) { const version = versionFromString ( args . pythonversion ) ; if ( version ) { options . pythonVersion = version ; } else { console . error ( `'${ args . pythonversion }' is not a supported Python version; specify 3.3, 3.4, etc.` ) ; return ExitStatus . ParameterError ; } } if ( args [ 'venv-path' ] ) { options . venvPath = combinePaths ( process . cwd ( ) , normalizePath ( args [ 'venv-path' ] ) ) ; } if ( args [ 'typeshed-path' ] ) { options . typeshedPath = combinePaths ( process . cwd ( ) , normalizePath ( args [ 'typeshed-path' ] ) ) ; } if ( args . createstub ) { options . typeStubTargetImportName = args . createstub ; } options . analyzeUnannotatedFunctions = ! args . skipunannotated ; if ( args . verbose ) { options . verboseOutput = true ; } if ( args . lib ) { options . useLibraryCodeForTypes = true ; } options . checkOnlyOpenFiles = false ; if ( ! ! args . stats && ! ! args . verbose ) { options . logTypeEvaluationTime = true ; } const treatWarningsAsErrors = ! ! args . warnings ; const logLevel = options . logTypeEvaluationTime ? LogLevel . Log : LogLevel . Error ; const output = args . outputjson ? new StderrConsoleWithLevel ( logLevel ) : new StandardConsoleWithLevel ( logLevel ) ; const fileSystem = new PyrightFileSystem ( createFromRealFileSystem ( output , new ChokidarFileWatcherProvider ( output ) ) ) ; if ( args [ 'verifytypes' ] !== undefined ) { return verifyPackageTypes ( fileSystem , args [ 'verifytypes' ] || '' , ! ! args . verbose , ! ! args . outputjson , args [ 'ignoreexternal' ] ) ; } else if ( args [ 'ignoreexternal' ] !== undefined ) { console . error ( `'--ignoreexternal' is valid only when used with '--verifytypes'` ) ; return ExitStatus . ParameterError ; } const watch = args . watch !== undefined ; options . watchForSourceChanges = watch ; options . watchForConfigChanges = watch ; const service = new AnalyzerService ( '<default>' , fileSystem , output , ( ) => new FullAccessHost ( fileSystem ) ) ; const exitStatus = createDeferred < ExitStatus > ( ) ; service . setCompletionCallback ( ( results ) => { if ( results . fatalErrorOccurred ) { exitStatus . resolve ( ExitStatus . FatalError ) ; return ; } if ( results . configParseErrorOccurred ) { exitStatus . resolve ( ExitStatus . ConfigFileParseError ) ; return ; } let errorCount = 0 ; if ( results . diagnostics . length > 0 && ! args . createstub && ! args [ 'verifytypes' ] ) { if ( args . outputjson ) { const report = reportDiagnosticsAsJson ( results . diagnostics , results . filesInProgram , results . elapsedTime ) ; errorCount += report . errorCount ; if ( treatWarningsAsErrors ) { errorCount += report . warningCount ; } } else { const report = reportDiagnosticsAsText ( results . diagnostics ) ; errorCount += report . errorCount ; if ( treatWarningsAsErrors ) { errorCount += report . warningCount ; } } } if ( args . createstub && results . filesRequiringAnalysis === 0 ) { try { service . writeTypeStub ( cancellationNone ) ; service . dispose ( ) ; console . log ( `Type stub was created for '${ args . createstub }'` ) ; } catch ( err ) { let errMessage = '' ; if ( err instanceof Error ) { errMessage = ': ' + err . message ; } console . error ( `Error occurred when creating type stub: ` + errMessage ) ; exitStatus . resolve ( ExitStatus . FatalError ) ; return ; } exitStatus . resolve ( ExitStatus . NoErrors ) ; return ; } if ( ! args . outputjson ) { if ( ! watch ) { timingStats . printSummary ( console ) ; } if ( args . stats ) { service . printStats ( ) ; timingStats . printDetails ( console ) ; } if ( args . dependencies ) { service . printDependencies ( ! ! args . verbose ) ; } } if ( ! watch ) { exitStatus . resolve ( errorCount > 0 ? ExitStatus . ErrorsReported : ExitStatus . NoErrors ) ; return ; } else if ( ! args . outputjson ) { console . log ( 'Watching for file changes...' ) ; } } ) ; service . setOptions ( options ) ; return await exitStatus . promise ; } function verifyPackageTypes ( fileSystem , packageName , verboseOutput , outputJson , ignoreUnknownTypesFromImports ) { try { const verifier = new PackageTypeVerifier ( fileSystem , packageName , ignoreUnknownTypesFromImports ) ; const report = verifier . verify ( ) ; const jsonReport = buildTypeCompletenessReport ( packageName , report ) ; if ( outputJson ) { console . log ( JSON . stringify ( jsonReport , undefined , 4 ) ) ; } else { printTypeCompletenessReportText ( jsonReport , verboseOutput ) ; } return jsonReport . typeCompleteness ! . completenessScore < 1 ? ExitStatus . ErrorsReported : ExitStatus . NoErrors ; } catch ( err ) { let errMessage = '' ; if ( err instanceof Error ) { errMessage = ': ' + err . message ; } console . error ( `Error occurred when verifying types: ` + errMessage ) ; return ExitStatus . FatalError ; } } function accumulateReportDiagnosticStats ( diag , report ) { if ( diag . severity === 'error' ) { report . summary . errorCount ++ ; } else if ( diag . severity === 'warning' ) { report . summary . warningCount ++ ; } else if ( diag . severity === 'information' ) { report . summary . informationCount ++ ; } } function buildTypeCompletenessReport ( packageName , completenessReport ) { const report = { version : getVersionString ( ) , time : Date . now ( ) . toString ( ) , generalDiagnostics : [ ] , summary : { filesAnalyzed : completenessReport . modules . size , errorCount : 0 , warningCount : 0 , informationCount : 0 , timeInSec : timingStats . getTotalDuration ( ) , } , } ; completenessReport . generalDiagnostics . forEach ( ( diag ) => { const jsonDiag = convertDiagnosticToJson ( '' , diag ) ; report . generalDiagnostics . push ( jsonDiag ) ; accumulateReportDiagnosticStats ( jsonDiag , report ) ; } ) ; report . typeCompleteness = { packageName , ignoreUnknownTypesFromImports : completenessReport . ignoreExternal , packageRootDirectory : completenessReport . rootDirectory , pyTypedPath : completenessReport . pyTypedPath , exportedSymbolCounts : { withKnownType : 0 , withUnknownType : 0 , } , otherSymbolCounts : { withKnownType : 0 , withUnknownType : 0 , } , missingFunctionDocStringCount : completenessReport . missingFunctionDocStringCount , missingClassDocStringCount : completenessReport . missingClassDocStringCount , missingDefaultParamCount : completenessReport . missingDefaultParamCount , completenessScore : 0 , modules : [ ] , symbols : [ ] , } ; completenessReport . modules . forEach ( ( module ) => { const jsonModule = { name : module . name , } ; report . typeCompleteness ! . modules . push ( jsonModule ) ; } ) ; completenessReport . symbols . forEach ( ( symbol ) => { const jsonSymbol = { category : PackageTypeVerifier . getSymbolCategoryString ( symbol . category ) , name : symbol . fullName , referenceCount : symbol . referenceCount , isExported : symbol . isExported , isTypeKnown : symbol . typeKnownStatus === TypeKnownStatus . Known , diagnostics : symbol . diagnostics . map ( ( diag ) => convertDiagnosticToJson ( diag . filePath , diag . diagnostic ) ) , } ; const alternateNames = completenessReport . alternateSymbolNames . get ( symbol . fullName ) ; if ( alternateNames ) { jsonSymbol . alternateNames = alternateNames ; } report . typeCompleteness ! . symbols . push ( jsonSymbol ) ; if ( symbol . typeKnownStatus === TypeKnownStatus . Known ) { if ( symbol . isExported ) { report . typeCompleteness ! . exportedSymbolCounts . withKnownType ++ ; } else { report . typeCompleteness ! . otherSymbolCounts . withKnownType ++ ; } } else { if ( symbol . isExported ) { report . typeCompleteness ! . exportedSymbolCounts . withUnknownType ++ ; } else { report . typeCompleteness ! . otherSymbolCounts . withUnknownType ++ ; } } } ) ; const unknownSymbolCount = report . typeCompleteness . exportedSymbolCounts . withUnknownType ; const knownSymbolCount = report . typeCompleteness . exportedSymbolCounts . withKnownType ; const totalSymbolCount = unknownSymbolCount + knownSymbolCount ; if ( totalSymbolCount > 0 ) { report . typeCompleteness ! . completenessScore = knownSymbolCount / totalSymbolCount ; } return report ; } function printTypeCompletenessReportText ( results , verboseOutput ) { const completenessReport = results . typeCompleteness ! ; console . log ( `Package name: "${ completenessReport . packageName }"` ) ; if ( completenessReport . packageRootDirectory !== undefined ) { console . log ( `Package directory: "${ completenessReport . packageRootDirectory }"` ) ; } if ( completenessReport . pyTypedPath !== undefined ) { console . log ( `Path of py.typed file: "${ completenessReport . pyTypedPath }"` ) ; } if ( completenessReport . modules . length > 0 ) { console . log ( '' ) ; console . log ( `Public modules: ${ completenessReport . modules . length }` ) ; completenessReport . modules . forEach ( ( module ) => { console . log ( `   ${ module . name }` ) ; } ) ; } if ( completenessReport . symbols . length > 0 && verboseOutput ) { console . log ( '' ) ; console . log ( `Exported symbols: ${ completenessReport . symbols . length }` ) ; completenessReport . symbols . forEach ( ( symbol ) => { if ( symbol . isExported ) { const refCount = symbol . referenceCount > 1 ? ` (${ symbol . referenceCount } references)` : '' ; console . log ( `   ${ symbol . name }${ refCount }` ) ; } } ) ; console . log ( '' ) ; console . log ( `Other referenced symbols: ${ completenessReport . symbols . length }` ) ; completenessReport . symbols . forEach ( ( symbol ) => { if ( ! symbol . isExported ) { const refCount = symbol . referenceCount > 1 ? ` (${ symbol . referenceCount } references)` : '' ; console . log ( `   ${ symbol . name }${ refCount }` ) ; } } ) ; } results . generalDiagnostics . forEach ( ( diag ) => { logDiagnosticToConsole ( diag ) ; } ) ; console . log ( '' ) ; console . log ( `Symbols used in public interface:` ) ; results . typeCompleteness ! . symbols . forEach ( ( symbol ) => { let diagnostics = symbol . diagnostics ; if ( ! verboseOutput ) { diagnostics = diagnostics . filter ( ( diag ) => diag . severity === 'error' ) ; } if ( diagnostics . length > 0 ) { console . log ( `${ symbol . name }` ) ; diagnostics . forEach ( ( diag ) => { logDiagnosticToConsole ( diag ) ; } ) ; } } ) ; console . log ( '' ) ; console . log ( `Symbols exported by "${ completenessReport . packageName }": ${ completenessReport . exportedSymbolCounts . withKnownType + completenessReport . exportedSymbolCounts . withUnknownType }` ) ; console . log ( `  With known type: ${ completenessReport . exportedSymbolCounts . withKnownType }` ) ; console . log ( `  With partially unknown type: ${ completenessReport . exportedSymbolCounts . withUnknownType }` ) ; if ( completenessReport . ignoreUnknownTypesFromImports ) { console . log ( `    (Ignoring unknown types imported from other packages)` ) ; } console . log ( `  Functions without docstring: ${ completenessReport . missingFunctionDocStringCount }` ) ; console . log ( `  Functions without default param: ${ completenessReport . missingDefaultParamCount }` ) ; console . log ( `  Classes without docstring: ${ completenessReport . missingClassDocStringCount }` ) ; console . log ( '' ) ; console . log ( `Other symbols referenced but not exported by "${ completenessReport . packageName }": ${ completenessReport . otherSymbolCounts . withKnownType + completenessReport . otherSymbolCounts . withUnknownType }` ) ; console . log ( `  With known type: ${ completenessReport . otherSymbolCounts . withKnownType }` ) ; console . log ( `  With partially unknown type: ${ completenessReport . otherSymbolCounts . withUnknownType }` ) ; console . log ( '' ) ; console . log ( `Type completeness score: ${ Math . round ( completenessReport . completenessScore * 1000 ) / 10 }%` ) ; console . log ( '' ) ; console . info ( `Completed in ${ results . summary . timeInSec }sec` ) ; console . log ( '' ) ; } function printUsage ( ) { console . log ( 'Usage: ' + toolName + ' [options] files...\n' + '  Options:\n' + '  --createstub <IMPORT>              Create type stub file(s) for import\n' + '  --dependencies                     Emit import dependency information\n' + '  -h,--help                          Show this help message\n' + '  --ignoreexternal                   Ignore external imports for --verifytypes\n' + '  --lib                              Use library code to infer types when stubs are missing\n' + '  --outputjson                       Output results in JSON format\n' + '  -p,--project <FILE OR DIRECTORY>   Use the configuration file at this location\n' + '  --pythonplatform <PLATFORM>        Analyze for a specific platform (Darwin, Linux, Windows)\n' + '  --pythonversion <VERSION>          Analyze for a specific version (3.3, 3.4, etc.)\n' + '  --skipunannotated                  Do not analyze functions and methods with no type annotations\n' + '  --stats                            Print detailed performance stats\n' + '  -t,--typeshed-path <DIRECTORY>     Use typeshed type stubs at this location\n' + '  -v,--venv-path <DIRECTORY>         Directory that contains virtual environments\n' + '  --verbose                          Emit verbose diagnostics\n' + '  --verifytypes <PACKAGE>            Verify type completeness of a py.typed package\n' + '  --version                          Print Pyright version\n' + '  --warnings                         Use exit code of 1 if warnings are reported\n' + '  -w,--watch                         Continue to run and watch for changes\n' ) ; } function getVersionString ( ) { const version = require ( '../package.json' ) . version ; return version . toString ( ) ; } function printVersion ( ) { console . log ( `${ toolName } ${ getVersionString ( ) }` ) ; } function reportDiagnosticsAsJson ( fileDiagnostics , filesInProgram , timeInSec ) { const report = { version : getVersionString ( ) , time : Date . now ( ) . toString ( ) , generalDiagnostics : [ ] , summary : { filesAnalyzed : filesInProgram , errorCount : 0 , warningCount : 0 , informationCount : 0 , timeInSec , } , } ; fileDiagnostics . forEach ( ( fileDiag ) => { fileDiag . diagnostics . forEach ( ( diag ) => { if ( diag . category === DiagnosticCategory . Error || diag . category === DiagnosticCategory . Warning || diag . category === DiagnosticCategory . Information ) { const jsonDiag = convertDiagnosticToJson ( fileDiag . filePath , diag ) ; report . generalDiagnostics . push ( jsonDiag ) ; accumulateReportDiagnosticStats ( jsonDiag , report ) ; } } ) ; } ) ; console . log ( JSON . stringify ( report , undefined , 4 ) ) ; return { errorCount : report . summary . errorCount , warningCount : report . summary . warningCount , informationCount : report . summary . informationCount , diagnosticCount : report . summary . errorCount + report . summary . warningCount + report . summary . informationCount , } ; } function convertDiagnosticToJson ( filePath , diag ) { return { file : filePath , severity : diag . category === DiagnosticCategory . Error ? 'error' : diag . category === DiagnosticCategory . Warning ? 'warning' : 'information' , message : diag . message , range : isEmptyRange ( diag . range ) ? undefined : diag . range , rule : diag . getRule ( ) , } ; } function reportDiagnosticsAsText ( fileDiagnostics ) { let errorCount = 0 ; let warningCount = 0 ; let informationCount = 0 ; fileDiagnostics . forEach ( ( fileDiagnostics ) => { const fileErrorsAndWarnings = fileDiagnostics . diagnostics . filter ( ( diag ) => diag . category !== DiagnosticCategory . UnusedCode && diag . category !== DiagnosticCategory . Deprecated ) ; if ( fileErrorsAndWarnings . length > 0 ) { console . log ( `${ fileDiagnostics . filePath }` ) ; fileErrorsAndWarnings . forEach ( ( diag ) => { logDiagnosticToConsole ( convertDiagnosticToJson ( fileDiagnostics . filePath , diag ) ) ; if ( diag . category === DiagnosticCategory . Error ) { errorCount ++ ; } else if ( diag . category === DiagnosticCategory . Warning ) { warningCount ++ ; } else if ( diag . category === DiagnosticCategory . Information ) { informationCount ++ ; } } ) ; } } ) ; console . log ( `${ errorCount . toString ( ) } ${ errorCount === 1 ? 'error' : 'errors' }, ` + `${ warningCount . toString ( ) } ${ warningCount === 1 ? 'warning' : 'warnings' }, ` + `${ informationCount . toString ( ) } ${ informationCount === 1 ? 'info' : 'infos' } ` ) ; return { errorCount , warningCount , informationCount , diagnosticCount : errorCount + warningCount + informationCount , } ; } function logDiagnosticToConsole ( diag , prefix = '  ' ) { let message = prefix ; if ( diag . file ) { message += `${ diag . file }:` ; } if ( diag . range && ! isEmptyRange ( diag . range ) ) { message += chalk . yellow ( `${ diag . range . start . line + 1 }` ) + ':' + chalk . yellow ( `${ diag . range . start . character + 1 }` ) + ' - ' ; } const [ firstLine , ... remainingLines ] = diag . message . split ( '\n' ) ; message += diag . severity === 'error' ? chalk . red ( 'error' ) : diag . severity === 'warning' ? chalk . cyan ( 'warning' ) : chalk . blue ( 'info' ) ; message += `: ${ firstLine }` ; if ( remainingLines . length > 0 ) { message += '\n' + prefix + remainingLines . join ( '\n' + prefix ) ; } if ( diag . rule ) { message += chalk . gray ( ` (${ diag . rule })` ) ; } console . log ( message ) ; } export async function main ( ) { if ( process . env . NODE_ENV === 'production' ) { require ( 'source-map-support' ) . install ( ) ; } const exitCode = await processArgs ( ) ; process . exitCode = exitCode ; }