mirror of
https://github.com/microsoft/pyright.git
synced 2025-12-23 09:19:29 +00:00
Some checks failed
Run mypy_primer on push / Run mypy_primer on push (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled
* pull-pylance-with-pyright-1.1.406-20251008-232729 * fixed linting issue --------- Co-authored-by: Azure Piplines <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: HeeJae Chang <hechang@microsoft.com>
1369 lines
51 KiB
TypeScript
1369 lines
51 KiB
TypeScript
/*
|
|
* pyright.ts
|
|
* Copyright (c) Microsoft Corporation.
|
|
* Licensed under the MIT license.
|
|
* Author: Eric Traut
|
|
*
|
|
* Command-line entry point for pyright type checker.
|
|
*/
|
|
|
|
// Add the start timer at the very top of the file, before we import other modules.
|
|
|
|
/* eslint-disable */
|
|
import { timingStats } from './common/timing';
|
|
/* eslint-enable */
|
|
|
|
import chalk from 'chalk';
|
|
import commandLineArgs, { CommandLineOptions, OptionDefinition } from 'command-line-args';
|
|
import * as os from 'os';
|
|
|
|
import { ChildProcess, fork } from 'child_process';
|
|
import { AnalysisResults } from './analyzer/analysis';
|
|
import { PackageTypeReport, TypeKnownStatus } from './analyzer/packageTypeReport';
|
|
import { PackageTypeVerifier } from './analyzer/packageTypeVerifier';
|
|
import { AnalyzerService } from './analyzer/service';
|
|
import { maxSourceFileSize } from './analyzer/sourceFile';
|
|
import { SourceFileInfo } from './analyzer/sourceFileInfo';
|
|
import { initializeDependencies } from './common/asyncInitialization';
|
|
import { ChokidarFileWatcherProvider } from './common/chokidarFileWatcherProvider';
|
|
import { CommandLineOptions as PyrightCommandLineOptions } from './common/commandLineOptions';
|
|
import { ConsoleInterface, LogLevel, StandardConsole, StderrConsole } from './common/console';
|
|
import { fail } from './common/debug';
|
|
import { createDeferred } from './common/deferred';
|
|
import { Diagnostic, DiagnosticCategory, compareDiagnostics } from './common/diagnostic';
|
|
import { FileDiagnostics } from './common/diagnosticSink';
|
|
import { FullAccessHost } from './common/fullAccessHost';
|
|
import { combinePaths, normalizePath } from './common/pathUtils';
|
|
import { PythonVersion } from './common/pythonVersion';
|
|
import { RealTempFile, createFromRealFileSystem } from './common/realFileSystem';
|
|
import { ServiceKeys } from './common/serviceKeys';
|
|
import { ServiceProvider } from './common/serviceProvider';
|
|
import { createServiceProvider } from './common/serviceProviderExtensions';
|
|
import { getStdin } from './common/streamUtils';
|
|
import { Range, isEmptyRange } from './common/textRange';
|
|
import { Uri } from './common/uri/uri';
|
|
import { getFileSpec, tryStat } from './common/uri/uriUtils';
|
|
import { PyrightFileSystem } from './pyrightFileSystem';
|
|
|
|
const toolName = 'pyright';
|
|
|
|
type SeverityLevel = 'error' | 'warning' | 'information';
|
|
|
|
// These values are publicly documented. Do not change them.
|
|
enum ExitStatus {
|
|
NoErrors = 0,
|
|
ErrorsReported = 1,
|
|
FatalError = 2,
|
|
ConfigFileParseError = 3,
|
|
ParameterError = 4,
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightJsonResults {
|
|
version: string;
|
|
time: string;
|
|
generalDiagnostics: PyrightJsonDiagnostic[];
|
|
summary: PyrightJsonSummary;
|
|
typeCompleteness?: PyrightTypeCompletenessReport;
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightSymbolCount {
|
|
withKnownType: number;
|
|
withAmbiguousType: number;
|
|
withUnknownType: number;
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightTypeCompletenessReport {
|
|
packageName: string;
|
|
packageRootDirectory?: string | undefined;
|
|
moduleName: string;
|
|
moduleRootDirectory?: string | undefined;
|
|
ignoreUnknownTypesFromImports: boolean;
|
|
pyTypedPath?: string | undefined;
|
|
exportedSymbolCounts: PyrightSymbolCount;
|
|
otherSymbolCounts: PyrightSymbolCount;
|
|
missingFunctionDocStringCount: number;
|
|
missingClassDocStringCount: number;
|
|
missingDefaultParamCount: number;
|
|
completenessScore: number;
|
|
modules: PyrightPublicModuleReport[];
|
|
symbols: PyrightPublicSymbolReport[];
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightPublicModuleReport {
|
|
name: string;
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightPublicSymbolReport {
|
|
category: string;
|
|
name: string;
|
|
referenceCount: number;
|
|
isTypeKnown: boolean;
|
|
isTypeAmbiguous: boolean;
|
|
isExported: boolean;
|
|
diagnostics: PyrightJsonDiagnostic[];
|
|
alternateNames?: string[] | undefined;
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightJsonDiagnostic {
|
|
file: string;
|
|
severity: SeverityLevel;
|
|
message: string;
|
|
range?: Range | undefined;
|
|
rule?: string | undefined;
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface PyrightJsonSummary {
|
|
filesAnalyzed: number;
|
|
errorCount: number;
|
|
warningCount: number;
|
|
informationCount: number;
|
|
timeInSec: number;
|
|
}
|
|
|
|
// The schema for this object is publicly documented. Do not change it.
|
|
interface DiagnosticResult {
|
|
errorCount: number;
|
|
warningCount: number;
|
|
informationCount: number;
|
|
diagnosticCount: number;
|
|
}
|
|
|
|
const cancellationNone = Object.freeze({
|
|
isCancellationRequested: false,
|
|
onCancellationRequested: function () {
|
|
return {
|
|
dispose() {
|
|
/* empty */
|
|
},
|
|
};
|
|
},
|
|
});
|
|
|
|
async function processArgs(): Promise<ExitStatus> {
|
|
const optionDefinitions: OptionDefinition[] = [
|
|
{ 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: 'level', type: String },
|
|
{ name: 'outputjson', type: Boolean },
|
|
{ name: 'project', alias: 'p', type: String },
|
|
{ name: 'pythonpath', type: String },
|
|
{ name: 'pythonplatform', type: String },
|
|
{ name: 'pythonversion', type: String },
|
|
{ name: 'skipunannotated', type: Boolean },
|
|
{ name: 'stats', type: Boolean },
|
|
{ name: 'threads', type: parseThreadsArgValue },
|
|
{ name: 'typeshed-path', type: String },
|
|
{ name: 'typeshedpath', alias: 't', type: String },
|
|
{ name: 'venv-path', type: String },
|
|
{ name: 'venvpath', 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: CommandLineOptions;
|
|
|
|
try {
|
|
args = commandLineArgs(optionDefinitions);
|
|
} catch (e: any) {
|
|
const argErr: { name: string; optionName: string } = 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(console);
|
|
return ExitStatus.NoErrors;
|
|
}
|
|
|
|
for (const [arg, value] of Object.entries(args)) {
|
|
if (value === null && arg !== 'threads') {
|
|
console.error(`'${arg}' option requires a value`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
|
|
if (args.outputjson) {
|
|
const incompatibleArgs = ['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', 'threads'];
|
|
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', 'threads'];
|
|
for (const arg of incompatibleArgs) {
|
|
if (args[arg] !== undefined) {
|
|
console.error(`'createstub' option cannot be used with '${arg}' option`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (args.threads) {
|
|
const incompatibleArgs = ['watch', 'stats', 'dependencies'];
|
|
for (const arg of incompatibleArgs) {
|
|
if (args[arg] !== undefined) {
|
|
console.error(`'threads' option cannot be used with '${arg}' option`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
}
|
|
|
|
const options = new PyrightCommandLineOptions(process.cwd(), false);
|
|
const tempFile = new RealTempFile();
|
|
|
|
// Assume any relative paths are relative to the working directory.
|
|
if (args.files && Array.isArray(args.files)) {
|
|
let fileSpecList = args.files;
|
|
|
|
// Has the caller indicated that the file list will be supplied by stdin?
|
|
if (args.files.length === 1 && args.files[0] === '-') {
|
|
try {
|
|
const stdText = await getStdin();
|
|
fileSpecList = stdText
|
|
.replace(/[\r\n]/g, ' ')
|
|
.trim()
|
|
.split(' ')
|
|
.map((s) => s.trim())
|
|
.filter((s) => !!s);
|
|
} catch (e) {
|
|
console.error('Invalid file list specified by stdin input');
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
|
|
options.configSettings.includeFileSpecsOverride = fileSpecList;
|
|
options.configSettings.includeFileSpecsOverride = options.configSettings.includeFileSpecsOverride.map((f) =>
|
|
combinePaths(process.cwd(), f)
|
|
);
|
|
|
|
// Verify the specified file specs to make sure their wildcard roots exist.
|
|
const tempFileSystem = new PyrightFileSystem(createFromRealFileSystem(tempFile));
|
|
|
|
for (const fileDesc of options.configSettings.includeFileSpecsOverride) {
|
|
const includeSpec = getFileSpec(Uri.file(process.cwd(), tempFile), fileDesc);
|
|
try {
|
|
const stat = tryStat(tempFileSystem, includeSpec.wildcardRoot);
|
|
if (!stat) {
|
|
console.error(`File or directory "${includeSpec.wildcardRoot}" does not exist`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
} catch {
|
|
// Ignore exception in this case.
|
|
}
|
|
}
|
|
}
|
|
|
|
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.configSettings.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 = PythonVersion.fromString(args.pythonversion);
|
|
if (version) {
|
|
options.configSettings.pythonVersion = version;
|
|
} else {
|
|
console.error(`'${args.pythonversion}' is not a supported Python version; specify 3.3, 3.4, etc.`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
|
|
if (args.pythonpath !== undefined) {
|
|
const incompatibleArgs = ['venv-path', 'venvpath'];
|
|
for (const arg of incompatibleArgs) {
|
|
if (args[arg] !== undefined) {
|
|
console.error(`'pythonpath' option cannot be used with '${arg}' option`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
|
|
options.configSettings.pythonPath = combinePaths(process.cwd(), normalizePath(args['pythonpath']));
|
|
}
|
|
|
|
if (args['venv-path']) {
|
|
console.warn(`'venv-path' option is deprecated; use 'venvpath' instead`);
|
|
options.configSettings.venvPath = combinePaths(process.cwd(), normalizePath(args['venv-path']));
|
|
}
|
|
|
|
if (args['venvpath']) {
|
|
options.configSettings.venvPath = combinePaths(process.cwd(), normalizePath(args['venvpath']));
|
|
}
|
|
|
|
if (args['typeshed-path']) {
|
|
console.warn(`'typeshed-path' option is deprecated; use 'typeshedpath' instead`);
|
|
options.configSettings.typeshedPath = combinePaths(process.cwd(), normalizePath(args['typeshed-path']));
|
|
}
|
|
|
|
if (args['typeshedpath']) {
|
|
options.configSettings.typeshedPath = combinePaths(process.cwd(), normalizePath(args['typeshedpath']));
|
|
}
|
|
|
|
if (args.createstub) {
|
|
options.languageServerSettings.typeStubTargetImportName = args.createstub;
|
|
}
|
|
|
|
if (args.skipunannotated) {
|
|
options.configSettings.analyzeUnannotatedFunctions = false;
|
|
}
|
|
|
|
if (args.verbose) {
|
|
options.configSettings.verboseOutput = true;
|
|
}
|
|
|
|
// Always enable autoSearchPaths when using the command line.
|
|
options.configSettings.autoSearchPaths = true;
|
|
|
|
if (args.lib) {
|
|
console.warn(`The --lib option is deprecated. Pyright now defaults to using library code to infer types.`);
|
|
}
|
|
|
|
let minSeverityLevel: SeverityLevel = 'information';
|
|
if (args.level && typeof args.level === 'string') {
|
|
const levelValue = args.level.toLowerCase();
|
|
if (levelValue === 'error' || levelValue === 'warning') {
|
|
minSeverityLevel = levelValue;
|
|
} else {
|
|
console.error(`'${args.level}' is not a valid value for --level; specify error or warning.`);
|
|
return ExitStatus.ParameterError;
|
|
}
|
|
}
|
|
|
|
options.languageServerSettings.checkOnlyOpenFiles = false;
|
|
|
|
if (!!args.stats && !!args.verbose) {
|
|
options.languageServerSettings.logTypeEvaluationTime = true;
|
|
}
|
|
|
|
let logLevel = LogLevel.Error;
|
|
if (args.stats || args.verbose) {
|
|
logLevel = LogLevel.Info;
|
|
}
|
|
|
|
// If using outputjson, redirect all console output to stderr so it doesn't mess
|
|
// up the JSON output, which goes to stdout.
|
|
const output = args.outputjson ? new StderrConsole(logLevel) : new StandardConsole(logLevel);
|
|
const fileSystem = new PyrightFileSystem(
|
|
createFromRealFileSystem(tempFile, output, new ChokidarFileWatcherProvider(output))
|
|
);
|
|
|
|
const serviceProvider = createServiceProvider(fileSystem, output, tempFile);
|
|
|
|
// The package type verification uses a different path.
|
|
if (args['verifytypes'] !== undefined) {
|
|
return verifyPackageTypes(
|
|
serviceProvider,
|
|
args['verifytypes'] || '',
|
|
options,
|
|
!!args.outputjson,
|
|
minSeverityLevel,
|
|
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.languageServerSettings.watchForSourceChanges = watch;
|
|
options.languageServerSettings.watchForConfigChanges = watch;
|
|
|
|
const service = new AnalyzerService('<default>', serviceProvider, {
|
|
console: output,
|
|
hostFactory: () => new FullAccessHost(serviceProvider),
|
|
// Refresh service 2 seconds after the last library file change is detected.
|
|
libraryReanalysisTimeProvider: () => 2 * 1000,
|
|
shouldRunAnalysis: () => true,
|
|
});
|
|
|
|
if ('threads' in args) {
|
|
let threadCount = args['threads'];
|
|
|
|
// If the thread count was unspecified, use the number of
|
|
// logical CPUs (i.e. hardware threads). We find empirically
|
|
// that going below 4 threads usually doesn't help.
|
|
if (threadCount === null) {
|
|
threadCount = os.cpus().length;
|
|
if (threadCount < 4) {
|
|
threadCount = 1;
|
|
}
|
|
}
|
|
|
|
if (threadCount > 1) {
|
|
return runMultiThreaded(args, options, threadCount, service, minSeverityLevel, output);
|
|
}
|
|
}
|
|
|
|
return runSingleThreaded(args, options, service, minSeverityLevel, output);
|
|
}
|
|
|
|
async function runSingleThreaded(
|
|
args: CommandLineOptions,
|
|
options: PyrightCommandLineOptions,
|
|
service: AnalyzerService,
|
|
minSeverityLevel: SeverityLevel,
|
|
output: ConsoleInterface
|
|
) {
|
|
const watch = args.watch !== undefined;
|
|
const treatWarningsAsErrors = !!args.warnings;
|
|
|
|
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 (!args.createstub && !args.verifytypes) {
|
|
// Sort all file diagnostics by the file URI so
|
|
// we have a deterministic ordering.
|
|
const fileDiagnostics = results.diagnostics.sort((a, b) =>
|
|
a.fileUri.toString() < b.fileUri.toString() ? -1 : 1
|
|
);
|
|
|
|
if (args.outputjson) {
|
|
const report = reportDiagnosticsAsJson(
|
|
fileDiagnostics,
|
|
minSeverityLevel,
|
|
results.filesInProgram,
|
|
results.elapsedTime
|
|
);
|
|
errorCount += report.errorCount;
|
|
if (treatWarningsAsErrors) {
|
|
errorCount += report.warningCount;
|
|
}
|
|
} else {
|
|
printVersion(output);
|
|
const report = reportDiagnosticsAsText(fileDiagnostics, minSeverityLevel);
|
|
errorCount += report.errorCount;
|
|
if (treatWarningsAsErrors) {
|
|
errorCount += report.warningCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (args.createstub && results.requiringAnalysisCount.files === 0) {
|
|
try {
|
|
service.writeTypeStub(cancellationNone);
|
|
service.dispose();
|
|
console.info(`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) {
|
|
// Print the total time.
|
|
timingStats.printSummary(output);
|
|
}
|
|
|
|
if (args.stats) {
|
|
// Print the stats details.
|
|
service.printStats();
|
|
timingStats.printDetails(console);
|
|
|
|
if (args.verbose) {
|
|
service.printDetailedAnalysisTimes();
|
|
}
|
|
}
|
|
|
|
if (args.dependencies) {
|
|
service.printDependencies(!!args.verbose);
|
|
}
|
|
}
|
|
|
|
if (!watch) {
|
|
exitStatus.resolve(errorCount > 0 ? ExitStatus.ErrorsReported : ExitStatus.NoErrors);
|
|
return;
|
|
} else if (!args.outputjson) {
|
|
console.info('Watching for file changes...');
|
|
}
|
|
});
|
|
|
|
// This will trigger the analyzer.
|
|
service.setOptions(options);
|
|
service.enumerateSourceFiles(0);
|
|
|
|
return await exitStatus.promise;
|
|
}
|
|
|
|
async function runMultiThreaded(
|
|
args: CommandLineOptions,
|
|
options: PyrightCommandLineOptions,
|
|
maxThreadCount: number,
|
|
service: AnalyzerService,
|
|
minSeverityLevel: SeverityLevel,
|
|
output: ConsoleInterface
|
|
) {
|
|
const workers: ChildProcess[] = [];
|
|
const startTime = Date.now();
|
|
const treatWarningsAsErrors = !!args.warnings;
|
|
const exitStatus = createDeferred<ExitStatus>();
|
|
|
|
// Specify that only open files should be checked. This will allow us
|
|
// to control which files are checked by which workers.
|
|
options.languageServerSettings.checkOnlyOpenFiles = true;
|
|
|
|
// This will trigger discovery of files in the project.
|
|
service.setOptions(options);
|
|
service.enumerateSourceFiles(0);
|
|
const program = service.backgroundAnalysisProgram.program;
|
|
|
|
// Get the list of "tracked" source files -- those that will be type checked.
|
|
const sourceFilesToAnalyze = program.getSourceFileInfoList().filter((info) => info.isTracked);
|
|
|
|
// Don't create more workers than there are files.
|
|
const workerCount = Math.min(maxThreadCount, sourceFilesToAnalyze.length);
|
|
|
|
// Split the source files into affinity queues, one for each worker. We assume
|
|
// that files that are next to each other in the directory hierarchy probably
|
|
// have more common imports, so we want to analyze them with the same worker
|
|
// if possible to maximize type cache hits.
|
|
const affinityQueues: SourceFileInfo[][] = new Array<SourceFileInfo[]>(workerCount);
|
|
const filesPerAffinityQueue = sourceFilesToAnalyze.length / workerCount;
|
|
|
|
for (let i = 0; i < sourceFilesToAnalyze.length; i++) {
|
|
const affinityIndex = Math.floor(i / filesPerAffinityQueue);
|
|
if (affinityQueues[affinityIndex] === undefined) {
|
|
affinityQueues[affinityIndex] = [];
|
|
}
|
|
|
|
affinityQueues[affinityIndex].push(sourceFilesToAnalyze[i]);
|
|
}
|
|
|
|
output.info(`Found ${sourceFilesToAnalyze.length} files to analyze`);
|
|
output.info(`Using ${workerCount} threads`);
|
|
|
|
let fileDiagnostics: FileDiagnostics[] = [];
|
|
let pendingAnalysisCount = 0;
|
|
|
|
const sendMessageToWorker = (worker: ChildProcess, message: string, data: any) => {
|
|
worker.send(JSON.stringify({ action: message, data: data }));
|
|
};
|
|
|
|
const analyzeNextFile = (workerIndex: number) => {
|
|
const worker = workers[workerIndex];
|
|
let nextFileToAnalyze: SourceFileInfo | undefined;
|
|
|
|
// Determine the next file to analyze for this worker.
|
|
for (let i = 0; i < affinityQueues.length; i++) {
|
|
const affinityIndex = (workerIndex + i) % affinityQueues.length;
|
|
if (affinityQueues[affinityIndex].length > 0) {
|
|
nextFileToAnalyze = affinityQueues[affinityIndex].shift()!;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nextFileToAnalyze) {
|
|
// Tell the worker to analyze the next file.
|
|
const fileUri = nextFileToAnalyze.uri.toString();
|
|
|
|
sendMessageToWorker(worker, 'analyzeFile', fileUri);
|
|
|
|
pendingAnalysisCount++;
|
|
} else {
|
|
// Kill the worker since there's nothing left to do.
|
|
worker.kill();
|
|
|
|
if (pendingAnalysisCount === 0) {
|
|
// If there are no more files to analyze and all pending analysis
|
|
// is complete, report the results and exit.
|
|
if (!exitStatus.resolved) {
|
|
const elapsedTime = (Date.now() - startTime) / 1000;
|
|
let errorCount = 0;
|
|
|
|
// Sort all file diagnostics by the file URI so
|
|
// we have a deterministic ordering.
|
|
fileDiagnostics = fileDiagnostics.sort((a, b) =>
|
|
a.fileUri.toString() < b.fileUri.toString() ? -1 : 1
|
|
);
|
|
|
|
if (args.outputjson) {
|
|
const report = reportDiagnosticsAsJson(
|
|
fileDiagnostics,
|
|
minSeverityLevel,
|
|
sourceFilesToAnalyze.length,
|
|
elapsedTime
|
|
);
|
|
errorCount += report.errorCount;
|
|
if (treatWarningsAsErrors) {
|
|
errorCount += report.warningCount;
|
|
}
|
|
} else {
|
|
printVersion(output);
|
|
const report = reportDiagnosticsAsText(fileDiagnostics, minSeverityLevel);
|
|
errorCount += report.errorCount;
|
|
if (treatWarningsAsErrors) {
|
|
errorCount += report.warningCount;
|
|
}
|
|
|
|
// Print the total time.
|
|
output.info(`Completed in ${elapsedTime}sec`);
|
|
}
|
|
|
|
exitStatus.resolve(errorCount > 0 ? ExitStatus.ErrorsReported : ExitStatus.NoErrors);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Launch worker processes.
|
|
for (let i = 0; i < workerCount; i++) {
|
|
const mainModulePath = process.mainModule!.filename;
|
|
|
|
// Ensure forked processes use the temp folder owned by the main process.
|
|
// This allows for automatic deletion when the main process exits.
|
|
const worker = fork(mainModulePath, [
|
|
'worker',
|
|
i.toString(),
|
|
service.serviceProvider.get(ServiceKeys.tempFile).tmpdir().getFilePath(),
|
|
]);
|
|
|
|
worker.on('message', (message) => {
|
|
let messageObj: any;
|
|
|
|
try {
|
|
messageObj = JSON.parse(message as string);
|
|
} catch {
|
|
output.error(`Invalid message from worker: ${message}`);
|
|
exitStatus.resolve(ExitStatus.FatalError);
|
|
}
|
|
|
|
// If the exit status has already been resolved, another thread
|
|
// generated a fatal error, so we shouldn't continue.
|
|
if (exitStatus.resolved) {
|
|
return;
|
|
}
|
|
|
|
switch (messageObj.action) {
|
|
case 'analysisResults': {
|
|
pendingAnalysisCount--;
|
|
const results = messageObj.data as AnalysisResults;
|
|
|
|
if (results.fatalErrorOccurred) {
|
|
output.error(`Fatal error from worker`);
|
|
exitStatus.resolve(ExitStatus.FatalError);
|
|
return;
|
|
}
|
|
|
|
if (results.configParseErrorOccurred) {
|
|
exitStatus.resolve(ExitStatus.ConfigFileParseError);
|
|
return;
|
|
}
|
|
|
|
for (const fileDiag of results.diagnostics) {
|
|
fileDiagnostics.push(FileDiagnostics.fromJsonObj(fileDiag));
|
|
}
|
|
|
|
analyzeNextFile(i);
|
|
break;
|
|
}
|
|
|
|
default: {
|
|
output.error(`Unknown message from worker: ${message}`);
|
|
exitStatus.resolve(ExitStatus.FatalError);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
worker.on('error', (err) => {
|
|
output.error(`Failed to start child process: ${err}`);
|
|
exitStatus.resolve(ExitStatus.FatalError);
|
|
});
|
|
|
|
sendMessageToWorker(worker, 'setOptions', options);
|
|
workers.push(worker);
|
|
|
|
// Tell the worker to analyze the next file.
|
|
analyzeNextFile(i);
|
|
}
|
|
|
|
return await exitStatus.promise;
|
|
}
|
|
|
|
// This is the message loop for a worker process used used for
|
|
// multi-threaded analysis.
|
|
function runWorkerMessageLoop(workerNum: number, tempFolderName: string) {
|
|
let serviceProvider: ServiceProvider | undefined;
|
|
let service: AnalyzerService | undefined;
|
|
let fileSystem: PyrightFileSystem | undefined;
|
|
let lastOpenFileUri: Uri | undefined;
|
|
|
|
const sendMessageToParent = (message: string, data: any) => {
|
|
process.send?.(JSON.stringify({ action: message, data: data }));
|
|
};
|
|
|
|
process.on('message', (message) => {
|
|
let messageObj: any;
|
|
|
|
try {
|
|
messageObj = JSON.parse(message as string);
|
|
} catch {
|
|
console.error(`Invalid message from parent: ${message}`);
|
|
return;
|
|
}
|
|
|
|
switch (messageObj.action) {
|
|
case 'setOptions': {
|
|
const options = new PyrightCommandLineOptions(process.cwd(), false);
|
|
|
|
Object.keys(messageObj.data).forEach((key) => {
|
|
(options as any)[key] = messageObj.data[key];
|
|
});
|
|
|
|
let logLevel = LogLevel.Error;
|
|
if (options.configSettings.verboseOutput) {
|
|
logLevel = LogLevel.Info;
|
|
}
|
|
|
|
const output = new StderrConsole(logLevel);
|
|
const tempFile = new RealTempFile(tempFolderName);
|
|
fileSystem = new PyrightFileSystem(
|
|
createFromRealFileSystem(tempFile, output, new ChokidarFileWatcherProvider(output))
|
|
);
|
|
|
|
serviceProvider = createServiceProvider(fileSystem, output, tempFile);
|
|
service = new AnalyzerService('<default>', serviceProvider, {
|
|
console: output,
|
|
hostFactory: () => new FullAccessHost(serviceProvider!),
|
|
// Refresh service 2 seconds after the last library file change is detected.
|
|
libraryReanalysisTimeProvider: () => 2 * 1000,
|
|
shouldRunAnalysis: () => true,
|
|
});
|
|
|
|
service.setCompletionCallback((results) => {
|
|
// We're interested only in diagnostics for the last open file.
|
|
const fileDiags = results.diagnostics.filter((fileDiag) =>
|
|
fileDiag.fileUri.equals(lastOpenFileUri)
|
|
);
|
|
|
|
// Convert JSON-compatible format.
|
|
const resultsObj = {
|
|
...results,
|
|
diagnostics: fileDiags.map((fileDiag) => FileDiagnostics.toJsonObj(fileDiag)),
|
|
};
|
|
|
|
sendMessageToParent('analysisResults', resultsObj);
|
|
});
|
|
|
|
service.setOptions(options);
|
|
break;
|
|
}
|
|
|
|
case 'analyzeFile': {
|
|
if (serviceProvider && fileSystem && service) {
|
|
const uri = Uri.parse(messageObj.data as string, serviceProvider);
|
|
|
|
// Check the file's length before attempting to read its full contents.
|
|
const fileStat = fileSystem.statSync(uri);
|
|
if (fileStat.size > maxSourceFileSize) {
|
|
console.error(
|
|
`File length of "${uri}" is ${fileStat.size} ` +
|
|
`which exceeds the maximum supported file size of ${maxSourceFileSize}`
|
|
);
|
|
throw new Error('File larger than max');
|
|
}
|
|
|
|
const fileContents = fileSystem.readFileSync(uri, 'utf8');
|
|
|
|
lastOpenFileUri = uri;
|
|
service?.setFileOpened(uri, /* version */ 1, fileContents);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function verifyPackageTypes(
|
|
serviceProvider: ServiceProvider,
|
|
packageName: string,
|
|
options: PyrightCommandLineOptions,
|
|
outputJson: boolean,
|
|
minSeverityLevel: SeverityLevel,
|
|
ignoreUnknownTypesFromImports: boolean
|
|
): ExitStatus {
|
|
try {
|
|
const host = new FullAccessHost(serviceProvider);
|
|
const verifier = new PackageTypeVerifier(
|
|
serviceProvider,
|
|
host,
|
|
options,
|
|
packageName,
|
|
ignoreUnknownTypesFromImports
|
|
);
|
|
const report = verifier.verify();
|
|
const jsonReport = buildTypeCompletenessReport(packageName, report, minSeverityLevel);
|
|
|
|
if (outputJson) {
|
|
console.info(JSON.stringify(jsonReport, /* replacer */ undefined, 4));
|
|
} else {
|
|
printTypeCompletenessReportText(jsonReport, !!options.configSettings.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: PyrightJsonDiagnostic, report: PyrightJsonResults) {
|
|
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: string,
|
|
completenessReport: PackageTypeReport,
|
|
minSeverityLevel: SeverityLevel
|
|
): PyrightJsonResults {
|
|
const report: PyrightJsonResults = {
|
|
version: getVersionString(),
|
|
time: Date.now().toString(),
|
|
generalDiagnostics: [],
|
|
summary: {
|
|
filesAnalyzed: completenessReport.modules.size,
|
|
errorCount: 0,
|
|
warningCount: 0,
|
|
informationCount: 0,
|
|
timeInSec: timingStats.getTotalDuration(),
|
|
},
|
|
};
|
|
|
|
// Add the general diagnostics.
|
|
completenessReport.generalDiagnostics.forEach((diag) => {
|
|
const jsonDiag = convertDiagnosticToJson('', diag);
|
|
if (isDiagnosticIncluded(jsonDiag.severity, minSeverityLevel)) {
|
|
report.generalDiagnostics.push(jsonDiag);
|
|
}
|
|
accumulateReportDiagnosticStats(jsonDiag, report);
|
|
});
|
|
|
|
report.typeCompleteness = {
|
|
packageName,
|
|
packageRootDirectory: completenessReport.packageRootDirectoryUri?.getFilePath(),
|
|
moduleName: completenessReport.moduleName,
|
|
moduleRootDirectory: completenessReport.moduleRootDirectoryUri?.getFilePath(),
|
|
ignoreUnknownTypesFromImports: completenessReport.ignoreExternal,
|
|
pyTypedPath: completenessReport.pyTypedPathUri?.getFilePath(),
|
|
exportedSymbolCounts: {
|
|
withKnownType: 0,
|
|
withAmbiguousType: 0,
|
|
withUnknownType: 0,
|
|
},
|
|
otherSymbolCounts: {
|
|
withKnownType: 0,
|
|
withAmbiguousType: 0,
|
|
withUnknownType: 0,
|
|
},
|
|
missingFunctionDocStringCount: completenessReport.missingFunctionDocStringCount,
|
|
missingClassDocStringCount: completenessReport.missingClassDocStringCount,
|
|
missingDefaultParamCount: completenessReport.missingDefaultParamCount,
|
|
completenessScore: 0,
|
|
modules: [],
|
|
symbols: [],
|
|
};
|
|
|
|
// Add the modules.
|
|
completenessReport.modules.forEach((module) => {
|
|
const jsonModule: PyrightPublicModuleReport = {
|
|
name: module.name,
|
|
};
|
|
|
|
report.typeCompleteness!.modules.push(jsonModule);
|
|
});
|
|
|
|
// Add the symbols.
|
|
completenessReport.symbols.forEach((symbol) => {
|
|
const diagnostics: PyrightJsonDiagnostic[] = [];
|
|
|
|
// Convert and filter the diagnostics.
|
|
symbol.diagnostics.forEach((diag) => {
|
|
const jsonDiag = convertDiagnosticToJson(diag.uri.getFilePath(), diag.diagnostic);
|
|
if (isDiagnosticIncluded(jsonDiag.severity, minSeverityLevel)) {
|
|
diagnostics.push(jsonDiag);
|
|
}
|
|
});
|
|
|
|
const jsonSymbol: PyrightPublicSymbolReport = {
|
|
category: PackageTypeVerifier.getSymbolCategoryString(symbol.category),
|
|
name: symbol.fullName,
|
|
referenceCount: symbol.referenceCount,
|
|
isExported: symbol.isExported,
|
|
isTypeKnown: symbol.typeKnownStatus === TypeKnownStatus.Known,
|
|
isTypeAmbiguous: symbol.typeKnownStatus === TypeKnownStatus.Ambiguous,
|
|
diagnostics,
|
|
};
|
|
|
|
const alternateNames = completenessReport.alternateSymbolNames.get(symbol.fullName);
|
|
if (alternateNames) {
|
|
jsonSymbol.alternateNames = alternateNames;
|
|
}
|
|
|
|
report.typeCompleteness!.symbols.push(jsonSymbol);
|
|
|
|
// Accumulate counts for report.
|
|
if (symbol.typeKnownStatus === TypeKnownStatus.Known) {
|
|
if (symbol.isExported) {
|
|
report.typeCompleteness!.exportedSymbolCounts.withKnownType++;
|
|
} else {
|
|
report.typeCompleteness!.otherSymbolCounts.withKnownType++;
|
|
}
|
|
} else if (symbol.typeKnownStatus === TypeKnownStatus.Ambiguous) {
|
|
if (symbol.isExported) {
|
|
report.typeCompleteness!.exportedSymbolCounts.withAmbiguousType++;
|
|
} else {
|
|
report.typeCompleteness!.otherSymbolCounts.withAmbiguousType++;
|
|
}
|
|
} else {
|
|
if (symbol.isExported) {
|
|
report.typeCompleteness!.exportedSymbolCounts.withUnknownType++;
|
|
} else {
|
|
report.typeCompleteness!.otherSymbolCounts.withUnknownType++;
|
|
}
|
|
}
|
|
});
|
|
|
|
const unknownSymbolCount = report.typeCompleteness.exportedSymbolCounts.withUnknownType;
|
|
const ambiguousSymbolCount = report.typeCompleteness.exportedSymbolCounts.withAmbiguousType;
|
|
const knownSymbolCount = report.typeCompleteness.exportedSymbolCounts.withKnownType;
|
|
const totalSymbolCount = unknownSymbolCount + ambiguousSymbolCount + knownSymbolCount;
|
|
|
|
if (totalSymbolCount > 0) {
|
|
report.typeCompleteness!.completenessScore = knownSymbolCount / totalSymbolCount;
|
|
}
|
|
|
|
return report;
|
|
}
|
|
|
|
function printTypeCompletenessReportText(results: PyrightJsonResults, verboseOutput: boolean) {
|
|
const completenessReport = results.typeCompleteness!;
|
|
|
|
console.info(`Module name: "${completenessReport.moduleName}"`);
|
|
if (completenessReport.packageRootDirectory !== undefined) {
|
|
console.info(`Package directory: "${completenessReport.packageRootDirectory}"`);
|
|
}
|
|
if (completenessReport.moduleRootDirectory !== undefined) {
|
|
console.info(`Module directory: "${completenessReport.moduleRootDirectory}"`);
|
|
}
|
|
|
|
if (completenessReport.pyTypedPath !== undefined) {
|
|
console.info(`Path of py.typed file: "${completenessReport.pyTypedPath}"`);
|
|
}
|
|
|
|
// Print list of public modules.
|
|
if (completenessReport.modules.length > 0) {
|
|
console.info('');
|
|
console.info(`Public modules: ${completenessReport.modules.length}`);
|
|
completenessReport.modules.forEach((module) => {
|
|
console.info(` ${module.name}`);
|
|
});
|
|
}
|
|
|
|
// Print list of all symbols.
|
|
if (completenessReport.symbols.length > 0 && verboseOutput) {
|
|
console.info('');
|
|
console.info(`Exported symbols: ${completenessReport.symbols.filter((sym) => sym.isExported).length}`);
|
|
completenessReport.symbols.forEach((symbol) => {
|
|
if (symbol.isExported) {
|
|
const refCount = symbol.referenceCount > 1 ? ` (${symbol.referenceCount} references)` : '';
|
|
console.info(` ${symbol.name}${refCount}`);
|
|
}
|
|
});
|
|
|
|
console.info('');
|
|
console.info(`Other referenced symbols: ${completenessReport.symbols.filter((sym) => !sym.isExported).length}`);
|
|
completenessReport.symbols.forEach((symbol) => {
|
|
if (!symbol.isExported) {
|
|
const refCount = symbol.referenceCount > 1 ? ` (${symbol.referenceCount} references)` : '';
|
|
console.info(` ${symbol.name}${refCount}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Print all the general diagnostics.
|
|
results.generalDiagnostics.forEach((diag) => {
|
|
logDiagnosticToConsole(diag);
|
|
});
|
|
|
|
// Print all the symbol-specific diagnostics.
|
|
console.info('');
|
|
console.info(`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.info(`${symbol.name}`);
|
|
diagnostics.forEach((diag) => {
|
|
logDiagnosticToConsole(diag);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Print other stats.
|
|
console.info('');
|
|
console.info(
|
|
`Symbols exported by "${completenessReport.packageName}": ${
|
|
completenessReport.exportedSymbolCounts.withKnownType +
|
|
completenessReport.exportedSymbolCounts.withAmbiguousType +
|
|
completenessReport.exportedSymbolCounts.withUnknownType
|
|
}`
|
|
);
|
|
console.info(` With known type: ${completenessReport.exportedSymbolCounts.withKnownType}`);
|
|
console.info(` With ambiguous type: ${completenessReport.exportedSymbolCounts.withAmbiguousType}`);
|
|
console.info(` With unknown type: ${completenessReport.exportedSymbolCounts.withUnknownType}`);
|
|
if (completenessReport.ignoreUnknownTypesFromImports) {
|
|
console.info(` (Ignoring unknown types imported from other packages)`);
|
|
}
|
|
console.info('');
|
|
console.info(
|
|
`Other symbols referenced but not exported by "${completenessReport.packageName}": ${
|
|
completenessReport.otherSymbolCounts.withKnownType +
|
|
completenessReport.otherSymbolCounts.withAmbiguousType +
|
|
completenessReport.otherSymbolCounts.withUnknownType
|
|
}`
|
|
);
|
|
console.info(` With known type: ${completenessReport.otherSymbolCounts.withKnownType}`);
|
|
console.info(` With ambiguous type: ${completenessReport.otherSymbolCounts.withAmbiguousType}`);
|
|
console.info(` With unknown type: ${completenessReport.otherSymbolCounts.withUnknownType}`);
|
|
console.info('');
|
|
console.info(`Symbols without documentation:`);
|
|
console.info(` Functions without docstring: ${completenessReport.missingFunctionDocStringCount}`);
|
|
console.info(` Functions without default param: ${completenessReport.missingDefaultParamCount}`);
|
|
console.info(` Classes without docstring: ${completenessReport.missingClassDocStringCount}`);
|
|
console.info('');
|
|
console.info(`Type completeness score: ${Math.round(completenessReport.completenessScore * 1000) / 10}%`);
|
|
console.info('');
|
|
console.info(`Completed in ${results.summary.timeInSec}sec`);
|
|
console.info('');
|
|
}
|
|
|
|
function printUsage() {
|
|
console.info(
|
|
'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' +
|
|
' --level <LEVEL> Minimum diagnostic level (error or warning)\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' +
|
|
' --pythonpath <FILE> Path to the Python interpreter\n' +
|
|
' --pythonversion <VERSION> Analyze for a specific version (3.3, 3.4, etc.)\n' +
|
|
' --skipunannotated Skip analysis of functions with no type annotations\n' +
|
|
' --stats Print detailed performance stats\n' +
|
|
' -t,--typeshedpath <DIRECTORY> Use typeshed type stubs at this location\n' +
|
|
' --threads <optional COUNT> Use separate threads to parallelize type checking \n' +
|
|
' -v,--venvpath <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 and exit\n' +
|
|
' --warnings Use exit code of 1 if warnings are reported\n' +
|
|
' -w,--watch Continue to run and watch for changes\n' +
|
|
' - Read files from stdin\n'
|
|
);
|
|
}
|
|
|
|
function getVersionString() {
|
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
const version = require('../package.json').version;
|
|
return version.toString();
|
|
}
|
|
|
|
function printVersion(console: ConsoleInterface) {
|
|
console.info(`${toolName} ${getVersionString()}`);
|
|
}
|
|
|
|
function reportDiagnosticsAsJson(
|
|
fileDiagnostics: FileDiagnostics[],
|
|
minSeverityLevel: SeverityLevel,
|
|
filesInProgram: number,
|
|
timeInSec: number
|
|
): DiagnosticResult {
|
|
const report: PyrightJsonResults = {
|
|
version: getVersionString(),
|
|
time: Date.now().toString(),
|
|
generalDiagnostics: [],
|
|
summary: {
|
|
filesAnalyzed: filesInProgram,
|
|
errorCount: 0,
|
|
warningCount: 0,
|
|
informationCount: 0,
|
|
timeInSec,
|
|
},
|
|
};
|
|
|
|
fileDiagnostics.forEach((fileDiag) => {
|
|
fileDiag.diagnostics.sort(compareDiagnostics).forEach((diag) => {
|
|
if (
|
|
diag.category === DiagnosticCategory.Error ||
|
|
diag.category === DiagnosticCategory.Warning ||
|
|
diag.category === DiagnosticCategory.Information
|
|
) {
|
|
const jsonDiag = convertDiagnosticToJson(fileDiag.fileUri.getFilePath(), diag);
|
|
if (isDiagnosticIncluded(jsonDiag.severity, minSeverityLevel)) {
|
|
report.generalDiagnostics.push(jsonDiag);
|
|
}
|
|
|
|
accumulateReportDiagnosticStats(jsonDiag, report);
|
|
}
|
|
});
|
|
});
|
|
|
|
console.info(JSON.stringify(report, /* replacer */ undefined, 4));
|
|
|
|
// Output a blank line to help tools that are attempting to parse the
|
|
// JSON output when used in watch mode.
|
|
console.info('');
|
|
|
|
return {
|
|
errorCount: report.summary.errorCount,
|
|
warningCount: report.summary.warningCount,
|
|
informationCount: report.summary.informationCount,
|
|
diagnosticCount: report.summary.errorCount + report.summary.warningCount + report.summary.informationCount,
|
|
};
|
|
}
|
|
|
|
function isDiagnosticIncluded(diagSeverity: SeverityLevel, minSeverityLevel: SeverityLevel) {
|
|
// Errors are always included.
|
|
if (diagSeverity === 'error') {
|
|
return true;
|
|
}
|
|
|
|
// Warnings are included only if the min severity level is below error.
|
|
if (diagSeverity === 'warning') {
|
|
return minSeverityLevel !== 'error';
|
|
}
|
|
|
|
// Informations are included only if the min severity level is 'information'.
|
|
return minSeverityLevel === 'information';
|
|
}
|
|
|
|
function convertDiagnosticCategoryToSeverity(category: DiagnosticCategory): SeverityLevel {
|
|
switch (category) {
|
|
case DiagnosticCategory.Error:
|
|
return 'error';
|
|
|
|
case DiagnosticCategory.Warning:
|
|
return 'warning';
|
|
|
|
case DiagnosticCategory.Information:
|
|
return 'information';
|
|
|
|
default:
|
|
fail('Unexpected diagnostic category');
|
|
}
|
|
}
|
|
|
|
function convertDiagnosticToJson(filePath: string, diag: Diagnostic): PyrightJsonDiagnostic {
|
|
return {
|
|
file: filePath,
|
|
severity: convertDiagnosticCategoryToSeverity(diag.category),
|
|
message: diag.message,
|
|
range: isEmptyRange(diag.range) ? undefined : diag.range,
|
|
rule: diag.getRule(),
|
|
};
|
|
}
|
|
|
|
function reportDiagnosticsAsText(
|
|
fileDiagnostics: FileDiagnostics[],
|
|
minSeverityLevel: SeverityLevel
|
|
): DiagnosticResult {
|
|
let errorCount = 0;
|
|
let warningCount = 0;
|
|
let informationCount = 0;
|
|
|
|
fileDiagnostics.forEach((fileDiagnostics) => {
|
|
// Don't report unused code or deprecated diagnostics.
|
|
const fileErrorsAndWarnings = fileDiagnostics.diagnostics
|
|
.filter(
|
|
(diag) =>
|
|
diag.category !== DiagnosticCategory.UnusedCode &&
|
|
diag.category !== DiagnosticCategory.UnreachableCode &&
|
|
diag.category !== DiagnosticCategory.Deprecated &&
|
|
isDiagnosticIncluded(convertDiagnosticCategoryToSeverity(diag.category), minSeverityLevel)
|
|
)
|
|
.sort(compareDiagnostics);
|
|
|
|
if (fileErrorsAndWarnings.length > 0) {
|
|
console.info(`${fileDiagnostics.fileUri.toUserVisibleString()}`);
|
|
fileErrorsAndWarnings.forEach((diag) => {
|
|
const jsonDiag = convertDiagnosticToJson(fileDiagnostics.fileUri.getFilePath(), diag);
|
|
logDiagnosticToConsole(jsonDiag);
|
|
|
|
if (diag.category === DiagnosticCategory.Error) {
|
|
errorCount++;
|
|
} else if (diag.category === DiagnosticCategory.Warning) {
|
|
warningCount++;
|
|
} else if (diag.category === DiagnosticCategory.Information) {
|
|
informationCount++;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
console.info(
|
|
`${errorCount.toString()} ${errorCount === 1 ? 'error' : 'errors'}, ` +
|
|
`${warningCount.toString()} ${warningCount === 1 ? 'warning' : 'warnings'}, ` +
|
|
`${informationCount.toString()} ${informationCount === 1 ? 'information' : 'informations'}`
|
|
);
|
|
|
|
return {
|
|
errorCount,
|
|
warningCount,
|
|
informationCount,
|
|
diagnosticCount: errorCount + warningCount + informationCount,
|
|
};
|
|
}
|
|
|
|
function logDiagnosticToConsole(diag: PyrightJsonDiagnostic, 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}`) +
|
|
' - ';
|
|
} else {
|
|
message += ' ';
|
|
}
|
|
|
|
const [firstLine, ...remainingLines] = diag.message.split('\n');
|
|
|
|
message +=
|
|
diag.severity === 'error'
|
|
? chalk.red('error')
|
|
: diag.severity === 'warning'
|
|
? chalk.cyan('warning')
|
|
: chalk.blue('information');
|
|
message += `: ${firstLine}`;
|
|
if (remainingLines.length > 0) {
|
|
message += '\n' + prefix + remainingLines.join('\n' + prefix);
|
|
}
|
|
|
|
if (diag.rule) {
|
|
message += chalk.gray(` (${diag.rule})`);
|
|
}
|
|
|
|
console.info(message);
|
|
}
|
|
|
|
function parseThreadsArgValue(input: string | null): any {
|
|
if (input === null || input === 'auto') {
|
|
return null;
|
|
}
|
|
|
|
const value = parseInt(input, 10);
|
|
if (isNaN(value) || value < 1) {
|
|
return null;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
// Increase the default stack trace limit from 16 to 64 to help diagnose
|
|
// crashes with deep stack traces.
|
|
Error.stackTraceLimit = 64;
|
|
|
|
export async function main() {
|
|
await initializeDependencies();
|
|
|
|
// Is this a worker process for multi-threaded analysis?
|
|
if (process.argv[2] === 'worker') {
|
|
const workerNumber = parseInt(process.argv[3]);
|
|
runWorkerMessageLoop(workerNumber, process.argv[4]);
|
|
return;
|
|
}
|
|
|
|
const exitCode = await processArgs();
|
|
process.exitCode = exitCode;
|
|
// Don't call process.exit; stdout may not have been flushed which can break readers.
|
|
// https://github.com/nodejs/node/issues/6379
|
|
// https://github.com/nodejs/node/issues/6456
|
|
// https://github.com/nodejs/node/issues/19218
|
|
}
|