Added new eslint rule to enforce ordering of class methods by accessibility (public, protected, private).

This commit is contained in:
Eric Traut 2023-04-25 17:24:33 -07:00
parent 006a22467d
commit 280d088afd
28 changed files with 2288 additions and 2281 deletions

View File

@ -28,6 +28,13 @@
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/ban-types": 0,
"@typescript-eslint/camelcase": 0,
"@typescript-eslint/member-ordering": [
"error",
{
"classes": ["field", "constructor", ["public-get", "public-set"], "public-method", ["protected-get", "protected-set"], "protected-method", ["private-get", "private-set"], "private-method"],
"interfaces": []
}
],
"@typescript-eslint/no-empty-interface": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-namespace": 0,

View File

@ -244,6 +244,10 @@ export class BackgroundAnalysisProgram {
this._backgroundAnalysis?.shutdown();
}
protected _getIndices(): Indices | undefined {
return undefined;
}
private _ensurePartialStubPackages(execEnv: ExecutionEnvironment) {
this._backgroundAnalysis?.ensurePartialStubPackages(execEnv.root);
return this._importResolver.ensurePartialStubPackages(execEnv);
@ -267,10 +271,6 @@ export class BackgroundAnalysisProgram {
}
}
}
protected _getIndices(): Indices | undefined {
return undefined;
}
}
export type BackgroundAnalysisProgramFactory = (

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -168,6 +168,26 @@ export class AnalyzerService {
Extensions.createProgramExtensions(this._program, { addInterimFile: this.addInterimFile.bind(this) });
}
get fs() {
return this._backgroundAnalysisProgram.importResolver.fileSystem;
}
get cancellationProvider() {
return this._options.cancellationProvider!;
}
get librarySearchPathsToWatch() {
return this._librarySearchPathsToWatch;
}
get backgroundAnalysisProgram(): BackgroundAnalysisProgram {
return this._backgroundAnalysisProgram;
}
get test_program() {
return this._program;
}
clone(
instanceName: string,
serviceId: string,
@ -221,30 +241,6 @@ export class AnalyzerService {
this._clearLibraryReanalysisTimer();
}
private get _console() {
return this._options.console!;
}
private get _hostFactory() {
return this._options.hostFactory!;
}
private get _importResolverFactory() {
return this._options.importResolverFactory!;
}
get cancellationProvider() {
return this._options.cancellationProvider!;
}
get librarySearchPathsToWatch() {
return this._librarySearchPathsToWatch;
}
get backgroundAnalysisProgram(): BackgroundAnalysisProgram {
return this._backgroundAnalysisProgram;
}
static createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver {
return new ImportResolver(fs, options, host);
}
@ -529,11 +525,6 @@ export class AnalyzerService {
}
}
// test only APIs
get test_program() {
return this._program;
}
test_getConfigOptions(commandLineOptions: CommandLineOptions): ConfigOptions {
return this._getConfigOptions(this._backgroundAnalysisProgram.host, commandLineOptions);
}
@ -546,6 +537,92 @@ export class AnalyzerService {
return this._shouldHandleSourceFileWatchChanges(path, isFile);
}
writeTypeStub(token: CancellationToken): void {
const typingsSubdirPath = this._getTypeStubFolder();
this._program.writeTypeStub(
this._typeStubTargetPath ?? '',
this._typeStubTargetIsSingleFile,
typingsSubdirPath,
token
);
}
writeTypeStubInBackground(token: CancellationToken): Promise<any> {
const typingsSubdirPath = this._getTypeStubFolder();
return this._backgroundAnalysisProgram.writeTypeStub(
this._typeStubTargetPath ?? '',
this._typeStubTargetIsSingleFile,
typingsSubdirPath,
token
);
}
invalidateAndForceReanalysis(
rebuildUserFileIndexing = true,
rebuildLibraryIndexing = true,
refreshOptions?: RefreshOptions
) {
this._backgroundAnalysisProgram.invalidateAndForceReanalysis(
rebuildUserFileIndexing,
rebuildLibraryIndexing,
refreshOptions
);
}
// Forces the service to stop all analysis, discard all its caches,
// and research for files.
restart() {
this._applyConfigOptions(this._hostFactory());
this._backgroundAnalysisProgram.restart();
}
private get _console() {
return this._options.console!;
}
private get _hostFactory() {
return this._options.hostFactory!;
}
private get _importResolverFactory() {
return this._options.importResolverFactory!;
}
private get _program() {
return this._backgroundAnalysisProgram.program;
}
private get _configOptions() {
return this._backgroundAnalysisProgram.configOptions;
}
private get _watchForSourceChanges() {
return !!this._commandLineOptions?.watchForSourceChanges;
}
private get _watchForLibraryChanges() {
return !!this._commandLineOptions?.watchForLibraryChanges && !!this._options.libraryReanalysisTimeProvider;
}
private get _watchForConfigChanges() {
return !!this._commandLineOptions?.watchForConfigChanges;
}
private get _typeCheckingMode() {
return this._commandLineOptions?.typeCheckingMode;
}
private get _verboseOutput(): boolean {
return !!this._configOptions.verboseOutput;
}
private get _typeStubTargetImportName() {
return this._commandLineOptions?.typeStubTargetImportName;
}
// Calculates the effective options based on the command-line options,
// an optional config file, and default values.
private _getConfigOptions(host: Host, commandLineOptions: CommandLineOptions): ConfigOptions {
@ -840,84 +917,6 @@ export class AnalyzerService {
return configOptions;
}
writeTypeStub(token: CancellationToken): void {
const typingsSubdirPath = this._getTypeStubFolder();
this._program.writeTypeStub(
this._typeStubTargetPath ?? '',
this._typeStubTargetIsSingleFile,
typingsSubdirPath,
token
);
}
writeTypeStubInBackground(token: CancellationToken): Promise<any> {
const typingsSubdirPath = this._getTypeStubFolder();
return this._backgroundAnalysisProgram.writeTypeStub(
this._typeStubTargetPath ?? '',
this._typeStubTargetIsSingleFile,
typingsSubdirPath,
token
);
}
invalidateAndForceReanalysis(
rebuildUserFileIndexing = true,
rebuildLibraryIndexing = true,
refreshOptions?: RefreshOptions
) {
this._backgroundAnalysisProgram.invalidateAndForceReanalysis(
rebuildUserFileIndexing,
rebuildLibraryIndexing,
refreshOptions
);
}
// Forces the service to stop all analysis, discard all its caches,
// and research for files.
restart() {
this._applyConfigOptions(this._hostFactory());
this._backgroundAnalysisProgram.restart();
}
get fs() {
return this._backgroundAnalysisProgram.importResolver.fileSystem;
}
private get _program() {
return this._backgroundAnalysisProgram.program;
}
private get _configOptions() {
return this._backgroundAnalysisProgram.configOptions;
}
private get _watchForSourceChanges() {
return !!this._commandLineOptions?.watchForSourceChanges;
}
private get _watchForLibraryChanges() {
return !!this._commandLineOptions?.watchForLibraryChanges && !!this._options.libraryReanalysisTimeProvider;
}
private get _watchForConfigChanges() {
return !!this._commandLineOptions?.watchForConfigChanges;
}
private get _typeCheckingMode() {
return this._commandLineOptions?.typeCheckingMode;
}
private get _verboseOutput(): boolean {
return !!this._configOptions.verboseOutput;
}
private get _typeStubTargetImportName() {
return this._commandLineOptions?.typeStubTargetImportName;
}
private _getTypeStubFolder() {
const stubPath =
this._configOptions.stubPath ??

View File

@ -538,64 +538,6 @@ export class SourceFile {
return diagList;
}
// Get all task list diagnostics for the current file and add them
// to the specified diagnostic list
private _addTaskListDiagnostics(taskListTokens: TaskListToken[] | undefined, diagList: Diagnostic[]) {
// input validation
if (!taskListTokens || taskListTokens.length === 0 || !diagList) {
return;
}
// if we have no tokens, we're done
if (!this._parseResults?.tokenizerOutput?.tokens) {
return;
}
const tokenizerOutput = this._parseResults.tokenizerOutput;
for (let i = 0; i < tokenizerOutput.tokens.count; i++) {
const token = tokenizerOutput.tokens.getItemAt(i);
// if there are no comments, skip this token
if (!token.comments || token.comments.length === 0) {
continue;
}
for (const comment of token.comments) {
for (const token of taskListTokens) {
// Check if the comment matches the task list token.
// The comment must start with zero or more whitespace characters,
// followed by the taskListToken (case insensitive),
// followed by (0+ whitespace + EOL) OR (1+ NON-alphanumeric characters)
const regexStr = '^[\\s]*' + token.text + '([\\s]*$|[\\W]+)';
const regex = RegExp(regexStr, 'i'); // case insensitive
// if the comment doesn't match, skip it
if (!regex.test(comment.value)) {
continue;
}
// Calculate the range for the diagnostic
// This allows navigation to the comment via double clicking the item in the task list pane
let rangeStart = comment.start;
// The comment technically starts right after the comment identifier (#), but we want the caret right
// before the task list token (since there might be whitespace before it)
const indexOfToken = comment.value.toLowerCase().indexOf(token.text.toLowerCase());
rangeStart += indexOfToken;
const rangeEnd = TextRange.getEnd(comment);
const range = convertOffsetsToRange(rangeStart, rangeEnd, tokenizerOutput.lines!);
// Add the diagnostic to the list to send to VS,
// and trim whitespace from the comment so it's easier to read in the task list
diagList.push(
new Diagnostic(DiagnosticCategory.TaskItem, comment.value.trim(), range, token.priority)
);
}
}
}
}
getImports(): ImportResult[] {
return this._imports || [];
}
@ -1300,6 +1242,64 @@ export class SourceFile {
this._ipythonMode = enable ? IPythonMode.CellDocs : IPythonMode.None;
}
// Get all task list diagnostics for the current file and add them
// to the specified diagnostic list
private _addTaskListDiagnostics(taskListTokens: TaskListToken[] | undefined, diagList: Diagnostic[]) {
// input validation
if (!taskListTokens || taskListTokens.length === 0 || !diagList) {
return;
}
// if we have no tokens, we're done
if (!this._parseResults?.tokenizerOutput?.tokens) {
return;
}
const tokenizerOutput = this._parseResults.tokenizerOutput;
for (let i = 0; i < tokenizerOutput.tokens.count; i++) {
const token = tokenizerOutput.tokens.getItemAt(i);
// if there are no comments, skip this token
if (!token.comments || token.comments.length === 0) {
continue;
}
for (const comment of token.comments) {
for (const token of taskListTokens) {
// Check if the comment matches the task list token.
// The comment must start with zero or more whitespace characters,
// followed by the taskListToken (case insensitive),
// followed by (0+ whitespace + EOL) OR (1+ NON-alphanumeric characters)
const regexStr = '^[\\s]*' + token.text + '([\\s]*$|[\\W]+)';
const regex = RegExp(regexStr, 'i'); // case insensitive
// if the comment doesn't match, skip it
if (!regex.test(comment.value)) {
continue;
}
// Calculate the range for the diagnostic
// This allows navigation to the comment via double clicking the item in the task list pane
let rangeStart = comment.start;
// The comment technically starts right after the comment identifier (#), but we want the caret right
// before the task list token (since there might be whitespace before it)
const indexOfToken = comment.value.toLowerCase().indexOf(token.text.toLowerCase());
rangeStart += indexOfToken;
const rangeEnd = TextRange.getEnd(comment);
const range = convertOffsetsToRange(rangeStart, rangeEnd, tokenizerOutput.lines!);
// Add the diagnostic to the list to send to VS,
// and trim whitespace from the comment so it's easier to read in the task list
diagList.push(
new Diagnostic(DiagnosticCategory.TaskItem, comment.value.trim(), range, token.priority)
);
}
}
}
}
private _buildFileInfo(
configOptions: ConfigOptions,
fileContents: string,

View File

@ -50,9 +50,9 @@ import { TypeEvaluator } from './typeEvaluatorTypes';
import { ClassType, isFunction, isInstantiableClass, isNever, isUnknown, removeUnknownFromUnion } from './types';
class TrackedImport {
constructor(public importName: string) {}
isAccessed = false;
constructor(public importName: string) {}
}
class TrackedImportAs extends TrackedImport {

View File

@ -3693,10 +3693,6 @@ class ExpectedConstructorTypeTransformer extends TypeVarTransformer {
super();
}
private _isTypeVarLive(typeVar: TypeVarType) {
return this._liveTypeVarScopes.some((scopeId) => typeVar.scopeId === scopeId);
}
override transformTypeVar(typeVar: TypeVarType) {
// If the type variable is unrelated to the scopes we're solving,
// don't transform that type variable.
@ -3706,4 +3702,8 @@ class ExpectedConstructorTypeTransformer extends TypeVarTransformer {
return AnyType.create();
}
private _isTypeVarLive(typeVar: TypeVarType) {
return this._liveTypeVarScopes.some((scopeId) => typeVar.scopeId === scopeId);
}
}

View File

@ -47,39 +47,6 @@ export class BackgroundAnalysisBase {
// Don't allow instantiation of this type directly.
}
protected setup(worker: Worker) {
this._worker = worker;
// global channel to communicate from BG channel to main thread.
worker.on('message', (msg: AnalysisResponse) => this.onMessage(msg));
// this will catch any exception thrown from background thread,
// print log and ignore exception
worker.on('error', (msg) => {
this.log(LogLevel.Error, `Error occurred on background thread: ${JSON.stringify(msg)}`);
});
}
protected onMessage(msg: AnalysisResponse) {
switch (msg.requestType) {
case 'log': {
const logData = msg.data as LogData;
this.log(logData.level, logData.message);
break;
}
case 'analysisResult': {
// Change in diagnostics due to host such as file closed rather than
// analyzing files.
this._onAnalysisCompletion(convertAnalysisResults(msg.data));
break;
}
default:
debug.fail(`${msg.requestType} is not expected`);
}
}
setCompletionCallback(callback?: AnalysisCompleteCallback) {
this._onAnalysisCompletion = callback ?? nullCallback;
}
@ -146,53 +113,6 @@ export class BackgroundAnalysisBase {
this._startOrResumeAnalysis('analyze', indices, token);
}
private _startOrResumeAnalysis(
requestType: 'analyze' | 'resumeAnalysis',
indices: Indices | undefined,
token: CancellationToken
) {
const { port1, port2 } = new MessageChannel();
// Handle response from background thread to main thread.
port1.on('message', (msg: AnalysisResponse) => {
switch (msg.requestType) {
case 'analysisResult': {
this._onAnalysisCompletion(convertAnalysisResults(msg.data));
break;
}
case 'analysisPaused': {
port2.close();
port1.close();
// Analysis request has completed, but there is more to
// analyze, so queue another message to resume later.
this._startOrResumeAnalysis('resumeAnalysis', indices, token);
break;
}
case 'indexResult': {
const { path, indexResults } = msg.data;
indices?.setWorkspaceIndex(path, indexResults);
break;
}
case 'analysisDone': {
disposeCancellationToken(token);
port2.close();
port1.close();
break;
}
default:
debug.fail(`${msg.requestType} is not expected`);
}
});
const cancellationId = getCancellationTokenId(token);
this.enqueueRequest({ requestType, data: cancellationId, port: port2 });
}
startIndexing(
indexOptions: IndexOptions,
configOptions: ConfigOptions,
@ -272,6 +192,39 @@ export class BackgroundAnalysisBase {
this.enqueueRequest({ requestType: 'shutdown', data: null });
}
protected setup(worker: Worker) {
this._worker = worker;
// global channel to communicate from BG channel to main thread.
worker.on('message', (msg: AnalysisResponse) => this.onMessage(msg));
// this will catch any exception thrown from background thread,
// print log and ignore exception
worker.on('error', (msg) => {
this.log(LogLevel.Error, `Error occurred on background thread: ${JSON.stringify(msg)}`);
});
}
protected onMessage(msg: AnalysisResponse) {
switch (msg.requestType) {
case 'log': {
const logData = msg.data as LogData;
this.log(logData.level, logData.message);
break;
}
case 'analysisResult': {
// Change in diagnostics due to host such as file closed rather than
// analyzing files.
this._onAnalysisCompletion(convertAnalysisResults(msg.data));
break;
}
default:
debug.fail(`${msg.requestType} is not expected`);
}
}
protected enqueueRequest(request: AnalysisRequest) {
if (this._worker) {
this._worker.postMessage(request, request.port ? [request.port] : undefined);
@ -281,19 +234,62 @@ export class BackgroundAnalysisBase {
protected log(level: LogLevel, msg: string) {
log(this.console, level, msg);
}
private _startOrResumeAnalysis(
requestType: 'analyze' | 'resumeAnalysis',
indices: Indices | undefined,
token: CancellationToken
) {
const { port1, port2 } = new MessageChannel();
// Handle response from background thread to main thread.
port1.on('message', (msg: AnalysisResponse) => {
switch (msg.requestType) {
case 'analysisResult': {
this._onAnalysisCompletion(convertAnalysisResults(msg.data));
break;
}
case 'analysisPaused': {
port2.close();
port1.close();
// Analysis request has completed, but there is more to
// analyze, so queue another message to resume later.
this._startOrResumeAnalysis('resumeAnalysis', indices, token);
break;
}
case 'indexResult': {
const { path, indexResults } = msg.data;
indices?.setWorkspaceIndex(path, indexResults);
break;
}
case 'analysisDone': {
disposeCancellationToken(token);
port2.close();
port1.close();
break;
}
default:
debug.fail(`${msg.requestType} is not expected`);
}
});
const cancellationId = getCancellationTokenId(token);
this.enqueueRequest({ requestType, data: cancellationId, port: port2 });
}
}
export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase {
private _configOptions: ConfigOptions;
protected _importResolver: ImportResolver;
private _program: Program;
protected _importResolver: ImportResolver;
protected _logTracker: LogTracker;
get program(): Program {
return this._program;
}
protected constructor() {
super(workerData as InitializationData);
@ -315,6 +311,10 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
});
}
get program(): Program {
return this._program;
}
start() {
this.log(LogLevel.Info, `Background analysis(${threadId}) started`);
@ -499,6 +499,28 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
}
}
protected abstract createHost(): Host;
protected abstract createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver;
protected processIndexing(port: MessagePort, token: CancellationToken): void {
/* noop */
}
protected reportIndex(port: MessagePort, result: { path: string; indexResults: IndexResults }) {
port.postMessage({ requestType: 'indexResult', data: result });
}
protected override shutdown() {
this._program.dispose();
Extensions.destroyProgramExtensions(this._program.id);
super.shutdown();
}
protected analysisDone(port: MessagePort, cancellationId: string) {
port.postMessage({ requestType: 'analysisDone', data: cancellationId });
}
private _onMessageWrapper(msg: AnalysisRequest) {
try {
return this.onMessage(msg);
@ -541,24 +563,6 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
}
}
protected abstract createHost(): Host;
protected abstract createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver;
protected processIndexing(port: MessagePort, token: CancellationToken): void {
/* noop */
}
protected reportIndex(port: MessagePort, result: { path: string; indexResults: IndexResults }) {
port.postMessage({ requestType: 'indexResult', data: result });
}
protected override shutdown() {
this._program.dispose();
Extensions.destroyProgramExtensions(this._program.id);
super.shutdown();
}
private _reportDiagnostics(diagnostics: FileDiagnostics[], filesLeftToAnalyze: number, elapsedTime: number) {
if (parentPort) {
this._onAnalysisCompletion(parentPort, {
@ -580,10 +584,6 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase
private _analysisPaused(port: MessagePort, cancellationId: string) {
port.postMessage({ requestType: 'analysisPaused', data: cancellationId });
}
protected analysisDone(port: MessagePort, cancellationId: string) {
port.postMessage({ requestType: 'analysisDone', data: cancellationId });
}
}
function convertAnalysisResults(result: AnalysisResults): AnalysisResults {

View File

@ -26,7 +26,7 @@ import {
} from '../analyzer/types';
import { throwIfCancellationRequested } from '../common/cancellationUtils';
import { isNumber, isString } from '../common/core';
import { convertOffsetsToRange, convertOffsetToPosition } from '../common/positionUtils';
import { convertOffsetToPosition, convertOffsetsToRange } from '../common/positionUtils';
import { TextRange } from '../common/textRange';
import { TextRangeCollection } from '../common/textRangeCollection';
import { LanguageServerInterface } from '../languageServerBase';
@ -55,8 +55,8 @@ import {
ErrorNode,
ExceptNode,
ExpressionNode,
FormatStringNode,
ForNode,
FormatStringNode,
FunctionAnnotationNode,
FunctionNode,
GlobalNode,
@ -66,7 +66,6 @@ import {
ImportFromNode,
ImportNode,
IndexNode,
isExpressionNode,
LambdaNode,
ListComprehensionForNode,
ListComprehensionIfNode,
@ -117,6 +116,7 @@ import {
WithNode,
YieldFromNode,
YieldNode,
isExpressionNode,
} from '../parser/parseNodes';
import { ParseResults } from '../parser/parser';
import { KeywordType, NewLineType, OperatorType, StringTokenFlags, Token, TokenType } from '../parser/tokenizerTypes';
@ -570,18 +570,6 @@ class TreeDumper extends ParseTreeWalker {
}
}
private _log(value: string) {
this._output += `${this._indentation}${value}\r\n`;
}
private _getPrefix(node: ParseNode) {
const pos = convertOffsetToPosition(node.start, this._lines);
// VS code's output window expects 1 based values, print the line/char with 1 based.
return `[${node.id}] '${this._file}:${pos.line + 1}:${pos.character + 1}' => ${printParseNodeType(
node.nodeType
)} ${getTextSpanString(node, this._lines)} =>`;
}
reset() {
this._indentation = '';
this._output = '';
@ -1004,6 +992,18 @@ class TreeDumper extends ParseTreeWalker {
this._log(`${this._getPrefix(node)}`);
return true;
}
private _log(value: string) {
this._output += `${this._indentation}${value}\r\n`;
}
private _getPrefix(node: ParseNode) {
const pos = convertOffsetToPosition(node.start, this._lines);
// VS code's output window expects 1 based values, print the line/char with 1 based.
return `[${node.id}] '${this._file}:${pos.line + 1}:${pos.character + 1}' => ${printParseNodeType(
node.nodeType
)} ${getTextSpanString(node, this._lines)} =>`;
}
}
function getTypeParameterCategoryString(type: TypeParameterCategory) {

View File

@ -82,16 +82,6 @@ export class FileBasedToken implements CancellationToken {
// empty
}
cancel() {
if (!this.isCancelled) {
this.isCancelled = true;
if (this._emitter) {
this._emitter.fire(undefined);
this._disposeEmitter();
}
}
}
get isCancellationRequested(): boolean {
if (this.isCancelled) {
return true;
@ -115,6 +105,16 @@ export class FileBasedToken implements CancellationToken {
return this._emitter.event;
}
cancel() {
if (!this.isCancelled) {
this.isCancelled = true;
if (this._emitter) {
this._emitter.fire(undefined);
this._disposeEmitter();
}
}
}
dispose(): void {
this._disposeEmitter();
}

View File

@ -35,11 +35,6 @@ export type DiagnosticSeverityOverridesMap = { [ruleName: string]: DiagnosticSev
// that provided through a language client like the VS Code editor. These options
// are later combined with those from the config file to produce the final configuration.
export class CommandLineOptions {
constructor(executionRoot: string, fromVsCodeExtension: boolean) {
this.executionRoot = executionRoot;
this.fromVsCodeExtension = fromVsCodeExtension;
}
// A list of file specs to include in the analysis. Can contain
// directories, in which case all "*.py" files within those directories
// are included.
@ -142,4 +137,9 @@ export class CommandLineOptions {
// Analyze functions and methods that have no type annotations?
analyzeUnannotatedFunctions?: boolean;
constructor(executionRoot: string, fromVsCodeExtension: boolean) {
this.executionRoot = executionRoot;
this.fromVsCodeExtension = fromVsCodeExtension;
}
}

View File

@ -36,19 +36,6 @@ export enum PythonPlatform {
}
export class ExecutionEnvironment {
// Default to "." which indicates every file in the project.
constructor(
root: string,
defaultPythonVersion: PythonVersion | undefined,
defaultPythonPlatform: string | undefined,
defaultExtraPaths: string[] | undefined
) {
this.root = root || undefined;
this.pythonVersion = defaultPythonVersion || latestStablePythonVersion;
this.pythonPlatform = defaultPythonPlatform;
this.extraPaths = [...(defaultExtraPaths ?? [])];
}
// Root directory for execution - absolute or relative to the
// project root.
// Undefined if this is a rootless environment (e.g., open file mode).
@ -62,6 +49,19 @@ export class ExecutionEnvironment {
// Default to no extra paths.
extraPaths: string[] = [];
// Default to "." which indicates every file in the project.
constructor(
root: string,
defaultPythonVersion: PythonVersion | undefined,
defaultPythonPlatform: string | undefined,
defaultExtraPaths: string[] | undefined
) {
this.root = root || undefined;
this.pythonVersion = defaultPythonVersion || latestStablePythonVersion;
this.pythonPlatform = defaultPythonPlatform;
this.extraPaths = [...(defaultExtraPaths ?? [])];
}
}
export type DiagnosticLevel = 'none' | 'information' | 'warning' | 'error';
@ -677,13 +677,6 @@ export function matchFileSpecs(configOptions: ConfigOptions, filePath: string, i
// Internal configuration options. These are derived from a combination
// of the command line and from a JSON-based config file.
export class ConfigOptions {
constructor(projectRoot: string, typeCheckingMode?: string) {
this.projectRoot = projectRoot;
this.typeCheckingMode = typeCheckingMode;
this.diagnosticRuleSet = ConfigOptions.getDiagnosticRuleSet(typeCheckingMode);
this.functionSignatureDisplay = SignatureDisplayType.formatted;
}
// Absolute directory of project. All relative paths in the config
// are based on this path.
projectRoot: string;
@ -805,6 +798,13 @@ export class ConfigOptions {
// Controls how hover and completion function signatures are displayed.
functionSignatureDisplay: SignatureDisplayType;
constructor(projectRoot: string, typeCheckingMode?: string) {
this.projectRoot = projectRoot;
this.typeCheckingMode = typeCheckingMode;
this.diagnosticRuleSet = ConfigOptions.getDiagnosticRuleSet(typeCheckingMode);
this.functionSignatureDisplay = SignatureDisplayType.formatted;
}
static getDiagnosticRuleSet(typeCheckingMode?: string): DiagnosticRuleSet {
if (typeCheckingMode === 'strict') {
return getStrictDiagnosticRuleSet();

View File

@ -29,18 +29,6 @@ class DeferredImpl<T> implements Deferred<T> {
});
}
public resolve(_value?: T | PromiseLike<T>) {
// eslint-disable-next-line prefer-rest-params
this._resolve.apply(this._scope ? this._scope : this, arguments as any);
this._resolved = true;
}
public reject(_reason?: any) {
// eslint-disable-next-line prefer-rest-params
this._reject.apply(this._scope ? this._scope : this, arguments as any);
this._rejected = true;
}
get promise(): Promise<T> {
return this._promise;
}
@ -56,6 +44,18 @@ class DeferredImpl<T> implements Deferred<T> {
get completed(): boolean {
return this._rejected || this._resolved;
}
resolve(_value?: T | PromiseLike<T>) {
// eslint-disable-next-line prefer-rest-params
this._resolve.apply(this._scope ? this._scope : this, arguments as any);
this._resolved = true;
}
reject(_reason?: any) {
// eslint-disable-next-line prefer-rest-params
this._reject.apply(this._scope ? this._scope : this, arguments as any);
this._rejected = true;
}
}
export function createDeferred<T>(scope: any = null): Deferred<T> {

View File

@ -32,6 +32,12 @@ class OwningFileToken extends FileBasedToken {
super(cancellationFilePath, fs);
}
override get isCancellationRequested(): boolean {
// Since this object owns the file and it gets created when the
// token is cancelled, there's no point in checking the pipe.
return this.isCancelled;
}
override cancel() {
if (!this._disposed && !this.isCancelled) {
this._createPipe();
@ -39,12 +45,6 @@ class OwningFileToken extends FileBasedToken {
}
}
override get isCancellationRequested(): boolean {
// Since this object owns the file and it gets created when the
// token is cancelled, there's no point in checking the pipe.
return this.isCancelled;
}
override dispose(): void {
this._disposed = true;

View File

@ -60,6 +60,14 @@ export class LimitedAccessHost extends NoAccessHost {
}
export class FullAccessHost extends LimitedAccessHost {
constructor(protected _fs: FileSystem) {
super();
}
override get kind(): HostKind {
return HostKind.FullAccess;
}
static createHost(kind: HostKind, fs: FileSystem) {
switch (kind) {
case HostKind.NoAccess:
@ -73,14 +81,6 @@ export class FullAccessHost extends LimitedAccessHost {
}
}
constructor(protected _fs: FileSystem) {
super();
}
override get kind(): HostKind {
return HostKind.FullAccess;
}
override getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult {
const importFailureInfo = logInfo ?? [];
let result = this._executePythonInterpreter(pythonPath, (p) =>

View File

@ -116,13 +116,6 @@ class EggZipOpenFS extends ZipOpenFS {
private override isZip!: Set<PortablePath>;
private override notZip!: Set<PortablePath>;
// Hack to provide typed access to this private method.
private override getZipSync<T>(p: PortablePath, accept: (zipFs: ZipFS) => T): T {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
return super.getZipSync(p, accept);
}
override findZip(p: PortablePath) {
if (this.filter && !this.filter.test(p)) return null;
@ -173,6 +166,13 @@ class EggZipOpenFS extends ZipOpenFS {
};
}
}
// Hack to provide typed access to this private method.
private override getZipSync<T>(p: PortablePath, accept: (zipFs: ZipFS) => T): T {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-expect-error
return super.getZipSync(p, accept);
}
}
/* eslint-enable @typescript-eslint/naming-convention */

View File

@ -168,17 +168,17 @@ export interface WindowInterface {
}
export interface LanguageServerInterface {
readonly rootPath: string;
readonly console: ConsoleInterface;
readonly window: WindowInterface;
readonly supportAdvancedEdits: boolean;
getWorkspaceForFile(filePath: string): Promise<Workspace>;
getSettings(workspace: Workspace): Promise<ServerSettings>;
createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined;
reanalyze(): void;
restart(): void;
decodeTextDocumentUri(uriString: string): string;
readonly rootPath: string;
readonly console: ConsoleInterface;
readonly window: WindowInterface;
readonly supportAdvancedEdits: boolean;
}
export interface ServerOptions {
@ -394,99 +394,6 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
Extensions.createLanguageServiceExtensions(this);
}
// Convert uri to path
decodeTextDocumentUri(uriString: string): string {
return this._uriParser.decodeTextDocumentUri(uriString);
}
abstract createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined;
protected abstract executeCommand(params: ExecuteCommandParams, token: CancellationToken): Promise<any>;
protected abstract isLongRunningCommand(command: string): boolean;
protected abstract executeCodeAction(
params: CodeActionParams,
token: CancellationToken
): Promise<(Command | CodeAction)[] | undefined | null>;
abstract getSettings(workspace: Workspace): Promise<ServerSettings>;
protected isPythonPathImmutable(filePath: string): boolean {
// This function is called to determine if the file is using
// a special pythonPath separate from a workspace or not.
// The default is no.
return false;
}
protected async getConfiguration(scopeUri: string | undefined, section: string) {
if (this.client.hasConfigurationCapability) {
const item: ConfigurationItem = {};
if (scopeUri !== undefined) {
item.scopeUri = scopeUri;
}
if (section !== undefined) {
item.section = section;
}
return this._connection.workspace.getConfiguration(item);
}
if (this._defaultClientConfig) {
return getNestedProperty(this._defaultClientConfig, section);
}
return undefined;
}
protected isOpenFilesOnly(diagnosticMode: string): boolean {
return diagnosticMode !== 'workspace';
}
protected get allowModuleRename() {
return false;
}
protected getSeverityOverrides(value: string): DiagnosticSeverityOverrides | undefined {
const enumValue = value as DiagnosticSeverityOverrides;
if (getDiagnosticSeverityOverrides().includes(enumValue)) {
return enumValue;
}
return undefined;
}
protected getDiagnosticRuleName(value: string): DiagnosticRule | undefined {
const enumValue = value as DiagnosticRule;
if (getDiagLevelDiagnosticRules().includes(enumValue)) {
return enumValue;
}
return undefined;
}
protected abstract createHost(): Host;
protected abstract createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver;
protected createBackgroundAnalysisProgram(
serviceId: string,
console: ConsoleInterface,
configOptions: ConfigOptions,
importResolver: ImportResolver,
backgroundAnalysis?: BackgroundAnalysisBase,
maxAnalysisTime?: MaxAnalysisTime,
cacheManager?: CacheManager
): BackgroundAnalysisProgram {
return new BackgroundAnalysisProgram(
console,
configOptions,
importResolver,
backgroundAnalysis,
maxAnalysisTime,
/* disableChecker */ undefined,
cacheManager
);
}
// Provides access to the client's window.
get window(): RemoteWindow {
return this._connection.window;
@ -496,6 +403,15 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
return this.client.hasDocumentChangeCapability && this.client.hasDocumentAnnotationCapability;
}
// Convert uri to path
decodeTextDocumentUri(uriString: string): string {
return this._uriParser.decodeTextDocumentUri(uriString);
}
abstract createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined;
abstract getSettings(workspace: Workspace): Promise<ServerSettings>;
// Creates a service instance that's used for analyzing a
// program within a workspace.
createAnalyzerService(
@ -552,6 +468,142 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
});
}
updateSettingsForAllWorkspaces(): void {
const tasks: Promise<void>[] = [];
this._workspaceFactory.items().forEach((workspace) => {
// Updating settings can change workspace's file ownership. Make workspace uninitialized so that
// features can wait until workspace gets new settings.
// the file's ownership can also changed by `pyrightconfig.json` changes, but those are synchronous
// operation, so it won't affect this.
workspace.isInitialized = workspace.isInitialized.reset();
tasks.push(this.updateSettingsForWorkspace(workspace, workspace.isInitialized));
});
Promise.all(tasks).then(() => {
this._setupFileWatcher();
});
}
async updateSettingsForWorkspace(
workspace: Workspace,
status: InitStatus | undefined,
serverSettings?: ServerSettings
): Promise<void> {
status?.markCalled();
serverSettings = serverSettings ?? (await this.getSettings(workspace));
// Set logging level first.
(this.console as ConsoleWithLogLevel).level = serverSettings.logLevel ?? LogLevel.Info;
// Apply the new path to the workspace (before restarting the service).
serverSettings.pythonPath = this._workspaceFactory.applyPythonPath(workspace, serverSettings.pythonPath);
// Then use the updated settings to restart the service.
this.updateOptionsAndRestartService(workspace, serverSettings);
workspace.disableLanguageServices = !!serverSettings.disableLanguageServices;
workspace.disableOrganizeImports = !!serverSettings.disableOrganizeImports;
// Don't use workspace.isInitialized directly since it might have been
// reset due to pending config change event.
// The workspace is now open for business.
status?.resolve();
}
updateOptionsAndRestartService(
workspace: Workspace,
serverSettings: ServerSettings,
typeStubTargetImportName?: string
) {
AnalyzerServiceExecutor.runWithOptions(this.rootPath, workspace, serverSettings, typeStubTargetImportName);
workspace.searchPathsToWatch = workspace.service.librarySearchPathsToWatch ?? [];
}
protected get allowModuleRename() {
return false;
}
protected abstract executeCommand(params: ExecuteCommandParams, token: CancellationToken): Promise<any>;
protected abstract isLongRunningCommand(command: string): boolean;
protected abstract executeCodeAction(
params: CodeActionParams,
token: CancellationToken
): Promise<(Command | CodeAction)[] | undefined | null>;
protected isPythonPathImmutable(filePath: string): boolean {
// This function is called to determine if the file is using
// a special pythonPath separate from a workspace or not.
// The default is no.
return false;
}
protected async getConfiguration(scopeUri: string | undefined, section: string) {
if (this.client.hasConfigurationCapability) {
const item: ConfigurationItem = {};
if (scopeUri !== undefined) {
item.scopeUri = scopeUri;
}
if (section !== undefined) {
item.section = section;
}
return this._connection.workspace.getConfiguration(item);
}
if (this._defaultClientConfig) {
return getNestedProperty(this._defaultClientConfig, section);
}
return undefined;
}
protected isOpenFilesOnly(diagnosticMode: string): boolean {
return diagnosticMode !== 'workspace';
}
protected getSeverityOverrides(value: string): DiagnosticSeverityOverrides | undefined {
const enumValue = value as DiagnosticSeverityOverrides;
if (getDiagnosticSeverityOverrides().includes(enumValue)) {
return enumValue;
}
return undefined;
}
protected getDiagnosticRuleName(value: string): DiagnosticRule | undefined {
const enumValue = value as DiagnosticRule;
if (getDiagLevelDiagnosticRules().includes(enumValue)) {
return enumValue;
}
return undefined;
}
protected abstract createHost(): Host;
protected abstract createImportResolver(fs: FileSystem, options: ConfigOptions, host: Host): ImportResolver;
protected createBackgroundAnalysisProgram(
serviceId: string,
console: ConsoleInterface,
configOptions: ConfigOptions,
importResolver: ImportResolver,
backgroundAnalysis?: BackgroundAnalysisBase,
maxAnalysisTime?: MaxAnalysisTime,
cacheManager?: CacheManager
): BackgroundAnalysisProgram {
return new BackgroundAnalysisProgram(
console,
configOptions,
importResolver,
backgroundAnalysis,
maxAnalysisTime,
/* disableChecker */ undefined,
cacheManager
);
}
protected setupConnection(supportedCommands: string[], supportedCodeActions: string[]): void {
// After the server has started the client sends an initialize request. The server receives
// in the passed params the rootPath of the workspace plus the client capabilities.
@ -730,49 +782,6 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
this._setupFileWatcher();
}
private _setupFileWatcher() {
if (!this.client.hasWatchFileCapability) {
return;
}
const watchKind = WatchKind.Create | WatchKind.Change | WatchKind.Delete;
// Set default (config files and all workspace files) first.
const watchers: FileSystemWatcher[] = [
...configFileNames.map((fileName) => ({ globPattern: `**/${fileName}`, kind: watchKind })),
{ globPattern: '**', kind: watchKind },
];
// Add all python search paths to watch list
if (this.client.hasWatchFileRelativePathCapability) {
// Dedup search paths from all workspaces.
// Get rid of any search path under workspace root since it is already watched by
// "**" above.
const foldersToWatch = deduplicateFolders(
this._workspaceFactory
.getNonDefaultWorkspaces()
.map((w) => w.searchPathsToWatch.filter((p) => !p.startsWith(w.rootPath)))
);
foldersToWatch.forEach((p) => {
const globPattern = isFile(this._serviceFS, p, /* treatZipDirectoryAsFile */ true)
? { baseUri: convertPathToUri(this._serviceFS, getDirectoryPath(p)), pattern: getFileName(p) }
: { baseUri: convertPathToUri(this._serviceFS, p), pattern: '**' };
watchers.push({ globPattern, kind: watchKind });
});
}
// File watcher is pylance wide service. Dispose all existing file watchers and create new ones.
this._connection.client.register(DidChangeWatchedFilesNotification.type, { watchers }).then((d) => {
if (this._lastFileWatcherRegistration) {
this._lastFileWatcherRegistration.dispose();
}
this._lastFileWatcherRegistration = d;
});
}
protected onDidChangeConfiguration(params: DidChangeConfigurationParams) {
this.console.log(`Received updated settings`);
if (params?.settings) {
@ -1312,22 +1321,6 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
);
}
updateSettingsForAllWorkspaces(): void {
const tasks: Promise<void>[] = [];
this._workspaceFactory.items().forEach((workspace) => {
// Updating settings can change workspace's file ownership. Make workspace uninitialized so that
// features can wait until workspace gets new settings.
// the file's ownership can also changed by `pyrightconfig.json` changes, but those are synchronous
// operation, so it won't affect this.
workspace.isInitialized = workspace.isInitialized.reset();
tasks.push(this.updateSettingsForWorkspace(workspace, workspace.isInitialized));
});
Promise.all(tasks).then(() => {
this._setupFileWatcher();
});
}
protected getCompletionOptions(workspace: Workspace, params?: CompletionParams): CompletionOptions {
return {
format: this.client.completionDocFormat,
@ -1387,42 +1380,6 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
}
}
async updateSettingsForWorkspace(
workspace: Workspace,
status: InitStatus | undefined,
serverSettings?: ServerSettings
): Promise<void> {
status?.markCalled();
serverSettings = serverSettings ?? (await this.getSettings(workspace));
// Set logging level first.
(this.console as ConsoleWithLogLevel).level = serverSettings.logLevel ?? LogLevel.Info;
// Apply the new path to the workspace (before restarting the service).
serverSettings.pythonPath = this._workspaceFactory.applyPythonPath(workspace, serverSettings.pythonPath);
// Then use the updated settings to restart the service.
this.updateOptionsAndRestartService(workspace, serverSettings);
workspace.disableLanguageServices = !!serverSettings.disableLanguageServices;
workspace.disableOrganizeImports = !!serverSettings.disableOrganizeImports;
// Don't use workspace.isInitialized directly since it might have been
// reset due to pending config change event.
// The workspace is now open for business.
status?.resolve();
}
updateOptionsAndRestartService(
workspace: Workspace,
serverSettings: ServerSettings,
typeStubTargetImportName?: string
) {
AnalyzerServiceExecutor.runWithOptions(this.rootPath, workspace, serverSettings, typeStubTargetImportName);
workspace.searchPathsToWatch = workspace.service.librarySearchPathsToWatch ?? [];
}
protected onWorkspaceCreated(workspace: Workspace) {
// Update settings on this workspace (but only if initialize has happened)
if (this._initialized) {
@ -1456,6 +1413,69 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
return this.createAnalyzerService(name, services, libraryReanalysisTimeProvider);
}
protected recordUserInteractionTime() {
// Tell all of the services that the user is actively
// interacting with one or more editors, so they should
// back off from performing any work.
this._workspaceFactory.items().forEach((workspace: { service: { recordUserInteractionTime: () => void } }) => {
workspace.service.recordUserInteractionTime();
});
}
protected getDocumentationUrlForDiagnosticRule(rule: string): string | undefined {
// Configuration.md is configured to have a link for every rule name.
return `https://github.com/microsoft/pyright/blob/main/docs/configuration.md#${rule}`;
}
protected abstract createProgressReporter(): ProgressReporter;
protected canNavigateToFile(path: string, fs: FileSystem): boolean {
return canNavigateToFile(fs, path);
}
private _setupFileWatcher() {
if (!this.client.hasWatchFileCapability) {
return;
}
const watchKind = WatchKind.Create | WatchKind.Change | WatchKind.Delete;
// Set default (config files and all workspace files) first.
const watchers: FileSystemWatcher[] = [
...configFileNames.map((fileName) => ({ globPattern: `**/${fileName}`, kind: watchKind })),
{ globPattern: '**', kind: watchKind },
];
// Add all python search paths to watch list
if (this.client.hasWatchFileRelativePathCapability) {
// Dedup search paths from all workspaces.
// Get rid of any search path under workspace root since it is already watched by
// "**" above.
const foldersToWatch = deduplicateFolders(
this._workspaceFactory
.getNonDefaultWorkspaces()
.map((w) => w.searchPathsToWatch.filter((p) => !p.startsWith(w.rootPath)))
);
foldersToWatch.forEach((p) => {
const globPattern = isFile(this._serviceFS, p, /* treatZipDirectoryAsFile */ true)
? { baseUri: convertPathToUri(this._serviceFS, getDirectoryPath(p)), pattern: getFileName(p) }
: { baseUri: convertPathToUri(this._serviceFS, p), pattern: '**' };
watchers.push({ globPattern, kind: watchKind });
});
}
// File watcher is pylance wide service. Dispose all existing file watchers and create new ones.
this._connection.client.register(DidChangeWatchedFilesNotification.type, { watchers }).then((d) => {
if (this._lastFileWatcherRegistration) {
this._lastFileWatcherRegistration.dispose();
}
this._lastFileWatcherRegistration = d;
});
}
private _sendDiagnostics(params: PublishDiagnosticsParams[]) {
for (const param of params) {
this._connection.sendDiagnostics(param);
@ -1596,24 +1616,4 @@ export abstract class LanguageServerBase implements LanguageServerInterface {
return convertedDiags;
}
protected recordUserInteractionTime() {
// Tell all of the services that the user is actively
// interacting with one or more editors, so they should
// back off from performing any work.
this._workspaceFactory.items().forEach((workspace: { service: { recordUserInteractionTime: () => void } }) => {
workspace.service.recordUserInteractionTime();
});
}
protected getDocumentationUrlForDiagnosticRule(rule: string): string | undefined {
// Configuration.md is configured to have a link for every rule name.
return `https://github.com/microsoft/pyright/blob/main/docs/configuration.md#${rule}`;
}
protected abstract createProgressReporter(): ProgressReporter;
protected canNavigateToFile(path: string, fs: FileSystem): boolean {
return canNavigateToFile(fs, path);
}
}

View File

@ -229,6 +229,10 @@ export class CallHierarchyProvider {
return callItems;
}
private get _evaluator(): TypeEvaluator {
return this._program.evaluator!;
}
private _getTargetDeclaration(referencesResult: ReferencesResult): {
targetDecl: Declaration;
callItemUri: string;
@ -271,10 +275,6 @@ export class CallHierarchyProvider {
return { targetDecl, callItemUri, symbolName };
}
private get _evaluator(): TypeEvaluator {
return this._program.evaluator!;
}
private _getIncomingCallsForDeclaration(
parseResults: ParseResults,
filePath: string,

View File

@ -24,8 +24,8 @@ import {
createSynthesizedAliasDeclaration,
getDeclarationsWithUsesLocalNameRemoved,
} from '../analyzer/declarationUtils';
import { getModuleNode, getStringNodeValueRange } from '../analyzer/parseTreeUtils';
import * as ParseTreeUtils from '../analyzer/parseTreeUtils';
import { getModuleNode, getStringNodeValueRange } from '../analyzer/parseTreeUtils';
import { ParseTreeWalker } from '../analyzer/parseTreeWalker';
import { ScopeType } from '../analyzer/scope';
import * as ScopeUtils from '../analyzer/scopeUtils';
@ -63,6 +63,45 @@ export enum DocumentSymbolCollectorUseCase {
// This walker looks for symbols that are semantically equivalent
// to the requested symbol.
export class DocumentSymbolCollector extends ParseTreeWalker {
private _results: CollectionResult[] = [];
private _dunderAllNameNodes = new Set<StringNode>();
private _initFunction: FunctionNode | undefined;
private _symbolNames: Set<string> = new Set<string>();
constructor(
symbolNames: string[],
private _declarations: Declaration[],
private _evaluator: TypeEvaluator,
private _cancellationToken: CancellationToken,
private _startingNode: ParseNode,
private _treatModuleInImportAndFromImportSame = false,
private _skipUnreachableCode = true,
private _useCase = DocumentSymbolCollectorUseCase.Reference
) {
super();
// Start with the symbols passed in
symbolNames.forEach((s) => this._symbolNames.add(s));
// Don't report strings in __all__ right away, that will
// break the assumption on the result ordering.
this._setDunderAllNodes(this._startingNode);
// Check if one of our symbols is __init__ and we
// have a class declaration in the list and we are
// computing symbols for references and not rename.
const initDeclaration = _declarations.find(
(d) => d.type === DeclarationType.Function && d.node.name.value === '__init__'
);
if (initDeclaration && _useCase === DocumentSymbolCollectorUseCase.Reference) {
const classDeclaration = _declarations.find((d) => d.type === DeclarationType.Class);
if (classDeclaration) {
this._initFunction = initDeclaration.node as FunctionNode;
this._symbolNames.add((classDeclaration.node as ClassNode).name.value);
}
}
}
static collectFromNode(
node: NameNode,
evaluator: TypeEvaluator,
@ -161,45 +200,6 @@ export class DocumentSymbolCollector extends ParseTreeWalker {
return resolvedDeclarations;
}
private _results: CollectionResult[] = [];
private _dunderAllNameNodes = new Set<StringNode>();
private _initFunction: FunctionNode | undefined;
private _symbolNames: Set<string> = new Set<string>();
constructor(
symbolNames: string[],
private _declarations: Declaration[],
private _evaluator: TypeEvaluator,
private _cancellationToken: CancellationToken,
private _startingNode: ParseNode,
private _treatModuleInImportAndFromImportSame = false,
private _skipUnreachableCode = true,
private _useCase = DocumentSymbolCollectorUseCase.Reference
) {
super();
// Start with the symbols passed in
symbolNames.forEach((s) => this._symbolNames.add(s));
// Don't report strings in __all__ right away, that will
// break the assumption on the result ordering.
this._setDunderAllNodes(this._startingNode);
// Check if one of our symbols is __init__ and we
// have a class declaration in the list and we are
// computing symbols for references and not rename.
const initDeclaration = _declarations.find(
(d) => d.type === DeclarationType.Function && d.node.name.value === '__init__'
);
if (initDeclaration && _useCase === DocumentSymbolCollectorUseCase.Reference) {
const classDeclaration = _declarations.find((d) => d.type === DeclarationType.Class);
if (classDeclaration) {
this._initFunction = initDeclaration.node as FunctionNode;
this._symbolNames.add((classDeclaration.node as ClassNode).name.value);
}
}
}
collect() {
this.walk(this._startingNode);
return this._results;
@ -283,7 +283,8 @@ export class DocumentSymbolCollector extends ParseTreeWalker {
)
);
if (match) {
// Special case for __init__ being one of our symbol names and we have a classname as the other.
// Special case for __init__ being one of our symbol names and we
// have a class name as the other.
if (this._initFunction) {
// If this is a method, must be an __init__ reference.
if (match.type === DeclarationType.Function) {

View File

@ -18,15 +18,15 @@ import {
isVariableDeclaration,
} from '../analyzer/declaration';
import { createSynthesizedAliasDeclaration, getNameFromDeclaration } from '../analyzer/declarationUtils';
import { createImportedModuleDescriptor, ImportResolver, ModuleNameAndType } from '../analyzer/importResolver';
import { ImportResolver, ModuleNameAndType, createImportedModuleDescriptor } from '../analyzer/importResolver';
import {
ImportStatement,
ImportStatements,
getDirectoryLeadingDotsPointsTo,
getImportGroupFromModuleNameAndType,
getRelativeModuleName,
getTopLevelImports,
haveSameParentModule,
ImportStatement,
ImportStatements,
} from '../analyzer/importStatementUtils';
import {
getDottedNameWithGivenNodeAsLastName,
@ -69,13 +69,13 @@ import {
ImportAsNode,
ImportFromAsNode,
ImportFromNode,
isExpressionNode,
MemberAccessNode,
ModuleNameNode,
ModuleNode,
NameNode,
ParseNode,
ParseNodeType,
isExpressionNode,
} from '../parser/parseNodes';
import { ParseResults } from '../parser/parser';
import { CollectionResult, DocumentSymbolCollector, DocumentSymbolCollectorUseCase } from './documentSymbolCollector';
@ -87,6 +87,42 @@ enum UpdateType {
}
export class RenameModuleProvider {
private readonly _newModuleFilePath: string;
private readonly _moduleNames: string[];
private readonly _newModuleNames: string[];
private readonly _onlyNameChanged: boolean;
private readonly _aliasIntroduced = new Set<ImportAsNode>();
private readonly _textEditTracker = new TextEditTracker();
private constructor(
private _fs: FileSystem,
private _evaluator: TypeEvaluator,
private _moduleFilePath: string,
newModuleFilePath: string,
private _moduleNameAndType: ModuleNameAndType,
private _newModuleNameAndType: ModuleNameAndType,
private _type: UpdateType,
public declarations: Declaration[],
private _token: CancellationToken
) {
// moduleName and newModuleName are always in the absolute path form.
this._newModuleFilePath = resolvePaths(newModuleFilePath);
this._moduleNames = this._moduleName.split('.');
this._newModuleNames = this._newModuleName.split('.');
this._onlyNameChanged = haveSameParentModule(this._moduleNames, this._newModuleNames);
assert(this._type !== UpdateType.Folder || this._onlyNameChanged, 'We only support simple rename for folder');
}
get lastModuleName() {
return this._moduleNames[this._moduleNames.length - 1];
}
get textEditTracker(): TextEditTracker {
return this._textEditTracker;
}
static createForModule(
importResolver: ImportResolver,
configOptions: ConfigOptions,
@ -302,6 +338,65 @@ export class RenameModuleProvider {
}
}
getEdits(): FileEditAction[] {
return this._textEditTracker.getEdits(this._token);
}
renameReferences(parseResults: ParseResults) {
switch (this._type) {
case UpdateType.Folder:
return this._renameFolderReferences(parseResults);
case UpdateType.File:
return this._renameModuleReferences(parseResults);
case UpdateType.Symbol:
return this._updateSymbolReferences(parseResults);
default:
return assertNever(this._type, `${this._type} is unknown`);
}
}
tryGetFirstSymbolUsage(parseResults: ParseResults, symbol?: { name: string; decls: Declaration[] }) {
const name = symbol?.name ?? getNameFromDeclaration(this.declarations[0]) ?? '';
const collector = new DocumentSymbolCollector(
[name],
symbol?.decls ?? this.declarations,
this._evaluator!,
this._token,
parseResults.parseTree,
/* treatModuleImportAndFromImportSame */ true,
/* skipUnreachableCode */ false
);
for (const result of collector.collect().sort((r1, r2) => r1.range.start - r2.range.start)) {
// We only care about symbol usages, not alias decl of the symbol.
if (
isImportModuleName(result.node) ||
isImportAlias(result.node) ||
isFromImportModuleName(result.node) ||
isFromImportName(result.node) ||
isFromImportAlias(result.node)
) {
continue;
}
return result.range.start;
}
return undefined;
}
private get _moduleName() {
return this._moduleNameAndType.moduleName;
}
private get _newLastModuleName() {
return this._newModuleNames[this._newModuleNames.length - 1];
}
private get _newModuleName() {
return this._newModuleNameAndType.moduleName;
}
private static _create(
importResolver: ImportResolver,
configOptions: ConfigOptions,
@ -356,89 +451,6 @@ export class RenameModuleProvider {
);
}
private readonly _newModuleFilePath: string;
private readonly _moduleNames: string[];
private readonly _newModuleNames: string[];
private readonly _onlyNameChanged: boolean;
private readonly _aliasIntroduced = new Set<ImportAsNode>();
private readonly _textEditTracker = new TextEditTracker();
private constructor(
private _fs: FileSystem,
private _evaluator: TypeEvaluator,
private _moduleFilePath: string,
newModuleFilePath: string,
private _moduleNameAndType: ModuleNameAndType,
private _newModuleNameAndType: ModuleNameAndType,
private _type: UpdateType,
public declarations: Declaration[],
private _token: CancellationToken
) {
// moduleName and newModuleName are always in the absolute path form.
this._newModuleFilePath = resolvePaths(newModuleFilePath);
this._moduleNames = this._moduleName.split('.');
this._newModuleNames = this._newModuleName.split('.');
this._onlyNameChanged = haveSameParentModule(this._moduleNames, this._newModuleNames);
assert(this._type !== UpdateType.Folder || this._onlyNameChanged, 'We only support simple rename for folder');
}
get lastModuleName() {
return this._moduleNames[this._moduleNames.length - 1];
}
get textEditTracker(): TextEditTracker {
return this._textEditTracker;
}
getEdits(): FileEditAction[] {
return this._textEditTracker.getEdits(this._token);
}
renameReferences(parseResults: ParseResults) {
switch (this._type) {
case UpdateType.Folder:
return this._renameFolderReferences(parseResults);
case UpdateType.File:
return this._renameModuleReferences(parseResults);
case UpdateType.Symbol:
return this._updateSymbolReferences(parseResults);
default:
return assertNever(this._type, `${this._type} is unknown`);
}
}
tryGetFirstSymbolUsage(parseResults: ParseResults, symbol?: { name: string; decls: Declaration[] }) {
const name = symbol?.name ?? getNameFromDeclaration(this.declarations[0]) ?? '';
const collector = new DocumentSymbolCollector(
[name],
symbol?.decls ?? this.declarations,
this._evaluator!,
this._token,
parseResults.parseTree,
/* treatModuleImportAndFromImportSame */ true,
/* skipUnreachableCode */ false
);
for (const result of collector.collect().sort((r1, r2) => r1.range.start - r2.range.start)) {
// We only care about symbol usages, not alias decl of the symbol.
if (
isImportModuleName(result.node) ||
isImportAlias(result.node) ||
isFromImportModuleName(result.node) ||
isFromImportName(result.node) ||
isFromImportAlias(result.node)
) {
continue;
}
return result.range.start;
}
return undefined;
}
private _updateSymbolReferences(parseResults: ParseResults) {
const filePath = getFileInfo(parseResults.parseTree).filePath;
const isSource = filePath === this._moduleFilePath;
@ -1366,18 +1378,6 @@ export class RenameModuleProvider {
// ex) x.y.z used in "from x.y.z import ..."
return moduleName;
}
private get _moduleName() {
return this._moduleNameAndType.moduleName;
}
private get _newLastModuleName() {
return this._newModuleNames[this._newModuleNames.length - 1];
}
private get _newModuleName() {
return this._newModuleNameAndType.moduleName;
}
}
class ModuleNameCollector extends ParseTreeWalker {

View File

@ -59,6 +59,10 @@ export class SignatureHelpProvider {
return this._convert(this._getSignatureHelp());
}
private get _evaluator(): TypeEvaluator {
return this._program.evaluator!;
}
private _getSignatureHelp(): SignatureHelpResults | undefined {
throwIfCancellationRequested(this._token);
if (!this._parseResults) {
@ -214,10 +218,6 @@ export class SignatureHelpProvider {
return { signatures, activeSignature, activeParameter };
}
private get _evaluator(): TypeEvaluator {
return this._program.evaluator!;
}
private _makeSignature(callNode: CallNode, signature: CallSignature): SignatureInfo {
const functionType = signature.type;
const stringParts = this._evaluator.printFunctionParts(functionType, PrintTypeFlags.ExpandTypedDictArgs);

View File

@ -27,10 +27,6 @@ export class CharacterStream {
this._isEndOfStream = text.length === 0;
}
getText(): string {
return this._text;
}
get position(): number {
return this._position;
}
@ -44,6 +40,22 @@ export class CharacterStream {
return this._currentChar;
}
get nextChar(): number {
return this.position + 1 < this._text.length ? this._text.charCodeAt(this.position + 1) : 0;
}
get prevChar(): number {
return this.position - 1 >= 0 ? this._text.charCodeAt(this.position - 1) : 0;
}
get length(): number {
return this._text.length;
}
getText(): string {
return this._text;
}
// We also expose a (non-property) method that is
// the equivalent of currentChar above. This allows
// us to work around assumptions in the TypeScript
@ -53,14 +65,6 @@ export class CharacterStream {
return this._currentChar;
}
get nextChar(): number {
return this.position + 1 < this._text.length ? this._text.charCodeAt(this.position + 1) : 0;
}
get prevChar(): number {
return this.position - 1 >= 0 ? this._text.charCodeAt(this.position - 1) : 0;
}
isEndOfStream(): boolean {
return this._isEndOfStream;
}
@ -126,10 +130,6 @@ export class CharacterStream {
return this._text.charCodeAt(index);
}
get length(): number {
return this._text.length;
}
private _checkBounds(): void {
if (this._position < 0) {
this._position = 0;

View File

@ -19,7 +19,7 @@ import { assert } from '../common/debug';
import { Diagnostic, DiagnosticAddendum } from '../common/diagnostic';
import { DiagnosticSink } from '../common/diagnosticSink';
import { convertOffsetsToRange, convertPositionToOffset } from '../common/positionUtils';
import { latestStablePythonVersion, PythonVersion } from '../common/pythonVersion';
import { PythonVersion, latestStablePythonVersion } from '../common/pythonVersion';
import { TextRange } from '../common/textRange';
import { TextRangeCollection } from '../common/textRangeCollection';
import { timingStats } from '../common/timing';
@ -50,12 +50,10 @@ import {
ErrorNode,
ExceptNode,
ExpressionNode,
extendRange,
FormatStringNode,
ForNode,
FormatStringNode,
FunctionAnnotationNode,
FunctionNode,
getNextNodeId,
GlobalNode,
IfNode,
ImportAsNode,
@ -117,6 +115,8 @@ import {
WithNode,
YieldFromNode,
YieldNode,
extendRange,
getNextNodeId,
} from './parseNodes';
import * as StringTokenUtils from './stringTokenUtils';
import { Tokenizer, TokenizerOutput } from './tokenizer';
@ -129,11 +129,11 @@ import {
NumberToken,
OperatorToken,
OperatorType,
softKeywords,
StringToken,
StringTokenFlags,
Token,
TokenType,
softKeywords,
} from './tokenizerTypes';
interface ListResult<T> {
@ -148,6 +148,13 @@ interface SubscriptListResult {
}
export class ParseOptions {
isStubFile: boolean;
pythonVersion: PythonVersion;
reportInvalidStringEscapeSequence: boolean;
skipFunctionAndClassBody: boolean;
ipythonMode: IPythonMode;
reportErrorsForParsedStringContents: boolean;
constructor() {
this.isStubFile = false;
this.pythonVersion = latestStablePythonVersion;
@ -156,13 +163,6 @@ export class ParseOptions {
this.ipythonMode = IPythonMode.None;
this.reportErrorsForParsedStringContents = false;
}
isStubFile: boolean;
pythonVersion: PythonVersion;
reportInvalidStringEscapeSequence: boolean;
skipFunctionAndClassBody: boolean;
ipythonMode: IPythonMode;
reportErrorsForParsedStringContents: boolean;
}
export interface ParseResults {

View File

@ -245,6 +245,10 @@ export class PyrightFileSystem
this._partialStubPackagePaths.clear();
}
protected override _isMovedEntry(path: string) {
return this._partialStubPackagePaths.has(path) || super._isMovedEntry(path);
}
private _getRelativePathPartialStubs(path: string) {
const paths: string[] = [];
@ -279,8 +283,4 @@ export class PyrightFileSystem
searchAllStubs(path);
return paths;
}
protected override _isMovedEntry(path: string) {
return this._partialStubPackagePaths.has(path) || super._isMovedEntry(path);
}
}

View File

@ -28,10 +28,10 @@ import { UriParser } from '../../../common/uriParser';
import { LanguageServerInterface, MessageAction, ServerSettings, WindowInterface } from '../../../languageServerBase';
import { CodeActionProvider } from '../../../languageService/codeActionProvider';
import {
createInitStatus,
WellKnownWorkspaceKinds,
Workspace,
WorkspacePythonPathKind,
createInitStatus,
} from '../../../workspaceFactory';
import { TestAccessHost } from '../testAccessHost';
import { HostSpecificFeatures } from './testState';
@ -76,6 +76,10 @@ export class TestFeatures implements HostSpecificFeatures {
}
export class TestLanguageService implements LanguageServerInterface {
readonly rootPath = path.sep;
readonly window = new TestWindow();
readonly supportAdvancedEdits = true;
private readonly _workspace: Workspace;
private readonly _defaultWorkspace: Workspace;
private readonly _uriParser: UriParser;
@ -143,10 +147,6 @@ export class TestLanguageService implements LanguageServerInterface {
restart(): void {
// Don't do anything
}
readonly rootPath = path.sep;
readonly window = new TestWindow();
readonly supportAdvancedEdits = true;
}
class TestWindow implements WindowInterface {

View File

@ -1463,55 +1463,6 @@ export class TestState {
this._cancellationToken.resetCancelled();
}
private _convertGlobalOptionsToConfigOptions(projectRoot: string, mountPaths?: Map<string, string>): ConfigOptions {
const configOptions = new ConfigOptions(projectRoot);
// add more global options as we need them
const newConfigOptions = this._applyTestConfigOptions(configOptions, mountPaths);
// default tests to run use compact signatures.
newConfigOptions.functionSignatureDisplay = SignatureDisplayType.compact;
return newConfigOptions;
}
private _applyTestConfigOptions(configOptions: ConfigOptions, mountPaths?: Map<string, string>) {
// Always enable "test mode".
configOptions.internalTestMode = true;
// Always analyze all files
configOptions.checkOnlyOpenFiles = false;
// make sure we set typing path
if (configOptions.stubPath === undefined) {
configOptions.stubPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings'));
}
configOptions.include.push(getFileSpec(this.fs, configOptions.projectRoot, '.'));
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, typeshedFolder));
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, distlibFolder));
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, libFolder));
if (mountPaths) {
for (const mountPath of mountPaths.keys()) {
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, mountPath));
}
}
if (configOptions.functionSignatureDisplay === undefined) {
configOptions.functionSignatureDisplay === SignatureDisplayType.compact;
}
return configOptions;
}
protected getFileContent(fileName: string): string {
const files = this.testData.files.filter(
(f) => comparePaths(f.fileName, fileName, this.testFS.ignoreCase) === Comparison.EqualTo
);
return files[0].content;
}
convertPositionToOffset(fileName: string, position: Position): number {
const lines = this._getTextRangeCollection(fileName);
return convertPositionToOffset(position, lines)!;
@ -1523,6 +1474,20 @@ export class TestState {
return convertOffsetToPosition(offset, lines);
}
analyze() {
while (this.program.analyze()) {
// Continue to call analyze until it completes. Since we're not
// specifying a timeout, it should complete the first time.
}
}
protected getFileContent(fileName: string): string {
const files = this.testData.files.filter(
(f) => comparePaths(f.fileName, fileName, this.testFS.ignoreCase) === Comparison.EqualTo
);
return files[0].content;
}
protected convertOffsetsToRange(fileName: string, startOffset: number, endOffset: number): PositionRange {
const lines = this._getTextRangeCollection(fileName);
@ -1532,58 +1497,10 @@ export class TestState {
};
}
private _getParseResult(fileName: string) {
const file = this.program.getBoundSourceFile(fileName)!;
return file.getParseResults()!;
}
private _getTextRangeCollection(fileName: string): TextRangeCollection<TextRange> {
if (fileName in this._files) {
return this._getParseResult(fileName).tokenizerOutput.lines;
}
// slow path
const fileContents = this.fs.readFileSync(fileName, 'utf8');
const tokenizer = new Tokenizer();
return tokenizer.tokenize(fileContents).lines;
}
protected raiseError(message: string): never {
throw new Error(this._messageAtLastKnownMarker(message));
}
private _messageAtLastKnownMarker(message: string) {
const locationDescription = this.lastKnownMarker
? this.lastKnownMarker
: this._getLineColStringAtPosition(this.currentCaretPosition);
return `At ${locationDescription}: ${message}`;
}
private _checkPostEditInvariants() {
// blank for now
}
private _editScriptAndUpdateMarkers(fileName: string, editStart: number, editEnd: number, newText: string) {
// this.languageServiceAdapterHost.editScript(fileName, editStart, editEnd, newText);
for (const marker of this.testData.markers) {
if (marker.fileName === fileName) {
marker.position = this._updatePosition(marker.position, editStart, editEnd, newText);
}
}
for (const range of this.testData.ranges) {
if (range.fileName === fileName) {
range.pos = this._updatePosition(range.pos, editStart, editEnd, newText);
range.end = this._updatePosition(range.end, editStart, editEnd, newText);
}
}
this.testData.rangesByText = undefined;
}
private _removeWhitespace(text: string): string {
return text.replace(/\s/g, '');
}
protected createMultiMap<T>(values?: T[], getKey?: (t: T) => string): MultiMap<T> {
const map = new Map<string, T[]>() as MultiMap<T>;
map.add = multiMapAdd;
@ -1626,6 +1543,114 @@ export class TestState {
return this.getFileContent(fileName).slice(pos, end);
}
protected verifyCompletionItem(expected: _.FourSlashCompletionItem, actual: CompletionItem) {
assert.strictEqual(actual.label, expected.label);
assert.strictEqual(actual.detail, expected.detail);
assert.strictEqual(actual.kind, expected.kind);
assert.strictEqual(actual.insertText, expected.insertionText);
this._verifyEdit(actual.textEdit as TextEdit, expected.textEdit);
this._verifyEdits(actual.additionalTextEdits, expected.additionalTextEdits);
if (expected.detailDescription !== undefined) {
assert.strictEqual(actual.labelDetails?.description, expected.detailDescription);
}
if (expected.commitCharacters !== undefined) {
expect(expected.commitCharacters.sort()).toEqual(actual.commitCharacters?.sort() ?? []);
}
}
private _convertGlobalOptionsToConfigOptions(projectRoot: string, mountPaths?: Map<string, string>): ConfigOptions {
const configOptions = new ConfigOptions(projectRoot);
// add more global options as we need them
const newConfigOptions = this._applyTestConfigOptions(configOptions, mountPaths);
// default tests to run use compact signatures.
newConfigOptions.functionSignatureDisplay = SignatureDisplayType.compact;
return newConfigOptions;
}
private _applyTestConfigOptions(configOptions: ConfigOptions, mountPaths?: Map<string, string>) {
// Always enable "test mode".
configOptions.internalTestMode = true;
// Always analyze all files
configOptions.checkOnlyOpenFiles = false;
// make sure we set typing path
if (configOptions.stubPath === undefined) {
configOptions.stubPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings'));
}
configOptions.include.push(getFileSpec(this.fs, configOptions.projectRoot, '.'));
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, typeshedFolder));
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, distlibFolder));
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, libFolder));
if (mountPaths) {
for (const mountPath of mountPaths.keys()) {
configOptions.exclude.push(getFileSpec(this.fs, configOptions.projectRoot, mountPath));
}
}
if (configOptions.functionSignatureDisplay === undefined) {
configOptions.functionSignatureDisplay === SignatureDisplayType.compact;
}
return configOptions;
}
private _getParseResult(fileName: string) {
const file = this.program.getBoundSourceFile(fileName)!;
return file.getParseResults()!;
}
private _getTextRangeCollection(fileName: string): TextRangeCollection<TextRange> {
if (fileName in this._files) {
return this._getParseResult(fileName).tokenizerOutput.lines;
}
// slow path
const fileContents = this.fs.readFileSync(fileName, 'utf8');
const tokenizer = new Tokenizer();
return tokenizer.tokenize(fileContents).lines;
}
private _messageAtLastKnownMarker(message: string) {
const locationDescription = this.lastKnownMarker
? this.lastKnownMarker
: this._getLineColStringAtPosition(this.currentCaretPosition);
return `At ${locationDescription}: ${message}`;
}
private _checkPostEditInvariants() {
// blank for now
}
private _editScriptAndUpdateMarkers(fileName: string, editStart: number, editEnd: number, newText: string) {
// this.languageServiceAdapterHost.editScript(fileName, editStart, editEnd, newText);
for (const marker of this.testData.markers) {
if (marker.fileName === fileName) {
marker.position = this._updatePosition(marker.position, editStart, editEnd, newText);
}
}
for (const range of this.testData.ranges) {
if (range.fileName === fileName) {
range.pos = this._updatePosition(range.pos, editStart, editEnd, newText);
range.end = this._updatePosition(range.end, editStart, editEnd, newText);
}
}
this.testData.rangesByText = undefined;
}
private _removeWhitespace(text: string): string {
return text.replace(/\s/g, '');
}
private _getOnlyRange() {
const ranges = this.getRanges();
if (ranges.length !== 1) {
@ -1778,13 +1803,6 @@ export class TestState {
return position <= editStart ? position : position < editEnd ? -1 : position + length - +(editEnd - editStart);
}
public analyze() {
while (this.program.analyze()) {
// Continue to call analyze until it completes. Since we're not
// specifying a timeout, it should complete the first time.
}
}
private _getDiagnosticsPerFile() {
const sourceFiles = this._files.map((f) => this.program.getSourceFile(f));
const results = sourceFiles.map((sourceFile, index) => {
@ -1927,24 +1945,6 @@ export class TestState {
this.raiseError(`doesn't contain expected result: ${stringify(extra)}, actual: ${stringify(left)}`);
}
}
protected verifyCompletionItem(expected: _.FourSlashCompletionItem, actual: CompletionItem) {
assert.strictEqual(actual.label, expected.label);
assert.strictEqual(actual.detail, expected.detail);
assert.strictEqual(actual.kind, expected.kind);
assert.strictEqual(actual.insertText, expected.insertionText);
this._verifyEdit(actual.textEdit as TextEdit, expected.textEdit);
this._verifyEdits(actual.additionalTextEdits, expected.additionalTextEdits);
if (expected.detailDescription !== undefined) {
assert.strictEqual(actual.labelDetails?.description, expected.detailDescription);
}
if (expected.commitCharacters !== undefined) {
expect(expected.commitCharacters.sort()).toEqual(actual.commitCharacters?.sort() ?? []);
}
}
}
export function parseAndGetTestState(

View File

@ -19,8 +19,8 @@ import {
} from '../../../common/fileSystem';
import * as pathUtil from '../../../common/pathUtils';
import { bufferFrom, createIOError } from '../utils';
import { closeIterator, getIterator, Metadata, nextResult, SortedMap } from './../utils';
import { validate, ValidationFlags } from './pathValidation';
import { Metadata, SortedMap, closeIterator, getIterator, nextResult } from './../utils';
import { ValidationFlags, validate } from './pathValidation';
export const MODULE_PATH = pathUtil.normalizeSlashes('/');
@ -111,6 +111,13 @@ export class TestFileSystem implements FileSystem {
return Object.isFrozen(this);
}
/**
* Gets the file system shadowed by this file system.
*/
get shadowRoot() {
return this._shadowRoot;
}
/**
* Makes the file system read-only.
*/
@ -119,13 +126,6 @@ export class TestFileSystem implements FileSystem {
return this;
}
/**
* Gets the file system shadowed by this file system.
*/
get shadowRoot() {
return this._shadowRoot;
}
/**
* Snapshots the current file system, effectively shadowing itself. This is useful for
* generating file system patches using `.diff()` from one snapshot to the next. Performs
@ -201,14 +201,6 @@ export class TestFileSystem implements FileSystem {
return this._filemeta(node);
}
private _filemeta(node: Inode): Metadata {
if (!node.meta) {
const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot);
node.meta = new Metadata(parentMeta);
}
return node.meta;
}
/**
* Get the pathname of the current working directory.
*
@ -371,40 +363,6 @@ export class TestFileSystem implements FileSystem {
return URI.file(path).toString();
}
private _scan(path: string, stats: Stats, axis: Axis, traversal: Traversal, noFollow: boolean, results: string[]) {
if (axis === 'ancestors-or-self' || axis === 'self' || axis === 'descendants-or-self') {
if (!traversal.accept || traversal.accept(path, stats)) {
results.push(path);
}
}
if (axis === 'ancestors-or-self' || axis === 'ancestors') {
const dirname = pathUtil.getDirectoryPath(path);
if (dirname !== path) {
try {
const stats = this._stat(this._walk(dirname, noFollow));
if (!traversal.traverse || traversal.traverse(dirname, stats)) {
this._scan(dirname, stats, 'ancestors-or-self', traversal, noFollow, results);
}
} catch {
/* ignored */
}
}
}
if (axis === 'descendants-or-self' || axis === 'descendants') {
if (stats.isDirectory() && (!traversal.traverse || traversal.traverse(path, stats))) {
for (const file of this.readdirSync(path)) {
try {
const childpath = pathUtil.combinePaths(path, file);
const stats = this._stat(this._walk(childpath, noFollow));
this._scan(childpath, stats, 'descendants-or-self', traversal, noFollow, results);
} catch {
/* ignored */
}
}
}
}
}
/**
* Mounts a physical or virtual file system at a location in this virtual file system.
*
@ -562,27 +520,6 @@ export class TestFileSystem implements FileSystem {
return this._stat(this._walk(this._resolve(path), /* noFollow */ true));
}
private _stat(entry: WalkResult) {
const node = entry.node;
if (!node) {
throw createIOError(`ENOENT`, entry.realpath);
}
return new Stats(
node.dev,
node.ino,
node.mode,
node.nlink,
/* rdev */ 0,
/* size */ isFile(node) ? this._getSize(node) : isSymlink(node) ? node.symlink.length : 0,
/* blksize */ 4096,
/* blocks */ 0,
node.atimeMs,
node.mtimeMs,
node.ctimeMs,
node.birthtimeMs
);
}
/**
* Read a directory. If `path` is a symbolic link, it is dereferenced.
*
@ -640,15 +577,6 @@ export class TestFileSystem implements FileSystem {
this._mkdir(this._walk(this._resolve(path), /* noFollow */ true));
}
private _mkdir({ parent, links, node: existingNode, basename }: WalkResult) {
if (existingNode) {
throw createIOError('EEXIST');
}
const time = this.time();
const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /* mode */ 0o777, time);
this._addLink(parent, links, basename, node, time);
}
/**
* Remove a directory.
*
@ -949,6 +877,78 @@ export class TestFileSystem implements FileSystem {
// Do Nothing
}
private _mkdir({ parent, links, node: existingNode, basename }: WalkResult) {
if (existingNode) {
throw createIOError('EEXIST');
}
const time = this.time();
const node = this._mknod(parent ? parent.dev : ++devCount, S_IFDIR, /* mode */ 0o777, time);
this._addLink(parent, links, basename, node, time);
}
private _filemeta(node: Inode): Metadata {
if (!node.meta) {
const parentMeta = node.shadowRoot && this._shadowRoot && this._shadowRoot._filemeta(node.shadowRoot);
node.meta = new Metadata(parentMeta);
}
return node.meta;
}
private _scan(path: string, stats: Stats, axis: Axis, traversal: Traversal, noFollow: boolean, results: string[]) {
if (axis === 'ancestors-or-self' || axis === 'self' || axis === 'descendants-or-self') {
if (!traversal.accept || traversal.accept(path, stats)) {
results.push(path);
}
}
if (axis === 'ancestors-or-self' || axis === 'ancestors') {
const dirname = pathUtil.getDirectoryPath(path);
if (dirname !== path) {
try {
const stats = this._stat(this._walk(dirname, noFollow));
if (!traversal.traverse || traversal.traverse(dirname, stats)) {
this._scan(dirname, stats, 'ancestors-or-self', traversal, noFollow, results);
}
} catch {
/* ignored */
}
}
}
if (axis === 'descendants-or-self' || axis === 'descendants') {
if (stats.isDirectory() && (!traversal.traverse || traversal.traverse(path, stats))) {
for (const file of this.readdirSync(path)) {
try {
const childpath = pathUtil.combinePaths(path, file);
const stats = this._stat(this._walk(childpath, noFollow));
this._scan(childpath, stats, 'descendants-or-self', traversal, noFollow, results);
} catch {
/* ignored */
}
}
}
}
}
private _stat(entry: WalkResult) {
const node = entry.node;
if (!node) {
throw createIOError(`ENOENT`, entry.realpath);
}
return new Stats(
node.dev,
node.ino,
node.mode,
node.nlink,
/* rdev */ 0,
/* size */ isFile(node) ? this._getSize(node) : isSymlink(node) ? node.symlink.length : 0,
/* blksize */ 4096,
/* blocks */ 0,
node.atimeMs,
node.mtimeMs,
node.ctimeMs,
node.birthtimeMs
);
}
private static _diffWorker(
container: FileSet,
changed: TestFileSystem,
@ -1026,7 +1026,7 @@ export class TestFileSystem implements FileSystem {
return false;
}
// no difference if the root links are empty and unshadowed
// no difference if the root links are empty and not shadowed
if (!changed._lazy.links && !changed._shadowRoot && !base._lazy.links && !base._shadowRoot) {
return false;
}