diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c32994c8..e25e20041 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,8 +8,8 @@ "editor.formatOnSave": true }, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true, - "source.organizeImports": true + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib", } diff --git a/package.json b/package.json index 13e4aaa02..21dabd5f3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "check:eslint": "eslint .", "fix:eslint": "eslint --fix .", "check:prettier": "prettier -c .", - "fix:prettier": "prettier --write ." + "fix:prettier": "prettier --write .", + "typecheck": "npx lerna exec --stream --no-bail --ignore=pyright -- tsc --noEmit" }, "devDependencies": { "@types/glob": "^7.2.0", diff --git a/packages/pyright-internal/package-lock.json b/packages/pyright-internal/package-lock.json index 6dedf2826..bb5a0226a 100644 --- a/packages/pyright-internal/package-lock.json +++ b/packages/pyright-internal/package-lock.json @@ -23,7 +23,7 @@ "vscode-languageserver": "8.1.0", "vscode-languageserver-textdocument": "1.0.10", "vscode-languageserver-types": "3.17.3", - "vscode-uri": "^3.0.7" + "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/command-line-args": "^5.2.0", @@ -3887,9 +3887,9 @@ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "node_modules/vscode-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", - "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" }, "node_modules/walker": { "version": "1.0.8", @@ -6943,9 +6943,9 @@ "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" }, "vscode-uri": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.7.tgz", - "integrity": "sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==" + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==" }, "walker": { "version": "1.0.8", diff --git a/packages/pyright-internal/package.json b/packages/pyright-internal/package.json index e2919a544..ba0b66895 100644 --- a/packages/pyright-internal/package.json +++ b/packages/pyright-internal/package.json @@ -30,7 +30,7 @@ "vscode-languageserver": "8.1.0", "vscode-languageserver-textdocument": "1.0.10", "vscode-languageserver-types": "3.17.3", - "vscode-uri": "^3.0.7" + "vscode-uri": "^3.0.8" }, "devDependencies": { "@types/command-line-args": "^5.2.0", diff --git a/packages/pyright-internal/src/analyzer/analyzerFileInfo.ts b/packages/pyright-internal/src/analyzer/analyzerFileInfo.ts index 785871346..05967b75b 100644 --- a/packages/pyright-internal/src/analyzer/analyzerFileInfo.ts +++ b/packages/pyright-internal/src/analyzer/analyzerFileInfo.ts @@ -13,13 +13,14 @@ import { TextRangeDiagnosticSink } from '../common/diagnosticSink'; import { PythonVersion } from '../common/pythonVersion'; import { TextRange } from '../common/textRange'; import { TextRangeCollection } from '../common/textRangeCollection'; +import { Uri } from '../common/uri/uri'; import { Scope } from './scope'; import { IPythonMode } from './sourceFile'; import { SymbolTable } from './symbol'; // Maps import paths to the symbol table for the imported module. export interface AbsoluteModuleDescriptor { - importingFilePath: string; + importingFileUri: Uri; nameParts: string[]; } @@ -29,7 +30,7 @@ export interface LookupImportOptions { } export type ImportLookup = ( - filePathOrModule: string | AbsoluteModuleDescriptor, + fileUriOrModule: Uri | AbsoluteModuleDescriptor, options?: LookupImportOptions ) => ImportLookupResult | undefined; @@ -52,7 +53,7 @@ export interface AnalyzerFileInfo { lines: TextRangeCollection; typingSymbolAliases: Map; definedConstants: Map; - filePath: string; + fileUri: Uri; moduleName: string; isStubFile: boolean; isTypingStubFile: boolean; diff --git a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts index d40d71100..f9af56470 100644 --- a/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts +++ b/packages/pyright-internal/src/analyzer/backgroundAnalysisProgram.ts @@ -13,12 +13,13 @@ import { BackgroundAnalysisBase } from '../backgroundAnalysisBase'; import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { Diagnostic } from '../common/diagnostic'; import { FileDiagnostics } from '../common/diagnosticSink'; +import { ServiceProvider } from '../common/serviceProvider'; +import '../common/serviceProviderExtensions'; import { Range } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { AnalysisCompleteCallback, analyzeProgram } from './analysis'; import { ImportResolver } from './importResolver'; import { MaxAnalysisTime, OpenFileOptions, Program } from './program'; -import { ServiceProvider } from '../common/serviceProvider'; -import '../common/serviceProviderExtensions'; export enum InvalidatedReason { Reanalyzed, @@ -72,8 +73,8 @@ export class BackgroundAnalysisProgram { return this._backgroundAnalysis; } - hasSourceFile(filePath: string): boolean { - return !!this._program.getSourceFile(filePath); + hasSourceFile(fileUri: Uri): boolean { + return !!this._program.getSourceFile(fileUri); } setConfigOptions(configOptions: ConfigOptions) { @@ -90,9 +91,9 @@ export class BackgroundAnalysisProgram { this.configOptions.getExecutionEnvironments().forEach((e) => this._ensurePartialStubPackages(e)); } - setTrackedFiles(filePaths: string[]) { - this._backgroundAnalysis?.setTrackedFiles(filePaths); - const diagnostics = this._program.setTrackedFiles(filePaths); + setTrackedFiles(fileUris: Uri[]) { + this._backgroundAnalysis?.setTrackedFiles(fileUris); + const diagnostics = this._program.setTrackedFiles(fileUris); this._reportDiagnosticsForRemovedFiles(diagnostics); } @@ -101,35 +102,35 @@ export class BackgroundAnalysisProgram { this._program.setAllowedThirdPartyImports(importNames); } - setFileOpened(filePath: string, version: number | null, contents: string, options: OpenFileOptions) { - this._backgroundAnalysis?.setFileOpened(filePath, version, contents, options); - this._program.setFileOpened(filePath, version, contents, options); + setFileOpened(fileUri: Uri, version: number | null, contents: string, options: OpenFileOptions) { + this._backgroundAnalysis?.setFileOpened(fileUri, version, contents, options); + this._program.setFileOpened(fileUri, version, contents, options); } - getChainedFilePath(filePath: string): string | undefined { - return this._program.getChainedFilePath(filePath); + getChainedUri(fileUri: Uri): Uri | undefined { + return this._program.getChainedUri(fileUri); } - updateChainedFilePath(filePath: string, chainedFilePath: string | undefined) { - this._backgroundAnalysis?.updateChainedFilePath(filePath, chainedFilePath); - this._program.updateChainedFilePath(filePath, chainedFilePath); + updateChainedUri(fileUri: Uri, chainedUri: Uri | undefined) { + this._backgroundAnalysis?.updateChainedUri(fileUri, chainedUri); + this._program.updateChainedUri(fileUri, chainedUri); } - updateOpenFileContents(path: string, version: number | null, contents: string, options: OpenFileOptions) { - this._backgroundAnalysis?.setFileOpened(path, version, contents, options); - this._program.setFileOpened(path, version, contents, options); - this.markFilesDirty([path], /* evenIfContentsAreSame */ true); + updateOpenFileContents(uri: Uri, version: number | null, contents: string, options: OpenFileOptions) { + this._backgroundAnalysis?.setFileOpened(uri, version, contents, options); + this._program.setFileOpened(uri, version, contents, options); + this.markFilesDirty([uri], /* evenIfContentsAreSame */ true); } - setFileClosed(filePath: string, isTracked?: boolean) { - this._backgroundAnalysis?.setFileClosed(filePath, isTracked); - const diagnostics = this._program.setFileClosed(filePath, isTracked); + setFileClosed(fileUri: Uri, isTracked?: boolean) { + this._backgroundAnalysis?.setFileClosed(fileUri, isTracked); + const diagnostics = this._program.setFileClosed(fileUri, isTracked); this._reportDiagnosticsForRemovedFiles(diagnostics); } - addInterimFile(filePath: string) { - this._backgroundAnalysis?.addInterimFile(filePath); - this._program.addInterimFile(filePath); + addInterimFile(fileUri: Uri) { + this._backgroundAnalysis?.addInterimFile(fileUri); + this._program.addInterimFile(fileUri); } markAllFilesDirty(evenIfContentsAreSame: boolean) { @@ -137,9 +138,9 @@ export class BackgroundAnalysisProgram { this._program.markAllFilesDirty(evenIfContentsAreSame); } - markFilesDirty(filePaths: string[], evenIfContentsAreSame: boolean) { - this._backgroundAnalysis?.markFilesDirty(filePaths, evenIfContentsAreSame); - this._program.markFilesDirty(filePaths, evenIfContentsAreSame); + markFilesDirty(fileUris: Uri[], evenIfContentsAreSame: boolean) { + this._backgroundAnalysis?.markFilesDirty(fileUris, evenIfContentsAreSame); + this._program.markFilesDirty(fileUris, evenIfContentsAreSame); } setCompletionCallback(callback?: AnalysisCompleteCallback) { @@ -163,34 +164,34 @@ export class BackgroundAnalysisProgram { ); } - async analyzeFile(filePath: string, token: CancellationToken): Promise { + async analyzeFile(fileUri: Uri, token: CancellationToken): Promise { if (this._backgroundAnalysis) { - return this._backgroundAnalysis.analyzeFile(filePath, token); + return this._backgroundAnalysis.analyzeFile(fileUri, token); } - return this._program.analyzeFile(filePath, token); + return this._program.analyzeFile(fileUri, token); } libraryUpdated() { // empty } - async getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise { + async getDiagnosticsForRange(fileUri: Uri, range: Range, token: CancellationToken): Promise { if (this._backgroundAnalysis) { - return this._backgroundAnalysis.getDiagnosticsForRange(filePath, range, token); + return this._backgroundAnalysis.getDiagnosticsForRange(fileUri, range, token); } - return this._program.getDiagnosticsForRange(filePath, range); + return this._program.getDiagnosticsForRange(fileUri, range); } async writeTypeStub( - targetImportPath: string, + targetImportUri: Uri, targetIsSingleFile: boolean, - stubPath: string, + stubUri: Uri, token: CancellationToken ): Promise { if (this._backgroundAnalysis) { - return this._backgroundAnalysis.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token); + return this._backgroundAnalysis.writeTypeStub(targetImportUri, targetIsSingleFile, stubUri, token); } analyzeProgram( @@ -201,7 +202,7 @@ export class BackgroundAnalysisProgram { this._serviceProvider.console(), token ); - return this._program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token); + return this._program.writeTypeStub(targetImportUri, targetIsSingleFile, stubUri, token); } invalidateAndForceReanalysis(reason: InvalidatedReason) { @@ -245,7 +246,7 @@ export class BackgroundAnalysisProgram { } private _ensurePartialStubPackages(execEnv: ExecutionEnvironment) { - this._backgroundAnalysis?.ensurePartialStubPackages(execEnv.root); + this._backgroundAnalysis?.ensurePartialStubPackages(execEnv.root?.toString()); return this._importResolver.ensurePartialStubPackages(execEnv); } diff --git a/packages/pyright-internal/src/analyzer/binder.ts b/packages/pyright-internal/src/analyzer/binder.ts index 999e746f2..cb35a72bf 100644 --- a/packages/pyright-internal/src/analyzer/binder.ts +++ b/packages/pyright-internal/src/analyzer/binder.ts @@ -22,9 +22,10 @@ import { DiagnosticLevel } from '../common/configOptions'; import { assert, assertNever, fail } from '../common/debug'; import { CreateTypeStubFileAction, Diagnostic } from '../common/diagnostic'; import { DiagnosticRule } from '../common/diagnosticRules'; -import { getFileName, stripFileExtension } from '../common/pathUtils'; +import { stripFileExtension } from '../common/pathUtils'; import { convertTextRangeToRange } from '../common/positionUtils'; import { TextRange, getEmptyRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { Localizer } from '../localization/localize'; import { ArgumentCategory, @@ -407,7 +408,7 @@ export class Binder extends ParseTreeWalker { const classDeclaration: ClassDeclaration = { type: DeclarationType.Class, node, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(node.name, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -465,7 +466,7 @@ export class Binder extends ParseTreeWalker { node, isMethod: !!containingClassNode, isGenerator: false, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(node.name, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -539,7 +540,7 @@ export class Binder extends ParseTreeWalker { const paramDeclaration: ParameterDeclaration = { type: DeclarationType.Parameter, node: paramNode, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(paramNode, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -613,7 +614,7 @@ export class Binder extends ParseTreeWalker { const paramDeclaration: ParameterDeclaration = { type: DeclarationType.Parameter, node: paramNode, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(paramNode, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -765,7 +766,7 @@ export class Binder extends ParseTreeWalker { const paramDeclaration: TypeParameterDeclaration = { type: DeclarationType.TypeParameter, node: param, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(node, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -806,7 +807,7 @@ export class Binder extends ParseTreeWalker { const typeAliasDeclaration: TypeAliasDeclaration = { type: DeclarationType.TypeAlias, node, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(node.name, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -1414,7 +1415,7 @@ export class Binder extends ParseTreeWalker { node: node.name, isConstant: isConstantName(node.name.value), inferredTypeSource: node, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(node.name, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -1745,9 +1746,9 @@ export class Binder extends ParseTreeWalker { AnalyzerNodeInfo.setFlowNode(node, this._currentFlowNode!); - let resolvedPath = ''; + let resolvedPath = Uri.empty(); if (importInfo && importInfo.isImportFound && !importInfo.isNativeLib) { - resolvedPath = importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1]; + resolvedPath = importInfo.resolvedUris[importInfo.resolvedUris.length - 1]; } // If this file is a module __init__.py(i), relative imports of submodules @@ -1756,7 +1757,7 @@ export class Binder extends ParseTreeWalker { // symbols below) in case one of the imported symbols is the same name as the // submodule. In that case, we want to the symbol to appear later in the // declaration list because it should "win" when resolving the alias. - const fileName = stripFileExtension(getFileName(this._fileInfo.filePath)); + const fileName = stripFileExtension(this._fileInfo.fileUri.fileName); const isModuleInitFile = fileName === '__init__' && node.module.leadingDots === 1 && node.module.nameParts.length === 1; @@ -1814,7 +1815,7 @@ export class Binder extends ParseTreeWalker { const aliasDecl: AliasDeclaration = { type: DeclarationType.Alias, node, - path: resolvedPath, + uri: resolvedPath, loadSymbolsFromPath: true, range: getEmptyRange(), // Range is unknown for wildcard name import. usesLocalName: false, @@ -1834,7 +1835,7 @@ export class Binder extends ParseTreeWalker { const submoduleFallback: AliasDeclaration = { type: DeclarationType.Alias, node, - path: implicitImport.path, + uri: implicitImport.uri, loadSymbolsFromPath: true, range: getEmptyRange(), usesLocalName: false, @@ -1845,7 +1846,7 @@ export class Binder extends ParseTreeWalker { const aliasDecl: AliasDeclaration = { type: DeclarationType.Alias, node, - path: resolvedPath, + uri: resolvedPath, loadSymbolsFromPath: true, usesLocalName: false, symbolName: name, @@ -1926,7 +1927,7 @@ export class Binder extends ParseTreeWalker { submoduleFallback = { type: DeclarationType.Alias, node: importSymbolNode, - path: implicitImport.path, + uri: implicitImport.uri, loadSymbolsFromPath: true, range: getEmptyRange(), usesLocalName: false, @@ -1942,7 +1943,7 @@ export class Binder extends ParseTreeWalker { if (fileName === '__init__') { if (node.module.leadingDots === 1 && node.module.nameParts.length === 0) { loadSymbolsFromPath = false; - } else if (resolvedPath === this._fileInfo.filePath) { + } else if (resolvedPath.equals(this._fileInfo.fileUri)) { loadSymbolsFromPath = false; } } @@ -1951,7 +1952,7 @@ export class Binder extends ParseTreeWalker { const aliasDecl: AliasDeclaration = { type: DeclarationType.Alias, node: importSymbolNode, - path: resolvedPath, + uri: resolvedPath, loadSymbolsFromPath, usesLocalName: !!importSymbolNode.alias, symbolName: importedName, @@ -2304,7 +2305,7 @@ export class Binder extends ParseTreeWalker { node: node.target, isConstant: isConstantName(node.target.value), inferredTypeSource: node, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(node.target, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -2389,7 +2390,7 @@ export class Binder extends ParseTreeWalker { node: slotNameNode, isConstant: isConstantName(slotName), isDefinedBySlots: true, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(slotNameNode, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -2438,7 +2439,7 @@ export class Binder extends ParseTreeWalker { node: target, isConstant: isConstantName(target.value), inferredTypeSource: target.parent, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(target, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -2466,23 +2467,25 @@ export class Binder extends ParseTreeWalker { const aliasDecl = varSymbol.getDeclarations().find((decl) => decl.type === DeclarationType.Alias) as | AliasDeclaration | undefined; - const resolvedPath = - aliasDecl?.path && aliasDecl.loadSymbolsFromPath - ? aliasDecl.path - : aliasDecl?.submoduleFallback?.path && aliasDecl.submoduleFallback.loadSymbolsFromPath - ? aliasDecl.submoduleFallback.path + const resolvedUri = + aliasDecl?.uri && !aliasDecl.uri.isEmpty() && aliasDecl.loadSymbolsFromPath + ? aliasDecl.uri + : aliasDecl?.submoduleFallback?.uri && + !aliasDecl.submoduleFallback.uri.isEmpty() && + aliasDecl.submoduleFallback.loadSymbolsFromPath + ? aliasDecl.submoduleFallback.uri : undefined; - if (!resolvedPath) { + if (!resolvedUri) { return undefined; } - let lookupInfo = this._fileInfo.importLookup(resolvedPath); + let lookupInfo = this._fileInfo.importLookup(resolvedUri); if (lookupInfo?.dunderAllNames) { return lookupInfo.dunderAllNames; } - if (aliasDecl?.submoduleFallback?.path) { - lookupInfo = this._fileInfo.importLookup(aliasDecl.submoduleFallback.path); + if (aliasDecl?.submoduleFallback?.uri && !aliasDecl.submoduleFallback.uri.isEmpty()) { + lookupInfo = this._fileInfo.importLookup(aliasDecl.submoduleFallback.uri); return lookupInfo?.dunderAllNames; } @@ -2520,15 +2523,15 @@ export class Binder extends ParseTreeWalker { .getDeclarations() .find((decl) => decl.type === DeclarationType.Alias && decl.firstNamePart === firstNamePartValue); let newDecl: AliasDeclaration; - let pathOfLastSubmodule: string; - if (importInfo && importInfo.isImportFound && !importInfo.isNativeLib && importInfo.resolvedPaths.length > 0) { - pathOfLastSubmodule = importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1]; + let uriOfLastSubmodule: Uri; + if (importInfo && importInfo.isImportFound && !importInfo.isNativeLib && importInfo.resolvedUris.length > 0) { + uriOfLastSubmodule = importInfo.resolvedUris[importInfo.resolvedUris.length - 1]; } else { - pathOfLastSubmodule = UnresolvedModuleMarker; + uriOfLastSubmodule = UnresolvedModuleMarker; } const isResolved = - importInfo && importInfo.isImportFound && !importInfo.isNativeLib && importInfo.resolvedPaths.length > 0; + importInfo && importInfo.isImportFound && !importInfo.isNativeLib && importInfo.resolvedUris.length > 0; if (existingDecl) { newDecl = existingDecl as AliasDeclaration; @@ -2536,7 +2539,7 @@ export class Binder extends ParseTreeWalker { newDecl = { type: DeclarationType.Alias, node, - path: pathOfLastSubmodule, + uri: uriOfLastSubmodule, loadSymbolsFromPath: false, range: getEmptyRange(), usesLocalName: !!importAlias, @@ -2551,7 +2554,7 @@ export class Binder extends ParseTreeWalker { newDecl = { type: DeclarationType.Alias, node, - path: pathOfLastSubmodule, + uri: uriOfLastSubmodule, loadSymbolsFromPath: true, range: getEmptyRange(), usesLocalName: !!importAlias, @@ -2565,8 +2568,8 @@ export class Binder extends ParseTreeWalker { // See if there is import info for this part of the path. This allows us // to implicitly import all of the modules in a multi-part module name. const implicitImportInfo = AnalyzerNodeInfo.getImportInfo(node.module.nameParts[0]); - if (implicitImportInfo && implicitImportInfo.resolvedPaths.length) { - newDecl.path = implicitImportInfo.resolvedPaths[0]; + if (implicitImportInfo && implicitImportInfo.resolvedUris.length) { + newDecl.uri = implicitImportInfo.resolvedUris[0]; newDecl.loadSymbolsFromPath = true; this._addImplicitImportsToLoaderActions(implicitImportInfo, newDecl); } @@ -2574,7 +2577,7 @@ export class Binder extends ParseTreeWalker { // Add the implicit imports for this module if it's the last // name part we're resolving. if (importAlias || node.module.nameParts.length === 1) { - newDecl.path = pathOfLastSubmodule; + newDecl.uri = uriOfLastSubmodule; newDecl.loadSymbolsFromPath = true; newDecl.isUnresolved = false; @@ -2594,13 +2597,13 @@ export class Binder extends ParseTreeWalker { : undefined; if (!loaderActions) { const loaderActionPath = - importInfo && i < importInfo.resolvedPaths.length - ? importInfo.resolvedPaths[i] + importInfo && i < importInfo.resolvedUris.length + ? importInfo.resolvedUris[i] : UnresolvedModuleMarker; // Allocate a new loader action. loaderActions = { - path: loaderActionPath, + uri: loaderActionPath, loadSymbolsFromPath: false, implicitImports: new Map(), isUnresolved: !isResolved, @@ -2614,8 +2617,8 @@ export class Binder extends ParseTreeWalker { if (i === node.module.nameParts.length - 1) { // If this is the last name part we're resolving, add in the // implicit imports as well. - if (importInfo && i < importInfo.resolvedPaths.length) { - loaderActions.path = importInfo.resolvedPaths[i]; + if (importInfo && i < importInfo.resolvedUris.length) { + loaderActions.uri = importInfo.resolvedUris[i]; loaderActions.loadSymbolsFromPath = true; this._addImplicitImportsToLoaderActions(importInfo, loaderActions); } @@ -2625,8 +2628,8 @@ export class Binder extends ParseTreeWalker { // import all of the modules in a multi-part module name (e.g. "import a.b.c" // imports "a" and "a.b" and "a.b.c"). const implicitImportInfo = AnalyzerNodeInfo.getImportInfo(node.module.nameParts[i]); - if (implicitImportInfo && implicitImportInfo.resolvedPaths.length) { - loaderActions.path = implicitImportInfo.resolvedPaths[i]; + if (implicitImportInfo && implicitImportInfo.resolvedUris.length) { + loaderActions.uri = implicitImportInfo.resolvedUris[i]; loaderActions.loadSymbolsFromPath = true; this._addImplicitImportsToLoaderActions(implicitImportInfo, loaderActions); } @@ -3486,7 +3489,7 @@ export class Binder extends ParseTreeWalker { type: DeclarationType.Intrinsic, node, intrinsicType: type, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: getEmptyRange(), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -3561,7 +3564,7 @@ export class Binder extends ParseTreeWalker { inferredTypeSource: source, isInferenceAllowedInPyTyped: this._isInferenceAllowedInPyTyped(name.value), typeAliasName: isPossibleTypeAlias ? target : undefined, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(name, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -3609,7 +3612,7 @@ export class Binder extends ParseTreeWalker { isConstant: isConstantName(name.value), inferredTypeSource: source, isDefinedByMemberAccess: true, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(target.memberName, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, @@ -3701,7 +3704,7 @@ export class Binder extends ParseTreeWalker { isConstant: isConstantName(name.value), isFinal: finalInfo.isFinal, typeAliasName: target, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, typeAnnotationNode, range: convertTextRangeToRange(name, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, @@ -3774,7 +3777,7 @@ export class Binder extends ParseTreeWalker { isConstant: isConstantName(name.value), isDefinedByMemberAccess: true, isFinal: finalInfo.isFinal, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, typeAnnotationNode: finalInfo.isFinal && !finalInfo.finalTypeNode ? undefined : typeAnnotation, range: convertTextRangeToRange(target.memberName, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, @@ -4021,14 +4024,14 @@ export class Binder extends ParseTreeWalker { ? loaderActions.implicitImports.get(implicitImport.name) : undefined; if (existingLoaderAction) { - existingLoaderAction.path = implicitImport.path; + existingLoaderAction.uri = implicitImport.uri; existingLoaderAction.loadSymbolsFromPath = true; } else { if (!loaderActions.implicitImports) { loaderActions.implicitImports = new Map(); } loaderActions.implicitImports.set(implicitImport.name, { - path: implicitImport.path, + uri: implicitImport.uri, loadSymbolsFromPath: true, implicitImports: new Map(), }); @@ -4095,7 +4098,7 @@ export class Binder extends ParseTreeWalker { symbol.addDeclaration({ type: DeclarationType.SpecialBuiltInClass, node: annotationNode, - path: this._fileInfo.filePath, + uri: this._fileInfo.fileUri, range: convertTextRangeToRange(annotationNode, this._fileInfo.lines), moduleName: this._fileInfo.moduleName, isInExceptSuite: this._isInExceptSuite, diff --git a/packages/pyright-internal/src/analyzer/checker.ts b/packages/pyright-internal/src/analyzer/checker.ts index dc0992c23..86654d517 100644 --- a/packages/pyright-internal/src/analyzer/checker.ts +++ b/packages/pyright-internal/src/analyzer/checker.ts @@ -20,9 +20,9 @@ import { DiagnosticLevel } from '../common/configOptions'; import { assert, assertNever } from '../common/debug'; import { ActionKind, Diagnostic, DiagnosticAddendum, RenameShadowedFileAction } from '../common/diagnostic'; import { DiagnosticRule } from '../common/diagnosticRules'; -import { getFileExtension } from '../common/pathUtils'; import { PythonVersion, versionToString } from '../common/pythonVersion'; import { TextRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { DefinitionProvider } from '../languageService/definitionProvider'; import { Localizer } from '../localization/localize'; import { @@ -233,7 +233,9 @@ export class Checker extends ParseTreeWalker { const codeComplexity = AnalyzerNodeInfo.getCodeFlowComplexity(this._moduleNode); if (isPrintCodeComplexityEnabled) { - console.log(`Code complexity of module ${this._fileInfo.filePath} is ${codeComplexity.toString()}`); + console.log( + `Code complexity of module ${this._fileInfo.fileUri.toUserVisibleString()} is ${codeComplexity.toString()}` + ); } if (codeComplexity > maxCodeComplexity) { @@ -1566,11 +1568,12 @@ export class Checker extends ParseTreeWalker { } const resolvedAlias = this._evaluator.resolveAliasDeclaration(decl, /* resolveLocalNames */ true); - if (!resolvedAlias?.path || !isStubFile(resolvedAlias.path)) { + const resolvedAliasUri = resolvedAlias?.uri; + if (!resolvedAliasUri || !isStubFile(resolvedAliasUri)) { continue; } - const importResult = this._getImportResult(node, resolvedAlias.path); + const importResult = this._getImportResult(node, resolvedAliasUri); if (!importResult) { continue; } @@ -1667,22 +1670,22 @@ export class Checker extends ParseTreeWalker { return false; } - private _getImportResult(node: ImportFromAsNode, filePath: string) { - const execEnv = this._importResolver.getConfigOptions().findExecEnvironment(filePath); + private _getImportResult(node: ImportFromAsNode, uri: Uri) { + const execEnv = this._importResolver.getConfigOptions().findExecEnvironment(uri); const moduleNameNode = (node.parent as ImportFromNode).module; // Handle both absolute and relative imports. const moduleName = moduleNameNode.leadingDots === 0 - ? this._importResolver.getModuleNameForImport(filePath, execEnv).moduleName - : getRelativeModuleName(this._importResolver.fileSystem, this._fileInfo.filePath, filePath); + ? this._importResolver.getModuleNameForImport(uri, execEnv).moduleName + : getRelativeModuleName(this._importResolver.fileSystem, this._fileInfo.fileUri, uri); if (!moduleName) { return undefined; } return this._importResolver.resolveImport( - this._fileInfo.filePath, + this._fileInfo.fileUri, execEnv, createImportedModuleDescriptor(moduleName) ); @@ -3087,7 +3090,7 @@ export class Checker extends ParseTreeWalker { if (diagnostic && overload.details.declaration) { diagnostic.addRelatedInfo( Localizer.DiagnosticAddendum.overloadSignature(), - overload.details.declaration?.path ?? primaryDecl.path, + overload.details.declaration?.uri ?? primaryDecl.uri, overload.details.declaration?.range ?? primaryDecl.range ); } @@ -3280,7 +3283,7 @@ export class Checker extends ParseTreeWalker { } if (primaryDeclNode) { - diag.addRelatedInfo(primaryDeclInfo, primaryDecl.path, primaryDecl.range); + diag.addRelatedInfo(primaryDeclInfo, primaryDecl.uri, primaryDecl.range); } } }; @@ -4177,7 +4180,7 @@ export class Checker extends ParseTreeWalker { if ( stdlibPath && this._importResolver.isStdlibModule(desc, this._fileInfo.executionEnvironment) && - this._sourceMapper.isUserCode(this._fileInfo.filePath) + this._sourceMapper.isUserCode(this._fileInfo.fileUri) ) { // This means the user has a module that is overwriting the stdlib module. const diag = this._evaluator.addDiagnosticForTextRange( @@ -4186,7 +4189,7 @@ export class Checker extends ParseTreeWalker { DiagnosticRule.reportShadowedImports, Localizer.Diagnostic.stdlibModuleOverridden().format({ name: moduleName, - path: this._fileInfo.filePath, + path: this._fileInfo.fileUri.toUserVisibleString(), }), this._moduleNode ); @@ -4195,8 +4198,8 @@ export class Checker extends ParseTreeWalker { if (diag) { const renameAction: RenameShadowedFileAction = { action: ActionKind.RenameShadowedFileAction, - oldFile: this._fileInfo.filePath, - newFile: this._sourceMapper.getNextFileName(this._fileInfo.filePath), + oldUri: this._fileInfo.fileUri, + newUri: this._sourceMapper.getNextFileName(this._fileInfo.fileUri), }; diag.addAction(renameAction); } @@ -4245,7 +4248,7 @@ export class Checker extends ParseTreeWalker { namePartNodes[namePartNodes.length - 1].start, CancellationToken.None ); - const paths = definitions ? definitions.map((d) => d.path) : []; + const paths = definitions ? definitions.map((d) => d.uri) : []; paths.forEach((p) => { if (!p.startsWith(stdlibPath) && !isStubFile(p) && this._sourceMapper.isUserCode(p)) { // This means the user has a module that is overwriting the stdlib module. @@ -4254,7 +4257,7 @@ export class Checker extends ParseTreeWalker { DiagnosticRule.reportShadowedImports, Localizer.Diagnostic.stdlibModuleOverridden().format({ name: nameParts.join('.'), - path: p, + path: p.toUserVisibleString(), }), node ); @@ -4262,8 +4265,8 @@ export class Checker extends ParseTreeWalker { if (diag) { const renameAction: RenameShadowedFileAction = { action: ActionKind.RenameShadowedFileAction, - oldFile: p, - newFile: this._sourceMapper.getNextFileName(p), + oldUri: p, + newUri: this._sourceMapper.getNextFileName(p), }; diag.addAction(renameAction); } @@ -4755,7 +4758,7 @@ export class Checker extends ParseTreeWalker { decl.type !== DeclarationType.Function || ParseTreeUtils.isSuiteEmpty(decl.node.suite) ) ) { - if (getFileExtension(decls[0].path).toLowerCase() !== '.pyi') { + if (!decls[0].uri.hasExtension('.pyi')) { if (!isSymbolImplemented(name)) { diagAddendum.addMessage( Localizer.DiagnosticAddendum.missingProtocolMember().format({ @@ -4881,7 +4884,7 @@ export class Checker extends ParseTreeWalker { if (fieldDecls.length > 0) { diagnostic.addRelatedInfo( Localizer.DiagnosticAddendum.dataClassFieldLocation(), - fieldDecls[0].path, + fieldDecls[0].uri, fieldDecls[0].range ); } @@ -5103,7 +5106,16 @@ export class Checker extends ParseTreeWalker { const updatedClassType = ClassType.cloneWithNewTypeParameters(classType, updatedTypeParams); const objectObject = ClassType.cloneAsInstance(objectType); - const dummyTypeObject = ClassType.createInstantiable('__varianceDummy', '', '', '', 0, 0, undefined, undefined); + const dummyTypeObject = ClassType.createInstantiable( + '__varianceDummy', + '', + '', + Uri.empty(), + 0, + 0, + undefined, + undefined + ); updatedTypeParams.forEach((param, paramIndex) => { // Skip variadics and ParamSpecs. @@ -5382,7 +5394,7 @@ export class Checker extends ParseTreeWalker { ) ), }), - secondaryDecl.path, + secondaryDecl.uri, secondaryDecl.range ); } @@ -5760,7 +5772,7 @@ export class Checker extends ParseTreeWalker { baseClass: this._evaluator.printType(convertToInstance(overriddenClassAndSymbol.classType)), type: this._evaluator.printType(overriddenType), }), - overriddenDecl.path, + overriddenDecl.uri, overriddenDecl.range ); @@ -5769,7 +5781,7 @@ export class Checker extends ParseTreeWalker { baseClass: this._evaluator.printType(convertToInstance(overrideClassAndSymbol.classType)), type: this._evaluator.printType(overrideType), }), - overrideDecl.path, + overrideDecl.uri, overrideDecl.range ); } @@ -6005,7 +6017,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenMethod(), - origDecl.path, + origDecl.uri, origDecl.range ); } @@ -6030,7 +6042,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.finalMethod(), - origDecl.path, + origDecl.uri, origDecl.range ); } @@ -6060,7 +6072,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenMethod(), - origDecl.path, + origDecl.uri, origDecl.range ); } @@ -6123,7 +6135,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenMethod(), - origDecl.path, + origDecl.uri, origDecl.range ); } @@ -6160,7 +6172,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenMethod(), - origDecl.path, + origDecl.uri, origDecl.range ); } @@ -6247,7 +6259,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenSymbol(), - origDecl.path, + origDecl.uri, origDecl.range ); } @@ -6306,7 +6318,7 @@ export class Checker extends ParseTreeWalker { if (diag) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenSymbol(), - overrideFinalVarDecl.path, + overrideFinalVarDecl.uri, overrideFinalVarDecl.range ); } @@ -6353,7 +6365,7 @@ export class Checker extends ParseTreeWalker { if (diag && origDecl) { diag.addRelatedInfo( Localizer.DiagnosticAddendum.overriddenSymbol(), - origDecl.path, + origDecl.uri, origDecl.range ); } diff --git a/packages/pyright-internal/src/analyzer/circularDependency.ts b/packages/pyright-internal/src/analyzer/circularDependency.ts index 0655c24da..441e52f67 100644 --- a/packages/pyright-internal/src/analyzer/circularDependency.ts +++ b/packages/pyright-internal/src/analyzer/circularDependency.ts @@ -10,10 +10,12 @@ * by picking the alphabetically-first module in the cycle. */ -export class CircularDependency { - private _paths: string[] = []; +import { Uri } from '../common/uri/uri'; - appendPath(path: string) { +export class CircularDependency { + private _paths: Uri[] = []; + + appendPath(path: Uri) { this._paths.push(path); } diff --git a/packages/pyright-internal/src/analyzer/dataClasses.ts b/packages/pyright-internal/src/analyzer/dataClasses.ts index a08e08b42..a2d698736 100644 --- a/packages/pyright-internal/src/analyzer/dataClasses.ts +++ b/packages/pyright-internal/src/analyzer/dataClasses.ts @@ -867,7 +867,7 @@ function getDescriptorForConverterField( descriptorName, getClassFullName(converterNode, fileInfo.moduleName, descriptorName), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.None, getTypeSourceId(converterNode), /* declaredMetaclass */ undefined, diff --git a/packages/pyright-internal/src/analyzer/declaration.ts b/packages/pyright-internal/src/analyzer/declaration.ts index c5ef21ece..5a68c85dd 100644 --- a/packages/pyright-internal/src/analyzer/declaration.ts +++ b/packages/pyright-internal/src/analyzer/declaration.ts @@ -10,6 +10,7 @@ */ import { Range } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ClassNode, ExpressionNode, @@ -31,7 +32,7 @@ import { YieldNode, } from '../parser/parseNodes'; -export const UnresolvedModuleMarker = '*** unresolved ***'; +export const UnresolvedModuleMarker = Uri.parse('unresolved-module-marker://**/*', /* isCaseSensitive */ true); export const enum DeclarationType { Intrinsic, @@ -57,9 +58,9 @@ export interface DeclarationBase { node: ParseNode; // The file and range within that file that - // contains the declaration. Unless this is an alias, then path refers to the + // contains the declaration. Unless this is an alias, then uri refers to the // file the alias is referring to. - path: string; + uri: Uri; range: Range; // The dot-separated import name for the file that @@ -222,10 +223,10 @@ export interface AliasDeclaration extends DeclarationBase { // This interface represents a set of actions that the python loader // performs when a module import is encountered. export interface ModuleLoaderActions { - // The resolved path of the implicit import. This can be empty - // if the resolved path doesn't reference a module (e.g. it's + // The resolved uri of the implicit import. This can be empty + // if the resolved uri doesn't reference a module (e.g. it's // a directory). - path: string; + uri: Uri; // Is this a dummy entry for an unresolved import? isUnresolved?: boolean; @@ -285,5 +286,5 @@ export function isIntrinsicDeclaration(decl: Declaration): decl is IntrinsicDecl } export function isUnresolvedAliasDeclaration(decl: Declaration): boolean { - return isAliasDeclaration(decl) && decl.path === UnresolvedModuleMarker; + return isAliasDeclaration(decl) && decl.uri === UnresolvedModuleMarker; } diff --git a/packages/pyright-internal/src/analyzer/declarationUtils.ts b/packages/pyright-internal/src/analyzer/declarationUtils.ts index 4b2502f99..94228c624 100644 --- a/packages/pyright-internal/src/analyzer/declarationUtils.ts +++ b/packages/pyright-internal/src/analyzer/declarationUtils.ts @@ -8,6 +8,7 @@ */ import { getEmptyRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { NameNode, ParseNodeType } from '../parser/parseNodes'; import { ImportLookup, ImportLookupResult } from './analyzerFileInfo'; import { AliasDeclaration, Declaration, DeclarationType, ModuleLoaderActions, isAliasDeclaration } from './declaration'; @@ -78,7 +79,7 @@ export function areDeclarationsSame( return false; } - if (decl1.path !== decl2.path) { + if (!decl1.uri.equals(decl2.uri)) { return false; } @@ -174,16 +175,16 @@ export function getNameNodeForDeclaration(declaration: Declaration): NameNode | throw new Error(`Shouldn't reach here`); } -export function isDefinedInFile(decl: Declaration, filePath: string) { +export function isDefinedInFile(decl: Declaration, fileUri: Uri) { if (isAliasDeclaration(decl)) { // Alias decl's path points to the original symbol // the alias is pointing to. So, we need to get the // filepath in that the alias is defined from the node. - return getFileInfoFromNode(decl.node)?.filePath === filePath; + return getFileInfoFromNode(decl.node)?.fileUri.equals(fileUri); } // Other decls, the path points to the file the symbol is defined in. - return decl.path === filePath; + return decl.uri.equals(fileUri); } export function getDeclarationsWithUsesLocalNameRemoved(decls: Declaration[]) { @@ -199,13 +200,13 @@ export function getDeclarationsWithUsesLocalNameRemoved(decls: Declaration[]) { }); } -export function createSynthesizedAliasDeclaration(path: string): AliasDeclaration { +export function createSynthesizedAliasDeclaration(uri: Uri): AliasDeclaration { // The only time this decl is used is for IDE services such as // the find all references, hover provider and etc. return { type: DeclarationType.Alias, node: undefined!, - path, + uri, loadSymbolsFromPath: false, range: getEmptyRange(), implicitImports: new Map(), @@ -267,8 +268,10 @@ export function resolveAliasDeclaration( } let lookupResult: ImportLookupResult | undefined; - if (curDeclaration.path && curDeclaration.loadSymbolsFromPath) { - lookupResult = importLookup(curDeclaration.path, { skipFileNeededCheck: options.skipFileNeededCheck }); + if (!curDeclaration.uri.isEmpty() && curDeclaration.loadSymbolsFromPath) { + lookupResult = importLookup(curDeclaration.uri, { + skipFileNeededCheck: options.skipFileNeededCheck, + }); } const symbol: Symbol | undefined = lookupResult @@ -284,11 +287,11 @@ export function resolveAliasDeclaration( // when useLibraryCodeForTypes is disabled), b should be evaluated as Unknown, // not as a module. if ( - curDeclaration.path && + !curDeclaration.uri.isEmpty() && curDeclaration.submoduleFallback.type === DeclarationType.Alias && - curDeclaration.submoduleFallback.path + !curDeclaration.submoduleFallback.uri.isEmpty() ) { - const lookupResult = importLookup(curDeclaration.submoduleFallback.path, { + const lookupResult = importLookup(curDeclaration.submoduleFallback.uri, { skipFileNeededCheck: options.skipFileNeededCheck, skipParsing: true, }); @@ -393,7 +396,7 @@ export function resolveAliasDeclaration( // the module is foo, and the foo.__init__.py file contains the statement // "from foo import bar", we want to import the foo/bar.py submodule. if ( - curDeclaration.path === declaration.path && + curDeclaration.uri.equals(declaration.uri) && curDeclaration.type === DeclarationType.Alias && curDeclaration.submoduleFallback ) { diff --git a/packages/pyright-internal/src/analyzer/enums.ts b/packages/pyright-internal/src/analyzer/enums.ts index 736d152c4..31f0f31b7 100644 --- a/packages/pyright-internal/src/analyzer/enums.ts +++ b/packages/pyright-internal/src/analyzer/enums.ts @@ -81,7 +81,7 @@ export function createEnumType( className, getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.EnumClass, getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, diff --git a/packages/pyright-internal/src/analyzer/importResolver.ts b/packages/pyright-internal/src/analyzer/importResolver.ts index 53ed2ade0..3a22228ff 100644 --- a/packages/pyright-internal/src/analyzer/importResolver.ts +++ b/packages/pyright-internal/src/analyzer/importResolver.ts @@ -14,34 +14,14 @@ import { appendArray, flatten, getMapValues, getOrAdd } from '../common/collecti import { ConfigOptions, ExecutionEnvironment, matchFileSpecs } from '../common/configOptions'; import { Host } from '../common/host'; import { stubsSuffix } from '../common/pathConsts'; -import { - changeAnyExtension, - combinePathComponents, - combinePaths, - containsPath, - ensureTrailingDirectorySeparator, - getDirectoryPath, - getFileExtension, - getFileName, - getFileSystemEntriesFromDirEntries, - getPathComponents, - getRelativePathComponentsFromDirectory, - isDirectory, - isDiskPathRoot, - isFile, - normalizePath, - realCasePath, - resolvePaths, - stripFileExtension, - stripTrailingDirectorySeparator, - tryRealpath, - tryStat, -} from '../common/pathUtils'; +import { stripFileExtension } from '../common/pathUtils'; import { PythonVersion, versionFromString } from '../common/pythonVersion'; import { ServiceProvider } from '../common/serviceProvider'; import { ServiceKeys } from '../common/serviceProviderExtensions'; import * as StringUtils from '../common/stringUtils'; import { equateStringsCaseInsensitive } from '../common/stringUtils'; +import { Uri } from '../common/uri/uri'; +import { getFileSystemEntriesFromDirEntries, isDirectory, isFile, tryRealpath, tryStat } from '../common/uri/uriUtils'; import { isIdentifierChar, isIdentifierStartChar } from '../parser/characters'; import { ImplicitImport, ImportResult, ImportType } from './importResult'; import { getDirectoryLeadingDotsPointsTo } from './importStatementUtils'; @@ -114,16 +94,18 @@ export const supportedFileExtensions = [...supportedSourceFileExtensions, ...sup const allowPartialResolutionForThirdPartyPackages = false; export class ImportResolver { - private _cachedPythonSearchPaths: { paths: string[]; failureInfo: string[] } | undefined; + private _cachedPythonSearchPaths: { paths: Uri[]; failureInfo: string[] } | undefined; private _cachedImportResults = new Map(); private _cachedModuleNameResults = new Map>(); - private _cachedTypeshedRoot: string | undefined; - private _cachedTypeshedStdLibPath: string | undefined; + private _cachedTypeshedRoot: Uri | undefined; + private _cachedTypeshedStdLibPath: Uri | undefined; private _cachedTypeshedStdLibModuleVersions: Map | undefined; - private _cachedTypeshedThirdPartyPath: string | undefined; - private _cachedTypeshedThirdPartyPackagePaths: Map | undefined; - private _cachedTypeshedThirdPartyPackageRoots: string[] | undefined; + private _cachedTypeshedThirdPartyPath: Uri | undefined; + private _cachedTypeshedThirdPartyPackagePaths: Map | undefined; + private _cachedTypeshedThirdPartyPackageRoots: Uri[] | undefined; private _cachedEntriesForPath = new Map(); + private _cachedFilesForPath = new Map(); + private _cachedDirExistenceForRoot = new Map(); private _stdlibModules: Set | undefined; protected cachedParentImportResults: ParentDirectoryCache; @@ -135,17 +117,21 @@ export class ImportResolver { return this.serviceProvider.fs(); } + get tmp() { + return this.serviceProvider.tmp(); + } + get partialStubs() { return this.serviceProvider.tryGet(ServiceKeys.partialStubs); } - static isSupportedImportSourceFile(path: string) { - const fileExtension = getFileExtension(path).toLowerCase(); + static isSupportedImportSourceFile(uri: Uri) { + const fileExtension = uri.lastExtension.toLowerCase(); return supportedSourceFileExtensions.some((ext) => fileExtension === ext); } - static isSupportedImportFile(path: string) { - const fileExtension = getFileExtension(path).toLowerCase(); + static isSupportedImportFile(uri: Uri) { + const fileExtension = uri.lastExtension.toLowerCase(); return supportedFileExtensions.some((ext) => fileExtension === ext); } @@ -163,34 +149,34 @@ export class ImportResolver { // Resolves the import and returns the path if it exists, otherwise // returns undefined. resolveImport( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor ): ImportResult { // Wrap internal call to resolveImportInternal() to prevent calling any // child class version of resolveImport(). - return this.resolveImportInternal(sourceFilePath, execEnv, moduleDescriptor); + return this.resolveImportInternal(sourceFileUri, execEnv, moduleDescriptor); } getCompletionSuggestions( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor ) { - const suggestions = this._getCompletionSuggestionsStrict(sourceFilePath, execEnv, moduleDescriptor); + const suggestions = this._getCompletionSuggestionsStrict(sourceFileUri, execEnv, moduleDescriptor); // We only do parent import resolution for absolute path. if (moduleDescriptor.leadingDots > 0) { return suggestions; } - const root = this.getParentImportResolutionRoot(sourceFilePath, execEnv.root); - const origin = ensureTrailingDirectorySeparator(getDirectoryPath(sourceFilePath)); + const root = this.getParentImportResolutionRoot(sourceFileUri, execEnv.root); + const origin = sourceFileUri.getDirectory(); - let current = origin; - while (this._shouldWalkUp(current, root, execEnv)) { + let current: Uri | undefined = origin; + while (this._shouldWalkUp(current, root, execEnv) && current) { this._getCompletionSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, current, moduleDescriptor, @@ -198,11 +184,7 @@ export class ImportResolver { /* strictOnly */ false ); - let success; - [success, current] = this._tryWalkUp(current); - if (!success) { - break; - } + current = this._tryWalkUp(current); } return suggestions; @@ -218,8 +200,8 @@ export class ImportResolver { } // Returns the implementation file(s) for the given stub file. - getSourceFilesFromStub(stubFilePath: string, execEnv: ExecutionEnvironment, _mapCompiled: boolean): string[] { - const sourceFilePaths: string[] = []; + getSourceFilesFromStub(stubFileUri: Uri, execEnv: ExecutionEnvironment, _mapCompiled: boolean): Uri[] { + const sourceFileUris: Uri[] = []; // When ImportResolver resolves an import to a stub file, a second resolve is done // ignoring stub files, which gives us an approximation of where the implementation @@ -227,17 +209,17 @@ export class ImportResolver { this._cachedImportResults.forEach((map) => { map.forEach((result) => { if (result.isStubFile && result.isImportFound && result.nonStubImportResult) { - if (result.resolvedPaths[result.resolvedPaths.length - 1] === stubFilePath) { + if (result.resolvedUris[result.resolvedUris.length - 1].equals(stubFileUri)) { if (result.nonStubImportResult.isImportFound) { - const nonEmptyPath = - result.nonStubImportResult.resolvedPaths[ - result.nonStubImportResult.resolvedPaths.length - 1 + const nonEmptyUri = + result.nonStubImportResult.resolvedUris[ + result.nonStubImportResult.resolvedUris.length - 1 ]; - if (nonEmptyPath.endsWith('.py') || nonEmptyPath.endsWith('.pyi')) { + if (nonEmptyUri.pathEndsWith('.py') || nonEmptyUri.pathEndsWith('.pyi')) { // We allow pyi in case there are multiple pyi for a compiled module such as // numpy.random.mtrand - sourceFilePaths.push(nonEmptyPath); + sourceFileUris.push(nonEmptyUri); } } } @@ -247,15 +229,15 @@ export class ImportResolver { // We haven't seen an import of that stub, attempt to find the source // in some other ways. - if (sourceFilePaths.length === 0) { + if (sourceFileUris.length === 0) { // Simple case where the stub and source files are next to each other. - const sourceFilePath = changeAnyExtension(stubFilePath, '.py'); - if (this.dirExistsCached(sourceFilePath)) { - sourceFilePaths.push(sourceFilePath); + const sourceFileUri = stubFileUri.replaceExtension('.py'); + if (this.dirExistsCached(sourceFileUri)) { + sourceFileUris.push(sourceFileUri); } } - if (sourceFilePaths.length === 0) { + if (sourceFileUris.length === 0) { // The stub and the source file may have the same name, but be located // in different folder hierarchies. // Example: @@ -264,53 +246,48 @@ export class ImportResolver { // We get the relative path(s) of the stub to its import root(s), // in theory there can be more than one, then look for source // files in all the import roots using the same relative path(s). - const importRootPaths = this.getImportRoots(execEnv); + const importRoots = this.getImportRoots(execEnv); const relativeStubPaths: string[] = []; - for (const importRootPath of importRootPaths) { - if (containsPath(importRootPath, stubFilePath, true)) { - const parts = getRelativePathComponentsFromDirectory(importRootPath, stubFilePath, true); + for (const importRootUri of importRoots) { + if (stubFileUri.isChild(importRootUri)) { + const parts = Array.from(importRootUri.getRelativePathComponents(stubFileUri)); - // Note that relative paths have an empty parts[0] - if (parts.length > 1) { + if (parts.length >= 1) { // Handle the case where the symbol was resolved to a stubs package // rather than the real package. We'll strip off the "-stubs" suffix // in this case. - if (parts[1].endsWith(stubsSuffix)) { - parts[1] = parts[1].substr(0, parts[1].length - stubsSuffix.length); + if (parts[0].endsWith(stubsSuffix)) { + parts[0] = parts[0].slice(0, parts[0].length - stubsSuffix.length); } - const relativeStubPath = combinePathComponents(parts); - if (relativeStubPath) { - relativeStubPaths.push(relativeStubPath); - } + relativeStubPaths.push(parts.join('/')); } } } for (const relativeStubPath of relativeStubPaths) { - for (const importRootPath of importRootPaths) { - const absoluteStubPath = resolvePaths(importRootPath, relativeStubPath); - let absoluteSourcePath = changeAnyExtension(absoluteStubPath, '.py'); + for (const importRootUri of importRoots) { + const absoluteStubPath = importRootUri.combinePaths(relativeStubPath); + let absoluteSourcePath = absoluteStubPath.replaceExtension('.py'); if (this.fileExistsCached(absoluteSourcePath)) { - sourceFilePaths.push(absoluteSourcePath); + sourceFileUris.push(absoluteSourcePath); } else { - const filePathWithoutExtension = stripFileExtension(absoluteSourcePath); + const filePathWithoutExtension = absoluteSourcePath.stripExtension(); - if (filePathWithoutExtension.endsWith('__init__')) { + if (filePathWithoutExtension.pathEndsWith('__init__')) { // Did not match: /package/__init__.py // Try equivalent: /package.py - absoluteSourcePath = - filePathWithoutExtension.substr(0, filePathWithoutExtension.length - 9) + '.py'; + absoluteSourcePath = filePathWithoutExtension.getDirectory().packageUri; if (this.fileExistsCached(absoluteSourcePath)) { - sourceFilePaths.push(absoluteSourcePath); + sourceFileUris.push(absoluteSourcePath); } } else { // Did not match: /package.py // Try equivalent: /package/__init__.py - absoluteSourcePath = combinePaths(filePathWithoutExtension, '__init__.py'); + absoluteSourcePath = filePathWithoutExtension.initPyUri; if (this.fileExistsCached(absoluteSourcePath)) { - sourceFilePaths.push(absoluteSourcePath); + sourceFileUris.push(absoluteSourcePath); } } } @@ -318,23 +295,27 @@ export class ImportResolver { } } - return sourceFilePaths; + return sourceFileUris; } // Returns the module name (of the form X.Y.Z) that needs to be imported // from the current context to access the module with the specified file path. // In a sense, it's performing the inverse of resolveImport. getModuleNameForImport( - filePath: string, + fileUri: Uri, execEnv: ExecutionEnvironment, allowInvalidModuleName = false, detectPyTyped = false ) { // Cache results of the reverse of resolveImport as we cache resolveImport. - const cache = getOrAdd(this._cachedModuleNameResults, execEnv.root, () => new Map()); - const key = `${allowInvalidModuleName}.${detectPyTyped}.${filePath}`; + const cache = getOrAdd( + this._cachedModuleNameResults, + execEnv.root?.key, + () => new Map() + ); + const key = `${allowInvalidModuleName}.${detectPyTyped}.${fileUri.key}`; return getOrAdd(cache, key, () => - this._getModuleNameForImport(filePath, execEnv, allowInvalidModuleName, detectPyTyped) + this._getModuleNameForImport(fileUri, execEnv, allowInvalidModuleName, detectPyTyped) ); } @@ -386,7 +367,7 @@ export class ImportResolver { // is where the third party folder is in the roots. const thirdPartyRoot = this._getThirdPartyTypeshedPath(this._configOptions.typeshedPath, importFailureInfo); if (thirdPartyRoot) { - roots.push(combinePaths(thirdPartyRoot, '...')); + roots.push(thirdPartyRoot.combinePaths('...')); } } else { const thirdPartyPaths = this._getThirdPartyTypeshedPackageRoots(importFailureInfo); @@ -417,12 +398,12 @@ export class ImportResolver { const ps = this.partialStubs; const ignored: string[] = []; - const paths: string[] = []; + const paths: Uri[] = []; const typeshedPathEx = this.getTypeshedPathEx(execEnv, ignored); // Add paths to search stub packages. addPaths(this._configOptions.stubPath); - addPaths(execEnv.root); + addPaths(execEnv.root ?? this._configOptions.projectRoot); execEnv.extraPaths.forEach((p) => addPaths(p)); addPaths(typeshedPathEx); this.getPythonSearchPaths(ignored).forEach((p) => addPaths(p)); @@ -431,7 +412,7 @@ export class ImportResolver { this._invalidateFileSystemCache(); return true; - function addPaths(path?: string) { + function addPaths(path?: Uri) { if (!path || ps.isPathScanned(path)) { return; } @@ -440,7 +421,7 @@ export class ImportResolver { } } - getPythonSearchPaths(importFailureInfo: string[]) { + getPythonSearchPaths(importFailureInfo: string[]): Uri[] { // Find the site packages for the configured virtual environment. if (!this._cachedPythonSearchPaths) { const info: string[] = []; @@ -457,10 +438,10 @@ export class ImportResolver { return this._cachedPythonSearchPaths.paths; } - getTypeshedStdlibExcludeList(customTypeshedPath: string, pythonVersion: PythonVersion): string[] { + getTypeshedStdlibExcludeList(customTypeshedPath: Uri, pythonVersion: PythonVersion): Uri[] { const unused: string[] = []; const typeshedStdlibPath = this._getStdlibTypeshedPath(customTypeshedPath, pythonVersion, unused); - const excludes: string[] = []; + const excludes: Uri[] = []; if (!typeshedStdlibPath) { return excludes; @@ -474,10 +455,10 @@ export class ImportResolver { if (versionRange.max !== undefined && pythonVersion > versionRange.max) { // Add excludes for both the ".pyi" file and the directory that contains it // (in case it's using a "__init__.pyi" file). - const moduleDirPath = combinePaths(typeshedStdlibPath, ...moduleName.split('.')); + const moduleDirPath = typeshedStdlibPath.combinePaths(...moduleName.split('.')); excludes.push(moduleDirPath); - const moduleFilePath = moduleDirPath + '.pyi'; + const moduleFilePath = moduleDirPath.replaceExtension('.pyi'); excludes.push(moduleFilePath); } }); @@ -485,28 +466,28 @@ export class ImportResolver { return excludes; } - protected readdirEntriesCached(path: string): Dirent[] { - const cachedValue = this._cachedEntriesForPath.get(path); + protected readdirEntriesCached(uri: Uri): Dirent[] { + const cachedValue = this._cachedEntriesForPath.get(uri.key); if (cachedValue) { return cachedValue; } let newCacheValue: Dirent[]; try { - newCacheValue = this.fileSystem.readdirEntriesSync(path); + newCacheValue = this.fileSystem.readdirEntriesSync(uri); } catch { newCacheValue = []; } // Populate cache for next time. - this._cachedEntriesForPath.set(path, newCacheValue); + this._cachedEntriesForPath.set(uri.key, newCacheValue); return newCacheValue; } // Resolves the import and returns the path if it exists, otherwise // returns undefined. protected resolveImportInternal( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor ): ImportResult { @@ -514,7 +495,7 @@ export class ImportResolver { const importFailureInfo: string[] = []; const importResult = this._resolveImportStrict( importName, - sourceFilePath, + sourceFileUri, execEnv, moduleDescriptor, importFailureInfo @@ -527,8 +508,7 @@ export class ImportResolver { // If the import is absolute and no other method works, try resolving the // absolute in the importing file's directory, then the parent directory, // and so on, until the import root is reached. - sourceFilePath = realCasePath(normalizePath(sourceFilePath), this.fileSystem); - const origin = ensureTrailingDirectorySeparator(getDirectoryPath(sourceFilePath)); + const origin = sourceFileUri.getDirectory(); const result = this.cachedParentImportResults.getImportResult(origin, importName, importResult); if (result) { @@ -537,18 +517,18 @@ export class ImportResolver { } // Check whether the given file is in the parent directory import resolution cache. - const root = this.getParentImportResolutionRoot(sourceFilePath, execEnv.root); - if (!this.cachedParentImportResults.checkValidPath(this.fileSystem, sourceFilePath, root)) { + const root = this.getParentImportResolutionRoot(sourceFileUri, execEnv.root); + if (!this.cachedParentImportResults.checkValidPath(this.fileSystem, sourceFileUri, root)) { return importResult; } const importPath: ImportPath = { importPath: undefined }; // Going up the given folder one by one until we can resolve the import. - let current = origin; - while (this._shouldWalkUp(current, root, execEnv)) { + let current: Uri | undefined = origin; + while (this._shouldWalkUp(current, root, execEnv) && current) { const result = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, current, execEnv, moduleDescriptor, @@ -560,7 +540,7 @@ export class ImportResolver { /* allowPyi */ true ); - this.cachedParentImportResults.checked(current, importName, importPath); + this.cachedParentImportResults.checked(current!, importName, importPath); if (result.isImportFound) { // This will make cache to point to actual path that contains the module we found @@ -575,35 +555,30 @@ export class ImportResolver { return this.filterImplicitImports(result, moduleDescriptor.importedSymbols); } - let success; - [success, current] = this._tryWalkUp(current); - if (!success) { - break; - } + current = this._tryWalkUp(current); } - this.cachedParentImportResults.checked(current, importName, importPath); + if (current) { + this.cachedParentImportResults.checked(current, importName, importPath); + } return importResult; } - protected fileExistsCached(path: string): boolean { - const splitPath = this._splitPath(path); - - if (!splitPath[0] || !splitPath[1]) { - if (!this.fileSystem.existsSync(path)) { - return false; - } - return tryStat(this.fileSystem, path)?.isFile() ?? false; + protected fileExistsCached(uri: Uri): boolean { + const directory = uri.getDirectory(); + if (directory.equals(uri)) { + // Started at root, so this can't be a file. + return false; } - - const entries = this.readdirEntriesCached(splitPath[0]); - const entry = entries.find((entry) => entry.name === splitPath[1]); + const fileName = uri.fileName; + const entries = this.readdirEntriesCached(directory); + const entry = entries.find((entry) => entry.name === fileName); if (entry?.isFile()) { return true; } if (entry?.isSymbolicLink()) { - const realPath = tryRealpath(this.fileSystem, path); + const realPath = tryRealpath(this.fileSystem, uri); if (realPath && this.fileSystem.existsSync(realPath) && isFile(this.fileSystem, realPath)) { return true; } @@ -612,24 +587,30 @@ export class ImportResolver { return false; } - protected dirExistsCached(path: string): boolean { - const splitPath = this._splitPath(path); - - if (!splitPath[0] || !splitPath[1]) { - if (!this.fileSystem.existsSync(path)) { - return false; + protected dirExistsCached(uri: Uri): boolean { + const parent = uri.getDirectory(); + if (parent.equals(uri)) { + // Started at root. No entries to read, so have to check ourselves. + let cachedExistence = this._cachedDirExistenceForRoot.get(uri.key); + // Check if the value was in the cache or not. Undefined means it wasn't. + if (cachedExistence === undefined) { + cachedExistence = tryStat(this.fileSystem, uri)?.isDirectory() ?? false; + this._cachedDirExistenceForRoot.set(uri.key, cachedExistence); } - return tryStat(this.fileSystem, path)?.isDirectory() ?? false; + return cachedExistence; } - const entries = this.readdirEntriesCached(splitPath[0]); - const entry = entries.find((entry) => entry.name === splitPath[1]); + // Otherwise not a root, so read the entries we have cached to see if + // the directory exists or not. + const directoryName = uri.fileName; + const entries = this.readdirEntriesCached(parent); + const entry = entries.find((entry) => entry.name === directoryName); if (entry?.isDirectory()) { return true; } if (entry?.isSymbolicLink()) { - const realPath = tryRealpath(this.fileSystem, path); + const realPath = tryRealpath(this.fileSystem, uri); if (realPath && this.fileSystem.existsSync(realPath) && isDirectory(this.fileSystem, realPath)) { return true; } @@ -639,7 +620,7 @@ export class ImportResolver { } protected addResultsToCache( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, importName: string, importResult: ImportResult, @@ -647,10 +628,10 @@ export class ImportResolver { fromUserFile: boolean ) { // If the import is relative, include the source file path in the key. - const relativeSourceFilePath = moduleDescriptor && moduleDescriptor.leadingDots > 0 ? sourceFilePath : ''; + const relativeSourceFileUri = moduleDescriptor && moduleDescriptor.leadingDots > 0 ? sourceFileUri : undefined; - getOrAdd(this._cachedImportResults, execEnv.root, () => new Map()).set( - this._getImportCacheKey(relativeSourceFilePath, importName, fromUserFile), + getOrAdd(this._cachedImportResults, execEnv.root?.key, () => new Map()).set( + this._getImportCacheKey(relativeSourceFileUri, importName, fromUserFile), importResult ); @@ -660,8 +641,8 @@ export class ImportResolver { // Follows import resolution algorithm defined in PEP-420: // https://www.python.org/dev/peps/pep-0420/ protected resolveAbsoluteImport( - sourceFilePath: string | undefined, - rootPath: string, + sourceFileUri: Uri | undefined, + rootPath: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, importName: string, @@ -715,7 +696,7 @@ export class ImportResolver { // Intended to be overridden by subclasses to provide additional stub // path capabilities. Return undefined if no extra stub path were found. - protected getTypeshedPathEx(execEnv: ExecutionEnvironment, importFailureInfo: string[]): string | undefined { + protected getTypeshedPathEx(execEnv: ExecutionEnvironment, importFailureInfo: string[]): Uri | undefined { return undefined; } @@ -723,7 +704,7 @@ export class ImportResolver { // resolving capabilities. Return undefined if no stubs were found for // this import. protected resolveImportEx( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, importName: string, @@ -737,27 +718,27 @@ export class ImportResolver { // resolving capabilities for native (compiled) modules. Returns undefined // if no stubs were found for this import. protected resolveNativeImportEx( - libraryFilePath: string, + libraryFileUri: Uri, importName: string, importFailureInfo: string[] = [] - ): string | undefined { + ): Uri | undefined { return undefined; } - protected getNativeModuleName(fileName: string): string | undefined { - const fileExtension = getFileExtension(fileName, /* multiDotExtension */ false).toLowerCase(); + protected getNativeModuleName(uri: Uri): string | undefined { + const fileExtension = uri.lastExtension.toLowerCase(); if (this._isNativeModuleFileExtension(fileExtension)) { - return stripFileExtension(stripFileExtension(fileName)); + return stripFileExtension(uri.fileName, /* multiDotExtension */ true); } return undefined; } protected getModuleNameFromPath( - containerPath: string, - filePath: string, + containerPath: Uri, + fileUri: Uri, stripTopContainerDir = false ): string | undefined { - const moduleNameInfo = this.getModuleNameInfoFromPath(containerPath, filePath, stripTopContainerDir); + const moduleNameInfo = this.getModuleNameInfoFromPath(containerPath, fileUri, stripTopContainerDir); if (!moduleNameInfo || moduleNameInfo.containsInvalidCharacters) { return undefined; } @@ -766,30 +747,27 @@ export class ImportResolver { } protected getModuleNameInfoFromPath( - containerPath: string, - filePath: string, + containerPath: Uri, + fileUri: Uri, stripTopContainerDir = false ): ModuleNameInfoFromPath | undefined { - containerPath = ensureTrailingDirectorySeparator(containerPath); - let filePathWithoutExtension = stripFileExtension(filePath); + let fileUriWithoutExtension = fileUri.stripExtension(); // If module is native, strip platform part, such as 'cp36-win_amd64' in 'mtrand.cp36-win_amd64'. - if (this._isNativeModuleFileExtension(getFileExtension(filePath))) { - filePathWithoutExtension = stripFileExtension(filePathWithoutExtension); + if (this._isNativeModuleFileExtension(fileUri.lastExtension)) { + fileUriWithoutExtension = fileUriWithoutExtension.stripExtension(); } - if (!filePathWithoutExtension.startsWith(containerPath)) { + if (!fileUriWithoutExtension.startsWith(containerPath)) { return undefined; } // Strip off the '/__init__' if it's present. - if (filePathWithoutExtension.endsWith('__init__')) { - filePathWithoutExtension = filePathWithoutExtension.substr(0, filePathWithoutExtension.length - 9); + if (fileUriWithoutExtension.pathEndsWith('__init__')) { + fileUriWithoutExtension = fileUriWithoutExtension.getDirectory(); } - const relativeFilePath = filePathWithoutExtension.substr(containerPath.length); - const parts = getPathComponents(relativeFilePath); - parts.shift(); + const parts = Array.from(containerPath.getRelativePathComponents(fileUriWithoutExtension)); if (stripTopContainerDir) { if (parts.length === 0) { return undefined; @@ -857,22 +835,22 @@ export class ImportResolver { return '.'.repeat(moduleDescriptor.leadingDots) + moduleDescriptor.nameParts.join('.'); } - protected getParentImportResolutionRoot(sourceFilePath: string, executionRoot: string | undefined) { + protected getParentImportResolutionRoot(sourceFileUri: Uri, executionRoot: Uri | undefined) { if (executionRoot) { - return ensureTrailingDirectorySeparator(realCasePath(normalizePath(executionRoot), this.fileSystem)); + return this.fileSystem.realCasePath(executionRoot); } - return ensureTrailingDirectorySeparator(getDirectoryPath(sourceFilePath)); + return sourceFileUri.getDirectory(); } private _resolveImportStrict( importName: string, - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, importFailureInfo: string[] ) { - const fromUserFile = matchFileSpecs(this._configOptions, sourceFilePath); + const fromUserFile = matchFileSpecs(this._configOptions, sourceFileUri); const notFoundResult: ImportResult = { importName, isRelative: false, @@ -882,7 +860,7 @@ export class ImportResolver { isInitFilePresent: false, isStubPackage: false, importFailureInfo, - resolvedPaths: [], + resolvedUris: [], importType: ImportType.Local, isStubFile: false, isNativeLib: false, @@ -896,7 +874,7 @@ export class ImportResolver { // Is it a relative import? if (moduleDescriptor.leadingDots > 0) { const cachedResults = this._lookUpResultsInCache( - sourceFilePath, + sourceFileUri, execEnv, importName, moduleDescriptor, @@ -908,7 +886,7 @@ export class ImportResolver { } const relativeImport = this._resolveRelativeImport( - sourceFilePath, + sourceFileUri, execEnv, moduleDescriptor, importName, @@ -919,7 +897,7 @@ export class ImportResolver { relativeImport.isRelative = true; return this.addResultsToCache( - sourceFilePath, + sourceFileUri, execEnv, importName, relativeImport, @@ -929,7 +907,7 @@ export class ImportResolver { } } else { const cachedResults = this._lookUpResultsInCache( - sourceFilePath, + sourceFileUri, execEnv, importName, moduleDescriptor, @@ -953,7 +931,7 @@ export class ImportResolver { } const bestImport = this._resolveBestAbsoluteImport( - sourceFilePath, + sourceFileUri, execEnv, moduleDescriptor, /* allowPyi */ true @@ -963,7 +941,7 @@ export class ImportResolver { if (bestImport.isStubFile) { bestImport.nonStubImportResult = this._resolveBestAbsoluteImport( - sourceFilePath, + sourceFileUri, execEnv, moduleDescriptor, /* allowPyi */ false @@ -971,7 +949,7 @@ export class ImportResolver { } return this.addResultsToCache( - sourceFilePath, + sourceFileUri, execEnv, importName, bestImport, @@ -982,7 +960,7 @@ export class ImportResolver { } return this.addResultsToCache( - sourceFilePath, + sourceFileUri, execEnv, importName, notFoundResult, @@ -992,32 +970,26 @@ export class ImportResolver { } private _getCompletionSuggestionsStrict( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor - ): Map { + ): Map { const importFailureInfo: string[] = []; - const suggestions = new Map(); + const suggestions = new Map(); // Is it a relative import? if (moduleDescriptor.leadingDots > 0) { - this._getCompletionSuggestionsRelative(sourceFilePath, execEnv, moduleDescriptor, suggestions); + this._getCompletionSuggestionsRelative(sourceFileUri, execEnv, moduleDescriptor, suggestions); } else { // First check for a typeshed file. if (moduleDescriptor.nameParts.length > 0) { - this._getCompletionSuggestionsTypeshedPath( - sourceFilePath, - execEnv, - moduleDescriptor, - true, - suggestions - ); + this._getCompletionSuggestionsTypeshedPath(sourceFileUri, execEnv, moduleDescriptor, true, suggestions); } // Look for it in the root directory of the execution environment. if (execEnv.root) { this._getCompletionSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, execEnv.root, moduleDescriptor, @@ -1027,7 +999,7 @@ export class ImportResolver { for (const extraPath of execEnv.extraPaths) { this._getCompletionSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, extraPath, moduleDescriptor, @@ -1038,7 +1010,7 @@ export class ImportResolver { // Check for a typings file. if (this._configOptions.stubPath) { this._getCompletionSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, this._configOptions.stubPath, moduleDescriptor, @@ -1047,13 +1019,13 @@ export class ImportResolver { } // Check for a typeshed file. - this._getCompletionSuggestionsTypeshedPath(sourceFilePath, execEnv, moduleDescriptor, false, suggestions); + this._getCompletionSuggestionsTypeshedPath(sourceFileUri, execEnv, moduleDescriptor, false, suggestions); // Look for the import in the list of third-party packages. const pythonSearchPaths = this.getPythonSearchPaths(importFailureInfo); for (const searchPath of pythonSearchPaths) { this._getCompletionSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, searchPath, moduleDescriptor, @@ -1066,7 +1038,7 @@ export class ImportResolver { } private _getModuleNameForImport( - filePath: string, + fileUri: Uri, execEnv: ExecutionEnvironment, allowInvalidModuleName: boolean, detectPyTyped: boolean @@ -1093,7 +1065,7 @@ export class ImportResolver { ); if (stdLibTypeshedPath) { - moduleName = this.getModuleNameFromPath(stdLibTypeshedPath, filePath); + moduleName = this.getModuleNameFromPath(stdLibTypeshedPath, fileUri); if (moduleName) { const moduleDescriptor: ImportedModuleDescriptor = { leadingDots: 0, @@ -1122,7 +1094,7 @@ export class ImportResolver { // Look for it in the root directory of the execution environment. if (execEnv.root) { - const candidateModuleNameInfo = this.getModuleNameInfoFromPath(execEnv.root, filePath); + const candidateModuleNameInfo = this.getModuleNameInfoFromPath(execEnv.root, fileUri); if (candidateModuleNameInfo) { if (candidateModuleNameInfo.containsInvalidCharacters) { @@ -1136,7 +1108,7 @@ export class ImportResolver { } for (const extraPath of execEnv.extraPaths) { - const candidateModuleNameInfo = this.getModuleNameInfoFromPath(extraPath, filePath); + const candidateModuleNameInfo = this.getModuleNameInfoFromPath(extraPath, fileUri); if (candidateModuleNameInfo) { if (candidateModuleNameInfo.containsInvalidCharacters) { @@ -1155,7 +1127,7 @@ export class ImportResolver { // Check for a typings file. if (this._configOptions.stubPath) { - const candidateModuleNameInfo = this.getModuleNameInfoFromPath(this._configOptions.stubPath, filePath); + const candidateModuleNameInfo = this.getModuleNameInfoFromPath(this._configOptions.stubPath, fileUri); if (candidateModuleNameInfo) { if (candidateModuleNameInfo.containsInvalidCharacters) { @@ -1184,7 +1156,7 @@ export class ImportResolver { if (thirdPartyTypeshedPath) { const candidateModuleName = this.getModuleNameFromPath( thirdPartyTypeshedPath, - filePath, + fileUri, /* stripTopContainerDir */ true ); @@ -1199,7 +1171,7 @@ export class ImportResolver { const thirdPartyTypeshedPathEx = this.getTypeshedPathEx(execEnv, importFailureInfo); if (thirdPartyTypeshedPathEx) { - const candidateModuleName = this.getModuleNameFromPath(thirdPartyTypeshedPathEx, filePath); + const candidateModuleName = this.getModuleNameFromPath(thirdPartyTypeshedPathEx, fileUri); // Does this candidate look better than the previous best module name? // We'll always try to use the shortest version. @@ -1214,7 +1186,7 @@ export class ImportResolver { const pythonSearchPaths = this.getPythonSearchPaths(importFailureInfo); for (const searchPath of pythonSearchPaths) { - const candidateModuleNameInfo = this.getModuleNameInfoFromPath(searchPath, filePath); + const candidateModuleNameInfo = this.getModuleNameInfoFromPath(searchPath, fileUri); if (candidateModuleNameInfo) { if (candidateModuleNameInfo.containsInvalidCharacters) { @@ -1233,24 +1205,18 @@ export class ImportResolver { } if (detectPyTyped && importType === ImportType.ThirdParty) { - const root = this.getParentImportResolutionRoot(filePath, execEnv.root); + const root = this.getParentImportResolutionRoot(fileUri, execEnv.root); // Go up directories one by one looking for a py.typed file. - let current = ensureTrailingDirectorySeparator(getDirectoryPath(filePath)); + let current: Uri | undefined = fileUri.getDirectory(); while (this._shouldWalkUp(current, root, execEnv)) { - if (this.fileExistsCached(combinePaths(current, 'py.typed'))) { - const pyTypedInfo = getPyTypedInfo(this.fileSystem, current); - if (pyTypedInfo && !pyTypedInfo.isPartiallyTyped) { - isThirdPartyPyTypedPresent = true; - } - break; + const pyTypedInfo = this._getPyTypedInfo(current!); + if (pyTypedInfo && !pyTypedInfo.isPartiallyTyped) { + isThirdPartyPyTypedPresent = true; } + break; - let success; - [success, current] = this._tryWalkUp(current); - if (!success) { - break; - } + current = this._tryWalkUp(current); } } @@ -1280,24 +1246,12 @@ export class ImportResolver { private _invalidateFileSystemCache() { this._cachedEntriesForPath.clear(); - } - - // Splits a path into the name of the containing directory and - // a file or dir within that containing directory. - private _splitPath(path: string): [string, string] { - const pathComponents = getPathComponents(path); - if (pathComponents.length <= 1) { - return [path, '']; - } - - const containingPath = combinePathComponents(pathComponents.slice(0, -1)); - const fileOrDirName = pathComponents[pathComponents.length - 1]; - - return [containingPath, fileOrDirName]; + this._cachedFilesForPath.clear(); + this._cachedDirExistenceForRoot.clear(); } private _resolveAbsoluteImport( - rootPath: string, + rootPath: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, importName: string, @@ -1316,7 +1270,7 @@ export class ImportResolver { // Starting at the specified path, walk the file system to find the // specified module. - const resolvedPaths: string[] = []; + const resolvedPaths: Uri[] = []; let dirPath = rootPath; let isNamespacePackage = false; let isInitFilePresent = false; @@ -1324,14 +1278,13 @@ export class ImportResolver { let isStubFile = false; let isNativeLib = false; let implicitImports = new Map(); - let packageDirectory: string | undefined; + let packageDirectory: Uri | undefined; let pyTypedInfo: PyTypedInfo | undefined; // Handle the "from . import XXX" case. if (moduleDescriptor.nameParts.length === 0) { - const fileNameWithoutExtension = '__init__'; - const pyFilePath = combinePaths(dirPath, fileNameWithoutExtension + '.py'); - const pyiFilePath = combinePaths(dirPath, fileNameWithoutExtension + '.pyi'); + const pyFilePath = dirPath.initPyUri; + const pyiFilePath = dirPath.initPyiUri; if (allowPyi && this.fileExistsCached(pyiFilePath)) { importFailureInfo.push(`Resolved import with file '${pyiFilePath}'`); @@ -1342,7 +1295,7 @@ export class ImportResolver { resolvedPaths.push(pyFilePath); } else { importFailureInfo.push(`Partially resolved import with directory '${dirPath}'`); - resolvedPaths.push(''); + resolvedPaths.push(Uri.empty()); isNamespacePackage = true; } @@ -1351,10 +1304,10 @@ export class ImportResolver { for (let i = 0; i < moduleDescriptor.nameParts.length; i++) { const isFirstPart = i === 0; const isLastPart = i === moduleDescriptor.nameParts.length - 1; - dirPath = combinePaths(dirPath, moduleDescriptor.nameParts[i]); + dirPath = dirPath.combinePaths(moduleDescriptor.nameParts[i]); if (useStubPackage && isFirstPart) { - dirPath += stubsSuffix; + dirPath = dirPath.addPath(stubsSuffix); isStubPackage = true; } @@ -1366,9 +1319,8 @@ export class ImportResolver { } // See if we can find an __init__.py[i] in this directory. - const fileNameWithoutExtension = '__init__'; - const pyFilePath = combinePaths(dirPath, fileNameWithoutExtension + '.py'); - const pyiFilePath = combinePaths(dirPath, fileNameWithoutExtension + '.pyi'); + const pyFilePath = dirPath.initPyUri; + const pyiFilePath = dirPath.initPyiUri; isInitFilePresent = false; if (allowPyi && this.fileExistsCached(pyiFilePath)) { @@ -1385,16 +1337,14 @@ export class ImportResolver { } if (!pyTypedInfo && lookForPyTyped) { - if (this.fileExistsCached(combinePaths(dirPath, 'py.typed'))) { - pyTypedInfo = getPyTypedInfo(this.fileSystem, dirPath); - } + pyTypedInfo = this._getPyTypedInfo(dirPath); } if (!isLastPart) { // We are not at the last part, and we found a directory, // so continue to look for the next part. if (!isInitFilePresent) { - resolvedPaths.push(''); + resolvedPaths.push(Uri.empty()); isNamespacePackage = true; pyTypedInfo = undefined; } @@ -1413,11 +1363,9 @@ export class ImportResolver { // We weren't able to find a directory or we found a directory with // no __init__.py[i] file. See if we can find a ".py" or ".pyi" file // with this name. - let fileDirectory = stripTrailingDirectorySeparator(dirPath); - const fileNameWithoutExtension = getFileName(fileDirectory); - fileDirectory = getDirectoryPath(fileDirectory); - const pyFilePath = combinePaths(fileDirectory, fileNameWithoutExtension + '.py'); - const pyiFilePath = combinePaths(fileDirectory, fileNameWithoutExtension + '.pyi'); + const pyFilePath = dirPath.packageUri; + const pyiFilePath = dirPath.packageStubUri; + const fileDirectory = dirPath.getDirectory(); if (allowPyi && this.fileExistsCached(pyiFilePath)) { importFailureInfo.push(`Resolved import with file '${pyiFilePath}'`); @@ -1431,11 +1379,9 @@ export class ImportResolver { } else { if (allowNativeLib && this.dirExistsCached(fileDirectory)) { const filesInDir = this._getFilesInDirectory(fileDirectory); - const nativeLibFileName = filesInDir.find((f) => - this._isNativeModuleFileName(fileNameWithoutExtension, f) - ); - if (nativeLibFileName) { - const nativeLibPath = combinePaths(fileDirectory, nativeLibFileName); + const dirName = dirPath.fileName; + const nativeLibPath = filesInDir.find((f) => this._isNativeModuleFileName(dirName, f)); + if (nativeLibPath) { // Try resolving native library to a custom stub. isNativeLib = this._resolveNativeModuleStub( nativeLibPath, @@ -1450,7 +1396,7 @@ export class ImportResolver { if (!isNativeLib && foundDirectory) { importFailureInfo.push(`Partially resolved import with directory '${dirPath}'`); - resolvedPaths.push(''); + resolvedPaths.push(Uri.empty()); if (isLastPart) { implicitImports = this._findImplicitImports(importName, dirPath, [pyFilePath, pyiFilePath]); isNamespacePackage = true; @@ -1461,9 +1407,7 @@ export class ImportResolver { } if (!pyTypedInfo && lookForPyTyped) { - if (this.fileExistsCached(combinePaths(fileDirectory, 'py.typed'))) { - pyTypedInfo = getPyTypedInfo(this.fileSystem, fileDirectory); - } + pyTypedInfo = this._getPyTypedInfo(fileDirectory); } break; } @@ -1487,7 +1431,7 @@ export class ImportResolver { isPartlyResolved, importFailureInfo, importType: ImportType.Local, - resolvedPaths, + resolvedUris: resolvedPaths, searchPath: rootPath, isStubFile, isNativeLib, @@ -1498,27 +1442,27 @@ export class ImportResolver { }; } - private _getImportCacheKey(sourceFilePath: string, importName: string, fromUserFile: boolean) { - return `${sourceFilePath}-${importName}-${fromUserFile}`; + private _getImportCacheKey(sourceFileUri: Uri | undefined, importName: string, fromUserFile: boolean) { + return `${sourceFileUri?.key ?? ''}-${importName}-${fromUserFile}`; } private _lookUpResultsInCache( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, importName: string, moduleDescriptor: ImportedModuleDescriptor, fromUserFile: boolean ) { - const cacheForExecEnv = this._cachedImportResults.get(execEnv.root); + const cacheForExecEnv = this._cachedImportResults.get(execEnv.root?.key ?? ''); if (!cacheForExecEnv) { return undefined; } // If the import is relative, include the source file path in the key. - const relativeSourceFilePath = moduleDescriptor.leadingDots > 0 ? sourceFilePath : ''; + const relativeSourceFileUri = moduleDescriptor.leadingDots > 0 ? sourceFileUri : undefined; const cachedEntry = cacheForExecEnv.get( - this._getImportCacheKey(relativeSourceFilePath, importName, fromUserFile) + this._getImportCacheKey(relativeSourceFileUri, importName, fromUserFile) ); if (!cachedEntry) { @@ -1548,7 +1492,7 @@ export class ImportResolver { } private _resolveBestAbsoluteImport( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, allowPyi: boolean @@ -1560,7 +1504,7 @@ export class ImportResolver { if (allowPyi && this._configOptions.stubPath) { importFailureInfo.push(`Looking in stubPath '${this._configOptions.stubPath}'`); const typingsImport = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, this._configOptions.stubPath, execEnv, moduleDescriptor, @@ -1583,7 +1527,7 @@ export class ImportResolver { // skip the typings import and continue searching. if ( typingsImport.isNamespacePackage && - !typingsImport.resolvedPaths[typingsImport.resolvedPaths.length - 1] + !typingsImport.resolvedUris[typingsImport.resolvedUris.length - 1] ) { if (this._isNamespacePackageResolved(moduleDescriptor, typingsImport.implicitImports)) { return typingsImport; @@ -1602,7 +1546,7 @@ export class ImportResolver { importFailureInfo.push(`Looking in root directory of execution environment ` + `'${execEnv.root}'`); localImport = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, execEnv.root, execEnv, moduleDescriptor, @@ -1620,7 +1564,7 @@ export class ImportResolver { for (const extraPath of execEnv.extraPaths) { importFailureInfo.push(`Looking in extraPath '${extraPath}'`); localImport = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, extraPath, execEnv, moduleDescriptor, @@ -1642,7 +1586,7 @@ export class ImportResolver { importFailureInfo.push(`Looking in python search path '${searchPath}'`); const thirdPartyImport = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, searchPath, execEnv, moduleDescriptor, @@ -1676,7 +1620,7 @@ export class ImportResolver { // Call the extensibility hook for subclasses. const extraResults = this.resolveImportEx( - sourceFilePath, + sourceFileUri, execEnv, moduleDescriptor, importName, @@ -1740,8 +1684,8 @@ export class ImportResolver { if (newImport.isImportFound) { // Prefer traditional packages over namespace packages. - const soFarIndex = bestImportSoFar.resolvedPaths.findIndex((path) => !!path); - const newIndex = newImport.resolvedPaths.findIndex((path) => !!path); + const soFarIndex = bestImportSoFar.resolvedUris.findIndex((path) => !path.isEmpty()); + const newIndex = newImport.resolvedUris.findIndex((path) => !path.isEmpty()); if (soFarIndex !== newIndex) { if (soFarIndex < 0) { return newImport; @@ -1798,7 +1742,7 @@ export class ImportResolver { } // All else equal, prefer shorter resolution paths. - if (bestImportSoFar.resolvedPaths.length > newImport.resolvedPaths.length) { + if (bestImportSoFar.resolvedUris.length > newImport.resolvedUris.length) { return newImport; } } else if (newImport.isPartlyResolved) { @@ -1811,8 +1755,8 @@ export class ImportResolver { // bestSoFar: a/~b/~c/~d new: a Result: bestSoFar wins // bestSoFar: ~a/~b/~c/~d new: a Result: new wins // bestSoFar: a/~b/~c/~d new: a/b Result: new wins - const soFarIndex = bestImportSoFar.resolvedPaths.findIndex((path) => !!path); - const newIndex = newImport.resolvedPaths.findIndex((path) => !!path); + const soFarIndex = bestImportSoFar.resolvedUris.findIndex((path) => !path.isEmpty()); + const newIndex = newImport.resolvedUris.findIndex((path) => !path.isEmpty()); if (soFarIndex !== newIndex) { if (soFarIndex < 0) { @@ -1850,7 +1794,7 @@ export class ImportResolver { } path` ); - let typeshedPaths: string[] | undefined; + let typeshedPaths: Uri[] | undefined; if (isStdLib) { const path = this._getStdlibTypeshedPath( this._configOptions.typeshedPath, @@ -1891,14 +1835,14 @@ export class ImportResolver { } // Finds all of the stdlib modules and returns a Set containing all of their names. - private _buildStdlibCache(stdlibRoot: string | undefined): Set { + private _buildStdlibCache(stdlibRoot: Uri | undefined): Set { const cache = new Set(); if (stdlibRoot) { - const readDir = (root: string, prefix: string | undefined) => { + const readDir = (root: Uri, prefix: string | undefined) => { this.readdirEntriesCached(root).forEach((entry) => { if (entry.isDirectory()) { - const dirRoot = combinePaths(root, entry.name); + const dirRoot = root.combinePaths(entry.name); readDir(dirRoot, prefix ? `${prefix}.${entry.name}` : entry.name); } else if (entry.name.includes('.py')) { const stripped = stripFileExtension(entry.name); @@ -1920,13 +1864,13 @@ export class ImportResolver { // the pypi-registered name of the package and an inner directory contains // the name of the package as it is referenced by import statements. These // don't always match. - private _buildTypeshedThirdPartyPackageMap(thirdPartyDir: string | undefined) { - this._cachedTypeshedThirdPartyPackagePaths = new Map(); + private _buildTypeshedThirdPartyPackageMap(thirdPartyDir: Uri | undefined) { + this._cachedTypeshedThirdPartyPackagePaths = new Map(); if (thirdPartyDir) { this.readdirEntriesCached(thirdPartyDir).forEach((outerEntry) => { if (outerEntry.isDirectory()) { - const innerDirPath = combinePaths(thirdPartyDir, outerEntry.name); + const innerDirPath = thirdPartyDir.combinePaths(outerEntry.name); this.readdirEntriesCached(innerDirPath).forEach((innerEntry) => { if (innerEntry.name === '@python2') { @@ -1961,15 +1905,15 @@ export class ImportResolver { } private _getCompletionSuggestionsTypeshedPath( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, isStdLib: boolean, - suggestions: Map + suggestions: Map ) { const importFailureInfo: string[] = []; - let typeshedPaths: string[] | undefined; + let typeshedPaths: Uri[] | undefined; if (isStdLib) { const path = this._getStdlibTypeshedPath( this._configOptions.typeshedPath, @@ -2001,7 +1945,7 @@ export class ImportResolver { typeshedPaths.forEach((typeshedPath) => { if (this.dirExistsCached(typeshedPath)) { this._getCompletionSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, typeshedPath, moduleDescriptor, @@ -2015,7 +1959,7 @@ export class ImportResolver { // If moduleDescriptor is provided, it is filtered based on the VERSIONS // file in the typeshed stubs. private _getStdlibTypeshedPath( - customTypeshedPath: string | undefined, + customTypeshedPath: Uri | undefined, pythonVersion: PythonVersion, importFailureInfo: string[], moduleDescriptor?: ImportedModuleDescriptor @@ -2037,13 +1981,13 @@ export class ImportResolver { return subdirectory; } - private _getThirdPartyTypeshedPath(customTypeshedPath: string | undefined, importFailureInfo: string[]) { + private _getThirdPartyTypeshedPath(customTypeshedPath: Uri | undefined, importFailureInfo: string[]) { return this._getTypeshedSubdirectory(/* isStdLib */ false, customTypeshedPath, importFailureInfo); } private _isStdlibTypeshedStubValidForVersion( moduleDescriptor: ImportedModuleDescriptor, - customTypeshedPath: string | undefined, + customTypeshedPath: Uri | undefined, pythonVersion: PythonVersion, importFailureInfo: string[] ) { @@ -2074,7 +2018,7 @@ export class ImportResolver { } private _readTypeshedStdLibVersions( - customTypeshedPath: string | undefined, + customTypeshedPath: Uri | undefined, importFailureInfo: string[] ): Map { const versionRangeMap = new Map(); @@ -2087,7 +2031,7 @@ export class ImportResolver { ); if (typeshedStdLibPath) { - const versionsFilePath = combinePaths(typeshedStdLibPath, 'VERSIONS'); + const versionsFilePath = typeshedStdLibPath.combinePaths('VERSIONS'); try { const fileStats = this.fileSystem.statSync(versionsFilePath); if (fileStats.size > 0 && fileStats.size < 256 * 1024) { @@ -2141,7 +2085,7 @@ export class ImportResolver { moduleDescriptor: ImportedModuleDescriptor, importFailureInfo: string[], includeMatchOnly = true - ): string[] | undefined { + ): Uri[] | undefined { const typeshedPath = this._getThirdPartyTypeshedPath(this._configOptions.typeshedPath, importFailureInfo); if (!this._cachedTypeshedThirdPartyPackagePaths) { @@ -2172,12 +2116,12 @@ export class ImportResolver { return this._cachedTypeshedThirdPartyPackageRoots!; } - private _getTypeshedRoot(customTypeshedPath: string | undefined, importFailureInfo: string[]) { + private _getTypeshedRoot(customTypeshedPath: Uri | undefined, importFailureInfo: string[]) { if (this._cachedTypeshedRoot !== undefined) { return this._cachedTypeshedRoot; } - let typeshedPath = ''; + let typeshedPath = undefined; // Did the user specify a typeshed path? If not, we'll look in the // python search paths, then in the typeshed-fallback directory. @@ -2189,7 +2133,7 @@ export class ImportResolver { // If typeshed directory wasn't found in other locations, use the fallback. if (!typeshedPath) { - typeshedPath = PythonPathUtils.getTypeShedFallbackPath(this.fileSystem) || ''; + typeshedPath = PythonPathUtils.getTypeShedFallbackPath(this.fileSystem) ?? Uri.empty(); } this._cachedTypeshedRoot = typeshedPath; @@ -2198,7 +2142,7 @@ export class ImportResolver { private _getTypeshedSubdirectory( isStdLib: boolean, - customTypeshedPath: string | undefined, + customTypeshedPath: Uri | undefined, importFailureInfo: string[] ) { // See if we have it cached. @@ -2230,7 +2174,7 @@ export class ImportResolver { } private _resolveRelativeImport( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, importName: string, @@ -2239,10 +2183,7 @@ export class ImportResolver { importFailureInfo.push('Attempting to resolve relative import'); // Determine which search path this file is part of. - const directory = getDirectoryLeadingDotsPointsTo( - getDirectoryPath(sourceFilePath), - moduleDescriptor.leadingDots - ); + const directory = getDirectoryLeadingDotsPointsTo(sourceFileUri.getDirectory(), moduleDescriptor.leadingDots); if (!directory) { importFailureInfo.push(`Invalid relative path '${importName}'`); return undefined; @@ -2250,7 +2191,7 @@ export class ImportResolver { // Now try to match the module parts from the current directory location. const absImport = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, directory, execEnv, moduleDescriptor, @@ -2265,7 +2206,7 @@ export class ImportResolver { // the same folder for the real module. Otherwise, it will // error out on runtime. absImport.nonStubImportResult = this.resolveAbsoluteImport( - sourceFilePath, + sourceFileUri, directory, execEnv, moduleDescriptor, @@ -2283,7 +2224,7 @@ export class ImportResolver { isNamespacePackage: false, isStubPackage: false, importFailureInfo, - resolvedPaths: [], + resolvedUris: [], importType: ImportType.Local, isStubFile: false, isNativeLib: false, @@ -2297,45 +2238,54 @@ export class ImportResolver { } private _getCompletionSuggestionsRelative( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, moduleDescriptor: ImportedModuleDescriptor, - suggestions: Map + suggestions: Map ) { // Determine which search path this file is part of. - const directory = getDirectoryLeadingDotsPointsTo( - getDirectoryPath(sourceFilePath), - moduleDescriptor.leadingDots - ); + const directory = getDirectoryLeadingDotsPointsTo(sourceFileUri.getDirectory(), moduleDescriptor.leadingDots); if (!directory) { return; } // Now try to match the module parts from the current directory location. - this._getCompletionSuggestionsAbsolute(sourceFilePath, execEnv, directory, moduleDescriptor, suggestions); + this._getCompletionSuggestionsAbsolute(sourceFileUri, execEnv, directory, moduleDescriptor, suggestions); } - private _getFilesInDirectory(dirPath: string): string[] { - const entriesInDir = this.readdirEntriesCached(dirPath); - const filesInDir = entriesInDir.filter((f) => f.isFile()).map((f) => f.name); + private _getFilesInDirectory(dirPath: Uri): Uri[] { + const cachedValue = this._cachedFilesForPath.get(dirPath.key); + if (cachedValue) { + return cachedValue; + } - // Add any symbolic links that point to files. - entriesInDir.forEach((f) => { - const linkPath = combinePaths(dirPath, f.name); - if (f.isSymbolicLink() && tryStat(this.fileSystem, linkPath)?.isFile()) { - filesInDir.push(f.name); - } - }); + let newCacheValue: Uri[] = []; + try { + const entriesInDir = this.readdirEntriesCached(dirPath); + const filesInDir = entriesInDir.filter((f) => f.isFile()); - return filesInDir; + // Add any symbolic links that point to files. + entriesInDir.forEach((f) => { + if (f.isSymbolicLink() && tryStat(this.fileSystem, dirPath.combinePaths(f.name))?.isFile()) { + filesInDir.push(f); + } + }); + + newCacheValue = filesInDir.map((f) => dirPath.combinePaths(f.name)); + } catch { + newCacheValue = []; + } + + this._cachedFilesForPath.set(dirPath.key, newCacheValue); + return newCacheValue; } private _getCompletionSuggestionsAbsolute( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, - rootPath: string, + rootPath: Uri, moduleDescriptor: ImportedModuleDescriptor, - suggestions: Map, + suggestions: Map, strictOnly = true ) { // Starting at the specified path, walk the file system to find the @@ -2358,7 +2308,7 @@ export class ImportResolver { // dot (or multiple) in a relative path. if (nameParts.length === 0) { this._addFilteredSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, dirPath, '', @@ -2373,7 +2323,7 @@ export class ImportResolver { // of the name. if (i === nameParts.length - 1) { this._addFilteredSuggestionsAbsolute( - sourceFilePath, + sourceFileUri, execEnv, dirPath, nameParts[i], @@ -2384,7 +2334,7 @@ export class ImportResolver { ); } - dirPath = combinePaths(dirPath, nameParts[i]); + dirPath = dirPath.combinePaths(nameParts[i]); if (!this.dirExistsCached(dirPath)) { break; } @@ -2393,11 +2343,11 @@ export class ImportResolver { } private _addFilteredSuggestionsAbsolute( - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, - currentPath: string, + currentPath: Uri, filter: string, - suggestions: Map, + suggestions: Map, leadingDots: number, parentNameParts: string[], strictOnly: boolean @@ -2412,7 +2362,7 @@ export class ImportResolver { entries.files.forEach((file) => { // Strip multi-dot extensions to handle file names like "foo.cpython-32m.so". We want // to detect the ".so" but strip off the entire ".cpython-32m.so" extension. - const fileWithoutExtension = stripFileExtension(file, /* multiDotExtension */ true); + const fileWithoutExtension = file.stripAllExtensions().fileName; if (ImportResolver.isSupportedImportFile(file)) { if (fileWithoutExtension === '__init__') { @@ -2429,7 +2379,7 @@ export class ImportResolver { fileWithoutExtension, leadingDots, parentNameParts, - sourceFilePath, + sourceFileUri, execEnv, strictOnly ) @@ -2437,36 +2387,44 @@ export class ImportResolver { return; } - suggestions.set(fileWithoutExtension, combinePaths(currentPath, file)); + suggestions.set(fileWithoutExtension, file); } }); entries.directories.forEach((dir) => { - if (filter && !dir.startsWith(filter)) { + const dirSuggestion = dir.fileName; + if (filter && !dirSuggestion.startsWith(filter)) { return; } if ( - !this._isUniqueValidSuggestion(dir, suggestions) || - !this._isResolvableSuggestion(dir, leadingDots, parentNameParts, sourceFilePath, execEnv, strictOnly) + !this._isUniqueValidSuggestion(dirSuggestion, suggestions) || + !this._isResolvableSuggestion( + dirSuggestion, + leadingDots, + parentNameParts, + sourceFileUri, + execEnv, + strictOnly + ) ) { return; } - const initPyiPath = combinePaths(currentPath, dir, '__init__.pyi'); + const initPyiPath = dir.initPyiUri; if (this.fileExistsCached(initPyiPath)) { - suggestions.set(dir, initPyiPath); + suggestions.set(dirSuggestion, initPyiPath); return; } - const initPyPath = combinePaths(currentPath, dir, '__init__.py'); + const initPyPath = dir.initPyUri; if (this.fileExistsCached(initPyPath)) { - suggestions.set(dir, initPyPath); + suggestions.set(dirSuggestion, initPyPath); return; } // It is a namespace package. there is no corresponding module path. - suggestions.set(dir, ''); + suggestions.set(dirSuggestion, Uri.empty()); }); } @@ -2476,11 +2434,11 @@ export class ImportResolver { name: string, leadingDots: number, parentNameParts: string[], - sourceFilePath: string, + sourceFileUri: Uri, execEnv: ExecutionEnvironment, strictOnly: boolean ) { - // We always resolve names based on sourceFilePath. + // We always resolve names based on sourceFileUri. const moduleDescriptor: ImportedModuleDescriptor = { leadingDots: leadingDots, nameParts: [...parentNameParts, name], @@ -2495,13 +2453,13 @@ export class ImportResolver { importResult = this._resolveImportStrict( importName, - sourceFilePath, + sourceFileUri, execEnv, moduleDescriptor, importFailureInfo ); } else { - importResult = this.resolveImportInternal(sourceFilePath, execEnv, moduleDescriptor); + importResult = this.resolveImportInternal(sourceFileUri, execEnv, moduleDescriptor); } if (importResult && importResult.isImportFound) { @@ -2514,7 +2472,7 @@ export class ImportResolver { return false; } - private _isUniqueValidSuggestion(suggestionToAdd: string, suggestions: Map) { + private _isUniqueValidSuggestion(suggestionToAdd: string, suggestions: Map) { if (suggestions.has(suggestionToAdd)) { return false; } @@ -2534,8 +2492,8 @@ export class ImportResolver { private _findImplicitImports( importingModuleName: string, - dirPath: string, - exclusions: string[] + dirPath: Uri, + exclusions: Uri[] ): Map { const implicitImportMap = new Map(); @@ -2547,32 +2505,31 @@ export class ImportResolver { ); // Add implicit file-based modules. - for (const fileName of entries.files) { - const fileExt = getFileExtension(fileName); + for (const filePath of entries.files) { + const fileExt = filePath.lastExtension; let strippedFileName: string; let isNativeLib = false; if (fileExt === '.py' || fileExt === '.pyi') { - strippedFileName = stripFileExtension(fileName); + strippedFileName = stripFileExtension(filePath.fileName); } else if ( this._isNativeModuleFileExtension(fileExt) && - !this.fileExistsCached(`${fileName}.py`) && - !this.fileExistsCached(`${fileName}.pyi`) + !this.fileExistsCached(filePath.packageUri) && + !this.fileExistsCached(filePath.packageStubUri) ) { // Native module. - strippedFileName = fileName.substr(0, fileName.indexOf('.')); + strippedFileName = filePath.stripAllExtensions().fileName; isNativeLib = true; } else { continue; } - const filePath = combinePaths(dirPath, fileName); - if (!exclusions.find((exclusion) => exclusion === filePath)) { + if (!exclusions.find((exclusion) => exclusion.equals(filePath))) { const implicitImport: ImplicitImport = { - isStubFile: fileName.endsWith('.pyi'), + isStubFile: filePath.hasExtension('.pyi'), isNativeLib, name: strippedFileName, - path: filePath, + uri: filePath, }; // Always prefer stub files over non-stub files. @@ -2580,14 +2537,14 @@ export class ImportResolver { if (!entry || !entry.isStubFile) { // Try resolving resolving native lib to a custom stub. if (isNativeLib) { - const nativeLibPath = combinePaths(dirPath, fileName); + const nativeLibPath = filePath; const nativeStubPath = this.resolveNativeImportEx( nativeLibPath, `${importingModuleName}.${strippedFileName}`, [] ); if (nativeStubPath) { - implicitImport.path = nativeStubPath; + implicitImport.uri = nativeStubPath; implicitImport.isNativeLib = false; } } @@ -2597,11 +2554,11 @@ export class ImportResolver { } // Add implicit directory-based modules. - for (const dirName of entries.directories) { - const pyFilePath = combinePaths(dirPath, dirName, '__init__.py'); - const pyiFilePath = pyFilePath + 'i'; + for (const dirPath of entries.directories) { + const pyFilePath = dirPath.initPyUri; + const pyiFilePath = dirPath.initPyiUri; let isStubFile = false; - let path = ''; + let path: Uri | undefined; if (this.fileExistsCached(pyiFilePath)) { isStubFile = true; @@ -2611,13 +2568,13 @@ export class ImportResolver { } if (path) { - if (!exclusions.find((exclusion) => exclusion === path)) { + if (!exclusions.find((exclusion) => exclusion.equals(path))) { const implicitImport: ImplicitImport = { isStubFile, isNativeLib: false, - name: dirName, - path, - pyTypedInfo: getPyTypedInfo(this.fileSystem, combinePaths(dirPath, dirName)), + name: dirPath.fileName, + uri: path, + pyTypedInfo: this._getPyTypedInfo(dirPath), }; implicitImportMap.set(implicitImport.name, implicitImport); @@ -2628,13 +2585,22 @@ export class ImportResolver { return implicitImportMap; } + // Retrieves the pytyped info for a directory if it exists. This is a small perf optimization + // that allows skipping the search when the pytyped file doesn't exist. + private _getPyTypedInfo(filePath: Uri): PyTypedInfo | undefined { + if (!this.fileExistsCached(filePath.pytypedUri)) { + return undefined; + } + return getPyTypedInfo(this.fileSystem, filePath); + } + private _resolveNativeModuleStub( - nativeLibPath: string, + nativeLibPath: Uri, execEnv: ExecutionEnvironment, importName: string, moduleDescriptor: ImportedModuleDescriptor, importFailureInfo: string[], - resolvedPaths: string[] + resolvedPaths: Uri[] ): boolean { let moduleFullName = importName; @@ -2656,12 +2622,12 @@ export class ImportResolver { return true; } - private _isNativeModuleFileName(moduleName: string, fileName: string): boolean { + private _isNativeModuleFileName(moduleName: string, fileUri: Uri): boolean { // Strip off the final file extension and the part of the file name // that excludes all (multi-part) file extensions. This allows us to // handle file names like "foo.cpython-32m.so". - const fileExtension = getFileExtension(fileName, /* multiDotExtension */ false).toLowerCase(); - const withoutExtension = stripFileExtension(fileName, /* multiDotExtension */ true); + const fileExtension = fileUri.lastExtension.toLowerCase(); + const withoutExtension = stripFileExtension(fileUri.fileName, /* multiDotExtension */ true); return ( this._isNativeModuleFileExtension(fileExtension) && equateStringsCaseInsensitive(moduleName, withoutExtension) @@ -2672,21 +2638,21 @@ export class ImportResolver { return supportedNativeLibExtensions.some((ext) => ext === fileExtension); } - private _tryWalkUp(current: string): [success: boolean, path: string] { - if (isDiskPathRoot(current)) { - return [false, '']; + private _tryWalkUp(current: Uri | undefined): Uri | undefined { + if (!current || current.isEmpty() || current.isRoot()) { + return undefined; } - // Ensure we don't go around forever even if isDiskPathRoot returns false. - const next = ensureTrailingDirectorySeparator(normalizePath(combinePaths(current, '..'))); - if (next === current) { - return [false, '']; + // Ensure we don't go around forever even if isRoot returns false. + const next = current.combinePaths('..'); + if (next.equals(current)) { + return undefined; } - return [true, next]; + return next; } - private _shouldWalkUp(current: string, root: string, execEnv: ExecutionEnvironment) { - return current.length > root.length || (current === root && !execEnv.root); + private _shouldWalkUp(current: Uri | undefined, root: Uri, execEnv: ExecutionEnvironment) { + return current && (current.isChild(root) || (current.equals(root) && !execEnv.root)); } } diff --git a/packages/pyright-internal/src/analyzer/importResult.ts b/packages/pyright-internal/src/analyzer/importResult.ts index 2a01c5242..7f1bbcb0d 100644 --- a/packages/pyright-internal/src/analyzer/importResult.ts +++ b/packages/pyright-internal/src/analyzer/importResult.ts @@ -7,6 +7,7 @@ * Interface that describes the output of the import resolver. */ +import { Uri } from '../common/uri/uri'; import { PyTypedInfo } from './pyTypedUtils'; export const enum ImportType { @@ -19,7 +20,7 @@ export interface ImplicitImport { isStubFile: boolean; isNativeLib: boolean; name: string; - path: string; + uri: Uri; pyTypedInfo?: PyTypedInfo | undefined; } @@ -62,11 +63,11 @@ export interface ImportResult { // The resolved absolute paths for each of the files in the module name. // Parts that have no files (e.g. directories within a namespace // package) have empty strings for a resolvedPath. - resolvedPaths: string[]; + resolvedUris: Uri[]; // For absolute imports, the search path that was used to resolve // (or partially resolve) the module. - searchPath?: string; + searchPath?: Uri; // True if resolved file is a type hint (.pyi) file rather than // a python (.py) file. @@ -103,5 +104,5 @@ export interface ImportResult { pyTypedInfo?: PyTypedInfo | undefined; // The directory of the package, if found. - packageDirectory?: string | undefined; + packageDirectory?: Uri | undefined; } diff --git a/packages/pyright-internal/src/analyzer/importStatementUtils.ts b/packages/pyright-internal/src/analyzer/importStatementUtils.ts index f7d577456..ef2e62458 100644 --- a/packages/pyright-internal/src/analyzer/importStatementUtils.ts +++ b/packages/pyright-internal/src/analyzer/importStatementUtils.ts @@ -14,16 +14,11 @@ import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { addIfUnique, appendArray, createMapFromItems } from '../common/collectionUtils'; import { TextEditAction } from '../common/editAction'; import { ReadOnlyFileSystem } from '../common/fileSystem'; -import { - getDirectoryPath, - getFileName, - getRelativePathComponentsFromDirectory, - isFile, - stripFileExtension, -} from '../common/pathUtils'; import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; import { Position, Range, TextRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; +import { isFile } from '../common/uri/uriUtils'; import { ImportAsNode, ImportFromAsNode, @@ -35,18 +30,18 @@ import { ParseNodeType, } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; +import { TokenType } from '../parser/tokenizerTypes'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; import { ModuleNameAndType } from './importResolver'; import { ImportResult, ImportType } from './importResult'; -import * as SymbolNameUtils from './symbolNameUtils'; import { findTokenAfter, getTokenAt } from './parseTreeUtils'; -import { TokenType } from '../parser/tokenizerTypes'; +import * as SymbolNameUtils from './symbolNameUtils'; export interface ImportStatement { node: ImportNode | ImportFromNode; subnode?: ImportAsNode; importResult: ImportResult | undefined; - resolvedPath: string | undefined; + resolvedPath: Uri | undefined; moduleName: string; followsNonImportStatement: boolean; } @@ -625,10 +620,10 @@ function _getInsertionEditForAutoImportInsertion( function _processImportNode(node: ImportNode, localImports: ImportStatements, followsNonImportStatement: boolean) { node.list.forEach((importAsNode) => { const importResult = AnalyzerNodeInfo.getImportInfo(importAsNode.module); - let resolvedPath: string | undefined; + let resolvedPath: Uri | undefined; if (importResult && importResult.isImportFound) { - resolvedPath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + resolvedPath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; } const localImport: ImportStatement = { @@ -647,8 +642,8 @@ function _processImportNode(node: ImportNode, localImports: ImportStatements, fo // Don't overwrite existing import or import from statements // because we always want to prefer 'import from' over 'import' // in the map. - if (!localImports.mapByFilePath.has(resolvedPath)) { - localImports.mapByFilePath.set(resolvedPath, localImport); + if (!localImports.mapByFilePath.has(resolvedPath.key)) { + localImports.mapByFilePath.set(resolvedPath.key, localImport); } } }); @@ -661,10 +656,10 @@ function _processImportFromNode( includeImplicitImports: boolean ) { const importResult = AnalyzerNodeInfo.getImportInfo(node.module); - let resolvedPath: string | undefined; + let resolvedPath: Uri | undefined; if (importResult && importResult.isImportFound) { - resolvedPath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + resolvedPath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; } if (includeImplicitImports && importResult) { @@ -673,7 +668,7 @@ function _processImportFromNode( for (const implicitImport of importResult.implicitImports.values()) { const importFromAs = node.imports.find((i) => i.name.value === implicitImport.name); if (importFromAs) { - localImports.implicitImports.set(implicitImport.path, importFromAs); + localImports.implicitImports.set(implicitImport.uri.key, importFromAs); } } } @@ -690,7 +685,7 @@ function _processImportFromNode( // Add it to the map. if (resolvedPath) { - const prevEntry = localImports.mapByFilePath.get(resolvedPath); + const prevEntry = localImports.mapByFilePath.get(resolvedPath.key); // Overwrite existing import statements because we always want to prefer // 'import from' over 'import'. Also, overwrite existing 'import from' if // the module name is shorter. @@ -699,7 +694,7 @@ function _processImportFromNode( prevEntry.node.nodeType === ParseNodeType.Import || prevEntry.moduleName.length > localImport.moduleName.length ) { - localImports.mapByFilePath.set(resolvedPath, localImport); + localImports.mapByFilePath.set(resolvedPath.key, localImport); } } } @@ -850,23 +845,23 @@ function getConsecutiveNumberPairs(indices: number[]) { export function getRelativeModuleName( fs: ReadOnlyFileSystem, - sourcePath: string, - targetPath: string, + sourcePath: Uri, + targetPath: Uri, ignoreFolderStructure = false, sourceIsFile?: boolean ) { let srcPath = sourcePath; sourceIsFile = sourceIsFile !== undefined ? sourceIsFile : isFile(fs, sourcePath); if (sourceIsFile) { - srcPath = getDirectoryPath(sourcePath); + srcPath = sourcePath.getDirectory(); } let symbolName: string | undefined; let destPath = targetPath; if (sourceIsFile) { - destPath = getDirectoryPath(targetPath); + destPath = targetPath.getDirectory(); - const fileName = stripFileExtension(getFileName(targetPath)); + const fileName = targetPath.stripAllExtensions().fileName; if (fileName !== '__init__') { // ex) src: a.py, dest: b.py -> ".b" will be returned. symbolName = fileName; @@ -875,18 +870,18 @@ export function getRelativeModuleName( // like how it would return for sibling folder. // // if folder structure is not ignored, ".." will be returned - symbolName = getFileName(destPath); - destPath = getDirectoryPath(destPath); + symbolName = destPath.fileName; + destPath = destPath.getDirectory(); } } - const relativePaths = getRelativePathComponentsFromDirectory(srcPath, destPath, (f) => fs.realCasePath(f)); + const relativePaths = srcPath.getRelativePathComponents(destPath); // This assumes both file paths are under the same importing root. // So this doesn't handle paths pointing to 2 different import roots. // ex) user file A to library file B let currentPaths = '.'; - for (let i = 1; i < relativePaths.length; i++) { + for (let i = 0; i < relativePaths.length; i++) { const relativePath = relativePaths[i]; if (relativePath === '..') { currentPaths += '.'; @@ -907,25 +902,25 @@ export function getRelativeModuleName( return currentPaths; } -export function getDirectoryLeadingDotsPointsTo(fromDirectory: string, leadingDots: number) { +export function getDirectoryLeadingDotsPointsTo(fromDirectory: Uri, leadingDots: number) { let currentDirectory = fromDirectory; for (let i = 1; i < leadingDots; i++) { - if (currentDirectory === '') { + if (currentDirectory.isRoot()) { return undefined; } - currentDirectory = getDirectoryPath(currentDirectory); + currentDirectory = currentDirectory.getDirectory(); } return currentDirectory; } export function getResolvedFilePath(importResult: ImportResult | undefined) { - if (!importResult || !importResult.isImportFound || importResult.resolvedPaths.length === 0) { + if (!importResult || !importResult.isImportFound || importResult.resolvedUris.length === 0) { return undefined; } - if (importResult.resolvedPaths.length === 1 && importResult.resolvedPaths[0] === '') { + if (importResult.resolvedUris.length === 1 && importResult.resolvedUris[0].equals(Uri.empty())) { // Import is resolved to namespace package folder. if (importResult.packageDirectory) { return importResult.packageDirectory; @@ -940,7 +935,7 @@ export function getResolvedFilePath(importResult: ImportResult | undefined) { } // Regular case. - return importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + return importResult.resolvedUris[importResult.resolvedUris.length - 1]; } export function haveSameParentModule(module1: string[], module2: string[]) { diff --git a/packages/pyright-internal/src/analyzer/namedTuples.ts b/packages/pyright-internal/src/analyzer/namedTuples.ts index dde8b9f1e..6cf3f3ab7 100644 --- a/packages/pyright-internal/src/analyzer/namedTuples.ts +++ b/packages/pyright-internal/src/analyzer/namedTuples.ts @@ -114,7 +114,7 @@ export function createNamedTupleType( className, ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.ReadOnlyInstanceVariables, ParseTreeUtils.getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, @@ -207,7 +207,7 @@ export function createNamedTupleType( type: DeclarationType.Variable, node: stringNode as StringListNode, isRuntimeTypeExpression: true, - path: fileInfo.filePath, + uri: fileInfo.fileUri, range: convertOffsetsToRange( stringNode.start, TextRange.getEnd(stringNode), @@ -303,7 +303,7 @@ export function createNamedTupleType( const declaration: VariableDeclaration = { type: DeclarationType.Variable, node: entryNameNode, - path: fileInfo.filePath, + uri: fileInfo.fileUri, typeAnnotationNode: entryTypeNode, range: convertOffsetsToRange( entryNameNode.start, diff --git a/packages/pyright-internal/src/analyzer/packageTypeReport.ts b/packages/pyright-internal/src/analyzer/packageTypeReport.ts index 0f08ae003..9301475e8 100644 --- a/packages/pyright-internal/src/analyzer/packageTypeReport.ts +++ b/packages/pyright-internal/src/analyzer/packageTypeReport.ts @@ -10,6 +10,7 @@ */ import { Diagnostic, DiagnosticWithinFile } from '../common/diagnostic'; +import { Uri } from '../common/uri/uri'; import { ScopeType } from './scope'; export enum SymbolCategory { @@ -37,7 +38,7 @@ export interface SymbolInfo { category: SymbolCategory; name: string; fullName: string; - filePath: string; + fileUri: Uri; isExported: boolean; typeKnownStatus: TypeKnownStatus; referenceCount: number; @@ -47,7 +48,7 @@ export interface SymbolInfo { export interface ModuleInfo { name: string; - path: string; + uri: Uri; isExported: boolean; } @@ -57,10 +58,10 @@ export interface PackageTypeReport { packageName: string; moduleName: string; ignoreExternal: boolean; - packageRootDirectory: string | undefined; - moduleRootDirectory: string | undefined; + packageRootDirectoryUri: Uri | undefined; + moduleRootDirectoryUri: Uri | undefined; isModuleSingleFile: boolean; - pyTypedPath: string | undefined; + pyTypedPathUri: Uri | undefined; missingFunctionDocStringCount: number; missingClassDocStringCount: number; missingDefaultParamCount: number; @@ -85,20 +86,20 @@ export interface PackageTypeReport { export function getEmptyReport( packageName: string, - packageRootDirectory: string, + packageRootUri: Uri, moduleName: string, - moduleRootDirectory: string, + moduleRootUri: Uri, isModuleSingleFile: boolean, ignoreExternal: boolean ) { const report: PackageTypeReport = { packageName, ignoreExternal, - packageRootDirectory, + packageRootDirectoryUri: packageRootUri, moduleName, - moduleRootDirectory, + moduleRootDirectoryUri: moduleRootUri, isModuleSingleFile, - pyTypedPath: undefined, + pyTypedPathUri: undefined, missingFunctionDocStringCount: 0, missingClassDocStringCount: 0, missingDefaultParamCount: 0, diff --git a/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts b/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts index ed6a4f9f5..b4ab7db1f 100644 --- a/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts +++ b/packages/pyright-internal/src/analyzer/packageTypeVerifier.ts @@ -14,16 +14,11 @@ import { NullConsole } from '../common/console'; import { assert } from '../common/debug'; import { Diagnostic, DiagnosticAddendum, DiagnosticCategory } from '../common/diagnostic'; import { FullAccessHost } from '../common/fullAccessHost'; -import { - combinePaths, - getDirectoryPath, - getFileExtension, - getFileName, - stripFileExtension, - tryStat, -} from '../common/pathUtils'; +import { getFileExtension, stripFileExtension } from '../common/pathUtils'; import { ServiceProvider } from '../common/serviceProvider'; import { getEmptyRange, Range } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; +import { tryStat } from '../common/uri/uriUtils'; import { DeclarationType, FunctionDeclaration, VariableDeclaration } from './declaration'; import { createImportedModuleDescriptor, ImportResolver } from './importResolver'; import { @@ -67,7 +62,7 @@ import { type PublicSymbolSet = Set; interface ModuleDirectoryInfo { - moduleDirectory: string; + moduleDirectory: Uri; isModuleSingleFile: boolean; } @@ -83,8 +78,8 @@ export class PackageTypeVerifier { private _packageName: string, private _ignoreExternal = false ) { - const host = new FullAccessHost(_serviceProvider.fs()); - this._configOptions = new ConfigOptions(''); + const host = new FullAccessHost(_serviceProvider); + this._configOptions = new ConfigOptions(Uri.empty()); this._configOptions.defaultPythonPlatform = commandLineOptions.pythonPlatform; this._configOptions.defaultPythonVersion = commandLineOptions.pythonVersion; @@ -99,11 +94,11 @@ export class PackageTypeVerifier { this._configOptions.evaluateUnknownImportsAsAny = true; } - this._execEnv = this._configOptions.findExecEnvironment('.'); + this._execEnv = this._configOptions.findExecEnvironment(Uri.file('.', _serviceProvider.fs().isCaseSensitive)); this._importResolver = new ImportResolver( this._serviceProvider, this._configOptions, - new FullAccessHost(this._serviceProvider.fs()) + new FullAccessHost(this._serviceProvider) ); this._program = new Program(this._importResolver, this._configOptions, this._serviceProvider); } @@ -117,9 +112,9 @@ export class PackageTypeVerifier { const report = getEmptyReport( moduleNameParts[0], - packageDirectoryInfo?.moduleDirectory ?? '', + packageDirectoryInfo?.moduleDirectory ?? Uri.empty(), trimmedModuleName, - moduleDirectoryInfo?.moduleDirectory ?? '', + moduleDirectoryInfo?.moduleDirectory ?? Uri.empty(), moduleDirectoryInfo?.isModuleSingleFile ?? false, this._ignoreExternal ); @@ -134,7 +129,7 @@ export class PackageTypeVerifier { getEmptyRange() ) ); - } else if (!report.moduleRootDirectory) { + } else if (!report.moduleRootDirectoryUri) { commonDiagnostics.push( new Diagnostic( DiagnosticCategory.Error, @@ -144,14 +139,14 @@ export class PackageTypeVerifier { ); } else { let pyTypedInfo: PyTypedInfo | undefined; - if (report.moduleRootDirectory) { - pyTypedInfo = this._getDeepestPyTypedInfo(report.moduleRootDirectory, moduleNameParts); + if (report.moduleRootDirectoryUri) { + pyTypedInfo = this._getDeepestPyTypedInfo(report.moduleRootDirectoryUri, moduleNameParts); } // If we couldn't find any "py.typed" info in the module path, search again // starting at the package root. - if (!pyTypedInfo && report.packageRootDirectory) { - pyTypedInfo = this._getDeepestPyTypedInfo(report.packageRootDirectory, moduleNameParts); + if (!pyTypedInfo && report.packageRootDirectoryUri) { + pyTypedInfo = this._getDeepestPyTypedInfo(report.packageRootDirectoryUri, moduleNameParts); } if (!pyTypedInfo) { @@ -159,10 +154,10 @@ export class PackageTypeVerifier { new Diagnostic(DiagnosticCategory.Error, 'No py.typed file found', getEmptyRange()) ); } else { - report.pyTypedPath = pyTypedInfo.pyTypedPath; + report.pyTypedPathUri = pyTypedInfo.pyTypedPath; const publicModules = this._getListOfPublicModules( - report.moduleRootDirectory, + report.moduleRootDirectoryUri, report.isModuleSingleFile, trimmedModuleName ); @@ -239,12 +234,12 @@ export class PackageTypeVerifier { } } - private _getDeepestPyTypedInfo(rootDirectory: string, packageNameParts: string[]) { + private _getDeepestPyTypedInfo(rootDirectory: Uri, packageNameParts: string[]) { let subNameParts = Array.from(packageNameParts); // Find the deepest py.typed file that corresponds to the requested submodule. while (subNameParts.length >= 1) { - const packageSubdir = combinePaths(rootDirectory, ...subNameParts.slice(1)); + const packageSubdir = rootDirectory.combinePaths(...subNameParts.slice(1)); const pyTypedInfo = getPyTypedInfo(this._serviceProvider.fs(), packageSubdir); if (pyTypedInfo) { return pyTypedInfo; @@ -257,7 +252,11 @@ export class PackageTypeVerifier { } private _resolveImport(moduleName: string) { - return this._importResolver.resolveImport('', this._execEnv, createImportedModuleDescriptor(moduleName)); + return this._importResolver.resolveImport( + Uri.empty(), + this._execEnv, + createImportedModuleDescriptor(moduleName) + ); } private _getPublicSymbolsForModule( @@ -268,7 +267,7 @@ export class PackageTypeVerifier { const importResult = this._resolveImport(moduleName); if (importResult.isImportFound) { - const modulePath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + const modulePath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; this._program.addTrackedFiles([modulePath], /* isThirdPartyImport */ true, /* isInPyTypedPackage */ true); const sourceFile = this._program.getBoundSourceFile(modulePath); @@ -276,7 +275,7 @@ export class PackageTypeVerifier { if (sourceFile) { const module: ModuleInfo = { name: moduleName, - path: modulePath, + uri: modulePath, isExported: true, }; @@ -379,15 +378,15 @@ export class PackageTypeVerifier { ) ); } else { - const modulePath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + const modulePath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; const module: ModuleInfo = { name: moduleName, - path: modulePath, + uri: modulePath, isExported: true, }; - report.modules.set(modulePath, module); + report.modules.set(modulePath.key, module); this._program.addTrackedFiles([modulePath], /* isThirdPartyImport */ true, /* isInPyTypedPackage */ true); const sourceFile = this._program.getBoundSourceFile(modulePath); @@ -413,9 +412,9 @@ export class PackageTypeVerifier { // Scans the directory structure for a list of public modules // within the package. - private _getListOfPublicModules(moduleRootPath: string, isModuleSingleFile: boolean, moduleName: string): string[] { + private _getListOfPublicModules(moduleRoot: Uri, isModuleSingleFile: boolean, moduleName: string): string[] { const publicModules: string[] = []; - this._addPublicModulesRecursive(moduleRootPath, isModuleSingleFile, moduleName, publicModules); + this._addPublicModulesRecursive(moduleRoot, isModuleSingleFile, moduleName, publicModules); // Make sure modules are unique. There may be duplicates if a ".py" and ".pyi" // exist for some modules. @@ -433,7 +432,7 @@ export class PackageTypeVerifier { } private _addPublicModulesRecursive( - dirPath: string, + dirPath: Uri, isModuleSingleFile: boolean, modulePath: string, publicModules: string[] @@ -444,7 +443,7 @@ export class PackageTypeVerifier { let isFile = entry.isFile(); let isDirectory = entry.isDirectory(); if (entry.isSymbolicLink()) { - const stat = tryStat(this._serviceProvider.fs(), combinePaths(dirPath, entry.name)); + const stat = tryStat(this._serviceProvider.fs(), dirPath.combinePaths(entry.name)); if (stat) { isFile = stat.isFile(); isDirectory = stat.isDirectory(); @@ -477,7 +476,7 @@ export class PackageTypeVerifier { } else if (isDirectory) { if (!isPrivateOrProtectedName(entry.name) && this._isLegalModulePartName(entry.name)) { this._addPublicModulesRecursive( - combinePaths(dirPath, entry.name), + dirPath.combinePaths(entry.name), isModuleSingleFile, `${modulePath}.${entry.name}`, publicModules @@ -572,7 +571,7 @@ export class PackageTypeVerifier { const decls = symbol.getDeclarations(); const primaryDecl = decls.length > 0 ? decls[decls.length - 1] : undefined; const declRange = primaryDecl?.range || getEmptyRange(); - const declPath = primaryDecl?.path || ''; + const declPath = primaryDecl?.uri || Uri.empty(); const symbolCategory = this._getSymbolCategory(symbol, symbolType); const isExported = publicSymbols.has(fullName); @@ -590,7 +589,7 @@ export class PackageTypeVerifier { category: symbolCategory, name, fullName, - filePath: module.path, + fileUri: Uri.file(module.path, this._serviceProvider.fs().isCaseSensitive), isExported, typeKnownStatus: TypeKnownStatus.Known, referenceCount: 1, @@ -616,7 +615,7 @@ export class PackageTypeVerifier { const decls = symbol.getDeclarations(); const primaryDecl = decls.length > 0 ? decls[decls.length - 1] : undefined; const declRange = primaryDecl?.range || getEmptyRange(); - const declPath = primaryDecl?.path || ''; + const declPath = primaryDecl?.uri || Uri.empty(); const extraInfo = new DiagnosticAddendum(); if (baseSymbolType) { @@ -664,7 +663,7 @@ export class PackageTypeVerifier { symbolInfo: SymbolInfo, type: Type, declRange: Range, - declFilePath: string, + declFileUri: Uri, publicSymbols: PublicSymbolSet, skipDocStringCheck = false ): TypeKnownStatus { @@ -677,7 +676,7 @@ export class PackageTypeVerifier { symbolInfo, `Type argument ${index + 1} for type alias "${type.typeAliasInfo!.name}" has unknown type`, declRange, - declFilePath + declFileUri ); knownStatus = TypeKnownStatus.Unknown; } else if (isPartlyUnknown(typeArg)) { @@ -687,7 +686,7 @@ export class PackageTypeVerifier { type.typeAliasInfo!.name }" has partially unknown type`, declRange, - declFilePath + declFileUri ); knownStatus = TypeKnownStatus.PartiallyUnknown; } @@ -702,7 +701,7 @@ export class PackageTypeVerifier { 'Type is missing type annotation and could be inferred differently by type checkers' + ambiguousDiag.getString(), declRange, - declFilePath + declFileUri ); knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Ambiguous); } @@ -721,7 +720,7 @@ export class PackageTypeVerifier { symbolInfo.fullName }"`, declRange, - declFilePath + declFileUri ); knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown); break; @@ -736,7 +735,7 @@ export class PackageTypeVerifier { symbolInfo, subtype, declRange, - declFilePath, + declFileUri, publicSymbols ) ); @@ -753,7 +752,7 @@ export class PackageTypeVerifier { symbolInfo, overload, declRange, - declFilePath, + declFileUri, publicSymbols ) ); @@ -771,7 +770,7 @@ export class PackageTypeVerifier { publicSymbols, symbolInfo, declRange, - declFilePath, + declFileUri, undefined /* diag */, skipDocStringCheck ) @@ -821,7 +820,7 @@ export class PackageTypeVerifier { symbolInfo, accessType, getEmptyRange(), - '', + Uri.empty(), publicSymbols, skipDocStringCheck ) @@ -847,7 +846,7 @@ export class PackageTypeVerifier { symbolInfo, `Type argument ${index + 1} for class "${type.details.name}" has unknown type`, declRange, - declFilePath + declFileUri ); knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown); } else if (isPartlyUnknown(typeArg)) { @@ -859,7 +858,7 @@ export class PackageTypeVerifier { type.details.name }" has partially unknown type` + diag.getString(), declRange, - declFilePath + declFileUri ); knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.PartiallyUnknown); } @@ -877,7 +876,7 @@ export class PackageTypeVerifier { symbolInfo, `Module "${moduleSymbol.fullName}" is partially unknown`, declRange, - declFilePath + declFileUri ); knownStatus = this._updateKnownStatusIfWorse(knownStatus, moduleSymbol.typeKnownStatus); } @@ -899,15 +898,15 @@ export class PackageTypeVerifier { publicSymbols: PublicSymbolSet, symbolInfo?: SymbolInfo, declRange?: Range, - declFilePath?: string, + declFileUri?: Uri, diag?: DiagnosticAddendum, skipDocStringCheck = false ): TypeKnownStatus { let knownStatus = TypeKnownStatus.Known; // If the file path wasn't provided, try to get it from the type. - if (type.details.declaration && !declFilePath) { - declFilePath = type.details.declaration.path; + if (type.details.declaration && !declFileUri) { + declFileUri = type.details.declaration.uri; } type.details.parameters.forEach((param, index) => { @@ -929,7 +928,7 @@ export class PackageTypeVerifier { symbolInfo, `Type annotation for parameter "${param.name}" is missing`, declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } diag?.createAddendum().addMessage(`Type annotation for parameter "${param.name}" is missing`); @@ -941,7 +940,7 @@ export class PackageTypeVerifier { symbolInfo, `Type of parameter "${param.name}" is unknown`, declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); diag?.createAddendum().addMessage(`Type of parameter "${param.name}" is unknown`); } @@ -963,7 +962,7 @@ export class PackageTypeVerifier { symbolInfo, `Type of parameter "${param.name}" is partially unknown` + extraInfo.getString(), declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } @@ -986,7 +985,7 @@ export class PackageTypeVerifier { symbolInfo, `Return type is unknown`, declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } knownStatus = this._updateKnownStatusIfWorse(knownStatus, TypeKnownStatus.Unknown); @@ -1009,7 +1008,7 @@ export class PackageTypeVerifier { symbolInfo, `Return type is partially unknown` + extraInfo.getString(), declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } @@ -1030,7 +1029,7 @@ export class PackageTypeVerifier { symbolInfo, `Return type annotation is missing`, declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } diag?.createAddendum().addMessage(`Return type annotation is missing`); @@ -1060,7 +1059,7 @@ export class PackageTypeVerifier { symbolInfo, `No docstring found for function "${symbolInfo.fullName}"`, declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } @@ -1074,7 +1073,7 @@ export class PackageTypeVerifier { symbolInfo, `One or more default values in function "${symbolInfo.fullName}" is specified as "..."`, declRange ?? getEmptyRange(), - declFilePath ?? '' + declFileUri ?? Uri.empty() ); } @@ -1100,7 +1099,7 @@ export class PackageTypeVerifier { category: SymbolCategory.Class, name: type.details.name, fullName: type.details.fullName, - filePath: type.details.filePath, + fileUri: type.details.fileUri, isExported: publicSymbols.has(type.details.fullName), typeKnownStatus: TypeKnownStatus.Known, referenceCount: 1, @@ -1116,7 +1115,7 @@ export class PackageTypeVerifier { symbolInfo, `No docstring found for class "${type.details.fullName}"`, getEmptyRange(), - '' + Uri.empty() ); report.missingClassDocStringCount++; @@ -1155,7 +1154,7 @@ export class PackageTypeVerifier { // Add information for the metaclass. if (type.details.effectiveMetaclass) { if (!isInstantiableClass(type.details.effectiveMetaclass)) { - this._addSymbolError(symbolInfo, `Type of metaclass unknown`, getEmptyRange(), ''); + this._addSymbolError(symbolInfo, `Type of metaclass unknown`, getEmptyRange(), Uri.empty()); symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse( symbolInfo.typeKnownStatus, TypeKnownStatus.PartiallyUnknown @@ -1175,7 +1174,7 @@ export class PackageTypeVerifier { `Type of metaclass "${type.details.effectiveMetaclass}" is partially unknown` + diag.getString(), getEmptyRange(), - '' + Uri.empty() ); symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse( symbolInfo.typeKnownStatus, @@ -1188,7 +1187,7 @@ export class PackageTypeVerifier { // Add information for base classes. type.details.baseClasses.forEach((baseClass) => { if (!isInstantiableClass(baseClass)) { - this._addSymbolError(symbolInfo, `Type of base class unknown`, getEmptyRange(), ''); + this._addSymbolError(symbolInfo, `Type of base class unknown`, getEmptyRange(), Uri.empty()); symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse( symbolInfo.typeKnownStatus, TypeKnownStatus.PartiallyUnknown @@ -1208,7 +1207,7 @@ export class PackageTypeVerifier { symbolInfo, `Type of base class "${baseClass.details.fullName}" is partially unknown` + diag.getString(), getEmptyRange(), - '' + Uri.empty() ); symbolInfo.typeKnownStatus = this._updateKnownStatusIfWorse( @@ -1238,7 +1237,7 @@ export class PackageTypeVerifier { category: SymbolCategory.Module, name: type.moduleName, fullName: type.moduleName, - filePath: type.filePath, + fileUri: type.fileUri, isExported: publicSymbols.has(type.moduleName), typeKnownStatus: TypeKnownStatus.Known, referenceCount: 1, @@ -1448,19 +1447,21 @@ export class PackageTypeVerifier { private _getDirectoryInfoForModule(moduleName: string): ModuleDirectoryInfo | undefined { const importResult = this._importResolver.resolveImport( - '', + Uri.empty(), this._execEnv, createImportedModuleDescriptor(moduleName) ); if (importResult.isImportFound) { - const resolvedPath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + const resolvedPath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; // If it's a namespace package with no __init__.py(i), use the package // directory instead. - const moduleDirectory = resolvedPath ? getDirectoryPath(resolvedPath) : importResult.packageDirectory ?? ''; + const moduleDirectory = resolvedPath + ? resolvedPath.getDirectory() + : importResult.packageDirectory ?? Uri.empty(); let isModuleSingleFile = false; - if (resolvedPath && stripFileExtension(getFileName(resolvedPath)) !== '__init__') { + if (resolvedPath && stripFileExtension(resolvedPath.fileName) !== '__init__') { isModuleSingleFile = true; } @@ -1508,17 +1509,17 @@ export class PackageTypeVerifier { report.symbols.set(symbolInfo.fullName, symbolInfo); } - private _addSymbolError(symbolInfo: SymbolInfo, message: string, declRange: Range, declFilePath: string) { + private _addSymbolError(symbolInfo: SymbolInfo, message: string, declRange: Range, declUri: Uri) { symbolInfo.diagnostics.push({ diagnostic: new Diagnostic(DiagnosticCategory.Error, message, declRange), - filePath: declFilePath, + uri: declUri, }); } - private _addSymbolWarning(symbolInfo: SymbolInfo, message: string, declRange: Range, declFilePath: string) { + private _addSymbolWarning(symbolInfo: SymbolInfo, message: string, declRange: Range, declUri: Uri) { symbolInfo.diagnostics.push({ diagnostic: new Diagnostic(DiagnosticCategory.Warning, message, declRange), - filePath: declFilePath, + uri: declUri, }); } diff --git a/packages/pyright-internal/src/analyzer/parentDirectoryCache.ts b/packages/pyright-internal/src/analyzer/parentDirectoryCache.ts index 522b8118b..04e9fd42e 100644 --- a/packages/pyright-internal/src/analyzer/parentDirectoryCache.ts +++ b/packages/pyright-internal/src/analyzer/parentDirectoryCache.ts @@ -9,46 +9,46 @@ import { getOrAdd } from '../common/collectionUtils'; import { FileSystem } from '../common/fileSystem'; -import { ensureTrailingDirectorySeparator, normalizePath, realCasePath } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; import { ImportResult } from './importResult'; -export type ImportPath = { importPath: string | undefined }; +export type ImportPath = { importPath: Uri | undefined }; -type CacheEntry = { importResult: ImportResult; path: string; importName: string }; +type CacheEntry = { importResult: ImportResult; path: Uri; importName: string }; export class ParentDirectoryCache { private readonly _importChecked = new Map>(); private readonly _cachedResults = new Map>(); - private _libPathCache: string[] | undefined = undefined; + private _libPathCache: Uri[] | undefined = undefined; - constructor(private _importRootGetter: () => string[]) { + constructor(private _importRootGetter: () => Uri[]) { // empty } - getImportResult(path: string, importName: string, importResult: ImportResult): ImportResult | undefined { - const result = this._cachedResults.get(importName)?.get(path); + getImportResult(path: Uri, importName: string, importResult: ImportResult): ImportResult | undefined { + const result = this._cachedResults.get(importName)?.get(path.key); if (result) { // We already checked for the importName at the path. // Return the result if succeeded otherwise, return regular import result given. return result ?? importResult; } - const checked = this._importChecked.get(importName)?.get(path); + const checked = this._importChecked.get(importName)?.get(path.key); if (checked) { // We already checked for the importName at the path. if (!checked.importPath) { return importResult; } - return this._cachedResults.get(importName)?.get(checked.importPath) ?? importResult; + return this._cachedResults.get(importName)?.get(checked.importPath.key) ?? importResult; } return undefined; } - checkValidPath(fs: FileSystem, sourceFilePath: string, root: string): boolean { - if (!sourceFilePath.startsWith(root)) { + checkValidPath(fs: FileSystem, sourceFileUri: Uri, root: Uri): boolean { + if (!sourceFileUri.startsWith(root)) { // We don't search containing folders for libs. return false; } @@ -56,11 +56,11 @@ export class ParentDirectoryCache { this._libPathCache = this._libPathCache ?? this._importRootGetter() - .map((r) => ensureTrailingDirectorySeparator(realCasePath(normalizePath(r), fs))) + .map((r) => fs.realCasePath(r)) .filter((r) => r !== root) .filter((r) => r.startsWith(root)); - if (this._libPathCache.some((p) => sourceFilePath.startsWith(p))) { + if (this._libPathCache.some((p) => sourceFileUri.startsWith(p))) { // Make sure it is not lib folders under user code root. // ex) .venv folder return false; @@ -69,13 +69,13 @@ export class ParentDirectoryCache { return true; } - checked(path: string, importName: string, importPath: ImportPath) { - getOrAdd(this._importChecked, importName, () => new Map()).set(path, importPath); + checked(path: Uri, importName: string, importPath: ImportPath) { + getOrAdd(this._importChecked, importName, () => new Map()).set(path.key, importPath); } add(result: CacheEntry) { getOrAdd(this._cachedResults, result.importName, () => new Map()).set( - result.path, + result.path.key, result.importResult ); } diff --git a/packages/pyright-internal/src/analyzer/parseTreeUtils.ts b/packages/pyright-internal/src/analyzer/parseTreeUtils.ts index 956cd59a8..04a4e088b 100644 --- a/packages/pyright-internal/src/analyzer/parseTreeUtils.ts +++ b/packages/pyright-internal/src/analyzer/parseTreeUtils.ts @@ -2718,7 +2718,7 @@ export function getScopeIdForNode(node: ParseNode): string { } const fileInfo = AnalyzerNodeInfo.getFileInfo(node); - return `${fileInfo.filePath}.${node.start.toString()}-${name}`; + return `${fileInfo.fileUri.key}.${node.start.toString()}-${name}`; } // Walks up the parse tree and finds all scopes that can provide diff --git a/packages/pyright-internal/src/analyzer/program.ts b/packages/pyright-internal/src/analyzer/program.ts index bb88c7c42..127efbcec 100644 --- a/packages/pyright-internal/src/analyzer/program.ts +++ b/packages/pyright-internal/src/analyzer/program.ts @@ -21,21 +21,14 @@ import { FileDiagnostics } from '../common/diagnosticSink'; import { FileEditAction } from '../common/editAction'; import { EditableProgram, ProgramView } from '../common/extensibility'; import { LogTracker } from '../common/logTracker'; -import { - combinePaths, - getDirectoryPath, - getFileName, - getRelativePath, - makeDirectories, - normalizePath, - stripFileExtension, -} from '../common/pathUtils'; import { convertRangeToTextRange } from '../common/positionUtils'; import { ServiceProvider } from '../common/serviceProvider'; import '../common/serviceProviderExtensions'; import { ServiceKeys } from '../common/serviceProviderExtensions'; import { Range, doRangesIntersect } from '../common/textRange'; import { Duration, timingStats } from '../common/timing'; +import { Uri } from '../common/uri/uri'; +import { makeDirectories } from '../common/uri/uriUtils'; import { ParseResults } from '../parser/parser'; import { AbsoluteModuleDescriptor, ImportLookupResult, LookupImportOptions } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; @@ -73,7 +66,7 @@ export interface MaxAnalysisTime { } interface UpdateImportInfo { - path: string; + path: Uri; isTypeshedFile: boolean; isThirdPartyImport: boolean; isPyTypedPresent: boolean; @@ -84,14 +77,13 @@ export type PreCheckCallback = (parseResults: ParseResults, evaluator: TypeEvalu export interface ISourceFileFactory { createSourceFile( serviceProvider: ServiceProvider, - filePath: string, + fileUri: Uri, moduleName: string, isThirdPartyImport: boolean, isThirdPartyPyTypedPresent: boolean, editMode: SourceFileEditMode, console?: ConsoleInterface, logTracker?: LogTracker, - realFilePath?: string, ipythonMode?: IPythonMode ): SourceFile; } @@ -105,8 +97,7 @@ export namespace ISourceFileFactory { export interface OpenFileOptions { isTracked: boolean; ipythonMode: IPythonMode; - chainedFilePath: string | undefined; - realFilePath: string | undefined; + chainedFileUri: Uri | undefined; } // Track edit mode related information. @@ -195,7 +186,7 @@ export class Program { return this._console; } - get rootPath(): string { + get rootPath(): Uri { return this._configOptions.projectRoot; } @@ -241,7 +232,7 @@ export class Program { if (newContents) { // Create a text document so we can compute the edits. const textDocument = TextDocument.create( - fileInfo.sourceFile.getFilePath(), + fileInfo.sourceFile.getUri().toString(), 'python', 1, fileInfo.sourceFile.getFileContent() || '' @@ -249,7 +240,7 @@ export class Program { // Add an edit action to the list. edits.push({ - filePath: fileInfo.sourceFile.getFilePath(), + fileUri: fileInfo.sourceFile.getUri(), range: { start: { line: 0, character: 0 }, end: { line: textDocument.lineCount, character: 0 }, @@ -268,7 +259,7 @@ export class Program { // We don't need to care about file diagnostics since in edit mode // checker won't run. v.sourceFile.prepareForClose(); - this._removeSourceFileFromListAndMap(v.sourceFile.getFilePath(), i); + this._removeSourceFileFromListAndMap(v.sourceFile.getUri(), i); } } } @@ -299,26 +290,26 @@ export class Program { } // Sets the list of tracked files that make up the program. - setTrackedFiles(filePaths: string[]): FileDiagnostics[] { + setTrackedFiles(fileUris: Uri[]): FileDiagnostics[] { if (this._sourceFileList.length > 0) { // We need to determine which files to remove from the existing file list. - const newFileMap = new Map(); - filePaths.forEach((path) => { - newFileMap.set(path, path); + const newFileMap = new Map(); + fileUris.forEach((path) => { + newFileMap.set(path.key, path); }); // Files that are not in the tracked file list are // marked as no longer tracked. this._sourceFileList.forEach((oldFile) => { - const filePath = oldFile.sourceFile.getFilePath(); - if (!newFileMap.has(filePath)) { + const fileUri = oldFile.sourceFile.getUri(); + if (!newFileMap.has(fileUri.key)) { oldFile.isTracked = false; } }); } // Add the new files. Only the new items will be added. - this.addTrackedFiles(filePaths); + this.addTrackedFiles(fileUris); return this._removeUnneededFiles(); } @@ -338,25 +329,25 @@ export class Program { this._allowedThirdPartyImports = importNames; } - addTrackedFiles(filePaths: string[], isThirdPartyImport = false, isInPyTypedPackage = false) { - filePaths.forEach((filePath) => { - this.addTrackedFile(filePath, isThirdPartyImport, isInPyTypedPackage); + addTrackedFiles(fileUris: Uri[], isThirdPartyImport = false, isInPyTypedPackage = false) { + fileUris.forEach((fileUri) => { + this.addTrackedFile(fileUri, isThirdPartyImport, isInPyTypedPackage); }); } - addInterimFile(filePath: string): SourceFileInfo { + addInterimFile(fileUri: Uri): SourceFileInfo { // Double check not already there. - let fileInfo = this.getSourceFileInfo(filePath); + let fileInfo = this.getSourceFileInfo(fileUri); if (!fileInfo) { - fileInfo = this._createInterimFileInfo(filePath); + fileInfo = this._createInterimFileInfo(fileUri); this._addToSourceFileListAndMap(fileInfo); } return fileInfo; } - addTrackedFile(filePath: string, isThirdPartyImport = false, isInPyTypedPackage = false): SourceFile { - let sourceFileInfo = this.getSourceFileInfo(filePath); - const moduleImportInfo = this._getModuleImportInfoForFile(filePath); + addTrackedFile(fileUri: Uri, isThirdPartyImport = false, isInPyTypedPackage = false): SourceFile { + let sourceFileInfo = this.getSourceFileInfo(fileUri); + const moduleImportInfo = this._getModuleImportInfoForFile(fileUri); const importName = moduleImportInfo.moduleName; if (sourceFileInfo) { @@ -369,7 +360,7 @@ export class Program { const sourceFile = this._sourceFileFactory.createSourceFile( this.serviceProvider, - filePath, + fileUri, importName, isThirdPartyImport, isInPyTypedPackage, @@ -391,23 +382,22 @@ export class Program { return sourceFile; } - setFileOpened(filePath: string, version: number | null, contents: string, options?: OpenFileOptions) { - let sourceFileInfo = this.getSourceFileInfo(filePath); + setFileOpened(fileUri: Uri, version: number | null, contents: string, options?: OpenFileOptions) { + let sourceFileInfo = this.getSourceFileInfo(fileUri); if (!sourceFileInfo) { - const moduleImportInfo = this._getModuleImportInfoForFile(filePath); + const moduleImportInfo = this._getModuleImportInfoForFile(fileUri); const sourceFile = this._sourceFileFactory.createSourceFile( this.serviceProvider, - filePath, + fileUri, moduleImportInfo.moduleName, /* isThirdPartyImport */ false, moduleImportInfo.isThirdPartyPyTypedPresent, this._editModeTracker, this._console, this._logTracker, - options?.realFilePath, options?.ipythonMode ?? IPythonMode.None ); - const chainedFilePath = options?.chainedFilePath; + const chainedFilePath = options?.chainedFileUri; sourceFileInfo = new SourceFileInfo( sourceFile, /* isTypeshedFile */ false, @@ -435,26 +425,26 @@ export class Program { sourceFileInfo.sourceFile.setClientVersion(version, contents); } - getChainedFilePath(filePath: string): string | undefined { - const sourceFileInfo = this.getSourceFileInfo(filePath); - return sourceFileInfo?.chainedSourceFile?.sourceFile.getFilePath(); + getChainedUri(fileUri: Uri): Uri | undefined { + const sourceFileInfo = this.getSourceFileInfo(fileUri); + return sourceFileInfo?.chainedSourceFile?.sourceFile.getUri(); } - updateChainedFilePath(filePath: string, chainedFilePath: string | undefined) { - const sourceFileInfo = this.getSourceFileInfo(filePath); + updateChainedUri(fileUri: Uri, chainedFileUri: Uri | undefined) { + const sourceFileInfo = this.getSourceFileInfo(fileUri); if (!sourceFileInfo) { return; } - sourceFileInfo.chainedSourceFile = chainedFilePath ? this.getSourceFileInfo(chainedFilePath) : undefined; + sourceFileInfo.chainedSourceFile = chainedFileUri ? this.getSourceFileInfo(chainedFileUri) : undefined; sourceFileInfo.sourceFile.markDirty(); this._markFileDirtyRecursive(sourceFileInfo, new Set()); verifyNoCyclesInChainedFiles(this, sourceFileInfo); } - setFileClosed(filePath: string, isTracked?: boolean): FileDiagnostics[] { - const sourceFileInfo = this.getSourceFileInfo(filePath); + setFileClosed(fileUri: Uri, isTracked?: boolean): FileDiagnostics[] { + const sourceFileInfo = this.getSourceFileInfo(fileUri); if (sourceFileInfo) { sourceFileInfo.isOpenByClient = false; sourceFileInfo.isTracked = isTracked ?? sourceFileInfo.isTracked; @@ -493,12 +483,12 @@ export class Program { } } - markFilesDirty(filePaths: string[], evenIfContentsAreSame: boolean) { + markFilesDirty(fileUris: Uri[], evenIfContentsAreSame: boolean) { const markDirtySet = new Set(); - filePaths.forEach((filePath) => { - const sourceFileInfo = this.getSourceFileInfo(filePath); + fileUris.forEach((fileUri) => { + const sourceFileInfo = this.getSourceFileInfo(fileUri); if (sourceFileInfo) { - const fileName = getFileName(filePath); + const fileName = fileUri.fileName; // Handle builtins and __builtins__ specially. They are implicitly // included by all source files. @@ -576,9 +566,9 @@ export class Program { return this._configOptions.functionSignatureDisplay; } - containsSourceFileIn(folder: string): boolean { - for (const normalizedSourceFilePath of this._sourceFileMap.keys()) { - if (normalizedSourceFilePath.startsWith(folder)) { + containsSourceFileIn(folder: Uri): boolean { + for (const normalizedSourceFilePath of this._sourceFileMap.values()) { + if (normalizedSourceFilePath.sourceFile.getUri().startsWith(folder)) { return true; } } @@ -586,19 +576,19 @@ export class Program { return false; } - owns(filePath: string) { - const fileInfo = this.getSourceFileInfo(filePath); + owns(uri: Uri) { + const fileInfo = this.getSourceFileInfo(uri); if (fileInfo) { // If we already determined whether the file is tracked or not, don't do it again. // This will make sure we have consistent look at the state once it is loaded to the memory. return fileInfo.isTracked; } - return matchFileSpecs(this._configOptions, filePath); + return matchFileSpecs(this._configOptions, uri); } - getSourceFile(filePath: string): SourceFile | undefined { - const sourceFileInfo = this.getSourceFileInfo(filePath); + getSourceFile(uri: Uri): SourceFile | undefined { + const sourceFileInfo = this.getSourceFileInfo(uri); if (!sourceFileInfo) { return undefined; } @@ -606,20 +596,23 @@ export class Program { return sourceFileInfo.sourceFile; } - getBoundSourceFile(filePath: string): SourceFile | undefined { - return this.getBoundSourceFileInfo(filePath)?.sourceFile; + getBoundSourceFile(uri: Uri): SourceFile | undefined { + return this.getBoundSourceFileInfo(uri)?.sourceFile; } getSourceFileInfoList(): readonly SourceFileInfo[] { return this._sourceFileList; } - getSourceFileInfo(filePath: string): SourceFileInfo | undefined { - return this._sourceFileMap.get(filePath); + getSourceFileInfo(uri: Uri): SourceFileInfo | undefined { + if (!uri.isEmpty()) { + return this._sourceFileMap.get(uri.key); + } + return undefined; } - getBoundSourceFileInfo(filePath: string, content?: string, force?: boolean): SourceFileInfo | undefined { - const sourceFileInfo = this.getSourceFileInfo(filePath); + getBoundSourceFileInfo(uri: Uri, content?: string, force?: boolean): SourceFileInfo | undefined { + const sourceFileInfo = this.getSourceFileInfo(uri); if (!sourceFileInfo) { return undefined; } @@ -685,9 +678,9 @@ export class Program { // Performs parsing and analysis of a single file in the program. If the file is not part of // the program returns false to indicate analysis was not performed. - analyzeFile(filePath: string, token: CancellationToken = CancellationToken.None): boolean { + analyzeFile(fileUri: Uri, token: CancellationToken = CancellationToken.None): boolean { return this._runEvaluatorWithCancellationToken(token, () => { - const sourceFileInfo = this.getSourceFileInfo(filePath); + const sourceFileInfo = this.getSourceFileInfo(fileUri); if (sourceFileInfo && this._checkTypes(sourceFileInfo, token)) { return true; } @@ -710,19 +703,19 @@ export class Program { } getSourceMapper( - filePath: string, + fileUri: Uri, token: CancellationToken, mapCompiled?: boolean, preferStubs?: boolean ): SourceMapper { - const sourceFileInfo = this.getSourceFileInfo(filePath); - const execEnv = this._configOptions.findExecEnvironment(filePath); + const sourceFileInfo = this.getSourceFileInfo(fileUri); + const execEnv = this._configOptions.findExecEnvironment(fileUri); return this._createSourceMapper(execEnv, token, sourceFileInfo, mapCompiled, preferStubs); } - getParseResults(filePath: string): ParseResults | undefined { + getParseResults(fileUri: Uri): ParseResults | undefined { return this.getBoundSourceFileInfo( - filePath, + fileUri, /* content */ undefined, /* force */ true )?.sourceFile.getParseResults(); @@ -746,41 +739,39 @@ export class Program { sortedFiles.forEach((sfInfo) => { const checkTimeInMs = sfInfo.sourceFile.getCheckTime()!; - this._console.info(`${checkTimeInMs}ms: ${sfInfo.sourceFile.getFilePath()}`); + this._console.info(`${checkTimeInMs}ms: ${sfInfo.sourceFile.getUri()}`); }); } // Prints import dependency information for each of the files in // the program, skipping any typeshed files. - printDependencies(projectRootDir: string, verbose: boolean) { + printDependencies(projectRootDir: Uri, verbose: boolean) { const fs = this._importResolver.fileSystem; const sortedFiles = this._sourceFileList .filter((s) => !s.isTypeshedFile) .sort((a, b) => { - return fs.getOriginalFilePath(a.sourceFile.getFilePath()) < - fs.getOriginalFilePath(b.sourceFile.getFilePath()) - ? 1 - : -1; + return fs.getOriginalUri(a.sourceFile.getUri()) < fs.getOriginalUri(b.sourceFile.getUri()) ? 1 : -1; }); const zeroImportFiles: SourceFile[] = []; sortedFiles.forEach((sfInfo) => { this._console.info(''); - let filePath = fs.getOriginalFilePath(sfInfo.sourceFile.getFilePath()); - const relPath = getRelativePath(filePath, projectRootDir); + const fileUri = fs.getOriginalUri(sfInfo.sourceFile.getUri()); + let fileString = fileUri.toString(); + const relPath = projectRootDir.getRelativePathComponents(fileUri); if (relPath) { - filePath = relPath; + fileString = relPath.join('/'); } - this._console.info(`${filePath}`); + this._console.info(`${fileString}`); this._console.info( ` Imports ${sfInfo.imports.length} ` + `file${sfInfo.imports.length === 1 ? '' : 's'}` ); if (verbose) { sfInfo.imports.forEach((importInfo) => { - this._console.info(` ${fs.getOriginalFilePath(importInfo.sourceFile.getFilePath())}`); + this._console.info(` ${fs.getOriginalUri(importInfo.sourceFile.getUri())}`); }); } @@ -789,7 +780,7 @@ export class Program { ); if (verbose) { sfInfo.importedBy.forEach((importInfo) => { - this._console.info(` ${fs.getOriginalFilePath(importInfo.sourceFile.getFilePath())}`); + this._console.info(` ${fs.getOriginalUri(importInfo.sourceFile.getUri())}`); }); } @@ -804,33 +795,33 @@ export class Program { `${zeroImportFiles.length} file${zeroImportFiles.length === 1 ? '' : 's'}` + ` not explicitly imported` ); zeroImportFiles.forEach((importFile) => { - this._console.info(` ${fs.getOriginalFilePath(importFile.getFilePath())}`); + this._console.info(` ${fs.getOriginalUri(importFile.getUri())}`); }); } } - writeTypeStub(targetImportPath: string, targetIsSingleFile: boolean, stubPath: string, token: CancellationToken) { + writeTypeStub(targetImportPath: Uri, targetIsSingleFile: boolean, stubPath: Uri, token: CancellationToken) { for (const sourceFileInfo of this._sourceFileList) { throwIfCancellationRequested(token); - const filePath = sourceFileInfo.sourceFile.getFilePath(); + const fileUri = sourceFileInfo.sourceFile.getUri(); // Generate type stubs only for the files within the target path, // not any files that the target module happened to import. - const relativePath = getRelativePath(filePath, targetImportPath); + const relativePath = targetImportPath.getRelativePath(fileUri); if (relativePath !== undefined) { - let typeStubPath = normalizePath(combinePaths(stubPath, relativePath)); + let typeStubPath = stubPath.combinePaths(relativePath); // If the target is a single file implementation, as opposed to // a package in a directory, transform the name of the type stub // to __init__.pyi because we're placing it in a directory. if (targetIsSingleFile) { - typeStubPath = combinePaths(getDirectoryPath(typeStubPath), '__init__.pyi'); + typeStubPath = typeStubPath.getDirectory().combinePaths('__init__.pyi'); } else { - typeStubPath = stripFileExtension(typeStubPath) + '.pyi'; + typeStubPath = typeStubPath.replaceExtension('.pyi'); } - const typeStubDir = getDirectoryPath(typeStubPath); + const typeStubDir = typeStubPath.getDirectory(); try { makeDirectories(this.fileSystem, typeStubDir, stubPath); @@ -867,8 +858,8 @@ export class Program { return evaluator.printType(type, options); } - getTextOnRange(filePath: string, range: Range, token: CancellationToken): string | undefined { - const sourceFileInfo = this.getSourceFileInfo(filePath); + getTextOnRange(fileUri: Uri, range: Range, token: CancellationToken): string | undefined { + const sourceFileInfo = this.getSourceFileInfo(fileUri); if (!sourceFileInfo) { return undefined; } @@ -904,7 +895,7 @@ export class Program { ); if (diagnostics !== undefined) { fileDiagnostics.push({ - filePath: sourceFileInfo.sourceFile.getFilePath(), + fileUri: sourceFileInfo.sourceFile.getUri(), version: sourceFileInfo.sourceFile.getClientVersion(), diagnostics, }); @@ -921,7 +912,7 @@ export class Program { // This condition occurs when the user switches from workspace to // "open files only" mode. Clear all diagnostics for this file. fileDiagnostics.push({ - filePath: sourceFileInfo.sourceFile.getFilePath(), + fileUri: sourceFileInfo.sourceFile.getUri(), version: sourceFileInfo.sourceFile.getClientVersion(), diagnostics: [], }); @@ -932,8 +923,8 @@ export class Program { return fileDiagnostics; } - getDiagnosticsForRange(filePath: string, range: Range): Diagnostic[] { - const sourceFile = this.getSourceFile(filePath); + getDiagnosticsForRange(fileUri: Uri, range: Range): Diagnostic[] { + const sourceFile = this.getSourceFile(fileUri); if (!sourceFile) { return []; } @@ -958,7 +949,7 @@ export class Program { // Cloned program will use whatever user files the program currently has. const userFiles = this.getUserFiles(); - program.setTrackedFiles(userFiles.map((i) => i.sourceFile.getFilePath())); + program.setTrackedFiles(userFiles.map((i) => i.sourceFile.getUri())); program.markAllFilesDirty(/* evenIfContentsAreSame */ true); // Make sure we keep editor content (open file) which could be different than one in the file system. @@ -969,14 +960,13 @@ export class Program { } program.setFileOpened( - fileInfo.sourceFile.getFilePath(), + fileInfo.sourceFile.getUri(), version, fileInfo.sourceFile.getOpenFileContents() ?? '', { - chainedFilePath: fileInfo.chainedSourceFile?.sourceFile.getFilePath(), + chainedFileUri: fileInfo.chainedSourceFile?.sourceFile.getUri(), ipythonMode: fileInfo.sourceFile.getIPythonMode(), isTracked: fileInfo.isTracked, - realFilePath: fileInfo.sourceFile.getRealFilePath(), } ); } @@ -1071,14 +1061,14 @@ export class Program { // Clear only if there are any errors for this file. if (fileInfo.diagnosticsVersion !== undefined) { fileDiagnostics.push({ - filePath: fileInfo.sourceFile.getFilePath(), + fileUri: fileInfo.sourceFile.getUri(), version: fileInfo.sourceFile.getClientVersion(), diagnostics: [], }); } fileInfo.sourceFile.prepareForClose(); - this._removeSourceFileFromListAndMap(fileInfo.sourceFile.getFilePath(), i); + this._removeSourceFileFromListAndMap(fileInfo.sourceFile.getUri(), i); // Unlink any imports and remove them from the list if // they are no longer referenced. @@ -1099,14 +1089,14 @@ export class Program { // Clear if there are any errors for this import. if (importedFile.diagnosticsVersion !== undefined) { fileDiagnostics.push({ - filePath: importedFile.sourceFile.getFilePath(), + fileUri: importedFile.sourceFile.getUri(), version: importedFile.sourceFile.getClientVersion(), diagnostics: [], }); } importedFile.sourceFile.prepareForClose(); - this._removeSourceFileFromListAndMap(importedFile.sourceFile.getFilePath(), indexToRemove); + this._removeSourceFileFromListAndMap(importedFile.sourceFile.getUri(), indexToRemove); i--; } } @@ -1122,7 +1112,7 @@ export class Program { // out the errors for the now-closed file. if (!this._shouldCheckFile(fileInfo) && fileInfo.diagnosticsVersion !== undefined) { fileDiagnostics.push({ - filePath: fileInfo.sourceFile.getFilePath(), + fileUri: fileInfo.sourceFile.getUri(), version: fileInfo.sourceFile.getClientVersion(), diagnostics: [], }); @@ -1165,14 +1155,14 @@ export class Program { return true; } - const filePath = fileInfo.sourceFile.getFilePath(); + const fileUri = fileInfo.sourceFile.getUri(); // Avoid infinite recursion. - if (recursionSet.has(filePath)) { + if (recursionSet.has(fileUri.key)) { return false; } - recursionSet.add(filePath); + recursionSet.add(fileUri.key); for (const importerInfo of fileInfo.importedBy) { if (this._isImportNeededRecursive(importerInfo, recursionSet)) { @@ -1194,16 +1184,16 @@ export class Program { this._importResolver, execEnv, this._evaluator!, - (stubFilePath: string, implFilePath: string) => { - let stubFileInfo = this.getSourceFileInfo(stubFilePath); + (stubFileUri: Uri, implFileUri: Uri) => { + let stubFileInfo = this.getSourceFileInfo(stubFileUri); if (!stubFileInfo) { // Special case for import statement like "import X.Y". The SourceFile // for X might not be in memory since import `X.Y` only brings in Y. - stubFileInfo = this.addInterimFile(stubFilePath); + stubFileInfo = this.addInterimFile(stubFileUri); } - this._addShadowedFile(stubFileInfo, implFilePath); - return this.getBoundSourceFile(implFilePath); + this._addShadowedFile(stubFileInfo, implFileUri); + return this.getBoundSourceFile(implFileUri); }, (f) => { let fileInfo = this.getBoundSourceFileInfo(f); @@ -1296,6 +1286,10 @@ export class Program { return true; } + private _getSourceFileInfoFromKey(key: string) { + return this._sourceFileMap.get(key); + } + private _updateSourceFileImports(sourceFileInfo: SourceFileInfo, options: ConfigOptions): SourceFileInfo[] { const filesAdded: SourceFileInfo[] = []; @@ -1338,9 +1332,9 @@ export class Program { if (sourceFileInfo.chainedSourceFile.sourceFile.isFileDeleted()) { sourceFileInfo.chainedSourceFile = undefined; } else { - const filePath = sourceFileInfo.chainedSourceFile.sourceFile.getFilePath(); - newImportPathMap.set(filePath, { - path: filePath, + const fileUri = sourceFileInfo.chainedSourceFile.sourceFile.getUri(); + newImportPathMap.set(fileUri.key, { + path: fileUri, isTypeshedFile: false, isThirdPartyImport: false, isPyTypedPresent: false, @@ -1351,12 +1345,12 @@ export class Program { imports.forEach((importResult) => { if (importResult.isImportFound) { if (this._isImportAllowed(sourceFileInfo, importResult, importResult.isStubFile)) { - if (importResult.resolvedPaths.length > 0) { - const filePath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; - if (filePath) { + if (importResult.resolvedUris.length > 0) { + const fileUri = importResult.resolvedUris[importResult.resolvedUris.length - 1]; + if (!fileUri.isEmpty()) { const thirdPartyTypeInfo = getThirdPartyImportInfo(importResult); - newImportPathMap.set(filePath, { - path: filePath, + newImportPathMap.set(fileUri.key, { + path: fileUri, isTypeshedFile: !!importResult.isStdlibTypeshedFile || !!importResult.isThirdPartyTypeshedFile, isThirdPartyImport: thirdPartyTypeInfo.isThirdPartyImport, @@ -1370,8 +1364,8 @@ export class Program { if (this._isImportAllowed(sourceFileInfo, importResult, implicitImport.isStubFile)) { if (!implicitImport.isNativeLib) { const thirdPartyTypeInfo = getThirdPartyImportInfo(importResult); - newImportPathMap.set(implicitImport.path, { - path: implicitImport.path, + newImportPathMap.set(implicitImport.uri.key, { + path: implicitImport.uri, isTypeshedFile: !!importResult.isStdlibTypeshedFile || !!importResult.isThirdPartyTypeshedFile, isThirdPartyImport: thirdPartyTypeInfo.isThirdPartyImport, @@ -1391,7 +1385,7 @@ export class Program { if (options.verboseOutput) { this._console.info( `Could not resolve source for '${importResult.importName}' ` + - `in file '${sourceFileInfo.sourceFile.getFilePath()}'` + `in file '${sourceFileInfo.sourceFile.getUri().toUserVisibleString()}'` ); if (importResult.nonStubImportResult.importFailureInfo) { @@ -1405,7 +1399,7 @@ export class Program { } else if (options.verboseOutput) { this._console.info( `Could not import '${importResult.importName}' ` + - `in file '${sourceFileInfo.sourceFile.getFilePath()}'` + `in file '${sourceFileInfo.sourceFile.getUri().toUserVisibleString()}'` ); if (importResult.importFailureInfo) { importResult.importFailureInfo.forEach((diag) => { @@ -1417,17 +1411,17 @@ export class Program { const updatedImportMap = new Map(); sourceFileInfo.imports.forEach((importInfo) => { - const oldFilePath = importInfo.sourceFile.getFilePath(); + const oldFilePath = importInfo.sourceFile.getUri(); // A previous import was removed. - if (!newImportPathMap.has(oldFilePath)) { + if (!newImportPathMap.has(oldFilePath.key)) { importInfo.mutate((s) => { s.importedBy = s.importedBy.filter( - (fi) => fi.sourceFile.getFilePath() !== sourceFileInfo.sourceFile.getFilePath() + (fi) => !fi.sourceFile.getUri().equals(sourceFileInfo.sourceFile.getUri()) ); }); } else { - updatedImportMap.set(oldFilePath, importInfo); + updatedImportMap.set(oldFilePath.key, importInfo); } }); @@ -1469,9 +1463,9 @@ export class Program { // Update the imports list. It should now map the set of imports // specified by the source file. sourceFileInfo.mutate((s) => (s.imports = [])); - newImportPathMap.forEach((_, path) => { - if (this.getSourceFileInfo(path)) { - sourceFileInfo.mutate((s) => s.imports.push(this.getSourceFileInfo(path)!)); + newImportPathMap.forEach((_, key) => { + if (this._getSourceFileInfoFromKey(key)) { + sourceFileInfo.mutate((s) => s.imports.push(this._getSourceFileInfoFromKey(key)!)); } }); @@ -1480,26 +1474,29 @@ export class Program { sourceFileInfo.builtinsImport = undefined; const builtinsImport = sourceFileInfo.sourceFile.getBuiltinsImport(); if (builtinsImport && builtinsImport.isImportFound) { - const resolvedBuiltinsPath = builtinsImport.resolvedPaths[builtinsImport.resolvedPaths.length - 1]; + const resolvedBuiltinsPath = builtinsImport.resolvedUris[builtinsImport.resolvedUris.length - 1]; sourceFileInfo.builtinsImport = this.getSourceFileInfo(resolvedBuiltinsPath); } return filesAdded; } - private _removeSourceFileFromListAndMap(filePath: string, indexToRemove: number) { - this._sourceFileMap.delete(filePath); + private _removeSourceFileFromListAndMap(fileUri: Uri, indexToRemove: number) { + this._sourceFileMap.delete(fileUri.key); this._sourceFileList.splice(indexToRemove, 1); } private _addToSourceFileListAndMap(fileInfo: SourceFileInfo) { - const filePath = fileInfo.sourceFile.getFilePath(); + const fileUri = fileInfo.sourceFile.getUri(); // We should never add a file with the same path twice. - assert(!this._sourceFileMap.has(filePath)); + assert(!this._sourceFileMap.has(fileUri.key)); + + // We should never have an empty URI for a source file. + assert(!fileInfo.sourceFile.getUri().isEmpty()); this._sourceFileList.push(fileInfo); - this._sourceFileMap.set(filePath, fileInfo); + this._sourceFileMap.set(fileUri.key, fileInfo); } private static _getPrintTypeFlags(configOptions: ConfigOptions): PrintTypeFlags { @@ -1528,7 +1525,7 @@ export class Program { return flags; } - private _getModuleImportInfoForFile(filePath: string) { + private _getModuleImportInfoForFile(fileUri: Uri) { // We allow illegal module names (e.g. names that include "-" in them) // because we want a unique name for each module even if it cannot be // imported through an "import" statement. It's important to have a @@ -1536,7 +1533,7 @@ export class Program { // name. The type checker uses the fully-qualified (unique) module name // to differentiate between such types. const moduleNameAndType = this._importResolver.getModuleNameForImport( - filePath, + fileUri, this._configOptions.getDefaultExecEnvironment(), /* allowIllegalModuleName */ true, /* detectPyTyped */ true @@ -1549,7 +1546,7 @@ export class Program { // it "shadows" a type stub file for purposes of finding doc strings and definitions. // We need to track the relationship so if the original type stub is removed from the // program, we can remove the corresponding shadowed file and any files it imports. - private _addShadowedFile(stubFile: SourceFileInfo, shadowImplPath: string): SourceFile { + private _addShadowedFile(stubFile: SourceFileInfo, shadowImplPath: Uri): SourceFile { let shadowFileInfo = this.getSourceFileInfo(shadowImplPath); if (!shadowFileInfo) { @@ -1567,11 +1564,11 @@ export class Program { return shadowFileInfo.sourceFile; } - private _createInterimFileInfo(filePath: string) { - const moduleImportInfo = this._getModuleImportInfoForFile(filePath); + private _createInterimFileInfo(fileUri: Uri) { + const moduleImportInfo = this._getModuleImportInfoForFile(fileUri); const sourceFile = this._sourceFileFactory.createSourceFile( this.serviceProvider, - filePath, + fileUri, moduleImportInfo.moduleName, /* isThirdPartyImport */ false, /* isInPyTypedPackage */ false, @@ -1673,8 +1670,8 @@ export class Program { let nextImplicitImport = this._getImplicitImports(fileToAnalyze); while (nextImplicitImport) { - const implicitPath = nextImplicitImport.sourceFile.getFilePath(); - if (implicitSet.has(implicitPath)) { + const implicitPath = nextImplicitImport.sourceFile.getUri(); + if (implicitSet.has(implicitPath.key)) { // We've found a cycle. Break out of the loop. debug.fail( this.serviceProvider @@ -1683,7 +1680,7 @@ export class Program { ); } - implicitSet.add(implicitPath); + implicitSet.add(implicitPath.key); implicitImports.push(nextImplicitImport); this._parseFile(nextImplicitImport, /* content */ undefined, skipFileNeededCheck); @@ -1773,27 +1770,27 @@ export class Program { } private _lookUpImport = ( - filePathOrModule: string | AbsoluteModuleDescriptor, + fileUriOrModule: Uri | AbsoluteModuleDescriptor, options?: LookupImportOptions ): ImportLookupResult | undefined => { let sourceFileInfo: SourceFileInfo | undefined; - if (typeof filePathOrModule === 'string') { - sourceFileInfo = this.getSourceFileInfo(filePathOrModule); + if (Uri.isUri(fileUriOrModule)) { + sourceFileInfo = this.getSourceFileInfo(fileUriOrModule); } else { // Resolve the import. const importResult = this._importResolver.resolveImport( - filePathOrModule.importingFilePath, - this._configOptions.findExecEnvironment(filePathOrModule.importingFilePath), + fileUriOrModule.importingFileUri, + this._configOptions.findExecEnvironment(fileUriOrModule.importingFileUri), { leadingDots: 0, - nameParts: filePathOrModule.nameParts, + nameParts: fileUriOrModule.nameParts, importedSymbols: undefined, } ); - if (importResult.isImportFound && !importResult.isNativeLib && importResult.resolvedPaths.length > 0) { - const resolvedPath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + if (importResult.isImportFound && !importResult.isNativeLib && importResult.resolvedUris.length > 0) { + const resolvedPath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; if (resolvedPath) { // See if the source file already exists in the program. sourceFileInfo = this.getSourceFileInfo(resolvedPath); @@ -1872,7 +1869,7 @@ export class Program { } private _checkTypes(fileToCheck: SourceFileInfo, token: CancellationToken, chainedByList?: SourceFileInfo[]) { - return this._logTracker.log(`analyzing: ${fileToCheck.sourceFile.getFilePath()}`, (logState) => { + return this._logTracker.log(`analyzing: ${fileToCheck.sourceFile.getUri()}`, (logState) => { // If the file isn't needed because it was eliminated from the // transitive closure or deleted, skip the file rather than wasting // time on it. @@ -1911,7 +1908,7 @@ export class Program { } if (boundFile) { - const execEnv = this._configOptions.findExecEnvironment(fileToCheck.sourceFile.getFilePath()); + const execEnv = this._configOptions.findExecEnvironment(fileToCheck.sourceFile.getUri()); fileToCheck.sourceFile.check( this.configOptions, this._importResolver, @@ -2028,8 +2025,8 @@ export class Program { ) { // If the file is already in the closure map, we found a cyclical // dependency. Don't recur further. - const filePath = file.sourceFile.getFilePath(); - if (closureMap.has(filePath)) { + const fileUri = file.sourceFile.getUri(); + if (closureMap.has(fileUri.key)) { return; } @@ -2041,7 +2038,7 @@ export class Program { } // Add the file to the closure map. - closureMap.set(filePath, file); + closureMap.set(fileUri.key, file); // If this file hasn't already been parsed, parse it now. This will // discover any files it imports. Skip this if the file is part @@ -2074,13 +2071,13 @@ export class Program { return false; } - const filePath = sourceFileInfo.sourceFile.getFilePath(); + const fileUri = sourceFileInfo.sourceFile.getUri(); - filesVisited.set(filePath, sourceFileInfo); + filesVisited.set(fileUri.key, sourceFileInfo); let detectedCycle = false; - if (dependencyMap.has(filePath)) { + if (dependencyMap.has(fileUri.key)) { // We detect a cycle (partial or full). A full cycle is one that is // rooted in the file at the start of our dependency chain. A partial // cycle loops back on some other file in the dependency chain. We @@ -2097,7 +2094,7 @@ export class Program { } else { // If we've already checked this dependency along // some other path, we can skip it. - if (dependencyMap.has(filePath)) { + if (dependencyMap.has(fileUri.key)) { return false; } @@ -2105,7 +2102,7 @@ export class Program { // (for ordering information). Set the dependency map // entry to true to indicate that we're actively exploring // that dependency. - dependencyMap.set(filePath, true); + dependencyMap.set(fileUri.key, true); dependencyChain.push(sourceFileInfo); for (const imp of sourceFileInfo.imports) { @@ -2116,7 +2113,7 @@ export class Program { // Set the dependencyMap entry to false to indicate that we have // already explored this file and don't need to explore it again. - dependencyMap.set(filePath, false); + dependencyMap.set(fileUri.key, false); dependencyChain.pop(); } @@ -2126,7 +2123,7 @@ export class Program { private _logImportCycle(dependencyChain: SourceFileInfo[]) { const circDep = new CircularDependency(); dependencyChain.forEach((sourceFileInfo) => { - circDep.appendPath(sourceFileInfo.sourceFile.getFilePath()); + circDep.appendPath(sourceFileInfo.sourceFile.getUri()); }); circDep.normalizeOrder(); @@ -2137,15 +2134,15 @@ export class Program { } private _markFileDirtyRecursive(sourceFileInfo: SourceFileInfo, markSet: Set, forceRebinding = false) { - const filePath = sourceFileInfo.sourceFile.getFilePath(); + const fileUri = sourceFileInfo.sourceFile.getUri(); // Don't mark it again if it's already been visited. - if (markSet.has(filePath)) { + if (markSet.has(fileUri.key)) { return; } sourceFileInfo.sourceFile.markReanalysisRequired(forceRebinding); - markSet.add(filePath); + markSet.add(fileUri.key); sourceFileInfo.importedBy.forEach((dep) => { // Changes on chained source file can change symbols in the symbol table and diff --git a/packages/pyright-internal/src/analyzer/properties.ts b/packages/pyright-internal/src/analyzer/properties.ts index e4dbaed6f..10e79af39 100644 --- a/packages/pyright-internal/src/analyzer/properties.ts +++ b/packages/pyright-internal/src/analyzer/properties.ts @@ -70,7 +70,7 @@ export function createProperty( decoratorType.details.name, getClassFullName(decoratorNode, fileInfo.moduleName, `__property_${fget.details.name}`), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.PropertyClass | ClassTypeFlags.BuiltInClass, typeSourceId, /* declaredMetaclass */ undefined, @@ -171,7 +171,7 @@ export function clonePropertyWithSetter( classType.details.name, classType.details.fullName, classType.details.moduleName, - getFileInfo(errorNode).filePath, + getFileInfo(errorNode).fileUri, flagsToClone, classType.details.typeSourceId, classType.details.declaredMetaclass, @@ -228,7 +228,7 @@ export function clonePropertyWithDeleter( classType.details.name, classType.details.fullName, classType.details.moduleName, - getFileInfo(errorNode).filePath, + getFileInfo(errorNode).fileUri, classType.details.flags, classType.details.typeSourceId, classType.details.declaredMetaclass, diff --git a/packages/pyright-internal/src/analyzer/pyTypedUtils.ts b/packages/pyright-internal/src/analyzer/pyTypedUtils.ts index 22378333e..f58fb30c8 100644 --- a/packages/pyright-internal/src/analyzer/pyTypedUtils.ts +++ b/packages/pyright-internal/src/analyzer/pyTypedUtils.ts @@ -8,24 +8,23 @@ */ import { FileSystem } from '../common/fileSystem'; -import { combinePaths, isDirectory, isFile } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; +import { isDirectory, isFile } from '../common/uri/uriUtils'; export interface PyTypedInfo { - pyTypedPath: string; + pyTypedPath: Uri; isPartiallyTyped: boolean; } -const _pyTypedFileName = 'py.typed'; - -export function getPyTypedInfo(fileSystem: FileSystem, dirPath: string): PyTypedInfo | undefined { +export function getPyTypedInfo(fileSystem: FileSystem, dirPath: Uri): PyTypedInfo | undefined { if (!fileSystem.existsSync(dirPath) || !isDirectory(fileSystem, dirPath)) { return undefined; } let isPartiallyTyped = false; - const pyTypedPath = combinePaths(dirPath, _pyTypedFileName); + const pyTypedPath = dirPath.pytypedUri; - if (!fileSystem.existsSync(dirPath) || !isFile(fileSystem, pyTypedPath)) { + if (!fileSystem.existsSync(pyTypedPath) || !isFile(fileSystem, pyTypedPath)) { return undefined; } diff --git a/packages/pyright-internal/src/analyzer/pythonPathUtils.ts b/packages/pyright-internal/src/analyzer/pythonPathUtils.ts index 51f559603..13bd97e52 100644 --- a/packages/pyright-internal/src/analyzer/pythonPathUtils.ts +++ b/packages/pyright-internal/src/analyzer/pythonPathUtils.ts @@ -12,43 +12,32 @@ import { compareComparableValues } from '../common/core'; import { FileSystem } from '../common/fileSystem'; import { Host } from '../common/host'; import * as pathConsts from '../common/pathConsts'; -import { - combinePaths, - containsPath, - ensureTrailingDirectorySeparator, - getDirectoryPath, - getFileSystemEntries, - isDirectory, - normalizePath, - tryStat, -} from '../common/pathUtils'; -import { versionToString } from '../common/pythonVersion'; -import { PythonVersion } from '../common/pythonVersion'; +import { PythonVersion, versionToString } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; +import { getFileSystemEntries, isDirectory, tryStat } from '../common/uri/uriUtils'; export interface PythonPathResult { - paths: string[]; - prefix: string; + paths: Uri[]; + prefix: Uri | undefined; } export const stdLibFolderName = 'stdlib'; export const thirdPartyFolderName = 'stubs'; export function getTypeShedFallbackPath(fs: FileSystem) { - let moduleDirectory = fs.getModulePath(); - if (!moduleDirectory) { + const moduleDirectory = fs.getModulePath(); + if (!moduleDirectory || moduleDirectory.isEmpty()) { return undefined; } - moduleDirectory = getDirectoryPath(ensureTrailingDirectorySeparator(normalizePath(moduleDirectory))); - - const typeshedPath = combinePaths(moduleDirectory, pathConsts.typeshedFallback); + const typeshedPath = moduleDirectory.combinePaths(pathConsts.typeshedFallback); if (fs.existsSync(typeshedPath)) { return fs.realCasePath(typeshedPath); } // In the debug version of Pyright, the code is one level // deeper, so we need to look one level up for the typeshed fallback. - const debugTypeshedPath = combinePaths(getDirectoryPath(moduleDirectory), pathConsts.typeshedFallback); + const debugTypeshedPath = moduleDirectory.getDirectory().combinePaths(pathConsts.typeshedFallback); if (fs.existsSync(debugTypeshedPath)) { return fs.realCasePath(debugTypeshedPath); } @@ -56,8 +45,8 @@ export function getTypeShedFallbackPath(fs: FileSystem) { return undefined; } -export function getTypeshedSubdirectory(typeshedPath: string, isStdLib: boolean) { - return combinePaths(typeshedPath, isStdLib ? stdLibFolderName : thirdPartyFolderName); +export function getTypeshedSubdirectory(typeshedPath: Uri, isStdLib: boolean) { + return typeshedPath.combinePaths(isStdLib ? stdLibFolderName : thirdPartyFolderName); } export function findPythonSearchPaths( @@ -66,21 +55,21 @@ export function findPythonSearchPaths( host: Host, importFailureInfo: string[], includeWatchPathsOnly?: boolean | undefined, - workspaceRoot?: string | undefined -): string[] | undefined { + workspaceRoot?: Uri | undefined +): Uri[] | undefined { importFailureInfo.push('Finding python search paths'); if (configOptions.venvPath !== undefined && configOptions.venv) { const venvDir = configOptions.venv; - const venvPath = combinePaths(configOptions.venvPath, venvDir); + const venvPath = configOptions.venvPath.combinePaths(venvDir); - const foundPaths: string[] = []; - const sitePackagesPaths: string[] = []; + const foundPaths: Uri[] = []; + const sitePackagesPaths: Uri[] = []; [pathConsts.lib, pathConsts.lib64, pathConsts.libAlternate].forEach((libPath) => { const sitePackagesPath = findSitePackagesPath( fs, - combinePaths(venvPath, libPath), + venvPath.combinePaths(libPath), configOptions.defaultPythonVersion, importFailureInfo ); @@ -115,11 +104,7 @@ export function findPythonSearchPaths( const pathResult = host.getPythonSearchPaths(configOptions.pythonPath, importFailureInfo); if (includeWatchPathsOnly && workspaceRoot) { const paths = pathResult.paths - .filter( - (p) => - !containsPath(workspaceRoot, p, /* ignoreCase */ true) || - containsPath(pathResult.prefix, p, /* ignoreCase */ true) - ) + .filter((p) => !p.startsWith(workspaceRoot) || p.startsWith(pathResult.prefix)) .map((p) => fs.realCasePath(p)); return paths; @@ -135,10 +120,10 @@ export function isPythonBinary(p: string): boolean { function findSitePackagesPath( fs: FileSystem, - libPath: string, + libPath: Uri, pythonVersion: PythonVersion | undefined, importFailureInfo: string[] -): string | undefined { +): Uri | undefined { if (fs.existsSync(libPath)) { importFailureInfo.push(`Found path '${libPath}'; looking for ${pathConsts.sitePackages}`); } else { @@ -146,7 +131,7 @@ function findSitePackagesPath( return undefined; } - const sitePackagesPath = combinePaths(libPath, pathConsts.sitePackages); + const sitePackagesPath = libPath.combinePaths(pathConsts.sitePackages); if (fs.existsSync(sitePackagesPath)) { importFailureInfo.push(`Found path '${sitePackagesPath}'`); return sitePackagesPath; @@ -160,8 +145,8 @@ function findSitePackagesPath( // Candidate directories start with "python3.". const candidateDirs = entries.directories.filter((dirName) => { - if (dirName.startsWith('python3.')) { - const dirPath = combinePaths(libPath, dirName, pathConsts.sitePackages); + if (dirName.fileName.startsWith('python3.')) { + const dirPath = dirName.combinePaths(pathConsts.sitePackages); return fs.existsSync(dirPath); } return false; @@ -170,9 +155,11 @@ function findSitePackagesPath( // If there is a python3.X directory (where 3.X matches the configured python // version), prefer that over other python directories. if (pythonVersion) { - const preferredDir = candidateDirs.find((dirName) => dirName === `python${versionToString(pythonVersion)}`); + const preferredDir = candidateDirs.find( + (dirName) => dirName.fileName === `python${versionToString(pythonVersion)}` + ); if (preferredDir) { - const dirPath = combinePaths(libPath, preferredDir, pathConsts.sitePackages); + const dirPath = preferredDir.combinePaths(pathConsts.sitePackages); importFailureInfo.push(`Found path '${dirPath}'`); return dirPath; } @@ -182,7 +169,7 @@ function findSitePackagesPath( // first directory that starts with "python". Most of the time, there will be // only one. if (candidateDirs.length > 0) { - const dirPath = combinePaths(libPath, candidateDirs[0], pathConsts.sitePackages); + const dirPath = candidateDirs[0].combinePaths(pathConsts.sitePackages); importFailureInfo.push(`Found path '${dirPath}'`); return dirPath; } @@ -190,8 +177,8 @@ function findSitePackagesPath( return undefined; } -export function getPathsFromPthFiles(fs: FileSystem, parentDir: string): string[] { - const searchPaths: string[] = []; +export function getPathsFromPthFiles(fs: FileSystem, parentDir: Uri): Uri[] { + const searchPaths: Uri[] = []; // Get a list of all *.pth files within the specified directory. const pthFiles = fs @@ -200,7 +187,7 @@ export function getPathsFromPthFiles(fs: FileSystem, parentDir: string): string[ .sort((a, b) => compareComparableValues(a.name, b.name)); pthFiles.forEach((pthFile) => { - const filePath = fs.realCasePath(combinePaths(parentDir, pthFile.name)); + const filePath = fs.realCasePath(parentDir.combinePaths(pthFile.name)); const fileStats = tryStat(fs, filePath); // Skip all files that are much larger than expected. @@ -210,7 +197,7 @@ export function getPathsFromPthFiles(fs: FileSystem, parentDir: string): string[ lines.forEach((line) => { const trimmedLine = line.trim(); if (trimmedLine.length > 0 && !trimmedLine.startsWith('#') && !trimmedLine.match(/^import\s/)) { - const pthPath = combinePaths(parentDir, trimmedLine); + const pthPath = parentDir.combinePaths(trimmedLine); if (fs.existsSync(pthPath) && isDirectory(fs, pthPath)) { searchPaths.push(fs.realCasePath(pthPath)); } @@ -222,8 +209,8 @@ export function getPathsFromPthFiles(fs: FileSystem, parentDir: string): string[ return searchPaths; } -function addPathIfUnique(pathList: string[], pathToAdd: string) { - if (!pathList.some((path) => path === pathToAdd)) { +function addPathIfUnique(pathList: Uri[], pathToAdd: Uri) { + if (!pathList.some((path) => path.key === pathToAdd.key)) { pathList.push(pathToAdd); return true; } diff --git a/packages/pyright-internal/src/analyzer/service.ts b/packages/pyright-internal/src/analyzer/service.ts index a4eb1953a..e531b4a7c 100644 --- a/packages/pyright-internal/src/analyzer/service.ts +++ b/packages/pyright-internal/src/analyzer/service.ts @@ -24,32 +24,24 @@ import { FileSystem } from '../common/fileSystem'; import { FileWatcher, FileWatcherEventType, ignoredWatchEventFunction } from '../common/fileWatcher'; import { Host, HostFactory, NoAccessHost } from '../common/host'; import { defaultStubsDirectory } from '../common/pathConsts'; -import { - FileSpec, - combinePaths, - containsPath, - forEachAncestorDirectory, - getDirectoryPath, - getFileName, - getFileSpec, - getFileSystemEntries, - getPathComponents, - hasPythonExtension, - isDirectory, - isFile, - isFileSystemCaseSensitive, - makeDirectories, - normalizePath, - normalizeSlashes, - realCasePath, - stripFileExtension, - tryRealpath, - tryStat, -} from '../common/pathUtils'; +import { isRootedDiskPath } from '../common/pathUtils'; import { ServiceProvider } from '../common/serviceProvider'; import { ServiceKeys } from '../common/serviceProviderExtensions'; import { Range } from '../common/textRange'; import { timingStats } from '../common/timing'; +import { Uri } from '../common/uri/uri'; +import { + FileSpec, + forEachAncestorDirectory, + getFileSpec, + getFileSystemEntries, + hasPythonExtension, + isDirectory, + isFile, + makeDirectories, + tryRealpath, + tryStat, +} from '../common/uri/uriUtils'; import { AnalysisCompleteCallback } from './analysis'; import { BackgroundAnalysisProgram, @@ -69,7 +61,7 @@ export const pyprojectTomlName = 'pyproject.toml'; // the analyzer on any files that have not yet been analyzed? const _userActivityBackoffTimeInMs = 250; -const _gitDirectory = normalizeSlashes('/.git/'); +const _gitDirectory = '/.git/'; export interface AnalyzerServiceOptions { console?: ConsoleInterface; @@ -99,16 +91,16 @@ export class AnalyzerService { private readonly _backgroundAnalysisProgram: BackgroundAnalysisProgram; private readonly _serviceProvider: ServiceProvider; - private _executionRootPath: string; - private _typeStubTargetPath: string | undefined; + private _executionRootUri: Uri; + private _typeStubTargetUri: Uri | undefined; private _typeStubTargetIsSingleFile = false; private _sourceFileWatcher: FileWatcher | undefined; private _reloadConfigTimer: any; private _libraryReanalysisTimer: any; - private _configFilePath: string | undefined; + private _configFileUri: Uri | undefined; private _configFileWatcher: FileWatcher | undefined; private _libraryFileWatcher: FileWatcher | undefined; - private _librarySearchPathsToWatch: string[] | undefined; + private _librarySearchUrisToWatch: Uri[] | undefined; private _onCompletionCallback: AnalysisCompleteCallback | undefined; private _commandLineOptions: CommandLineOptions | undefined; private _analyzeTimer: any; @@ -122,7 +114,7 @@ export class AnalyzerService { constructor(instanceName: string, serviceProvider: ServiceProvider, options: AnalyzerServiceOptions) { this._instanceName = instanceName; - this._executionRootPath = ''; + this._executionRootUri = Uri.empty(); this._options = options; this._options.serviceId = this._options.serviceId ?? getNextServiceId(instanceName); @@ -143,7 +135,9 @@ export class AnalyzerService { this._options.cancellationProvider = options.cancellationProvider ?? new DefaultCancellationProvider(); this._options.hostFactory = options.hostFactory ?? (() => new NoAccessHost()); - this._options.configOptions = options.configOptions ?? new ConfigOptions(process.cwd()); + this._options.configOptions = + options.configOptions ?? + new ConfigOptions(Uri.file(process.cwd(), this._serviceProvider.fs().isCaseSensitive)); const importResolver = this._options.importResolverFactory( this._serviceProvider, this._options.configOptions, @@ -183,8 +177,8 @@ export class AnalyzerService { return this._options.cancellationProvider!; } - get librarySearchPathsToWatch() { - return this._librarySearchPathsToWatch; + get librarySearchUrisToWatch() { + return this._librarySearchUrisToWatch; } get backgroundAnalysisProgram(): BackgroundAnalysisProgram { @@ -223,12 +217,11 @@ export class AnalyzerService { const version = fileInfo.sourceFile.getClientVersion(); if (version !== undefined) { service.setFileOpened( - fileInfo.sourceFile.getFilePath(), + fileInfo.sourceFile.getUri(), version, fileInfo.sourceFile.getOpenFileContents()!, fileInfo.sourceFile.getIPythonMode(), - fileInfo.chainedSourceFile?.sourceFile.getFilePath(), - fileInfo.sourceFile.getRealFilePath() + fileInfo.chainedSourceFile?.sourceFile.getUri() ); } } @@ -288,93 +281,81 @@ export class AnalyzerService { this._backgroundAnalysisProgram.setConfigOptions(configOptions); - this._executionRootPath = realCasePath( - combinePaths(commandLineOptions.executionRoot, configOptions.projectRoot), - this.fs - ); + this._executionRootUri = configOptions.projectRoot; this._applyConfigOptions(host); } - hasSourceFile(filePath: string): boolean { - return this.backgroundAnalysisProgram.hasSourceFile(filePath); + hasSourceFile(uri: Uri): boolean { + return this.backgroundAnalysisProgram.hasSourceFile(uri); } - isTracked(filePath: string): boolean { - return this._program.owns(filePath); + isTracked(uri: Uri): boolean { + return this._program.owns(uri); } getUserFiles() { - return this._program.getUserFiles().map((i) => i.sourceFile.getFilePath()); + return this._program.getUserFiles().map((i) => i.sourceFile.getUri()); } getOpenFiles() { - return this._program.getOpened().map((i) => i.sourceFile.getFilePath()); + return this._program.getOpened().map((i) => i.sourceFile.getUri()); } setFileOpened( - path: string, + uri: Uri, version: number | null, contents: string, ipythonMode = IPythonMode.None, - chainedFilePath?: string, - realFilePath?: string + chainedFileUri?: Uri ) { // Open the file. Notebook cells are always tracked as they aren't 3rd party library files. // This is how it's worked in the past since each notebook used to have its own // workspace and the workspace include setting marked all cells as tracked. - this._backgroundAnalysisProgram.setFileOpened(path, version, contents, { - isTracked: this.isTracked(path) || ipythonMode !== IPythonMode.None, + this._backgroundAnalysisProgram.setFileOpened(uri, version, contents, { + isTracked: this.isTracked(uri) || ipythonMode !== IPythonMode.None, ipythonMode, - chainedFilePath, - realFilePath, + chainedFileUri: chainedFileUri, }); this._scheduleReanalysis(/* requireTrackedFileUpdate */ false); } - getChainedFilePath(path: string): string | undefined { - return this._backgroundAnalysisProgram.getChainedFilePath(path); + getChainedUri(uri: Uri): Uri | undefined { + return this._backgroundAnalysisProgram.getChainedUri(uri); } - updateChainedFilePath(path: string, chainedFilePath: string | undefined) { - this._backgroundAnalysisProgram.updateChainedFilePath(path, chainedFilePath); + updateChainedUri(uri: Uri, chainedFileUri: Uri | undefined) { + this._backgroundAnalysisProgram.updateChainedUri(uri, chainedFileUri); this._scheduleReanalysis(/* requireTrackedFileUpdate */ false); } - updateOpenFileContents( - path: string, - version: number | null, - contents: string, - ipythonMode = IPythonMode.None, - realFilePath?: string - ) { - this._backgroundAnalysisProgram.updateOpenFileContents(path, version, contents, { - isTracked: this.isTracked(path), + updateOpenFileContents(uri: Uri, version: number | null, contents: string, ipythonMode = IPythonMode.None) { + this._backgroundAnalysisProgram.updateOpenFileContents(uri, version, contents, { + isTracked: this.isTracked(uri), ipythonMode, - chainedFilePath: undefined, - realFilePath, + chainedFileUri: undefined, }); this._scheduleReanalysis(/* requireTrackedFileUpdate */ false); } - setFileClosed(path: string, isTracked?: boolean) { - this._backgroundAnalysisProgram.setFileClosed(path, isTracked); + setFileClosed(uri: Uri, isTracked?: boolean) { + this._backgroundAnalysisProgram.setFileClosed(uri, isTracked); this._scheduleReanalysis(/* requireTrackedFileUpdate */ false); } - addInterimFile(path: string) { - this._backgroundAnalysisProgram.addInterimFile(path); + addInterimFile(uri: Uri) { + this._backgroundAnalysisProgram.addInterimFile(uri); } - getParseResult(path: string) { - return this._program.getParseResults(path); + getParseResult(uri: Uri) { + return this._program.getParseResults(uri); } - getSourceFile(path: string) { - return this._program.getBoundSourceFile(path); + getSourceFile(uri: Uri) { + return this._program.getBoundSourceFile(uri); } - getTextOnRange(filePath: string, range: Range, token: CancellationToken) { - return this._program.getTextOnRange(filePath, range, token); + getTextOnRange(fileUri: Uri, range: Range, token: CancellationToken) { + return this._program.getTextOnRange(fileUri, range, token); } getEvaluator(): TypeEvaluator | undefined { @@ -401,15 +382,15 @@ export class AnalyzerService { } printDependencies(verbose: boolean) { - this._program.printDependencies(this._executionRootPath, verbose); + this._program.printDependencies(this._executionRootUri, verbose); } - analyzeFile(filePath: string, token: CancellationToken): Promise { - return this._backgroundAnalysisProgram.analyzeFile(filePath, token); + analyzeFile(fileUri: Uri, token: CancellationToken): Promise { + return this._backgroundAnalysisProgram.analyzeFile(fileUri, token); } - getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise { - return this._backgroundAnalysisProgram.getDiagnosticsForRange(filePath, range, token); + getDiagnosticsForRange(fileUri: Uri, range: Range, token: CancellationToken): Promise { + return this._backgroundAnalysisProgram.getDiagnosticsForRange(fileUri, range, token); } getConfigOptions() { @@ -434,36 +415,36 @@ export class AnalyzerService { return this._getConfigOptions(this._backgroundAnalysisProgram.host, commandLineOptions); } - test_getFileNamesFromFileSpecs(): string[] { + test_getFileNamesFromFileSpecs(): Uri[] { return this._getFileNamesFromFileSpecs(); } - test_shouldHandleSourceFileWatchChanges(path: string, isFile: boolean) { - return this._shouldHandleSourceFileWatchChanges(path, isFile); + test_shouldHandleSourceFileWatchChanges(uri: Uri, isFile: boolean) { + return this._shouldHandleSourceFileWatchChanges(uri, isFile); } - test_shouldHandleLibraryFileWatchChanges(path: string, libSearchPaths: string[]) { - return this._shouldHandleLibraryFileWatchChanges(path, libSearchPaths); + test_shouldHandleLibraryFileWatchChanges(uri: Uri, libSearchUris: Uri[]) { + return this._shouldHandleLibraryFileWatchChanges(uri, libSearchUris); } writeTypeStub(token: CancellationToken): void { - const typingsSubdirPath = this._getTypeStubFolder(); + const typingsSubdirUri = this._getTypeStubFolder(); this._program.writeTypeStub( - this._typeStubTargetPath ?? '', + this._typeStubTargetUri ?? Uri.empty(), this._typeStubTargetIsSingleFile, - typingsSubdirPath, + typingsSubdirUri, token ); } writeTypeStubInBackground(token: CancellationToken): Promise { - const typingsSubdirPath = this._getTypeStubFolder(); + const typingsSubdirUri = this._getTypeStubFolder(); return this._backgroundAnalysisProgram.writeTypeStub( - this._typeStubTargetPath ?? '', + this._typeStubTargetUri ?? Uri.empty(), this._typeStubTargetIsSingleFile, - typingsSubdirPath, + typingsSubdirUri, token ); } @@ -527,33 +508,39 @@ export class AnalyzerService { // Calculates the effective options based on the command-line options, // an optional config file, and default values. private _getConfigOptions(host: Host, commandLineOptions: CommandLineOptions): ConfigOptions { - let projectRoot = realCasePath(commandLineOptions.executionRoot, this.fs); - let configFilePath: string | undefined; - let pyprojectFilePath: string | undefined; + let projectRoot = this.fs.realCasePath( + Uri.file(commandLineOptions.executionRoot, this.fs.isCaseSensitive, /* checkRelative */ true) + ); + const executionRoot = this.fs.realCasePath( + Uri.file(commandLineOptions.executionRoot, this.fs.isCaseSensitive, /* checkRelative */ true) + ); + let configFilePath: Uri | undefined; + let pyprojectFilePath: Uri | undefined; if (commandLineOptions.configFilePath) { // If the config file path was specified, determine whether it's // a directory (in which case the default config file name is assumed) // or a file. - configFilePath = realCasePath( - combinePaths(commandLineOptions.executionRoot, normalizePath(commandLineOptions.configFilePath)), - this.fs + configFilePath = this.fs.realCasePath( + isRootedDiskPath(commandLineOptions.configFilePath) + ? Uri.file(commandLineOptions.configFilePath, this.fs.isCaseSensitive, /* checkRelative */ true) + : projectRoot.combinePaths(commandLineOptions.configFilePath) ); if (!this.fs.existsSync(configFilePath)) { - this._console.info(`Configuration file not found at ${configFilePath}.`); - configFilePath = realCasePath(commandLineOptions.executionRoot, this.fs); + this._console.info(`Configuration file not found at ${configFilePath.toUserVisibleString()}.`); + configFilePath = projectRoot; } else { - if (configFilePath.toLowerCase().endsWith('.json')) { - projectRoot = realCasePath(getDirectoryPath(configFilePath), this.fs); + if (configFilePath.lastExtension.endsWith('.json')) { + projectRoot = configFilePath.getDirectory(); } else { projectRoot = configFilePath; configFilePath = this._findConfigFile(configFilePath); if (!configFilePath) { - this._console.info(`Configuration file not found at ${projectRoot}.`); + this._console.info(`Configuration file not found at ${projectRoot.toUserVisibleString()}.`); } } } - } else if (projectRoot) { + } else if (commandLineOptions.executionRoot) { // In a project-based IDE like VS Code, we should assume that the // project root directory contains the config file. configFilePath = this._findConfigFile(projectRoot); @@ -566,7 +553,7 @@ export class AnalyzerService { } if (configFilePath) { - projectRoot = getDirectoryPath(configFilePath); + projectRoot = configFilePath.getDirectory(); } else { this._console.log(`No configuration file found.`); configFilePath = undefined; @@ -582,8 +569,8 @@ export class AnalyzerService { } if (pyprojectFilePath) { - projectRoot = getDirectoryPath(pyprojectFilePath); - this._console.log(`pyproject.toml file found at ${projectRoot}.`); + projectRoot = pyprojectFilePath.getDirectory(); + this._console.log(`pyproject.toml file found at ${projectRoot.toUserVisibleString()}.`); } else { this._console.log(`No pyproject.toml file found.`); } @@ -596,7 +583,9 @@ export class AnalyzerService { this._console.info( `Setting pythonPath for service "${this._instanceName}": ` + `"${commandLineOptions.pythonPath}"` ); - (configOptions.pythonPath = commandLineOptions.pythonPath), this.fs; + configOptions.pythonPath = this.fs.realCasePath( + Uri.file(commandLineOptions.pythonPath, this.fs.isCaseSensitive, /* checkRelative */ true) + ); } if (commandLineOptions.pythonEnvironmentName) { @@ -619,19 +608,19 @@ export class AnalyzerService { if (commandLineOptions.includeFileSpecs.length > 0) { commandLineOptions.includeFileSpecs.forEach((fileSpec) => { - configOptions.include.push(getFileSpec(this.serviceProvider, projectRoot, fileSpec)); + configOptions.include.push(getFileSpec(projectRoot, fileSpec)); }); } if (commandLineOptions.excludeFileSpecs.length > 0) { commandLineOptions.excludeFileSpecs.forEach((fileSpec) => { - configOptions.exclude.push(getFileSpec(this.serviceProvider, projectRoot, fileSpec)); + configOptions.exclude.push(getFileSpec(projectRoot, fileSpec)); }); } if (commandLineOptions.ignoreFileSpecs.length > 0) { commandLineOptions.ignoreFileSpecs.forEach((fileSpec) => { - configOptions.ignore.push(getFileSpec(this.serviceProvider, projectRoot, fileSpec)); + configOptions.ignore.push(getFileSpec(projectRoot, fileSpec)); }); } @@ -640,28 +629,26 @@ export class AnalyzerService { // If no config file was found and there are no explicit include // paths specified, assume the caller wants to include all source // files under the execution root path. - configOptions.include.push(getFileSpec(this.serviceProvider, commandLineOptions.executionRoot, '.')); + configOptions.include.push(getFileSpec(executionRoot, '.')); } if (commandLineOptions.excludeFileSpecs.length === 0) { // Add a few common excludes to avoid long scan times. defaultExcludes.forEach((exclude) => { - configOptions.exclude.push( - getFileSpec(this.serviceProvider, commandLineOptions.executionRoot, exclude) - ); + configOptions.exclude.push(getFileSpec(executionRoot, exclude)); }); } } - this._configFilePath = configFilePath || pyprojectFilePath; + this._configFileUri = configFilePath || pyprojectFilePath; // If we found a config file, parse it to compute the effective options. let configJsonObj: object | undefined; if (configFilePath) { - this._console.info(`Loading configuration file at ${configFilePath}`); + this._console.info(`Loading configuration file at ${configFilePath.toUserVisibleString()}`); configJsonObj = this._parseJsonConfigFile(configFilePath); } else if (pyprojectFilePath) { - this._console.info(`Loading pyproject.toml file at ${pyprojectFilePath}`); + this._console.info(`Loading pyproject.toml file at ${pyprojectFilePath.toUserVisibleString()}`); configJsonObj = this._parsePyprojectTomlFile(pyprojectFilePath); } @@ -674,20 +661,20 @@ export class AnalyzerService { commandLineOptions.diagnosticSeverityOverrides ); - const configFileDir = getDirectoryPath(this._configFilePath!); + const configFileDir = this._configFileUri!.getDirectory(); // If no include paths were provided, assume that all files within // the project should be included. if (configOptions.include.length === 0) { - this._console.info(`No include entries specified; assuming ${configFileDir}`); - configOptions.include.push(getFileSpec(this.serviceProvider, configFileDir, '.')); + this._console.info(`No include entries specified; assuming ${configFileDir.toUserVisibleString()}`); + configOptions.include.push(getFileSpec(configFileDir, '.')); } // If there was no explicit set of excludes, add a few common ones to avoid long scan times. if (configOptions.exclude.length === 0) { defaultExcludes.forEach((exclude) => { this._console.info(`Auto-excluding ${exclude}`); - configOptions.exclude.push(getFileSpec(this.serviceProvider, configFileDir, exclude)); + configOptions.exclude.push(getFileSpec(configFileDir, exclude)); }); if (configOptions.autoExcludeVenv === undefined) { @@ -709,7 +696,9 @@ export class AnalyzerService { if (commandLineOptions.includeFileSpecsOverride) { configOptions.include = []; commandLineOptions.includeFileSpecsOverride.forEach((include) => { - configOptions.include.push(getFileSpec(this.serviceProvider, include, '.')); + configOptions.include.push( + getFileSpec(Uri.file(include, this.fs.isCaseSensitive, /* checkRelative */ true), '.') + ); }); } @@ -729,17 +718,17 @@ export class AnalyzerService { // duplicates. if (commandLineOptions.venvPath) { if (!configOptions.venvPath) { - configOptions.venvPath = commandLineOptions.venvPath; + configOptions.venvPath = projectRoot.combinePaths(commandLineOptions.venvPath); } else { - reportDuplicateSetting('venvPath', configOptions.venvPath); + reportDuplicateSetting('venvPath', configOptions.venvPath.toUserVisibleString()); } } if (commandLineOptions.typeshedPath) { if (!configOptions.typeshedPath) { - configOptions.typeshedPath = realCasePath(commandLineOptions.typeshedPath, this.fs); + configOptions.typeshedPath = projectRoot.combinePaths(commandLineOptions.typeshedPath); } else { - reportDuplicateSetting('typeshedPath', configOptions.typeshedPath); + reportDuplicateSetting('typeshedPath', configOptions.typeshedPath.toUserVisibleString()); } } @@ -759,9 +748,7 @@ export class AnalyzerService { this._console.info(`Excluding typeshed stdlib stubs according to VERSIONS file:`); excludeList.forEach((exclude) => { this._console.info(` ${exclude}`); - configOptions.exclude.push( - getFileSpec(this.serviceProvider, commandLineOptions.executionRoot, exclude) - ); + configOptions.exclude.push(getFileSpec(executionRoot, exclude.getFilePath())); }); } @@ -788,9 +775,9 @@ export class AnalyzerService { if (commandLineOptions.stubPath) { if (!configOptions.stubPath) { - configOptions.stubPath = realCasePath(commandLineOptions.stubPath, this.fs); + configOptions.stubPath = this.fs.realCasePath(projectRoot.combinePaths(commandLineOptions.stubPath)); } else { - reportDuplicateSetting('stubPath', configOptions.stubPath); + reportDuplicateSetting('stubPath', configOptions.stubPath.toUserVisibleString()); } } @@ -801,33 +788,37 @@ export class AnalyzerService { } } else { // If no stub path was specified, use a default path. - configOptions.stubPath = normalizePath(combinePaths(configOptions.projectRoot, defaultStubsDirectory)); + configOptions.stubPath = configOptions.projectRoot.combinePaths(defaultStubsDirectory); } // Do some sanity checks on the specified settings and report missing // or inconsistent information. if (configOptions.venvPath) { if (!this.fs.existsSync(configOptions.venvPath) || !isDirectory(this.fs, configOptions.venvPath)) { - this._console.error(`venvPath ${configOptions.venvPath} is not a valid directory.`); + this._console.error( + `venvPath ${configOptions.venvPath.toUserVisibleString()} is not a valid directory.` + ); } // venvPath without venv means it won't do anything while resolveImport. // so first, try to set venv from existing configOption if it is null. if both are null, // then, resolveImport won't consider venv configOptions.venv = configOptions.venv ?? this._configOptions.venv; - if (configOptions.venv) { - const fullVenvPath = combinePaths(configOptions.venvPath, configOptions.venv); + if (configOptions.venv && configOptions.venvPath) { + const fullVenvPath = configOptions.venvPath.combinePaths(configOptions.venv); if (!this.fs.existsSync(fullVenvPath) || !isDirectory(this.fs, fullVenvPath)) { this._console.error( - `venv ${configOptions.venv} subdirectory not found in venv path ${configOptions.venvPath}.` + `venv ${ + configOptions.venv + } subdirectory not found in venv path ${configOptions.venvPath.toUserVisibleString()}.` ); } else { const importFailureInfo: string[] = []; if (findPythonSearchPaths(this.fs, configOptions, host, importFailureInfo) === undefined) { this._console.error( `site-packages directory cannot be located for venvPath ` + - `${configOptions.venvPath} and venv ${configOptions.venv}.` + `${configOptions.venvPath.toUserVisibleString()} and venv ${configOptions.venv}.` ); if (configOptions.verboseOutput) { @@ -849,7 +840,9 @@ export class AnalyzerService { if (configOptions.typeshedPath) { if (!this.fs.existsSync(configOptions.typeshedPath) || !isDirectory(this.fs, configOptions.typeshedPath)) { - this._console.error(`typeshedPath ${configOptions.typeshedPath} is not a valid directory.`); + this._console.error( + `typeshedPath ${configOptions.typeshedPath.toUserVisibleString()} is not a valid directory.` + ); } } @@ -859,9 +852,9 @@ export class AnalyzerService { private _getTypeStubFolder() { const stubPath = this._configOptions.stubPath ?? - realCasePath(combinePaths(this._configOptions.projectRoot, defaultStubsDirectory), this.fs); + this.fs.realCasePath(this._configOptions.projectRoot.combinePaths(defaultStubsDirectory)); - if (!this._typeStubTargetPath || !this._typeStubTargetImportName) { + if (!this._typeStubTargetUri || !this._typeStubTargetImportName) { const errMsg = `Import '${this._typeStubTargetImportName}'` + ` could not be resolved`; this._console.error(errMsg); throw new Error(errMsg); @@ -882,14 +875,14 @@ export class AnalyzerService { this.fs.mkdirSync(stubPath); } } catch (e: any) { - const errMsg = `Could not create typings directory '${stubPath}'`; + const errMsg = `Could not create typings directory '${stubPath.toUserVisibleString()}'`; this._console.error(errMsg); throw new Error(errMsg); } // Generate a typings subdirectory hierarchy. - const typingsSubdirPath = combinePaths(stubPath, typeStubInputTargetParts[0]); - const typingsSubdirHierarchy = combinePaths(stubPath, ...typeStubInputTargetParts); + const typingsSubdirPath = stubPath.combinePaths(typeStubInputTargetParts[0]); + const typingsSubdirHierarchy = stubPath.combinePaths(...typeStubInputTargetParts); try { // Generate a new typings subdirectory if necessary. @@ -897,7 +890,7 @@ export class AnalyzerService { makeDirectories(this.fs, typingsSubdirHierarchy, stubPath); } } catch (e: any) { - const errMsg = `Could not create typings subdirectory '${typingsSubdirHierarchy}'`; + const errMsg = `Could not create typings subdirectory '${typingsSubdirHierarchy.toUserVisibleString()}'`; this._console.error(errMsg); throw new Error(errMsg); } @@ -905,33 +898,33 @@ export class AnalyzerService { return typingsSubdirPath; } - private _findConfigFileHereOrUp(searchPath: string): string | undefined { + private _findConfigFileHereOrUp(searchPath: Uri): Uri | undefined { return forEachAncestorDirectory(searchPath, (ancestor) => this._findConfigFile(ancestor)); } - private _findConfigFile(searchPath: string): string | undefined { + private _findConfigFile(searchPath: Uri): Uri | undefined { for (const name of configFileNames) { - const fileName = combinePaths(searchPath, name); + const fileName = searchPath.combinePaths(name); if (this.fs.existsSync(fileName)) { - return realCasePath(fileName, this.fs); + return this.fs.realCasePath(fileName); } } return undefined; } - private _findPyprojectTomlFileHereOrUp(searchPath: string): string | undefined { + private _findPyprojectTomlFileHereOrUp(searchPath: Uri): Uri | undefined { return forEachAncestorDirectory(searchPath, (ancestor) => this._findPyprojectTomlFile(ancestor)); } - private _findPyprojectTomlFile(searchPath: string) { - const fileName = combinePaths(searchPath, pyprojectTomlName); + private _findPyprojectTomlFile(searchPath: Uri) { + const fileName = searchPath.combinePaths(pyprojectTomlName); if (this.fs.existsSync(fileName)) { - return realCasePath(fileName, this.fs); + return this.fs.realCasePath(fileName); } return undefined; } - private _parseJsonConfigFile(configPath: string): object | undefined { + private _parseJsonConfigFile(configPath: Uri): object | undefined { return this._attemptParseFile(configPath, (fileContents) => { const errors: JSONC.ParseError[] = []; const result = JSONC.parse(fileContents, errors, { allowTrailingComma: true }); @@ -943,7 +936,7 @@ export class AnalyzerService { }); } - private _parsePyprojectTomlFile(pyprojectPath: string): object | undefined { + private _parsePyprojectTomlFile(pyprojectPath: Uri): object | undefined { return this._attemptParseFile(pyprojectPath, (fileContents, attemptCount) => { try { const configObj = TOML.parse(fileContents); @@ -955,13 +948,15 @@ export class AnalyzerService { throw e; } - this._console.info(`Pyproject file "${pyprojectPath}" has no "[tool.pyright]" section.`); + this._console.info( + `Pyproject file "${pyprojectPath.toUserVisibleString()}" has no "[tool.pyright]" section.` + ); return undefined; }); } private _attemptParseFile( - filePath: string, + fileUri: Uri, parseCallback: (contents: string, attempt: number) => object | undefined ): object | undefined { let fileContents = ''; @@ -970,9 +965,9 @@ export class AnalyzerService { while (true) { // Attempt to read the file contents. try { - fileContents = this.fs.readFileSync(filePath, 'utf8'); + fileContents = this.fs.readFileSync(fileUri, 'utf8'); } catch { - this._console.error(`Config file "${filePath}" could not be read.`); + this._console.error(`Config file "${fileUri.toUserVisibleString()}" could not be read.`); this._reportConfigParseError(); return undefined; } @@ -993,7 +988,9 @@ export class AnalyzerService { // may have been partially written when we read it, resulting in parse // errors. We'll give it a little more time and try again. if (parseAttemptCount++ >= 5) { - this._console.error(`Config file "${filePath}" could not be parsed. Verify that format is correct.`); + this._console.error( + `Config file "${fileUri.toUserVisibleString()}" could not be parsed. Verify that format is correct.` + ); this._reportConfigParseError(); return undefined; } @@ -1002,16 +999,16 @@ export class AnalyzerService { return undefined; } - private _getFileNamesFromFileSpecs(): string[] { + private _getFileNamesFromFileSpecs(): Uri[] { // Use a map to generate a list of unique files. - const fileMap = new Map(); + const fileMap = new Map(); // Scan all matching files from file system. timingStats.findFilesTime.timeOperation(() => { const matchedFiles = this._matchFiles(this._configOptions.include, this._configOptions.exclude); for (const file of matchedFiles) { - fileMap.set(file, file); + fileMap.set(file.key, file); } }); @@ -1019,9 +1016,9 @@ export class AnalyzerService { // files in file system but only exist in memory (ex, virtual workspace) this._backgroundAnalysisProgram.program .getOpened() - .map((o) => o.sourceFile.getFilePath()) + .map((o) => o.sourceFile.getUri()) .filter((f) => matchFileSpecs(this._program.configOptions, f)) - .forEach((f) => fileMap.set(f, f)); + .forEach((f) => fileMap.set(f.key, f)); return Array.from(fileMap.values()); } @@ -1035,60 +1032,60 @@ export class AnalyzerService { // Are we in type stub generation mode? If so, we need to search // for a different set of files. if (this._typeStubTargetImportName) { - const execEnv = this._configOptions.findExecEnvironment(this._executionRootPath); + const execEnv = this._configOptions.findExecEnvironment(this._executionRootUri); const moduleDescriptor = createImportedModuleDescriptor(this._typeStubTargetImportName); const importResult = this._backgroundAnalysisProgram.importResolver.resolveImport( - '', + Uri.empty(), execEnv, moduleDescriptor ); if (importResult.isImportFound) { - const filesToImport: string[] = []; + const filesToImport: Uri[] = []; // Determine the directory that contains the root package. - const finalResolvedPath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + const finalResolvedPath = importResult.resolvedUris[importResult.resolvedUris.length - 1]; const isFinalPathFile = isFile(this.fs, finalResolvedPath); const isFinalPathInitFile = - isFinalPathFile && stripFileExtension(getFileName(finalResolvedPath)) === '__init__'; + isFinalPathFile && finalResolvedPath.stripAllExtensions().fileName === '__init__'; let rootPackagePath = finalResolvedPath; if (isFinalPathFile) { // If the module is a __init__.pyi? file, use its parent directory instead. - rootPackagePath = getDirectoryPath(rootPackagePath); + rootPackagePath = rootPackagePath.getDirectory(); } - for (let i = importResult.resolvedPaths.length - 2; i >= 0; i--) { - if (importResult.resolvedPaths[i]) { - rootPackagePath = importResult.resolvedPaths[i]; + for (let i = importResult.resolvedUris.length - 2; i >= 0; i--) { + if (!importResult.resolvedUris[i].isEmpty()) { + rootPackagePath = importResult.resolvedUris[i]; } else { // If there was no file corresponding to this portion // of the name path, assume that it's contained // within its parent directory. - rootPackagePath = getDirectoryPath(rootPackagePath); + rootPackagePath = rootPackagePath.getDirectory(); } } if (isDirectory(this.fs, rootPackagePath)) { - this._typeStubTargetPath = rootPackagePath; + this._typeStubTargetUri = rootPackagePath; } else if (isFile(this.fs, rootPackagePath)) { // This can occur if there is a "dir/__init__.py" at the same level as a // module "dir/module.py" that is specifically targeted for stub generation. - this._typeStubTargetPath = getDirectoryPath(rootPackagePath); + this._typeStubTargetUri = rootPackagePath.getDirectory(); } if (!finalResolvedPath) { this._typeStubTargetIsSingleFile = false; } else { filesToImport.push(finalResolvedPath); - this._typeStubTargetIsSingleFile = importResult.resolvedPaths.length === 1 && !isFinalPathInitFile; + this._typeStubTargetIsSingleFile = importResult.resolvedUris.length === 1 && !isFinalPathInitFile; } // Add the implicit import paths. importResult.filteredImplicitImports.forEach((implicitImport) => { - if (ImportResolver.isSupportedImportSourceFile(implicitImport.path)) { - filesToImport.push(implicitImport.path); + if (ImportResolver.isSupportedImportSourceFile(implicitImport.uri)) { + filesToImport.push(implicitImport.uri); } }); @@ -1098,7 +1095,7 @@ export class AnalyzerService { this._console.error(`Import '${this._typeStubTargetImportName}' not found`); } } else if (!this._options.skipScanningUserFiles) { - let fileList: string[] = []; + let fileList: Uri[] = []; this._console.log(`Searching for source files`); fileList = this._getFileNamesFromFileSpecs(); @@ -1117,18 +1114,14 @@ export class AnalyzerService { this._requireTrackedFileUpdate = false; } - private _matchFiles(include: FileSpec[], exclude: FileSpec[]): string[] { + private _matchFiles(include: FileSpec[], exclude: FileSpec[]): Uri[] { const envMarkers = [['bin', 'activate'], ['Scripts', 'activate'], ['pyvenv.cfg']]; - const results: string[] = []; + const results: Uri[] = []; const startTime = Date.now(); const longOperationLimitInSec = 10; let loggedLongOperationError = false; - const visitDirectoryUnchecked = ( - absolutePath: string, - includeRegExp: RegExp, - hasDirectoryWildcard: boolean - ) => { + const visitDirectoryUnchecked = (absolutePath: Uri, includeRegExp: RegExp, hasDirectoryWildcard: boolean) => { if (!loggedLongOperationError) { const secondsSinceStart = (Date.now() - startTime) * 0.001; @@ -1152,32 +1145,27 @@ export class AnalyzerService { } if (this._configOptions.autoExcludeVenv) { - if (envMarkers.some((f) => this.fs.existsSync(combinePaths(absolutePath, ...f)))) { + if (envMarkers.some((f) => this.fs.existsSync(absolutePath.combinePaths(...f)))) { // Save auto exclude paths in the configOptions once we found them. if (!FileSpec.isInPath(absolutePath, exclude)) { - exclude.push( - getFileSpec(this.serviceProvider, this._configOptions.projectRoot, `${absolutePath}/**`) - ); + exclude.push(getFileSpec(this._configOptions.projectRoot, `${absolutePath}/**`)); } - this._console.info(`Auto-excluding ${absolutePath}`); + this._console.info(`Auto-excluding ${absolutePath.toUserVisibleString()}`); return; } } const { files, directories } = getFileSystemEntries(this.fs, absolutePath); - for (const file of files) { - const filePath = combinePaths(absolutePath, file); - + for (const filePath of files) { if (FileSpec.matchIncludeFileSpec(includeRegExp, exclude, filePath)) { results.push(filePath); } } - for (const directory of directories) { - const dirPath = combinePaths(absolutePath, directory); - if (includeRegExp.test(dirPath) || hasDirectoryWildcard) { + for (const dirPath of directories) { + if (dirPath.matchesRegex(includeRegExp) || hasDirectoryWildcard) { if (!FileSpec.isInPath(dirPath, exclude)) { visitDirectory(dirPath, includeRegExp, hasDirectoryWildcard); } @@ -1186,23 +1174,23 @@ export class AnalyzerService { }; const seenDirs = new Set(); - const visitDirectory = (absolutePath: string, includeRegExp: RegExp, hasDirectoryWildcard: boolean) => { + const visitDirectory = (absolutePath: Uri, includeRegExp: RegExp, hasDirectoryWildcard: boolean) => { const realDirPath = tryRealpath(this.fs, absolutePath); if (!realDirPath) { this._console.warn(`Skipping broken link "${absolutePath}"`); return; } - if (seenDirs.has(realDirPath)) { + if (seenDirs.has(realDirPath.key)) { this._console.warn(`Skipping recursive symlink "${absolutePath}" -> "${realDirPath}"`); return; } - seenDirs.add(realDirPath); + seenDirs.add(realDirPath.key); try { visitDirectoryUnchecked(absolutePath, includeRegExp, hasDirectoryWildcard); } finally { - seenDirs.delete(realDirPath); + seenDirs.delete(realDirPath.key); } }; @@ -1220,7 +1208,9 @@ export class AnalyzerService { } if (!foundFileSpec) { - this._console.error(`File or directory "${includeSpec.wildcardRoot}" does not exist.`); + this._console.error( + `File or directory "${includeSpec.wildcardRoot.toUserVisibleString()}" does not exist.` + ); } } }); @@ -1244,7 +1234,7 @@ export class AnalyzerService { if (this._configOptions.include.length > 0) { const fileList = this._configOptions.include.map((spec) => { - return combinePaths(this._executionRootPath, spec.wildcardRoot); + return spec.wildcardRoot; }); try { @@ -1252,7 +1242,7 @@ export class AnalyzerService { this._console.info(`Adding fs watcher for directories:\n ${fileList.join('\n')}`); } - const isIgnored = ignoredWatchEventFunction(fileList); + const isIgnored = ignoredWatchEventFunction(fileList.map((f) => f.getFilePath())); this._sourceFileWatcher = this.fs.createFileSystemWatcher(fileList, (event, path) => { if (!path) { return; @@ -1262,17 +1252,17 @@ export class AnalyzerService { this._console.info(`SourceFile: Received fs event '${event}' for path '${path}'`); } - if (isIgnored(path)) { + if (isIgnored(path.getFilePath())) { return; } // Wholesale ignore events that appear to be from tmp file / .git modification. - if (path.endsWith('.tmp') || path.endsWith('.git') || path.includes(_gitDirectory)) { + if (path.pathEndsWith('.tmp') || path.pathEndsWith('.git') || path.pathIncludes(_gitDirectory)) { return; } // Make sure path is the true case. - path = realCasePath(path, this.fs); + path = this.fs.realCasePath(path); const eventInfo = getEventInfo(this.fs, this._console, this._program, event, path); if (!eventInfo) { @@ -1304,7 +1294,11 @@ export class AnalyzerService { this._scheduleReanalysis(/* requireTrackedFileUpdate */ true); }); } catch { - this._console.error(`Exception caught when installing fs watcher for:\n ${fileList.join('\n')}`); + this._console.error( + `Exception caught when installing fs watcher for:\n ${fileList + .map((f) => f.toUserVisibleString()) + .join('\n')}` + ); } } @@ -1313,7 +1307,7 @@ export class AnalyzerService { console: ConsoleInterface, program: Program, event: FileWatcherEventType, - path: string + path: Uri ) { // Due to the way we implemented file watcher, we will only get 2 events; 'add' and 'change'. // Here, we will convert those 2 to 3 events. 'add', 'change' and 'unlink'; @@ -1352,7 +1346,7 @@ export class AnalyzerService { } } - private _shouldHandleSourceFileWatchChanges(path: string, isFile: boolean) { + private _shouldHandleSourceFileWatchChanges(path: Uri, isFile: boolean) { if (isFile) { if (!hasPythonExtension(path) || isTemporaryFile(path)) { return false; @@ -1374,11 +1368,11 @@ export class AnalyzerService { return false; } - const parentPath = getDirectoryPath(path); + const parentPath = path.getDirectory(); const hasInit = parentPath.startsWith(this._configOptions.projectRoot) && - (this.fs.existsSync(combinePaths(parentPath, '__init__.py')) || - this.fs.existsSync(combinePaths(parentPath, '__init__.pyi'))); + (this.fs.existsSync(parentPath.combinePaths('__init__.py')) || + this.fs.existsSync(parentPath.combinePaths('__init__.pyi'))); // We don't have any file under the given path and its parent folder doesn't have __init__ then this folder change // doesn't have any meaning to us. @@ -1388,13 +1382,13 @@ export class AnalyzerService { return true; - function isTemporaryFile(path: string) { + function isTemporaryFile(path: Uri) { // Determine if this is an add or delete event related to a temporary // file. Some tools (like auto-formatters) create temporary files // alongside the original file and name them "x.py..py" where // is a 32-character random string of hex digits. We don't // want these events to trigger a full reanalysis. - const fileName = getFileName(path); + const fileName = path.fileName; const fileNameSplit = fileName.split('.'); if (fileNameSplit.length === 4) { if (fileNameSplit[3] === fileNameSplit[1] && fileNameSplit[2].length === 32) { @@ -1417,28 +1411,28 @@ export class AnalyzerService { this._removeLibraryFileWatcher(); if (!this._watchForLibraryChanges) { - this._librarySearchPathsToWatch = undefined; + this._librarySearchUrisToWatch = undefined; return; } // Watch the library paths for package install/uninstall. const importFailureInfo: string[] = []; - this._librarySearchPathsToWatch = findPythonSearchPaths( + this._librarySearchUrisToWatch = findPythonSearchPaths( this.fs, this._backgroundAnalysisProgram.configOptions, this._backgroundAnalysisProgram.host, importFailureInfo, /* includeWatchPathsOnly */ true, - this._executionRootPath + this._executionRootUri ); - const watchList = this._librarySearchPathsToWatch; + const watchList = this._librarySearchUrisToWatch; if (watchList && watchList.length > 0) { try { if (this._verboseOutput) { this._console.info(`Adding fs watcher for library directories:\n ${watchList.join('\n')}`); } - const isIgnored = ignoredWatchEventFunction(watchList); + const isIgnored = ignoredWatchEventFunction(watchList.map((f) => f.getFilePath())); this._libraryFileWatcher = this.fs.createFileSystemWatcher(watchList, (event, path) => { if (!path) { return; @@ -1448,7 +1442,7 @@ export class AnalyzerService { this._console.info(`LibraryFile: Received fs event '${event}' for path '${path}'`); } - if (isIgnored(path)) { + if (isIgnored(path.getFilePath())) { return; } @@ -1461,24 +1455,26 @@ export class AnalyzerService { this._scheduleLibraryAnalysis(isChange); }); } catch { - this._console.error(`Exception caught when installing fs watcher for:\n ${watchList.join('\n')}`); + this._console.error( + `Exception caught when installing fs watcher for:\n ${watchList + .map((w) => w.toUserVisibleString()) + .join('\n')}` + ); } } } - private _shouldHandleLibraryFileWatchChanges(path: string, libSearchPaths: string[]) { + private _shouldHandleLibraryFileWatchChanges(path: Uri, libSearchPaths: Uri[]) { if (this._program.getSourceFileInfo(path)) { return true; } // find the innermost matching search path let matchingSearchPath; - const tempFile = this.serviceProvider.tryGet(ServiceKeys.tempFile); - const ignoreCase = !isFileSystemCaseSensitive(this.fs, tempFile); for (const libSearchPath of libSearchPaths) { if ( - containsPath(libSearchPath, path, ignoreCase) && - (!matchingSearchPath || matchingSearchPath.length < libSearchPath.length) + path.isChild(libSearchPath) && + (!matchingSearchPath || matchingSearchPath.getPathLength() < libSearchPath.getPathLength()) ) { matchingSearchPath = libSearchPath; } @@ -1488,8 +1484,8 @@ export class AnalyzerService { return true; } - const parentComponents = getPathComponents(matchingSearchPath); - const childComponents = getPathComponents(path); + const parentComponents = matchingSearchPath.getPathComponents(); + const childComponents = path.getPathComponents(); for (let i = parentComponents.length; i < childComponents.length; i++) { if (childComponents[i].startsWith('.')) { @@ -1559,21 +1555,21 @@ export class AnalyzerService { return; } - if (this._configFilePath) { - this._configFileWatcher = this.fs.createFileSystemWatcher([this._configFilePath], (event) => { + if (this._configFileUri) { + this._configFileWatcher = this.fs.createFileSystemWatcher([this._configFileUri], (event) => { if (this._verboseOutput) { this._console.info(`Received fs event '${event}' for config file`); } this._scheduleReloadConfigFile(); }); - } else if (this._executionRootPath) { - this._configFileWatcher = this.fs.createFileSystemWatcher([this._executionRootPath], (event, path) => { + } else if (this._executionRootUri) { + this._configFileWatcher = this.fs.createFileSystemWatcher([this._executionRootUri], (event, path) => { if (!path) { return; } if (event === 'add' || event === 'change') { - const fileName = getFileName(path); + const fileName = path.fileName; if (fileName && configFileNames.some((name) => name === fileName)) { if (this._verboseOutput) { this._console.info(`Received fs event '${event}' for config file`); @@ -1610,8 +1606,8 @@ export class AnalyzerService { private _reloadConfigFile() { this._updateConfigFileWatcher(); - if (this._configFilePath) { - this._console.info(`Reloading configuration file at ${this._configFilePath}`); + if (this._configFileUri) { + this._console.info(`Reloading configuration file at ${this._configFileUri.toUserVisibleString()}`); const host = this._backgroundAnalysisProgram.host; @@ -1641,7 +1637,7 @@ export class AnalyzerService { log(this._console, logLevel, `Search paths for ${execEnv.root || ''}`); const roots = importResolver.getImportRoots(execEnv, /* forLogging */ true); roots.forEach((path) => { - log(this._console, logLevel, ` ${path}`); + log(this._console, logLevel, ` ${path.toUserVisibleString()}`); }); } } diff --git a/packages/pyright-internal/src/analyzer/sourceFile.ts b/packages/pyright-internal/src/analyzer/sourceFile.ts index 36b5c9aae..b6ba5f09e 100644 --- a/packages/pyright-internal/src/analyzer/sourceFile.ts +++ b/packages/pyright-internal/src/analyzer/sourceFile.ts @@ -20,13 +20,14 @@ import { DiagnosticSink, TextRangeDiagnosticSink } from '../common/diagnosticSin import { ServiceProvider } from '../common/extensibility'; import { FileSystem } from '../common/fileSystem'; import { LogTracker, getPathForLogging } from '../common/logTracker'; -import { getFileName, normalizeSlashes, stripFileExtension } from '../common/pathUtils'; +import { stripFileExtension } from '../common/pathUtils'; import { convertOffsetsToRange, convertTextRangeToRange } from '../common/positionUtils'; import { ServiceKeys } from '../common/serviceProviderExtensions'; import * as StringUtils from '../common/stringUtils'; import { Range, TextRange, getEmptyRange } from '../common/textRange'; import { TextRangeCollection } from '../common/textRangeCollection'; import { Duration, timingStats } from '../common/timing'; +import { Uri } from '../common/uri/uri'; import { Localizer } from '../localization/localize'; import { ModuleNode } from '../parser/parseNodes'; import { IParser, ModuleImport, ParseOptions, ParseResults, Parser } from '../parser/parser'; @@ -179,12 +180,9 @@ export class SourceFile { // Console interface to use for debugging. private _console: ConsoleInterface; - // File path unique to this file within the workspace. May not represent + // Uri unique to this file within the workspace. May not represent // a real file on disk. - private readonly _filePath: string; - - // File path on disk. May not be unique. - private readonly _realFilePath: string; + private readonly _uri: Uri; // Period-delimited import path for the module. private _moduleName: string; @@ -232,46 +230,42 @@ export class SourceFile { constructor( readonly serviceProvider: ServiceProvider, - filePath: string, + uri: Uri, moduleName: string, isThirdPartyImport: boolean, isThirdPartyPyTypedPresent: boolean, editMode: SourceFileEditMode, console?: ConsoleInterface, logTracker?: LogTracker, - realFilePath?: string, ipythonMode?: IPythonMode ) { this.fileSystem = serviceProvider.get(ServiceKeys.fs); this._console = console || new StandardConsole(); this._editMode = editMode; - this._filePath = filePath; - this._realFilePath = realFilePath ?? filePath; + this._uri = uri; this._moduleName = moduleName; - this._isStubFile = filePath.endsWith('.pyi'); + this._isStubFile = uri.hasExtension('.pyi'); this._isThirdPartyImport = isThirdPartyImport; this._isThirdPartyPyTypedPresent = isThirdPartyPyTypedPresent; - const fileName = getFileName(filePath); + const fileName = uri.fileName; this._isTypingStubFile = - this._isStubFile && - (this._filePath.endsWith(normalizeSlashes('stdlib/typing.pyi')) || fileName === 'typing_extensions.pyi'); + this._isStubFile && (this._uri.pathEndsWith('stdlib/typing.pyi') || fileName === 'typing_extensions.pyi'); this._isTypingExtensionsStubFile = this._isStubFile && fileName === 'typing_extensions.pyi'; - this._isTypeshedStubFile = - this._isStubFile && this._filePath.endsWith(normalizeSlashes('stdlib/_typeshed/__init__.pyi')); + this._isTypeshedStubFile = this._isStubFile && this._uri.pathEndsWith('stdlib/_typeshed/__init__.pyi'); this._isBuiltInStubFile = false; if (this._isStubFile) { if ( - this._filePath.endsWith(normalizeSlashes('stdlib/collections/__init__.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/asyncio/futures.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/asyncio/tasks.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/builtins.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/_importlib_modulespec.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/dataclasses.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/abc.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/enum.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/queue.pyi')) || - this._filePath.endsWith(normalizeSlashes('stdlib/types.pyi')) + this._uri.pathEndsWith('stdlib/collections/__init__.pyi') || + this._uri.pathEndsWith('stdlib/asyncio/futures.pyi') || + this._uri.pathEndsWith('stdlib/asyncio/tasks.pyi') || + this._uri.pathEndsWith('stdlib/builtins.pyi') || + this._uri.pathEndsWith('stdlib/_importlib_modulespec.pyi') || + this._uri.pathEndsWith('stdlib/dataclasses.pyi') || + this._uri.pathEndsWith('stdlib/abc.pyi') || + this._uri.pathEndsWith('stdlib/enum.pyi') || + this._uri.pathEndsWith('stdlib/queue.pyi') || + this._uri.pathEndsWith('stdlib/types.pyi') ) { this._isBuiltInStubFile = true; } @@ -282,16 +276,12 @@ export class SourceFile { this._ipythonMode = ipythonMode ?? IPythonMode.None; } - getRealFilePath(): string { - return this._realFilePath; - } - getIPythonMode(): IPythonMode { return this._ipythonMode; } - getFilePath(): string { - return this._filePath; + getUri(): Uri { + return this._uri; } getModuleName(): string { @@ -300,7 +290,7 @@ export class SourceFile { } // Synthesize a module name using the file path. - return stripFileExtension(getFileName(this._filePath)); + return stripFileExtension(this._uri.fileName); } setModuleName(name: string) { @@ -378,8 +368,8 @@ export class SourceFile { // that of the previous contents. try { // Read the file's contents. - if (this.fileSystem.existsSync(this._filePath)) { - const fileContents = this.fileSystem.readFileSync(this._filePath, 'utf8'); + if (this.fileSystem.existsSync(this._uri)) { + const fileContents = this.fileSystem.readFileSync(this._uri, 'utf8'); if (fileContents.length !== this._writableData.lastFileContentLength) { return true; @@ -465,16 +455,16 @@ export class SourceFile { // Otherwise, get content from file system. try { // Check the file's length before attempting to read its full contents. - const fileStat = this.fileSystem.statSync(this._filePath); + const fileStat = this.fileSystem.statSync(this._uri); if (fileStat.size > _maxSourceFileSize) { this._console.error( - `File length of "${this._filePath}" is ${fileStat.size} ` + + `File length of "${this._uri}" is ${fileStat.size} ` + `which exceeds the maximum supported file size of ${_maxSourceFileSize}` ); throw new Error('File larger than max'); } - return this.fileSystem.readFileSync(this._filePath, 'utf8'); + return this.fileSystem.readFileSync(this._uri, 'utf8'); } catch (error) { return undefined; } @@ -580,7 +570,7 @@ export class SourceFile { // (or at least cancel) prior to calling again. It returns true if a parse // was required and false if the parse information was up to date already. parse(configOptions: ConfigOptions, importResolver: ImportResolver, content?: string): boolean { - return this._logTracker.log(`parsing: ${this._getPathForLogging(this._filePath)}`, (logState) => { + return this._logTracker.log(`parsing: ${this._getPathForLogging(this._uri)}`, (logState) => { // If the file is already parsed, we can skip. if (!this.isParseRequired()) { logState.suppress(); @@ -608,7 +598,7 @@ export class SourceFile { diagSink.addError(`Source file could not be read`, getEmptyRange()); fileContents = ''; - if (!this.fileSystem.existsSync(this._realFilePath)) { + if (!this.fileSystem.existsSync(this._uri)) { this._writableData.isFileDeleted = true; } } @@ -618,7 +608,7 @@ export class SourceFile { // Parse the token stream, building the abstract syntax tree. const parseResults = this._parseFile( configOptions, - this._filePath, + this._uri, fileContents!, this._ipythonMode, diagSink @@ -632,7 +622,7 @@ export class SourceFile { this._writableData.parseResults.tokenizerOutput.pyrightIgnoreLines; // Resolve imports. - const execEnvironment = configOptions.findExecEnvironment(this._filePath); + const execEnvironment = configOptions.findExecEnvironment(this._uri); timingStats.resolveImportsTime.timeOperation(() => { const importResult = this._resolveImports( importResolver, @@ -648,7 +638,7 @@ export class SourceFile { // Is this file in a "strict" path? const useStrict = - configOptions.strict.find((strictFileSpec) => strictFileSpec.regExp.test(this._realFilePath)) !== + configOptions.strict.find((strictFileSpec) => this._uri.matchesRegex(strictFileSpec.regExp)) !== undefined; const commentDiags: CommentUtils.CommentDiagnostic[] = []; @@ -680,7 +670,10 @@ export class SourceFile { (typeof e.message === 'string' ? e.message : undefined) || JSON.stringify(e); this._console.error( - Localizer.Diagnostic.internalParseError().format({ file: this.getFilePath(), message }) + Localizer.Diagnostic.internalParseError().format({ + file: this.getUri().toUserVisibleString(), + message, + }) ); // Create dummy parse results. @@ -708,7 +701,10 @@ export class SourceFile { const diagSink = this.createDiagnosticSink(); diagSink.addError( - Localizer.Diagnostic.internalParseError().format({ file: this.getFilePath(), message }), + Localizer.Diagnostic.internalParseError().format({ + file: this.getUri().toUserVisibleString(), + message, + }), getEmptyRange() ); this._writableData.parseDiagnostics = diagSink.fetchAndClear(); @@ -740,7 +736,7 @@ export class SourceFile { assert(!this._writableData.isBindingInProgress, 'Bind called while binding in progress'); assert(this._writableData.parseResults !== undefined, 'Parse results not available'); - return this._logTracker.log(`binding: ${this._getPathForLogging(this._filePath)}`, () => { + return this._logTracker.log(`binding: ${this._getPathForLogging(this._uri)}`, () => { try { // Perform name binding. timingStats.bindTime.timeOperation(() => { @@ -777,12 +773,18 @@ export class SourceFile { (typeof e.message === 'string' ? e.message : undefined) || JSON.stringify(e); this._console.error( - Localizer.Diagnostic.internalBindError().format({ file: this.getFilePath(), message }) + Localizer.Diagnostic.internalBindError().format({ + file: this.getUri().toUserVisibleString(), + message, + }) ); const diagSink = this.createDiagnosticSink(); diagSink.addError( - Localizer.Diagnostic.internalBindError().format({ file: this.getFilePath(), message }), + Localizer.Diagnostic.internalBindError().format({ + file: this.getUri().toUserVisibleString(), + message, + }), getEmptyRange() ); this._writableData.bindDiagnostics = diagSink.fetchAndClear(); @@ -814,7 +816,7 @@ export class SourceFile { assert(this.isCheckingRequired(), 'Check called unnecessarily'); assert(this._writableData.parseResults !== undefined, 'Parse results not available'); - return this._logTracker.log(`checking: ${this._getPathForLogging(this._filePath)}`, () => { + return this._logTracker.log(`checking: ${this._getPathForLogging(this._uri)}`, () => { try { timingStats.typeCheckerTime.timeOperation(() => { const checkDuration = new Duration(); @@ -840,11 +842,17 @@ export class SourceFile { (typeof e.message === 'string' ? e.message : undefined) || JSON.stringify(e); this._console.error( - Localizer.Diagnostic.internalTypeCheckingError().format({ file: this.getFilePath(), message }) + Localizer.Diagnostic.internalTypeCheckingError().format({ + file: this.getUri().toUserVisibleString(), + message, + }) ); const diagSink = this.createDiagnosticSink(); diagSink.addError( - Localizer.Diagnostic.internalTypeCheckingError().format({ file: this.getFilePath(), message }), + Localizer.Diagnostic.internalTypeCheckingError().format({ + file: this.getUri().toUserVisibleString(), + message, + }), getEmptyRange() ); @@ -1097,7 +1105,7 @@ export class SourceFile { '\n' + cirDep .getPaths() - .map((path) => ' ' + path) + .map((path) => ' ' + path.toUserVisibleString()) .join('\n'), getEmptyRange() ); @@ -1120,7 +1128,7 @@ export class SourceFile { this._addTaskListDiagnostics(configOptions.taskListTokens, diagList); // If the file is in the ignore list, clear the diagnostic list. - if (configOptions.ignore.find((ignoreFileSpec) => ignoreFileSpec.regExp.test(this._realFilePath))) { + if (configOptions.ignore.find((ignoreFileSpec) => this._uri.matchesRegex(ignoreFileSpec.regExp))) { diagList = []; } @@ -1243,13 +1251,13 @@ export class SourceFile { futureImports, builtinsScope, diagnosticSink: analysisDiagnostics, - executionEnvironment: configOptions.findExecEnvironment(this._filePath), + executionEnvironment: configOptions.findExecEnvironment(this._uri), diagnosticRuleSet: this._diagnosticRuleSet, fileContents, lines: this._writableData.parseResults!.tokenizerOutput.lines, typingSymbolAliases: this._writableData.parseResults!.typingSymbolAliases, definedConstants: configOptions.defineConstant, - filePath: this._filePath, + fileUri: this._uri, moduleName: this.getModuleName(), isStubFile: this._isStubFile, isTypingStubFile: this._isTypingStubFile, @@ -1281,7 +1289,7 @@ export class SourceFile { const imports: ImportResult[] = []; const resolveAndAddIfNotSelf = (nameParts: string[], skipMissingImport = false) => { - const importResult = importResolver.resolveImport(this._filePath, execEnv, { + const importResult = importResolver.resolveImport(this._uri, execEnv, { leadingDots: 0, nameParts, importedSymbols: undefined, @@ -1292,7 +1300,7 @@ export class SourceFile { } // Avoid importing module from the module file itself. - if (importResult.resolvedPaths.length === 0 || importResult.resolvedPaths[0] !== this._filePath) { + if (importResult.resolvedUris.length === 0 || importResult.resolvedUris[0] !== this._uri) { imports.push(importResult); return importResult; } @@ -1314,7 +1322,7 @@ export class SourceFile { } for (const moduleImport of moduleImports) { - const importResult = importResolver.resolveImport(this._filePath, execEnv, { + const importResult = importResolver.resolveImport(this._uri, execEnv, { leadingDots: moduleImport.leadingDots, nameParts: moduleImport.nameParts, importedSymbols: moduleImport.importedSymbols, @@ -1347,24 +1355,24 @@ export class SourceFile { }; } - private _getPathForLogging(filepath: string) { - return getPathForLogging(this.fileSystem, filepath); + private _getPathForLogging(fileUri: Uri) { + return getPathForLogging(this.fileSystem, fileUri); } private _parseFile( configOptions: ConfigOptions, - filePath: string, + fileUri: Uri, fileContents: string, ipythonMode: IPythonMode, diagSink: DiagnosticSink ) { // Use the configuration options to determine the environment in which // this source file will be executed. - const execEnvironment = configOptions.findExecEnvironment(filePath); + const execEnvironment = configOptions.findExecEnvironment(fileUri); const parseOptions = new ParseOptions(); parseOptions.ipythonMode = ipythonMode; - if (filePath.endsWith('pyi')) { + if (fileUri.pathEndsWith('pyi')) { parseOptions.isStubFile = true; } parseOptions.pythonVersion = execEnvironment.pythonVersion; @@ -1378,7 +1386,7 @@ export class SourceFile { private _fireFileDirtyEvent() { this.serviceProvider.tryGet(ServiceKeys.stateMutationListeners)?.forEach((l) => { try { - l.fileDirty?.(this._filePath); + l.fileDirty?.(this._uri); } catch (ex: any) { const console = this.serviceProvider.tryGet(ServiceKeys.console); if (console) { diff --git a/packages/pyright-internal/src/analyzer/sourceFileInfoUtils.ts b/packages/pyright-internal/src/analyzer/sourceFileInfoUtils.ts index 1e15f0ff5..141db0d74 100644 --- a/packages/pyright-internal/src/analyzer/sourceFileInfoUtils.ts +++ b/packages/pyright-internal/src/analyzer/sourceFileInfoUtils.ts @@ -34,9 +34,9 @@ export function verifyNoCyclesInChainedFiles(program: return; } - const set = new Set([fileInfo.sourceFile.getFilePath()]); + const set = new Set([fileInfo.sourceFile.getUri().key]); while (nextChainedFile) { - const path = nextChainedFile.sourceFile.getFilePath(); + const path = nextChainedFile.sourceFile.getUri().key; if (set.has(path)) { // We found a cycle. fail( @@ -90,7 +90,7 @@ function _parseAllOpenCells(program: ProgramView): void { continue; } - program.getParseResults(file.sourceFile.getFilePath()); + program.getParseResults(file.sourceFile.getUri()); program.handleMemoryHighUsage(); } } diff --git a/packages/pyright-internal/src/analyzer/sourceMapper.ts b/packages/pyright-internal/src/analyzer/sourceMapper.ts index d6006a401..05d03d011 100644 --- a/packages/pyright-internal/src/analyzer/sourceMapper.ts +++ b/packages/pyright-internal/src/analyzer/sourceMapper.ts @@ -14,31 +14,31 @@ import { appendArray } from '../common/collectionUtils'; import { ExecutionEnvironment } from '../common/configOptions'; import { isDefined } from '../common/core'; import { assertNever } from '../common/debug'; -import { combinePaths, getAnyExtensionFromPath, stripFileExtension } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; import { ClassNode, ImportFromNode, ModuleNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; import { AliasDeclaration, ClassDeclaration, Declaration, FunctionDeclaration, + ParameterDeclaration, + SpecialBuiltInClassDeclaration, + VariableDeclaration, isAliasDeclaration, isClassDeclaration, isFunctionDeclaration, isParameterDeclaration, isSpecialBuiltInClassDeclaration, isVariableDeclaration, - ParameterDeclaration, - SpecialBuiltInClassDeclaration, - VariableDeclaration, } from './declaration'; import { ImportResolver } from './importResolver'; -import { SourceFileInfo } from './sourceFileInfo'; import { SourceFile } from './sourceFile'; +import { SourceFileInfo } from './sourceFileInfo'; import { isUserCode } from './sourceFileInfoUtils'; import { buildImportTree } from './sourceMapperUtils'; import { TypeEvaluator } from './typeEvaluatorTypes'; -import { ClassType, isFunction, isInstantiableClass, isOverloadedFunction } from './types'; import { lookUpClassMember } from './typeUtils'; +import { ClassType, isFunction, isInstantiableClass, isOverloadedFunction } from './types'; type ClassOrFunctionOrVariableDeclaration = | ClassDeclaration @@ -47,8 +47,8 @@ type ClassOrFunctionOrVariableDeclaration = | VariableDeclaration; // Creates and binds a shadowed file within the program. -export type ShadowFileBinder = (stubFilePath: string, implFilePath: string) => SourceFile | undefined; -export type BoundSourceGetter = (filePath: string) => SourceFileInfo | undefined; +export type ShadowFileBinder = (stubFileUri: Uri, implFileUri: Uri) => SourceFile | undefined; +export type BoundSourceGetter = (fileUri: Uri) => SourceFileInfo | undefined; export class SourceMapper { constructor( @@ -63,10 +63,10 @@ export class SourceMapper { private _cancelToken: CancellationToken ) {} - findModules(stubFilePath: string): ModuleNode[] { - const sourceFiles = this._isStubThatShouldBeMappedToImplementation(stubFilePath) - ? this._getBoundSourceFilesFromStubFile(stubFilePath) - : [this._boundSourceGetter(stubFilePath)?.sourceFile]; + findModules(stubFileUri: Uri): ModuleNode[] { + const sourceFiles = this._isStubThatShouldBeMappedToImplementation(stubFileUri) + ? this._getBoundSourceFilesFromStubFile(stubFileUri) + : [this._boundSourceGetter(stubFileUri)?.sourceFile]; return sourceFiles .filter(isDefined) @@ -74,8 +74,8 @@ export class SourceMapper { .filter(isDefined); } - getModuleNode(filePath: string): ModuleNode | undefined { - return this._boundSourceGetter(filePath)?.sourceFile.getParseResults()?.parseTree; + getModuleNode(fileUri: Uri): ModuleNode | undefined { + return this._boundSourceGetter(fileUri)?.sourceFile.getParseResults()?.parseTree; } findDeclarations(stubDecl: Declaration): Declaration[] { @@ -94,13 +94,13 @@ export class SourceMapper { return []; } - findDeclarationsByType(originatedPath: string, type: ClassType, useTypeAlias = false): Declaration[] { + findDeclarationsByType(originatedPath: Uri, type: ClassType, useTypeAlias = false): Declaration[] { const result: ClassOrFunctionOrVariableDeclaration[] = []; this._addClassTypeDeclarations(originatedPath, type, result, new Set(), useTypeAlias); return result; } - findClassDeclarationsByType(originatedPath: string, type: ClassType): ClassDeclaration[] { + findClassDeclarationsByType(originatedPath: Uri, type: ClassType): ClassDeclaration[] { const result = this.findDeclarationsByType(originatedPath, type); return result.filter((r) => isClassDeclaration(r)).map((r) => r as ClassDeclaration); } @@ -111,17 +111,17 @@ export class SourceMapper { .map((d) => d as FunctionDeclaration); } - isUserCode(path: string): boolean { - return isUserCode(this._boundSourceGetter(path)); + isUserCode(uri: Uri): boolean { + return isUserCode(this._boundSourceGetter(uri)); } - getNextFileName(path: string) { - const withoutExtension = stripFileExtension(path); + getNextFileName(uri: Uri) { + const withoutExtension = uri.stripExtension(); let suffix = 1; - let result = `${withoutExtension}_${suffix}.py`; + let result = withoutExtension.addExtension(`_${suffix}.py`); while (this.isUserCode(result) && suffix < 1000) { suffix += 1; - result = `${withoutExtension}_${suffix}.py`; + result = withoutExtension.addExtension(`_${suffix}.py`); } return result; } @@ -132,7 +132,7 @@ export class SourceMapper { ) { if (stubDecl.node.valueExpression.nodeType === ParseNodeType.Name) { const className = stubDecl.node.valueExpression.value; - const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.path); + const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.uri); return sourceFiles.flatMap((sourceFile) => this._findClassDeclarationsByName(sourceFile, className, recursiveDeclCache) @@ -144,7 +144,7 @@ export class SourceMapper { private _findClassOrTypeAliasDeclarations(stubDecl: ClassDeclaration, recursiveDeclCache = new Set()) { const className = this._getFullClassName(stubDecl.node); - const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.path); + const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.uri); return sourceFiles.flatMap((sourceFile) => this._findClassDeclarationsByName(sourceFile, className, recursiveDeclCache) @@ -156,7 +156,7 @@ export class SourceMapper { recursiveDeclCache = new Set() ): ClassOrFunctionOrVariableDeclaration[] { const functionName = stubDecl.node.name.value; - const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.path); + const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.uri); if (stubDecl.isMethod) { const classNode = ParseTreeUtils.getEnclosingClass(stubDecl.node); @@ -184,7 +184,7 @@ export class SourceMapper { } const variableName = stubDecl.node.value; - const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.path); + const sourceFiles = this._getBoundSourceFilesFromStubFile(stubDecl.uri); const classNode = ParseTreeUtils.getEnclosingClass(stubDecl.node); if (classNode) { @@ -270,7 +270,7 @@ export class SourceMapper { ): VariableDeclaration[] { let result: VariableDeclaration[] = []; - const uniqueId = `@${sourceFile.getFilePath()}/c/${className}/v/${variableName}`; + const uniqueId = `@${sourceFile.getUri()}/c/${className}/v/${variableName}`; if (recursiveDeclCache.has(uniqueId)) { return result; } @@ -283,7 +283,7 @@ export class SourceMapper { variableName, (decl, cache, result) => { if (isVariableDeclaration(decl)) { - if (this._isStubThatShouldBeMappedToImplementation(decl.path)) { + if (this._isStubThatShouldBeMappedToImplementation(decl.uri)) { for (const implDecl of this._findVariableDeclarations(decl, cache)) { if (isVariableDeclaration(implDecl)) { result.push(implDecl); @@ -309,7 +309,7 @@ export class SourceMapper { ): ClassOrFunctionOrVariableDeclaration[] { let result: ClassOrFunctionOrVariableDeclaration[] = []; - const uniqueId = `@${sourceFile.getFilePath()}/c/${className}/f/${functionName}`; + const uniqueId = `@${sourceFile.getUri()}/c/${className}/f/${functionName}`; if (recursiveDeclCache.has(uniqueId)) { return result; } @@ -322,7 +322,7 @@ export class SourceMapper { functionName, (decl, cache, result) => { if (isFunctionDeclaration(decl)) { - if (this._isStubThatShouldBeMappedToImplementation(decl.path)) { + if (this._isStubThatShouldBeMappedToImplementation(decl.uri)) { appendArray(result, this._findFunctionOrTypeAliasDeclarations(decl, cache)); } else { result.push(decl); @@ -343,7 +343,7 @@ export class SourceMapper { ): ClassOrFunctionOrVariableDeclaration[] { const result: ClassOrFunctionOrVariableDeclaration[] = []; - const uniqueId = `@${sourceFile.getFilePath()}/v/${variableName}`; + const uniqueId = `@${sourceFile.getUri()}/v/${variableName}`; if (recursiveDeclCache.has(uniqueId)) { return result; } @@ -377,7 +377,7 @@ export class SourceMapper { ): ClassOrFunctionOrVariableDeclaration[] { const result: ClassOrFunctionOrVariableDeclaration[] = []; - const uniqueId = `@${sourceFile.getFilePath()}/f/${functionName}`; + const uniqueId = `@${sourceFile.getUri()}/f/${functionName}`; if (recursiveDeclCache.has(uniqueId)) { return result; } @@ -438,7 +438,7 @@ export class SourceMapper { ): ClassOrFunctionOrVariableDeclaration[] { const result: ClassOrFunctionOrVariableDeclaration[] = []; - const uniqueId = `@${sourceFile.getFilePath()}[${parentNode.start}]${className}`; + const uniqueId = `@${sourceFile.getUri()}[${parentNode.start}]${className}`; if (recursiveDeclCache.has(uniqueId)) { return result; } @@ -464,7 +464,7 @@ export class SourceMapper { recursiveDeclCache: Set ) { if (isVariableDeclaration(decl)) { - if (this._isStubThatShouldBeMappedToImplementation(decl.path)) { + if (this._isStubThatShouldBeMappedToImplementation(decl.uri)) { appendArray(result, this._findVariableDeclarations(decl, recursiveDeclCache)); } else { result.push(decl); @@ -487,7 +487,7 @@ export class SourceMapper { recursiveDeclCache: Set ) { if (isClassDeclaration(decl)) { - if (this._isStubThatShouldBeMappedToImplementation(decl.path)) { + if (this._isStubThatShouldBeMappedToImplementation(decl.uri)) { appendArray(result, this._findClassOrTypeAliasDeclarations(decl, recursiveDeclCache)); } else { result.push(decl); @@ -495,7 +495,7 @@ export class SourceMapper { } else if (isSpecialBuiltInClassDeclaration(decl)) { result.push(decl); } else if (isFunctionDeclaration(decl)) { - if (this._isStubThatShouldBeMappedToImplementation(decl.path)) { + if (this._isStubThatShouldBeMappedToImplementation(decl.uri)) { appendArray(result, this._findFunctionOrTypeAliasDeclarations(decl, recursiveDeclCache)); } else { result.push(decl); @@ -525,7 +525,7 @@ export class SourceMapper { this._addClassOrFunctionDeclarations(overloadDecl, result, recursiveDeclCache); } } else if (isInstantiableClass(type)) { - this._addClassTypeDeclarations(decl.path, type, result, recursiveDeclCache); + this._addClassTypeDeclarations(decl.uri, type, result, recursiveDeclCache); } } } @@ -541,7 +541,7 @@ export class SourceMapper { // and second, clone the given decl and set path to the generated pyi for the // builtin module (ex, _io) to make resolveAliasDeclaration to work. // once the path is set, our regular code path will work as expected. - if (decl.path || !decl.node) { + if (!decl.uri.isEmpty() || !decl.node) { // If module actually exists, nothing we need to do. return decl; } @@ -563,20 +563,19 @@ export class SourceMapper { // ImportResolver might be able to generate or extract builtin module's info // from runtime if we provide right synthesized stub path. - const fakeStubPath = combinePaths( - stdLibPath, + const fakeStubPath = stdLibPath.combinePaths( getModuleName() .nameParts.map((n) => n.value) .join('.') + '.pyi' ); - const sources = this._getSourceFiles(fakeStubPath, fileInfo.filePath); + const sources = this._getSourceFiles(fakeStubPath, fileInfo.fileUri); if (sources.length === 0) { return decl; } const synthesizedDecl = { ...decl }; - synthesizedDecl.path = sources[0].getFilePath(); + synthesizedDecl.uri = sources[0].getUri(); return synthesizedDecl; @@ -595,14 +594,14 @@ export class SourceMapper { } private _addClassTypeDeclarations( - originated: string, + originated: Uri, type: ClassType, result: ClassOrFunctionOrVariableDeclaration[], recursiveDeclCache: Set, useTypeAlias = false ) { - const filePath = type.details.filePath; - const sourceFiles = this._getSourceFiles(filePath, /* stubToShadow */ undefined, originated); + const fileUri = type.details.fileUri; + const sourceFiles = this._getSourceFiles(fileUri, /* stubToShadow */ undefined, originated); const fullName = useTypeAlias && type.typeAliasInfo ? type.typeAliasInfo.fullName : type.details.fullName; const fullClassName = fullName.substring(type.details.moduleName.length + 1 /* +1 for trailing dot */); @@ -612,13 +611,13 @@ export class SourceMapper { } } - private _getSourceFiles(filePath: string, stubToShadow?: string, originated?: string) { + private _getSourceFiles(fileUri: Uri, stubToShadow?: Uri, originated?: Uri) { const sourceFiles: SourceFile[] = []; - if (this._isStubThatShouldBeMappedToImplementation(filePath)) { - appendArray(sourceFiles, this._getBoundSourceFilesFromStubFile(filePath, stubToShadow, originated)); + if (this._isStubThatShouldBeMappedToImplementation(fileUri)) { + appendArray(sourceFiles, this._getBoundSourceFilesFromStubFile(fileUri, stubToShadow, originated)); } else { - const sourceFileInfo = this._boundSourceGetter(filePath); + const sourceFileInfo = this._boundSourceGetter(fileUri); if (sourceFileInfo) { sourceFiles.push(sourceFileInfo.sourceFile); } @@ -645,14 +644,14 @@ export class SourceMapper { for (const decl of symbol.getDeclarations()) { if ( !isAliasDeclaration(decl) || - !decl.path || + decl.uri.isEmpty() || decl.node.nodeType !== ParseNodeType.ImportFrom || !decl.node.isWildcardImport ) { continue; } - const uniqueId = `@${decl.path}/l/${symbolName}`; + const uniqueId = `@${decl.uri.key}/l/${symbolName}`; if (recursiveDeclCache.has(uniqueId)) { continue; } @@ -667,7 +666,7 @@ export class SourceMapper { // function. recursiveDeclCache.add(uniqueId); - const sourceFiles = this._getSourceFiles(decl.path); + const sourceFiles = this._getSourceFiles(decl.uri); for (const sourceFile of sourceFiles) { const moduleNode = sourceFile.getParseResults()?.parseTree; if (!moduleNode) { @@ -728,28 +727,21 @@ export class SourceMapper { return fullName.reverse().join('.'); } - private _getBoundSourceFilesFromStubFile( - stubFilePath: string, - stubToShadow?: string, - originated?: string - ): SourceFile[] { - const paths = this._getSourcePathsFromStub( - stubFilePath, - originated ?? this._fromFile?.sourceFile.getFilePath() - ); - return paths.map((fp) => this._fileBinder(stubToShadow ?? stubFilePath, fp)).filter(isDefined); + private _getBoundSourceFilesFromStubFile(stubFileUri: Uri, stubToShadow?: Uri, originated?: Uri): SourceFile[] { + const paths = this._getSourcePathsFromStub(stubFileUri, originated ?? this._fromFile?.sourceFile.getUri()); + return paths.map((fp) => this._fileBinder(stubToShadow ?? stubFileUri, fp)).filter(isDefined); } - private _getSourcePathsFromStub(stubFilePath: string, fromFile: string | undefined): string[] { - // Attempt our stubFilePath to see if we can resolve it as a source file path - let results = this._importResolver.getSourceFilesFromStub(stubFilePath, this._execEnv, this._mapCompiled); + private _getSourcePathsFromStub(stubFileUri: Uri, fromFile: Uri | undefined): Uri[] { + // Attempt our stubFileUri to see if we can resolve it as a source file path + let results = this._importResolver.getSourceFilesFromStub(stubFileUri, this._execEnv, this._mapCompiled); if (results.length > 0) { return results; } // If that didn't work, try looking through the graph up to our fromFile. // One of them should be able to resolve to an actual file. - const stubFileImportTree = this._getStubFileImportTree(stubFilePath, fromFile); + const stubFileImportTree = this._getStubFileImportTree(stubFileUri, fromFile); // Go through the items in this tree until we find at least one path. for (let i = 0; i < stubFileImportTree.length; i++) { @@ -766,43 +758,41 @@ export class SourceMapper { return []; } - private _getStubFileImportTree(stubFilePath: string, fromFile: string | undefined): string[] { - if (!fromFile || !this._isStubThatShouldBeMappedToImplementation(stubFilePath)) { + private _getStubFileImportTree(stubFileUri: Uri, fromFile: Uri | undefined): Uri[] { + if (!fromFile || !this._isStubThatShouldBeMappedToImplementation(stubFileUri)) { // No path to search, just return the starting point. - return [stubFilePath]; + return [stubFileUri]; } else { // Otherwise recurse through the importedBy list up to our 'fromFile'. return buildImportTree( fromFile, - stubFilePath, + stubFileUri, (p) => { const boundSourceInfo = this._boundSourceGetter(p); - return boundSourceInfo - ? boundSourceInfo.importedBy.map((info) => info.sourceFile.getFilePath()) - : []; + return boundSourceInfo ? boundSourceInfo.importedBy.map((info) => info.sourceFile.getUri()) : []; }, this._cancelToken ).filter((p) => this._isStubThatShouldBeMappedToImplementation(p)); } } - private _isStubThatShouldBeMappedToImplementation(filePath: string): boolean { + private _isStubThatShouldBeMappedToImplementation(fileUri: Uri): boolean { if (this._preferStubs) { return false; } - const stub = isStubFile(filePath); + const stub = isStubFile(fileUri); if (!stub) { return false; } // If we get the same file as a source file, then we treat the file as a regular file even if it has "pyi" extension. return this._importResolver - .getSourceFilesFromStub(filePath, this._execEnv, this._mapCompiled) - .every((f) => f !== filePath); + .getSourceFilesFromStub(fileUri, this._execEnv, this._mapCompiled) + .every((f) => f !== fileUri); } } -export function isStubFile(filePath: string): boolean { - return getAnyExtensionFromPath(filePath, ['.pyi'], /* ignoreCase */ false) === '.pyi'; +export function isStubFile(uri: Uri): boolean { + return uri.lastExtension === '.pyi'; } diff --git a/packages/pyright-internal/src/analyzer/sourceMapperUtils.ts b/packages/pyright-internal/src/analyzer/sourceMapperUtils.ts index 3d9abeca2..8f407908e 100644 --- a/packages/pyright-internal/src/analyzer/sourceMapperUtils.ts +++ b/packages/pyright-internal/src/analyzer/sourceMapperUtils.ts @@ -5,6 +5,7 @@ */ import { CancellationToken } from 'vscode-jsonrpc'; +import { Uri } from '../common/uri/uri'; const MAX_TREE_SEARCH_COUNT = 1000; @@ -15,12 +16,7 @@ class NumberReference { // Builds an array of imports from the 'from' to the 'to' entry where 'from' // is on the front of the array and the item just before 'to' is on the // back of the array. -export function buildImportTree( - to: string, - from: string, - next: (from: string) => string[], - token: CancellationToken -): string[] { +export function buildImportTree(to: Uri, from: Uri, next: (from: Uri) => Uri[], token: CancellationToken): Uri[] { const totalCountRef = new NumberReference(); const results = _buildImportTreeImpl(to, from, next, [], totalCountRef, token); @@ -29,25 +25,25 @@ export function buildImportTree( } function _buildImportTreeImpl( - to: string, - from: string, - next: (from: string) => string[], - previous: string[], + to: Uri, + from: Uri, + next: (from: Uri) => Uri[], + previous: Uri[], totalSearched: NumberReference, token: CancellationToken -): string[] { +): Uri[] { // Exit early if cancellation is requested or we've exceeded max count if (totalSearched.value > MAX_TREE_SEARCH_COUNT || token.isCancellationRequested) { return []; } totalSearched.value += 1; - if (from === to) { + if (from.equals(to)) { // At the top, previous should have our way into this recursion. return previous.length ? previous : [from]; } - if (previous.length > 1 && previous.find((s) => s === from)) { + if (previous.length > 1 && previous.find((s) => s.equals(from))) { // Fail the search, we're stuck in a loop. return []; } diff --git a/packages/pyright-internal/src/analyzer/tracePrinter.ts b/packages/pyright-internal/src/analyzer/tracePrinter.ts index 979e8f344..9e4fd6479 100644 --- a/packages/pyright-internal/src/analyzer/tracePrinter.ts +++ b/packages/pyright-internal/src/analyzer/tracePrinter.ts @@ -8,8 +8,9 @@ import { isNumber, isString } from '../common/core'; import { assertNever } from '../common/debug'; -import { ensureTrailingDirectorySeparator, stripFileExtension } from '../common/pathUtils'; +import { stripFileExtension } from '../common/pathUtils'; import { convertOffsetToPosition } from '../common/positionUtils'; +import { Uri } from '../common/uri/uri'; import { ParseNode, ParseNodeType, isExpressionNode } from '../parser/parseNodes'; import { AbsoluteModuleDescriptor } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; @@ -22,10 +23,10 @@ export type PrintableType = ParseNode | Declaration | Symbol | Type | undefined; export interface TracePrinter { print(o: PrintableType): string; - printFileOrModuleName(filePathOrModule: string | AbsoluteModuleDescriptor): string; + printFileOrModuleName(fileUriOrModule: Uri | AbsoluteModuleDescriptor): string; } -export function createTracePrinter(roots: string[]): TracePrinter { +export function createTracePrinter(roots: Uri[]): TracePrinter { function wrap(value: string | undefined, ch = "'") { return value ? `${ch}${value}${ch}` : ''; } @@ -33,25 +34,22 @@ export function createTracePrinter(roots: string[]): TracePrinter { // Sort roots in desc order so that we compare longer path first // when getting relative path. // ex) d:/root/.env/lib/site-packages, d:/root/.env - roots = roots - .map((r) => ensureTrailingDirectorySeparator(r)) - .sort((a, b) => a.localeCompare(b)) - .reverse(); + roots = roots.sort((a, b) => a.key.localeCompare(b.key)).reverse(); const separatorRegExp = /[\\/]/g; - function printFileOrModuleName(filePathOrModule: string | AbsoluteModuleDescriptor | undefined) { - if (filePathOrModule) { - if (typeof filePathOrModule === 'string') { + function printFileOrModuleName(fileUriOrModule: Uri | AbsoluteModuleDescriptor | undefined) { + if (fileUriOrModule) { + if (Uri.isUri(fileUriOrModule)) { for (const root of roots) { - if (filePathOrModule.startsWith(root)) { - const subFile = filePathOrModule.substring(root.length); - return stripFileExtension(subFile).replace(separatorRegExp, '.'); + if (fileUriOrModule.isChild(root)) { + const subFile = root.getRelativePath(fileUriOrModule); + return stripFileExtension(subFile!).replace(separatorRegExp, '.'); } } - return filePathOrModule; - } else if (filePathOrModule.nameParts) { - return filePathOrModule.nameParts.join('.'); + return fileUriOrModule.toUserVisibleString(); + } else if (fileUriOrModule.nameParts) { + return fileUriOrModule.nameParts.join('.'); } } return ''; @@ -117,33 +115,33 @@ export function createTracePrinter(roots: string[]): TracePrinter { if (decl) { switch (decl.type) { case DeclarationType.Alias: - return `Alias, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `Alias, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.Class: - return `Class, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `Class, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.Function: - return `Function, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `Function, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.Intrinsic: return `Intrinsic, ${printNode(decl.node)} ${decl.intrinsicType} (${printFileOrModuleName( - decl.path + decl.uri )})`; case DeclarationType.Parameter: - return `Parameter, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `Parameter, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.TypeParameter: - return `TypeParameter, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `TypeParameter, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.SpecialBuiltInClass: - return `SpecialBuiltInClass, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `SpecialBuiltInClass, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.Variable: - return `Variable, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `Variable, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; case DeclarationType.TypeAlias: - return `TypeAlias, ${printNode(decl.node)} (${printFileOrModuleName(decl.path)})`; + return `TypeAlias, ${printNode(decl.node)} (${printFileOrModuleName(decl.uri)})`; default: assertNever(decl); @@ -174,7 +172,7 @@ export function createTracePrinter(roots: string[]): TracePrinter { return ''; } - let path = printPath ? `(${printFileOrModuleName(getFileInfo(node)?.filePath)})` : ''; + let path = printPath ? `(${printFileOrModuleName(getFileInfo(node)?.fileUri)})` : ''; const fileInfo = getFileInfo(node); if (fileInfo?.lines) { @@ -228,7 +226,7 @@ export function createTracePrinter(roots: string[]): TracePrinter { function isDeclaration(o: any): o is Declaration { const d = o as Declaration; - return d && isNumber(d.type) && isString(d.path) && isString(d.moduleName); + return d && isNumber(d.type) && Uri.isUri(d.uri) && isString(d.moduleName); } function isType(o: any): o is Type { diff --git a/packages/pyright-internal/src/analyzer/typeDocStringUtils.ts b/packages/pyright-internal/src/analyzer/typeDocStringUtils.ts index 7ea90d937..b29129ca2 100644 --- a/packages/pyright-internal/src/analyzer/typeDocStringUtils.ts +++ b/packages/pyright-internal/src/analyzer/typeDocStringUtils.ts @@ -33,6 +33,7 @@ import { TypeCategory, } from '../analyzer/types'; import { addIfNotNull, appendArray } from '../common/collectionUtils'; +import { Uri } from '../common/uri/uri'; import { ModuleNode, ParseNodeType } from '../parser/parseNodes'; import { TypeEvaluator } from './typeEvaluatorTypes'; import { @@ -160,7 +161,7 @@ export function getPropertyDocStringInherited( export function getVariableInStubFileDocStrings(decl: VariableDeclaration, sourceMapper: SourceMapper) { const docStrings: string[] = []; - if (!isStubFile(decl.path)) { + if (!isStubFile(decl.uri)) { return docStrings; } @@ -193,14 +194,14 @@ export function getModuleDocStringFromModuleNodes(modules: ModuleNode[]): string return undefined; } -export function getModuleDocStringFromPaths(filePaths: string[], sourceMapper: SourceMapper) { +export function getModuleDocStringFromUris(uris: Uri[], sourceMapper: SourceMapper) { const modules: ModuleNode[] = []; - for (const filePath of filePaths) { - if (isStubFile(filePath)) { - addIfNotNull(modules, sourceMapper.getModuleNode(filePath)); + for (const uri of uris) { + if (isStubFile(uri)) { + addIfNotNull(modules, sourceMapper.getModuleNode(uri)); } - appendArray(modules, sourceMapper.findModules(filePath)); + appendArray(modules, sourceMapper.findModules(uri)); } return getModuleDocStringFromModuleNodes(modules); @@ -213,8 +214,8 @@ export function getModuleDocString( ) { let docString = type.docString; if (!docString) { - const filePath = resolvedDecl?.path ?? type.filePath; - docString = getModuleDocStringFromPaths([filePath], sourceMapper); + const uri = resolvedDecl?.uri ?? type.fileUri; + docString = getModuleDocStringFromUris([uri], sourceMapper); } return docString; @@ -228,12 +229,7 @@ export function getClassDocString( let docString = classType.details.docString; if (!docString && resolvedDecl && isClassDeclaration(resolvedDecl)) { docString = _getFunctionOrClassDeclsDocString([resolvedDecl]); - if ( - !docString && - resolvedDecl && - isStubFile(resolvedDecl.path) && - resolvedDecl.type === DeclarationType.Class - ) { + if (!docString && resolvedDecl && isStubFile(resolvedDecl.uri) && resolvedDecl.type === DeclarationType.Class) { for (const implDecl of sourceMapper.findDeclarations(resolvedDecl)) { if (isVariableDeclaration(implDecl) && !!implDecl.docString) { docString = implDecl.docString; @@ -249,7 +245,7 @@ export function getClassDocString( } if (!docString && resolvedDecl) { - const implDecls = sourceMapper.findClassDeclarationsByType(resolvedDecl.path, classType); + const implDecls = sourceMapper.findClassDeclarationsByType(resolvedDecl.uri, classType); if (implDecls) { const classDecls = implDecls.filter((d) => isClassDeclaration(d)).map((d) => d); docString = _getFunctionOrClassDeclsDocString(classDecls); @@ -294,7 +290,7 @@ function _getOverloadedFunctionDocStrings( docStrings.push(overload.details.docString); } }); - } else if (resolvedDecl && isStubFile(resolvedDecl.path) && isFunctionDeclaration(resolvedDecl)) { + } else if (resolvedDecl && isStubFile(resolvedDecl.uri) && isFunctionDeclaration(resolvedDecl)) { const implDecls = sourceMapper.findFunctionDeclarations(resolvedDecl); const docString = _getFunctionOrClassDeclsDocString(implDecls); if (docString) { @@ -372,7 +368,7 @@ function _getFunctionDocString(type: Type, resolvedDecl: FunctionDeclaration | u function _getFunctionDocStringFromDeclaration(resolvedDecl: FunctionDeclaration, sourceMapper: SourceMapper) { let docString = _getFunctionOrClassDeclsDocString([resolvedDecl]); - if (!docString && isStubFile(resolvedDecl.path)) { + if (!docString && isStubFile(resolvedDecl.uri)) { const implDecls = sourceMapper.findFunctionDeclarations(resolvedDecl); docString = _getFunctionOrClassDeclsDocString(implDecls); } diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts index 2657f1484..228d150c1 100644 --- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts +++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts @@ -26,6 +26,7 @@ import { DiagnosticRule } from '../common/diagnosticRules'; import { convertOffsetToPosition, convertOffsetsToRange } from '../common/positionUtils'; import { PythonVersion } from '../common/pythonVersion'; import { TextRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { Localizer, ParameterizedString } from '../localization/localize'; import { ArgumentCategory, @@ -665,7 +666,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions `Type cache flag mismatch for node type ${node.nodeType} ` + `(parent ${node.parent?.nodeType ?? 'none'}): ` + `cached flags = ${expectedFlags}, access flags = ${flags}, ` + - `file = {${fileInfo.filePath} [${position.line + 1}:${position.character + 1}]}`; + `file = {${fileInfo.fileUri} [${position.line + 1}:${position.character + 1}]}`; if (evaluatorOptions.verifyTypeCacheEvaluatorFlags) { fail(message); } else { @@ -2895,7 +2896,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions function getTypeOfModule(node: ParseNode, symbolName: string, nameParts: string[]) { const fileInfo = AnalyzerNodeInfo.getFileInfo(node); - const lookupResult = importLookup({ nameParts, importingFilePath: fileInfo.filePath }); + const lookupResult = importLookup({ nameParts, importingFileUri: fileInfo.fileUri }); if (!lookupResult) { return undefined; @@ -5355,9 +5356,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (getAttrSymbol) { const isModuleGetAttrSupported = fileInfo.executionEnvironment.pythonVersion >= PythonVersion.V3_7 || - getAttrSymbol - .getDeclarations() - .some((decl) => decl.path.toLowerCase().endsWith('.pyi')); + getAttrSymbol.getDeclarations().some((decl) => decl.uri.hasExtension('.pyi')); if (isModuleGetAttrSupported) { const getAttrTypeResult = getEffectiveTypeOfSymbolForUsage(getAttrSymbol); @@ -8904,7 +8903,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (diagnostic && overrideDecl) { diagnostic.addRelatedInfo( Localizer.DiagnosticAddendum.overloadIndex().format({ index: bestMatch.overloadIndex + 1 }), - overrideDecl.path, + overrideDecl.uri, overrideDecl.range ); } @@ -9717,7 +9716,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions newClassName, '', '', - AnalyzerNodeInfo.getFileInfo(errorNode).filePath, + AnalyzerNodeInfo.getFileInfo(errorNode).fileUri, ClassTypeFlags.None, ParseTreeUtils.getTypeSourceId(errorNode), ClassType.cloneAsInstantiable(returnType), @@ -12628,7 +12627,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions className, ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, classFlags, ParseTreeUtils.getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, @@ -12689,7 +12688,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions className, ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.None, ParseTreeUtils.getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, @@ -15269,7 +15268,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions assignedName, ParseTreeUtils.getClassFullName(node, fileInfo.moduleName, assignedName), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.BuiltInClass | ClassTypeFlags.SpecialBuiltIn, /* typeSourceId */ 0, /* declaredMetaclass */ undefined, @@ -15835,7 +15834,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions node.name.value, ParseTreeUtils.getClassFullName(node, fileInfo.moduleName, node.name.value), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, classFlags, /* typeSourceId */ 0, /* declaredMetaclass */ undefined, @@ -16615,7 +16614,16 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions ); const updatedClassType = ClassType.cloneWithNewTypeParameters(classType, updatedTypeParams); - const dummyTypeObject = ClassType.createInstantiable('__varianceDummy', '', '', '', 0, 0, undefined, undefined); + const dummyTypeObject = ClassType.createInstantiable( + '__varianceDummy', + '', + '', + Uri.empty(), + 0, + 0, + undefined, + undefined + ); updatedTypeParams.forEach((param, paramIndex) => { // Skip variadics and ParamSpecs. @@ -16999,7 +17007,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions Localizer.DiagnosticAddendum.initSubclassLocation().format({ name: printType(convertToInstance(initSubclassMethodInfo.classType)), }), - initSubclassDecl.path, + initSubclassDecl.uri, initSubclassDecl.range ); } @@ -18381,7 +18389,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions const importInfo = AnalyzerNodeInfo.getImportInfo(parentNode.module); if (importInfo && importInfo.isImportFound && !importInfo.isNativeLib) { - const resolvedPath = importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1]; + const resolvedPath = importInfo.resolvedUris[importInfo.resolvedUris.length - 1]; const importLookupInfo = importLookup(resolvedPath); let reportError = false; @@ -18403,7 +18411,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions } } } - } else if (!resolvedPath) { + } else if (resolvedPath.isEmpty()) { // This corresponds to the "from . import a" form. reportError = true; } @@ -20207,15 +20215,15 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions namePartIndex >= 0 && importInfo && !importInfo.isNativeLib && - namePartIndex < importInfo.resolvedPaths.length + namePartIndex < importInfo.resolvedUris.length ) { - if (importInfo.resolvedPaths[namePartIndex]) { + if (importInfo.resolvedUris[namePartIndex]) { evaluateTypesForStatement(node); // Synthesize an alias declaration for this name part. The only // time this case is used is for IDE services such as // the find all references, hover provider and etc. - declarations.push(createSynthesizedAliasDeclaration(importInfo.resolvedPaths[namePartIndex])); + declarations.push(createSynthesizedAliasDeclaration(importInfo.resolvedUris[namePartIndex])); } } } else if (node.parent && node.parent.nodeType === ParseNodeType.Argument && node === node.parent.name) { @@ -20634,8 +20642,8 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions loaderActions: ModuleLoaderActions, importLookup: ImportLookup ): Type { - if (loaderActions.path && loaderActions.loadSymbolsFromPath) { - const lookupResults = importLookup(loaderActions.path); + if (!loaderActions.uri.isEmpty() && loaderActions.loadSymbolsFromPath) { + const lookupResults = importLookup(loaderActions.uri); if (lookupResults) { moduleType.fields = lookupResults.symbolTable; moduleType.docString = lookupResults.docString; @@ -20658,7 +20666,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions symbolType = UnknownType.create(); } else { const moduleName = moduleType.moduleName ? moduleType.moduleName + '.' + name : ''; - const importedModuleType = ModuleType.create(moduleName, implicitImport.path); + const importedModuleType = ModuleType.create(moduleName, implicitImport.uri); symbolType = applyLoaderActionsToModuleType(importedModuleType, implicitImport, importLookup); } @@ -20676,7 +20684,7 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions if (resolvedDecl.type === DeclarationType.Alias) { // Build a module type that corresponds to the declaration and // its associated loader actions. - const moduleType = ModuleType.create(resolvedDecl.moduleName, resolvedDecl.path); + const moduleType = ModuleType.create(resolvedDecl.moduleName, resolvedDecl.uri); if (resolvedDecl.symbolName && resolvedDecl.submoduleFallback) { return applyLoaderActionsToModuleType(moduleType, resolvedDecl.submoduleFallback, importLookup); } else { diff --git a/packages/pyright-internal/src/analyzer/typeGuards.ts b/packages/pyright-internal/src/analyzer/typeGuards.ts index ede5d65b9..5da99df58 100644 --- a/packages/pyright-internal/src/analyzer/typeGuards.ts +++ b/packages/pyright-internal/src/analyzer/typeGuards.ts @@ -1480,7 +1480,7 @@ function narrowTypeForIsInstance( className, ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.None, ParseTreeUtils.getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, @@ -2545,7 +2545,7 @@ function narrowTypeForCallable( className, ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.None, ParseTreeUtils.getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, diff --git a/packages/pyright-internal/src/analyzer/typeStubWriter.ts b/packages/pyright-internal/src/analyzer/typeStubWriter.ts index 1bbeee9a7..a79bf2d54 100644 --- a/packages/pyright-internal/src/analyzer/typeStubWriter.ts +++ b/packages/pyright-internal/src/analyzer/typeStubWriter.ts @@ -8,6 +8,7 @@ * and analyzed python source file. */ +import { Uri } from '../common/uri/uri'; import { ArgumentCategory, AssignmentNode, @@ -158,13 +159,13 @@ export class TypeStubWriter extends ParseTreeWalker { private _trackedImportFrom = new Map(); private _accessedImportedSymbols = new Set(); - constructor(private _stubPath: string, private _sourceFile: SourceFile, private _evaluator: TypeEvaluator) { + constructor(private _stubPath: Uri, private _sourceFile: SourceFile, private _evaluator: TypeEvaluator) { super(); // As a heuristic, we'll include all of the import statements // in "__init__.pyi" files even if they're not locally referenced // because these are often used as ways to re-export symbols. - if (this._stubPath.endsWith('__init__.pyi')) { + if (this._stubPath.fileName === '__init__.pyi') { this._includeAllImports = true; } } diff --git a/packages/pyright-internal/src/analyzer/typedDicts.ts b/packages/pyright-internal/src/analyzer/typedDicts.ts index 80b04c6f0..063c78aef 100644 --- a/packages/pyright-internal/src/analyzer/typedDicts.ts +++ b/packages/pyright-internal/src/analyzer/typedDicts.ts @@ -97,7 +97,7 @@ export function createTypedDictType( className, ParseTreeUtils.getClassFullName(errorNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.TypedDictClass, ParseTreeUtils.getTypeSourceId(errorNode), /* declaredMetaclass */ undefined, @@ -147,7 +147,7 @@ export function createTypedDictType( const declaration: VariableDeclaration = { type: DeclarationType.Variable, node: entry.name, - path: fileInfo.filePath, + uri: fileInfo.fileUri, typeAnnotationNode: entry.valueExpression, isRuntimeTypeExpression: true, range: convertOffsetsToRange( @@ -209,7 +209,7 @@ export function createTypedDictTypeInlined( className, ParseTreeUtils.getClassFullName(dictNode, fileInfo.moduleName, className), fileInfo.moduleName, - fileInfo.filePath, + fileInfo.fileUri, ClassTypeFlags.TypedDictClass, ParseTreeUtils.getTypeSourceId(dictNode), /* declaredMetaclass */ undefined, @@ -773,7 +773,7 @@ function getTypedDictFieldsFromDictSyntax( const declaration: VariableDeclaration = { type: DeclarationType.Variable, node: entry.keyExpression, - path: fileInfo.filePath, + uri: fileInfo.fileUri, typeAnnotationNode: entry.valueExpression, isRuntimeTypeExpression: !isInline, range: convertOffsetsToRange( diff --git a/packages/pyright-internal/src/analyzer/types.ts b/packages/pyright-internal/src/analyzer/types.ts index c485e076b..873f121b0 100644 --- a/packages/pyright-internal/src/analyzer/types.ts +++ b/packages/pyright-internal/src/analyzer/types.ts @@ -8,6 +8,7 @@ */ import { assert } from '../common/debug'; +import { Uri } from '../common/uri/uri'; import { ArgumentNode, ExpressionNode, NameNode, ParameterCategory } from '../parser/parseNodes'; import { FunctionDeclaration } from './declaration'; import { Symbol, SymbolTable } from './symbol'; @@ -383,18 +384,18 @@ export interface ModuleType extends TypeBase { // The period-delimited import name of this module. moduleName: string; - filePath: string; + fileUri: Uri; } export namespace ModuleType { - export function create(moduleName: string, filePath: string, symbolTable?: SymbolTable) { + export function create(moduleName: string, fileUri: Uri, symbolTable?: SymbolTable) { const newModuleType: ModuleType = { category: TypeCategory.Module, fields: symbolTable || new Map(), loaderFields: new Map(), flags: TypeFlags.Instantiable | TypeFlags.Instantiable, moduleName, - filePath, + fileUri, }; return newModuleType; } @@ -561,7 +562,7 @@ interface ClassDetails { name: string; fullName: string; moduleName: string; - filePath: string; + fileUri: Uri; flags: ClassTypeFlags; typeSourceId: TypeSourceId; baseClasses: Type[]; @@ -709,7 +710,7 @@ export namespace ClassType { name: string, fullName: string, moduleName: string, - filePath: string, + fileUri: Uri, flags: ClassTypeFlags, typeSourceId: TypeSourceId, declaredMetaclass: ClassType | UnknownType | undefined, @@ -722,7 +723,7 @@ export namespace ClassType { name, fullName, moduleName, - filePath, + fileUri, flags, typeSourceId, baseClasses: [], diff --git a/packages/pyright-internal/src/backgroundAnalysis.ts b/packages/pyright-internal/src/backgroundAnalysis.ts index 7db49ebef..aafa1b159 100644 --- a/packages/pyright-internal/src/backgroundAnalysis.ts +++ b/packages/pyright-internal/src/backgroundAnalysis.ts @@ -13,17 +13,17 @@ import { BackgroundAnalysisBase, BackgroundAnalysisRunnerBase } from './backgrou import { InitializationData } from './backgroundThreadBase'; import { getCancellationFolderName } from './common/cancellationUtils'; import { ConfigOptions } from './common/configOptions'; -import { ConsoleInterface } from './common/console'; import { FullAccessHost } from './common/fullAccessHost'; import { Host } from './common/host'; import { ServiceProvider } from './common/serviceProvider'; +import { getRootUri } from './common/uri/uriUtils'; export class BackgroundAnalysis extends BackgroundAnalysisBase { - constructor(console: ConsoleInterface) { - super(console); + constructor(serviceProvider: ServiceProvider) { + super(serviceProvider.console()); const initialData: InitializationData = { - rootDirectory: (global as any).__rootDirectory as string, + rootUri: getRootUri(serviceProvider.fs().isCaseSensitive)?.toString() ?? '', cancellationFolderName: getCancellationFolderName(), runner: undefined, }; @@ -40,7 +40,7 @@ export class BackgroundAnalysisRunner extends BackgroundAnalysisRunnerBase { } protected override createHost(): Host { - return new FullAccessHost(this.fs); + return new FullAccessHost(this.getServiceProvider()); } protected override createImportResolver( diff --git a/packages/pyright-internal/src/backgroundAnalysisBase.ts b/packages/pyright-internal/src/backgroundAnalysisBase.ts index a2fcc9151..43b621626 100644 --- a/packages/pyright-internal/src/backgroundAnalysisBase.ts +++ b/packages/pyright-internal/src/backgroundAnalysisBase.ts @@ -10,6 +10,7 @@ import { CancellationToken } from 'vscode-languageserver'; import { MessageChannel, MessagePort, Worker, parentPort, threadId, workerData } from 'worker_threads'; import { AnalysisCompleteCallback, AnalysisResults, analyzeProgram, nullCallback } from './analyzer/analysis'; +import { BackgroundAnalysisProgram, InvalidatedReason } from './analyzer/backgroundAnalysisProgram'; import { ImportResolver } from './analyzer/importResolver'; import { OpenFileOptions, Program } from './analyzer/program'; import { @@ -33,9 +34,9 @@ import { FileDiagnostics } from './common/diagnosticSink'; import { disposeCancellationToken, getCancellationTokenFromId } from './common/fileBasedCancellationUtils'; import { Host, HostKind } from './common/host'; import { LogTracker } from './common/logTracker'; -import { Range } from './common/textRange'; -import { BackgroundAnalysisProgram, InvalidatedReason } from './analyzer/backgroundAnalysisProgram'; import { ServiceProvider } from './common/serviceProvider'; +import { Range } from './common/textRange'; +import { Uri } from './common/uri/uri'; export class BackgroundAnalysisBase { private _worker: Worker | undefined; @@ -57,8 +58,8 @@ export class BackgroundAnalysisBase { this.enqueueRequest({ requestType: 'setConfigOptions', data: configOptions }); } - setTrackedFiles(filePaths: string[]) { - this.enqueueRequest({ requestType: 'setTrackedFiles', data: filePaths }); + setTrackedFiles(fileUris: Uri[]) { + this.enqueueRequest({ requestType: 'setTrackedFiles', data: fileUris.map((f) => f.toString()) }); } setAllowedThirdPartyImports(importNames: string[]) { @@ -69,36 +70,36 @@ export class BackgroundAnalysisBase { this.enqueueRequest({ requestType: 'ensurePartialStubPackages', data: { executionRoot } }); } - setFileOpened(filePath: string, version: number | null, contents: string, options: OpenFileOptions) { + setFileOpened(fileUri: Uri, version: number | null, contents: string, options: OpenFileOptions) { this.enqueueRequest({ requestType: 'setFileOpened', - data: { filePath, version, contents, options }, + data: { fileUri: fileUri.toString(), version, contents, options }, }); } - updateChainedFilePath(filePath: string, chainedFilePath: string | undefined) { + updateChainedUri(fileUri: Uri, chainedUri: Uri | undefined) { this.enqueueRequest({ - requestType: 'updateChainedFilePath', - data: { filePath, chainedFilePath }, + requestType: 'updateChainedFileUri', + data: { fileUri: fileUri.toString(), chainedUri: chainedUri?.toString() }, }); } - setFileClosed(filePath: string, isTracked?: boolean) { - this.enqueueRequest({ requestType: 'setFileClosed', data: { filePath, isTracked } }); + setFileClosed(fileUri: Uri, isTracked?: boolean) { + this.enqueueRequest({ requestType: 'setFileClosed', data: { fileUri, isTracked } }); } - addInterimFile(filePath: string) { - this.enqueueRequest({ requestType: 'addInterimFile', data: { filePath } }); + addInterimFile(fileUri: Uri) { + this.enqueueRequest({ requestType: 'addInterimFile', data: { fileUri: fileUri.toString() } }); } markAllFilesDirty(evenIfContentsAreSame: boolean) { this.enqueueRequest({ requestType: 'markAllFilesDirty', data: { evenIfContentsAreSame } }); } - markFilesDirty(filePaths: string[], evenIfContentsAreSame: boolean) { + markFilesDirty(fileUris: Uri[], evenIfContentsAreSame: boolean) { this.enqueueRequest({ requestType: 'markFilesDirty', - data: { filePaths, evenIfContentsAreSame }, + data: { fileUris: fileUris.map((f) => f.toString()), evenIfContentsAreSame }, }); } @@ -106,7 +107,7 @@ export class BackgroundAnalysisBase { this._startOrResumeAnalysis('analyze', program, token); } - async analyzeFile(filePath: string, token: CancellationToken): Promise { + async analyzeFile(fileUri: Uri, token: CancellationToken): Promise { throwIfCancellationRequested(token); const { port1, port2 } = new MessageChannel(); @@ -115,7 +116,7 @@ export class BackgroundAnalysisBase { const cancellationId = getCancellationTokenId(token); this.enqueueRequest({ requestType: 'analyzeFile', - data: { filePath, cancellationId }, + data: { fileUri: fileUri.toString(), cancellationId }, port: port2, }); @@ -127,7 +128,7 @@ export class BackgroundAnalysisBase { return result; } - async getDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken): Promise { + async getDiagnosticsForRange(fileUri: Uri, range: Range, token: CancellationToken): Promise { throwIfCancellationRequested(token); const { port1, port2 } = new MessageChannel(); @@ -136,7 +137,7 @@ export class BackgroundAnalysisBase { const cancellationId = getCancellationTokenId(token); this.enqueueRequest({ requestType: 'getDiagnosticsForRange', - data: { filePath, range, cancellationId }, + data: { fileUri: fileUri.toString(), range, cancellationId }, port: port2, }); @@ -149,9 +150,9 @@ export class BackgroundAnalysisBase { } async writeTypeStub( - targetImportPath: string, + targetImportPath: Uri, targetIsSingleFile: boolean, - stubPath: string, + stubPath: Uri, token: CancellationToken ): Promise { throwIfCancellationRequested(token); @@ -162,7 +163,12 @@ export class BackgroundAnalysisBase { const cancellationId = getCancellationTokenId(token); this.enqueueRequest({ requestType: 'writeTypeStub', - data: { targetImportPath, targetIsSingleFile, stubPath, cancellationId }, + data: { + targetImportPath: targetImportPath.toString(), + targetIsSingleFile, + stubPath: stubPath.toString(), + cancellationId, + }, port: port2, }); @@ -283,15 +289,15 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase protected importResolver: ImportResolver; protected logTracker: LogTracker; + protected isCaseSensitive = true; protected constructor(serviceProvider: ServiceProvider) { super(workerData as InitializationData, serviceProvider); // Stash the base directory into a global variable. const data = workerData as InitializationData; - this.log(LogLevel.Info, `Background analysis(${threadId}) root directory: ${data.rootDirectory}`); - - this._configOptions = new ConfigOptions(data.rootDirectory); + this.log(LogLevel.Info, `Background analysis(${threadId}) root directory: ${data.rootUri}`); + this._configOptions = new ConfigOptions(Uri.parse(data.rootUri, serviceProvider.fs().isCaseSensitive)); this.importResolver = this.createImportResolver(serviceProvider, this._configOptions, this.createHost()); const console = this.getConsole(); @@ -339,20 +345,20 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase case 'analyzeFile': { run(() => { - const { filePath, cancellationId } = msg.data; + const { fileUri, cancellationId } = msg.data; const token = getCancellationTokenFromId(cancellationId); - return this.handleAnalyzeFile(filePath, token); + return this.handleAnalyzeFile(fileUri, token); }, msg.port!); break; } case 'getDiagnosticsForRange': { run(() => { - const { filePath, range, cancellationId } = msg.data; + const { fileUri, range, cancellationId } = msg.data; const token = getCancellationTokenFromId(cancellationId); - return this.handleGetDiagnosticsForRange(filePath, range, token); + return this.handleGetDiagnosticsForRange(fileUri, range, token); }, msg.port!); break; } @@ -394,26 +400,26 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase } case 'setFileOpened': { - const { filePath, version, contents, options } = msg.data; - this.handleSetFileOpened(filePath, version, contents, options); + const { fileUri, version, contents, options } = msg.data; + this.handleSetFileOpened(fileUri, version, contents, options); break; } - case 'updateChainedFilePath': { - const { filePath, chainedFilePath } = msg.data; - this.handleUpdateChainedFilePath(filePath, chainedFilePath); + case 'updateChainedFileUri': { + const { fileUri, chainedUri } = msg.data; + this.handleUpdateChainedfileUri(fileUri, chainedUri); break; } case 'setFileClosed': { - const { filePath, isTracked } = msg.data; - this.handleSetFileClosed(filePath, isTracked); + const { fileUri, isTracked } = msg.data; + this.handleSetFileClosed(fileUri, isTracked); break; } case 'addInterimFile': { - const { filePath } = msg.data; - this.handleAddInterimFile(filePath); + const { fileUri } = msg.data; + this.handleAddInterimFile(fileUri); break; } @@ -424,8 +430,8 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase } case 'markFilesDirty': { - const { filePaths, evenIfContentsAreSame } = msg.data; - this.handleMarkFilesDirty(filePaths, evenIfContentsAreSame); + const { fileUris, evenIfContentsAreSame } = msg.data; + this.handleMarkFilesDirty(fileUris, evenIfContentsAreSame); break; } @@ -499,14 +505,14 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase } } - protected handleAnalyzeFile(filePath: string, token: CancellationToken) { + protected handleAnalyzeFile(fileUri: string, token: CancellationToken) { throwIfCancellationRequested(token); - return this.program.analyzeFile(filePath, token); + return this.program.analyzeFile(Uri.parse(fileUri, this.isCaseSensitive), token); } - protected handleGetDiagnosticsForRange(filePath: string, range: Range, token: CancellationToken) { + protected handleGetDiagnosticsForRange(fileUri: string, range: Range, token: CancellationToken) { throwIfCancellationRequested(token); - return this.program.getDiagnosticsForRange(filePath, range); + return this.program.getDiagnosticsForRange(Uri.parse(fileUri, this.isCaseSensitive), range); } protected handleWriteTypeStub( @@ -524,7 +530,12 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase token ); - this.program.writeTypeStub(targetImportPath, targetIsSingleFile, stubPath, token); + this.program.writeTypeStub( + Uri.parse(targetImportPath, this.isCaseSensitive), + targetIsSingleFile, + Uri.parse(stubPath, this.isCaseSensitive), + token + ); } protected handleSetImportResolver(hostKind: HostKind) { @@ -548,8 +559,8 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase this.program.setImportResolver(this.importResolver); } - protected handleSetTrackedFiles(filePaths: string[]) { - const diagnostics = this.program.setTrackedFiles(filePaths); + protected handleSetTrackedFiles(fileUris: string[]) { + const diagnostics = this.program.setTrackedFiles(fileUris.map((f) => Uri.parse(f, this.isCaseSensitive))); this._reportDiagnostics(diagnostics, this.program.getFilesToAnalyzeCount(), 0); } @@ -565,29 +576,35 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase } protected handleSetFileOpened( - filePath: string, + fileUri: string, version: number | null, contents: string, options: OpenFileOptions | undefined ) { - this.program.setFileOpened(filePath, version, contents, options); + this.program.setFileOpened(Uri.parse(fileUri, this.isCaseSensitive), version, contents, options); } - protected handleUpdateChainedFilePath(filePath: string, chainedFilePath: string | undefined) { - this.program.updateChainedFilePath(filePath, chainedFilePath); + protected handleUpdateChainedfileUri(fileUri: string, chainedfileUri: string | undefined) { + this.program.updateChainedUri( + Uri.parse(fileUri, this.isCaseSensitive), + chainedfileUri ? Uri.parse(chainedfileUri, this.isCaseSensitive) : undefined + ); } - protected handleSetFileClosed(filePath: string, isTracked: boolean | undefined) { - const diagnostics = this.program.setFileClosed(filePath, isTracked); + protected handleSetFileClosed(fileUri: string, isTracked: boolean | undefined) { + const diagnostics = this.program.setFileClosed(Uri.parse(fileUri, this.isCaseSensitive), isTracked); this._reportDiagnostics(diagnostics, this.program.getFilesToAnalyzeCount(), 0); } - protected handleAddInterimFile(filePath: string) { - this.program.addInterimFile(filePath); + protected handleAddInterimFile(fileUri: string) { + this.program.addInterimFile(Uri.parse(fileUri, this.isCaseSensitive)); } - protected handleMarkFilesDirty(filePaths: string[], evenIfContentsAreSame: boolean) { - this.program.markFilesDirty(filePaths, evenIfContentsAreSame); + protected handleMarkFilesDirty(fileUris: string[], evenIfContentsAreSame: boolean) { + this.program.markFilesDirty( + fileUris.map((f) => Uri.parse(f, this.isCaseSensitive)), + evenIfContentsAreSame + ); } protected handleMarkAllFilesDirty(evenIfContentsAreSame: boolean) { @@ -666,7 +683,7 @@ export abstract class BackgroundAnalysisRunnerBase extends BackgroundThreadBase function convertAnalysisResults(result: AnalysisResults): AnalysisResults { result.diagnostics = result.diagnostics.map((f: FileDiagnostics) => { return { - filePath: f.filePath, + fileUri: f.fileUri, version: f.version, diagnostics: convertDiagnostics(f.diagnostics), }; @@ -692,7 +709,7 @@ function convertDiagnostics(diagnostics: Diagnostic[]) { if (d._relatedInfo) { for (const info of d._relatedInfo) { - diag.addRelatedInfo(info.message, info.filePath, info.range); + diag.addRelatedInfo(info.message, info.fileUri, info.range); } } @@ -708,7 +725,7 @@ export type AnalysisRequestKind = | 'setAllowedThirdPartyImports' | 'ensurePartialStubPackages' | 'setFileOpened' - | 'updateChainedFilePath' + | 'updateChainedFileUri' | 'setFileClosed' | 'markAllFilesDirty' | 'markFilesDirty' diff --git a/packages/pyright-internal/src/backgroundThreadBase.ts b/packages/pyright-internal/src/backgroundThreadBase.ts index 425507f0d..ecf8f4523 100644 --- a/packages/pyright-internal/src/backgroundThreadBase.ts +++ b/packages/pyright-internal/src/backgroundThreadBase.ts @@ -12,11 +12,12 @@ import { OperationCanceledException, setCancellationFolderName } from './common/ import { ConfigOptions } from './common/configOptions'; import { ConsoleInterface, LogLevel } from './common/console'; import * as debug from './common/debug'; -import { FileSpec } from './common/pathUtils'; import { createFromRealFileSystem, RealTempFile } from './common/realFileSystem'; import { ServiceProvider } from './common/serviceProvider'; import './common/serviceProviderExtensions'; import { ServiceKeys } from './common/serviceProviderExtensions'; +import { Uri } from './common/uri/uri'; +import { FileSpec } from './common/uri/uriUtils'; export class BackgroundConsole implements ConsoleInterface { // We always generate logs in the background. For the foreground, @@ -48,9 +49,6 @@ export class BackgroundThreadBase { protected constructor(data: InitializationData, serviceProvider?: ServiceProvider) { setCancellationFolderName(data.cancellationFolderName); - // Stash the base directory into a global variable. - (global as any).__rootDirectory = data.rootDirectory; - // Make sure there's a file system and a console interface. this._serviceProvider = serviceProvider ?? new ServiceProvider(); if (!this._serviceProvider.tryGet(ServiceKeys.console)) { @@ -60,8 +58,17 @@ export class BackgroundThreadBase { this._serviceProvider.add(ServiceKeys.fs, createFromRealFileSystem(this.getConsole())); } if (!this._serviceProvider.tryGet(ServiceKeys.tempFile)) { - this._serviceProvider.add(ServiceKeys.tempFile, new RealTempFile()); + this._serviceProvider.add( + ServiceKeys.tempFile, + new RealTempFile(this._serviceProvider.fs().isCaseSensitive) + ); } + + // Stash the base directory into a global variable. + (global as any).__rootDirectory = Uri.parse( + data.rootUri, + this._serviceProvider.fs().isCaseSensitive + ).getFilePath(); } protected get fs() { @@ -170,7 +177,7 @@ export function getBackgroundWaiter(port: MessagePort): Promise { } export interface InitializationData { - rootDirectory: string; + rootUri: string; cancellationFolderName: string | undefined; runner: string | undefined; title?: string; diff --git a/packages/pyright-internal/src/commands/createTypeStub.ts b/packages/pyright-internal/src/commands/createTypeStub.ts index 94e72b3f6..78ce575f3 100644 --- a/packages/pyright-internal/src/commands/createTypeStub.ts +++ b/packages/pyright-internal/src/commands/createTypeStub.ts @@ -9,6 +9,7 @@ import { CancellationToken, ExecuteCommandParams } from 'vscode-languageserver'; import { OperationCanceledException } from '../common/cancellationUtils'; +import { Uri } from '../common/uri/uri'; import { LanguageServerInterface } from '../languageServerBase'; import { AnalyzerServiceExecutor } from '../languageService/analyzerServiceExecutor'; import { ServerCommand } from './commandController'; @@ -18,9 +19,9 @@ export class CreateTypeStubCommand implements ServerCommand { async execute(cmdParams: ExecuteCommandParams, token: CancellationToken): Promise { if (cmdParams.arguments && cmdParams.arguments.length >= 2) { - const workspaceRoot = cmdParams.arguments[0] as string; + const workspaceRoot = Uri.parse(cmdParams.arguments[0] as string, this._ls.rootUri.isCaseSensitive); const importName = cmdParams.arguments[1] as string; - const callingFile = cmdParams.arguments[2] as string; + const callingFile = Uri.parse(cmdParams.arguments[2] as string, this._ls.rootUri.isCaseSensitive); const service = await AnalyzerServiceExecutor.cloneService( this._ls, diff --git a/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts b/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts index c400648d7..171408edd 100644 --- a/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts +++ b/packages/pyright-internal/src/commands/dumpFileDebugInfoCommand.ts @@ -29,6 +29,7 @@ import { isNumber, isString } from '../common/core'; import { convertOffsetToPosition, convertOffsetsToRange } from '../common/positionUtils'; import { TextRange } from '../common/textRange'; import { TextRangeCollection } from '../common/textRangeCollection'; +import { Uri } from '../common/uri/uri'; import { LanguageServerInterface } from '../languageServerBase'; import { ArgumentCategory, @@ -132,11 +133,11 @@ export class DumpFileDebugInfoCommand implements ServerCommand { return []; } - const filePath = params.arguments[0] as string; + const fileUri = Uri.parse(params.arguments[0] as string, this._ls.rootUri.isCaseSensitive); const kind = params.arguments[1]; - const workspace = await this._ls.getWorkspaceForFile(filePath); - const parseResults = workspace.service.getParseResult(workspace.service.fs.realCasePath(filePath)); + const workspace = await this._ls.getWorkspaceForFile(fileUri); + const parseResults = workspace.service.getParseResult(workspace.service.fs.realCasePath(fileUri)); if (!parseResults) { return []; } @@ -157,7 +158,7 @@ export class DumpFileDebugInfoCommand implements ServerCommand { }, }; - collectingConsole.info(`* Dump debug info for '${filePath}'`); + collectingConsole.info(`* Dump debug info for '${fileUri.toUserVisibleString()}'`); switch (kind) { case 'tokens': { @@ -166,7 +167,7 @@ export class DumpFileDebugInfoCommand implements ServerCommand { for (let i = 0; i < parseResults.tokenizerOutput.tokens.count; i++) { const token = parseResults.tokenizerOutput.tokens.getItemAt(i); collectingConsole.info( - `[${i}] ${getTokenString(filePath, token, parseResults.tokenizerOutput.lines)}` + `[${i}] ${getTokenString(fileUri, token, parseResults.tokenizerOutput.lines)}` ); } break; @@ -174,7 +175,7 @@ export class DumpFileDebugInfoCommand implements ServerCommand { case 'nodes': { collectingConsole.info(`* Node info`); - const dumper = new TreeDumper(filePath, parseResults.tokenizerOutput.lines); + const dumper = new TreeDumper(fileUri, parseResults.tokenizerOutput.lines); dumper.walk(parseResults.parseTree); collectingConsole.info(dumper.output); @@ -189,7 +190,7 @@ export class DumpFileDebugInfoCommand implements ServerCommand { } collectingConsole.info(`* Type info`); - collectingConsole.info(`${getTypeEvaluatorString(filePath, evaluator, parseResults, start, end)}`); + collectingConsole.info(`${getTypeEvaluatorString(fileUri, evaluator, parseResults, start, end)}`); break; } case 'cachedtypes': { @@ -201,9 +202,7 @@ export class DumpFileDebugInfoCommand implements ServerCommand { } collectingConsole.info(`* Cached Type info`); - collectingConsole.info( - `${getTypeEvaluatorString(filePath, evaluator, parseResults, start, end, true)}` - ); + collectingConsole.info(`${getTypeEvaluatorString(fileUri, evaluator, parseResults, start, end, true)}`); break; } @@ -239,14 +238,14 @@ function stringify(value: any, replacer: (this: any, key: string, value: any) => } function getTypeEvaluatorString( - file: string, + uri: Uri, evaluator: TypeEvaluator, results: ParseResults, start: number, end: number, cacheOnly?: boolean ) { - const dumper = new TreeDumper(file, results.tokenizerOutput.lines); + const dumper = new TreeDumper(uri, results.tokenizerOutput.lines); const node = findNodeByOffset(results.parseTree, start) ?? findNodeByOffset(results.parseTree, end); if (!node) { return 'N/A'; @@ -549,7 +548,7 @@ class TreeDumper extends ParseTreeWalker { private _indentation = ''; private _output = ''; - constructor(private _file: string, private _lines: TextRangeCollection) { + constructor(private _uri: Uri, private _lines: TextRangeCollection) { super(); } @@ -604,7 +603,7 @@ class TreeDumper extends ParseTreeWalker { override visitBinaryOperation(node: BinaryOperationNode) { this._log( `${this._getPrefix(node)} ${getTokenString( - this._file, + this._uri, node.operatorToken, this._lines )} ${getOperatorTypeString(node.operator)}} parenthesized:(${node.parenthesized})` @@ -697,7 +696,7 @@ class TreeDumper extends ParseTreeWalker { `${this._getPrefix(node)} wildcard import:(${node.isWildcardImport}) paren:(${ node.usesParens }) wildcard token:(${ - node.wildcardToken ? getTokenString(this._file, node.wildcardToken, this._lines) : 'N/A' + node.wildcardToken ? getTokenString(this._uri, node.wildcardToken, this._lines) : 'N/A' }) missing import keyword:(${node.missingImportKeyword})` ); return true; @@ -784,7 +783,7 @@ class TreeDumper extends ParseTreeWalker { } override visitName(node: NameNode) { - this._log(`${this._getPrefix(node)} ${getTokenString(this._file, node.token, this._lines)} ${node.value}`); + this._log(`${this._getPrefix(node)} ${getTokenString(this._uri, node.token, this._lines)} ${node.value}`); return true; } @@ -834,7 +833,7 @@ class TreeDumper extends ParseTreeWalker { } override visitString(node: StringNode) { - this._log(`${this._getPrefix(node)} ${getTokenString(this._file, node.token, this._lines)} ${node.value}`); + this._log(`${this._getPrefix(node)} ${getTokenString(this._uri, node.token, this._lines)} ${node.value}`); return true; } @@ -866,7 +865,7 @@ class TreeDumper extends ParseTreeWalker { override visitUnaryOperation(node: UnaryOperationNode) { this._log( `${this._getPrefix(node)} ${getTokenString( - this._file, + this._uri, node.operatorToken, this._lines )} ${getOperatorTypeString(node.operator)}` @@ -988,7 +987,7 @@ class TreeDumper extends ParseTreeWalker { 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( + return `[${node.id}] '${this._uri.toString()}:${pos.line + 1}:${pos.character + 1}' => ${printParseNodeType( node.nodeType )} ${getTextSpanString(node, this._lines)} =>`; } @@ -1066,9 +1065,9 @@ function getErrorExpressionCategoryString(type: ErrorExpressionCategory) { } } -function getTokenString(file: string, token: Token, lines: TextRangeCollection) { +function getTokenString(uri: Uri, token: Token, lines: TextRangeCollection) { const pos = convertOffsetToPosition(token.start, lines); - let str = `'${file}:${pos.line + 1}:${pos.character + 1}' (`; + let str = `'${uri.toUserVisibleString()}:${pos.line + 1}:${pos.character + 1}' (`; str += getTokenTypeString(token.type); str += getNewLineInfo(token); str += getOperatorInfo(token); diff --git a/packages/pyright-internal/src/commands/quickActionCommand.ts b/packages/pyright-internal/src/commands/quickActionCommand.ts index 460ba03b5..4db117fd8 100644 --- a/packages/pyright-internal/src/commands/quickActionCommand.ts +++ b/packages/pyright-internal/src/commands/quickActionCommand.ts @@ -8,6 +8,7 @@ import { CancellationToken, ExecuteCommandParams } from 'vscode-languageserver'; +import { Uri } from '../common/uri/uri'; import { convertToFileTextEdits, convertToWorkspaceEdit } from '../common/workspaceEditUtils'; import { LanguageServerInterface } from '../languageServerBase'; import { performQuickAction } from '../languageService/quickActions'; @@ -19,20 +20,19 @@ export class QuickActionCommand implements ServerCommand { async execute(params: ExecuteCommandParams, token: CancellationToken): Promise { if (params.arguments && params.arguments.length >= 1) { - const docUri = params.arguments[0] as string; + const docUri = Uri.parse(params.arguments[0] as string, this._ls.rootUri.isCaseSensitive); const otherArgs = params.arguments.slice(1); - const filePath = this._ls.decodeTextDocumentUri(docUri); - const workspace = await this._ls.getWorkspaceForFile(filePath); + const workspace = await this._ls.getWorkspaceForFile(docUri); if (params.command === Commands.orderImports && workspace.disableOrganizeImports) { return []; } const editActions = workspace.service.run((p) => { - return performQuickAction(p, filePath, params.command, otherArgs, token); + return performQuickAction(p, docUri, params.command, otherArgs, token); }, token); - return convertToWorkspaceEdit(workspace.service.fs, convertToFileTextEdits(filePath, editActions ?? [])); + return convertToWorkspaceEdit(convertToFileTextEdits(docUri, editActions ?? [])); } } } diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts index a1cc69579..25a9cfaf0 100644 --- a/packages/pyright-internal/src/common/configOptions.ts +++ b/packages/pyright-internal/src/common/configOptions.ts @@ -18,19 +18,11 @@ import { TaskListToken } from './diagnostic'; import { DiagnosticRule } from './diagnosticRules'; import { FileSystem } from './fileSystem'; import { Host } from './host'; -import { - FileSpec, - combinePaths, - ensureTrailingDirectorySeparator, - getFileSpec, - isDirectory, - normalizePath, - realCasePath, - resolvePaths, -} from './pathUtils'; import { PythonVersion, latestStablePythonVersion, versionFromString, versionToString } from './pythonVersion'; import { ServiceProvider } from './serviceProvider'; import { ServiceKeys } from './serviceProviderExtensions'; +import { Uri } from './uri/uri'; +import { FileSpec, getFileSpec, isDirectory } from './uri/uriUtils'; export enum PythonPlatform { Darwin = 'Darwin', @@ -39,10 +31,9 @@ export enum PythonPlatform { } export class ExecutionEnvironment { - // Root directory for execution - absolute or relative to the - // project root. + // Root directory for execution. // Undefined if this is a rootless environment (e.g., open file mode). - root?: string; + root?: Uri; // Name of a virtual environment if there is one, otherwise // just the path to the python executable. @@ -55,18 +46,18 @@ export class ExecutionEnvironment { pythonPlatform?: string | undefined; // Default to no extra paths. - extraPaths: string[] = []; + extraPaths: Uri[] = []; // Default to "." which indicates every file in the project. constructor( name: string, - root: string, + root: Uri, defaultPythonVersion: PythonVersion | undefined, defaultPythonPlatform: string | undefined, - defaultExtraPaths: string[] | undefined + defaultExtraPaths: Uri[] | undefined ) { this.name = name; - this.root = root || undefined; + this.root = root; this.pythonVersion = defaultPythonVersion || latestStablePythonVersion; this.pythonPlatform = defaultPythonPlatform; this.extraPaths = Array.from(defaultExtraPaths ?? []); @@ -780,9 +771,9 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet { return diagSettings; } -export function matchFileSpecs(configOptions: ConfigOptions, filePath: string, isFile = true) { +export function matchFileSpecs(configOptions: ConfigOptions, uri: Uri, isFile = true) { for (const includeSpec of configOptions.include) { - if (FileSpec.matchIncludeFileSpec(includeSpec.regExp, configOptions.exclude, filePath, isFile)) { + if (FileSpec.matchIncludeFileSpec(includeSpec.regExp, configOptions.exclude, uri, isFile)) { return true; } } @@ -795,19 +786,19 @@ export function matchFileSpecs(configOptions: ConfigOptions, filePath: string, i export class ConfigOptions { // Absolute directory of project. All relative paths in the config // are based on this path. - projectRoot: string; + projectRoot: Uri; // Path to python interpreter. - pythonPath?: string | undefined; + pythonPath?: Uri | undefined; // Name of the python environment. pythonEnvironmentName?: string | undefined; // Path to use for typeshed definitions. - typeshedPath?: string | undefined; + typeshedPath?: Uri | undefined; // Path to custom typings (stub) modules. - stubPath?: string | undefined; + stubPath?: Uri | undefined; // A list of file specs to include in the analysis. Can contain // directories, in which case all "*.py" files within those directories @@ -887,7 +878,7 @@ export class ConfigOptions { // directories. This is used in conjunction with the "venv" name in // the config file to identify the python environment used for resolving // third-party modules. - venvPath?: string | undefined; + venvPath?: Uri | undefined; // Default venv environment. venv?: string | undefined; @@ -899,7 +890,7 @@ export class ConfigOptions { defaultPythonPlatform?: string | undefined; // Default extraPaths. Can be overridden by executionEnvironment. - defaultExtraPaths?: string[] | undefined; + defaultExtraPaths?: Uri[] | undefined; //--------------------------------------------------------------- // Internal-only switches @@ -917,7 +908,7 @@ export class ConfigOptions { // Controls how hover and completion function signatures are displayed. functionSignatureDisplay: SignatureDisplayType; - constructor(projectRoot: string, typeCheckingMode?: string) { + constructor(projectRoot: Uri, typeCheckingMode?: string) { this.projectRoot = projectRoot; this.typeCheckingMode = typeCheckingMode; this.diagnosticRuleSet = ConfigOptions.getDiagnosticRuleSet(typeCheckingMode); @@ -950,17 +941,15 @@ export class ConfigOptions { ); } - // Finds the best execution environment for a given file path. The + // Finds the best execution environment for a given file uri. The // specified file path should be absolute. // If no matching execution environment can be found, a default // execution environment is used. - findExecEnvironment(filePath: string): ExecutionEnvironment { + findExecEnvironment(file: Uri): ExecutionEnvironment { return ( this.executionEnvironments.find((env) => { - const envRoot = ensureTrailingDirectorySeparator( - normalizePath(combinePaths(this.projectRoot, env.root)) - ); - return filePath.startsWith(envRoot); + const envRoot = Uri.isUri(env.root) ? env.root : this.projectRoot.combinePaths(env.root || ''); + return file.startsWith(envRoot); }) ?? this.getDefaultExecEnvironment() ); } @@ -997,7 +986,7 @@ export class ConfigOptions { } else if (isAbsolute(fileSpec)) { console.error(`Ignoring path "${fileSpec}" in "include" array because it is not relative.`); } else { - this.include.push(getFileSpec(serviceProvider, this.projectRoot, fileSpec)); + this.include.push(getFileSpec(this.projectRoot, fileSpec)); } }); } @@ -1016,7 +1005,7 @@ export class ConfigOptions { } else if (isAbsolute(fileSpec)) { console.error(`Ignoring path "${fileSpec}" in "exclude" array because it is not relative.`); } else { - this.exclude.push(getFileSpec(serviceProvider, this.projectRoot, fileSpec)); + this.exclude.push(getFileSpec(this.projectRoot, fileSpec)); } }); } @@ -1035,7 +1024,7 @@ export class ConfigOptions { } else if (isAbsolute(fileSpec)) { console.error(`Ignoring path "${fileSpec}" in "ignore" array because it is not relative.`); } else { - this.ignore.push(getFileSpec(serviceProvider, this.projectRoot, fileSpec)); + this.ignore.push(getFileSpec(this.projectRoot, fileSpec)); } }); } @@ -1054,7 +1043,7 @@ export class ConfigOptions { } else if (isAbsolute(fileSpec)) { console.error(`Ignoring path "${fileSpec}" in "strict" array because it is not relative.`); } else { - this.strict.push(getFileSpec(serviceProvider, this.projectRoot, fileSpec)); + this.strict.push(getFileSpec(this.projectRoot, fileSpec)); } }); } @@ -1116,7 +1105,7 @@ export class ConfigOptions { if (typeof configObj.venvPath !== 'string') { console.error(`Config "venvPath" field must contain a string.`); } else { - this.venvPath = normalizePath(combinePaths(this.projectRoot, configObj.venvPath)); + this.venvPath = this.projectRoot.combinePaths(configObj.venvPath); } } @@ -1141,7 +1130,7 @@ export class ConfigOptions { if (typeof path !== 'string') { console.error(`Config "extraPaths" field ${pathIndex} must be a string.`); } else { - this.defaultExtraPaths!.push(normalizePath(combinePaths(this.projectRoot, path))); + this.defaultExtraPaths!.push(this.projectRoot.combinePaths(path)); } }); } @@ -1181,8 +1170,8 @@ export class ConfigOptions { console.error(`Config "typeshedPath" field must contain a string.`); } else { this.typeshedPath = configObj.typeshedPath - ? normalizePath(combinePaths(this.projectRoot, configObj.typeshedPath)) - : ''; + ? this.projectRoot.combinePaths(configObj.typeshedPath) + : undefined; } } @@ -1195,7 +1184,7 @@ export class ConfigOptions { console.error(`Config "typingsPath" field must contain a string.`); } else { console.error(`Config "typingsPath" is now deprecated. Please, use stubPath instead.`); - this.stubPath = normalizePath(combinePaths(this.projectRoot, configObj.typingsPath)); + this.stubPath = this.projectRoot.combinePaths(configObj.typingsPath); } } @@ -1203,7 +1192,7 @@ export class ConfigOptions { if (typeof configObj.stubPath !== 'string') { console.error(`Config "stubPath" field must contain a string.`); } else { - this.stubPath = normalizePath(combinePaths(this.projectRoot, configObj.stubPath)); + this.stubPath = this.projectRoot.combinePaths(configObj.stubPath); } } @@ -1345,20 +1334,20 @@ export class ConfigOptions { } ensureDefaultExtraPaths(fs: FileSystem, autoSearchPaths: boolean, extraPaths: string[] | undefined) { - const paths: string[] = []; + const paths: Uri[] = []; if (autoSearchPaths) { // Auto-detect the common scenario where the sources are under the src folder - const srcPath = resolvePaths(this.projectRoot, pathConsts.src); - if (fs.existsSync(srcPath) && !fs.existsSync(resolvePaths(srcPath, '__init__.py'))) { - paths.push(realCasePath(srcPath, fs)); + const srcPath = this.projectRoot.combinePaths(pathConsts.src); + if (fs.existsSync(srcPath) && !fs.existsSync(srcPath.combinePaths('__init__.py'))) { + paths.push(fs.realCasePath(srcPath)); } } if (extraPaths && extraPaths.length > 0) { for (const p of extraPaths) { - const path = resolvePaths(this.projectRoot, p); - paths.push(realCasePath(path, fs)); + const path = this.projectRoot.combinePaths(p); + paths.push(fs.realCasePath(path)); if (isDirectory(fs, path)) { appendArray(paths, getPathsFromPthFiles(fs, path)); } @@ -1384,7 +1373,7 @@ export class ConfigOptions { } private _getEnvironmentName(): string { - return this.pythonEnvironmentName || this.pythonPath || 'python'; + return this.pythonEnvironmentName || this.pythonPath?.toString() || 'python'; } private _convertBoolean(value: any, fieldName: string, defaultValue: boolean): boolean { @@ -1429,7 +1418,7 @@ export class ConfigOptions { // Validate the root. if (envObj.root && typeof envObj.root === 'string') { - newExecEnv.root = normalizePath(combinePaths(this.projectRoot, envObj.root)); + newExecEnv.root = this.projectRoot.combinePaths(envObj.root); } else { console.error(`Config executionEnvironments index ${index}: missing root value.`); } @@ -1449,7 +1438,7 @@ export class ConfigOptions { ` extraPaths field ${pathIndex} must be a string.` ); } else { - newExecEnv.extraPaths.push(normalizePath(combinePaths(this.projectRoot, path))); + newExecEnv.extraPaths.push(this.projectRoot.combinePaths(path)); } }); } diff --git a/packages/pyright-internal/src/common/diagnostic.ts b/packages/pyright-internal/src/common/diagnostic.ts index 7f6216999..4d8f528e4 100644 --- a/packages/pyright-internal/src/common/diagnostic.ts +++ b/packages/pyright-internal/src/common/diagnostic.ts @@ -11,6 +11,7 @@ import { Commands } from '../commands/commands'; import { appendArray } from './collectionUtils'; import { DiagnosticLevel } from './configOptions'; import { Range, TextRange } from './textRange'; +import { Uri } from './uri/uri'; const defaultMaxDepth = 5; const defaultMaxLineCount = 8; @@ -63,7 +64,7 @@ export interface DiagnosticAction { } export interface DiagnosticWithinFile { - filePath: string; + uri: Uri; diagnostic: Diagnostic; } @@ -74,13 +75,13 @@ export interface CreateTypeStubFileAction extends DiagnosticAction { export interface RenameShadowedFileAction extends DiagnosticAction { action: ActionKind.RenameShadowedFileAction; - oldFile: string; - newFile: string; + oldUri: Uri; + newUri: Uri; } export interface DiagnosticRelatedInfo { message: string; - filePath: string; + uri: Uri; range: Range; priority: TaskListPriority; } @@ -118,13 +119,8 @@ export class Diagnostic { return this._rule; } - addRelatedInfo( - message: string, - filePath: string, - range: Range, - priority: TaskListPriority = TaskListPriority.Normal - ) { - this._relatedInfo.push({ filePath, message, range, priority }); + addRelatedInfo(message: string, fileUri: Uri, range: Range, priority: TaskListPriority = TaskListPriority.Normal) { + this._relatedInfo.push({ uri: fileUri, message, range, priority }); } getRelatedInfo() { diff --git a/packages/pyright-internal/src/common/diagnosticSink.ts b/packages/pyright-internal/src/common/diagnosticSink.ts index b1949371f..37307b2c8 100644 --- a/packages/pyright-internal/src/common/diagnosticSink.ts +++ b/packages/pyright-internal/src/common/diagnosticSink.ts @@ -14,10 +14,11 @@ import { convertOffsetsToRange } from './positionUtils'; import { hashString } from './stringUtils'; import { Range, TextRange } from './textRange'; import { TextRangeCollection } from './textRangeCollection'; +import { Uri } from './uri/uri'; // Represents a collection of diagnostics within a file. export interface FileDiagnostics { - filePath: string; + fileUri: Uri; version: number | undefined; diagnostics: Diagnostic[]; } diff --git a/packages/pyright-internal/src/common/editAction.ts b/packages/pyright-internal/src/common/editAction.ts index c496941da..0736fea6a 100644 --- a/packages/pyright-internal/src/common/editAction.ts +++ b/packages/pyright-internal/src/common/editAction.ts @@ -8,6 +8,7 @@ */ import { Range, rangesAreEqual } from './textRange'; +import { Uri } from './uri/uri'; export interface TextEditAction { range: Range; @@ -15,7 +16,7 @@ export interface TextEditAction { } export interface FileEditAction extends TextEditAction { - filePath: string; + fileUri: Uri; } export interface FileEditActions { @@ -31,18 +32,18 @@ export interface FileOperation { export interface RenameFileOperation extends FileOperation { kind: 'rename'; - oldFilePath: string; - newFilePath: string; + oldFileUri: Uri; + newFileUri: Uri; } export interface CreateFileOperation extends FileOperation { kind: 'create'; - filePath: string; + fileUri: Uri; } export interface DeleteFileOperation extends FileOperation { kind: 'delete'; - filePath: string; + fileUri: Uri; } export namespace TextEditAction { @@ -53,13 +54,13 @@ export namespace TextEditAction { export namespace FileEditAction { export function is(value: any): value is FileEditAction { - return value.filePath !== undefined && TextEditAction.is(value); + return value.fileUri !== undefined && TextEditAction.is(value); } export function areEqual(e1: FileEditAction, e2: FileEditAction) { return ( e1 === e2 || - (e1.filePath === e2.filePath && + (e1.fileUri.equals(e2.fileUri) && rangesAreEqual(e1.range, e2.range) && e1.replacementText === e2.replacementText) ); diff --git a/packages/pyright-internal/src/common/envVarUtils.ts b/packages/pyright-internal/src/common/envVarUtils.ts index 3d9e50417..324017499 100644 --- a/packages/pyright-internal/src/common/envVarUtils.ts +++ b/packages/pyright-internal/src/common/envVarUtils.ts @@ -8,43 +8,35 @@ import * as os from 'os'; -import { - combinePaths, - ensureTrailingDirectorySeparator, - getPathComponents, - hasTrailingDirectorySeparator, -} from './pathUtils'; +import { Uri } from './uri/uri'; // Expands certain predefined variables supported within VS Code settings. // Ideally, VS Code would provide an API for doing this expansion, but // it doesn't. We'll handle the most common variables here as a convenience. -export function expandPathVariables(rootPath: string, path: string): string { - const pathParts = getPathComponents(path); +export function expandPathVariables(rootPath: Uri, path: string): string { + // Make sure the pathStr looks like a URI path. + let pathStr = path.replace(/\\/g, '/'); - const expandedParts: string[] = []; - for (const part of pathParts) { - const trimmedPart = part.trim(); + // Make sure all replacements look like URI paths too. + const replace = (match: RegExp, replaceValue: string) => { + pathStr = pathStr.replace(match, replaceValue.replace(/\\/g, '/')); + }; - if (trimmedPart === '${workspaceFolder}') { - expandedParts.push(rootPath); - } else if (trimmedPart === '${env:HOME}' && process.env.HOME !== undefined) { - expandedParts.push(process.env.HOME); - } else if (trimmedPart === '${env:USERNAME}' && process.env.USERNAME !== undefined) { - expandedParts.push(process.env.USERNAME); - } else if (trimmedPart === '${env:VIRTUAL_ENV}' && process.env.VIRTUAL_ENV !== undefined) { - expandedParts.push(process.env.VIRTUAL_ENV); - } else if (trimmedPart === '~' && os.homedir) { - expandedParts.push(os.homedir() || process.env.HOME || process.env.USERPROFILE || '~'); - } else { - expandedParts.push(part); - } + // Replace everything inline. + pathStr = pathStr.replace(/\$\{workspaceFolder\}/g, rootPath.getPath()); + if (process.env.HOME !== undefined) { + replace(/\$\{env:HOME\}/g, process.env.HOME || ''); + } + if (process.env.USERNAME !== undefined) { + replace(/\$\{env:USERNAME\}/g, process.env.USERNAME || ''); + } + if (process.env.VIRTUAL_ENV !== undefined) { + replace(/\$\{env:VIRTUAL_ENV\}/g, process.env.VIRTUAL_ENV || ''); + } + if (os.homedir) { + replace(/\/~/g, os.homedir() || process.env.HOME || process.env.USERPROFILE || '~'); + replace(/^~/g, os.homedir() || process.env.HOME || process.env.USERPROFILE || '~'); } - if (expandedParts.length === 0) { - return path; - } - - const root = expandedParts.shift()!; - const expandedPath = combinePaths(root, ...expandedParts); - return hasTrailingDirectorySeparator(path) ? ensureTrailingDirectorySeparator(expandedPath) : expandedPath; + return pathStr; } diff --git a/packages/pyright-internal/src/common/extensibility.ts b/packages/pyright-internal/src/common/extensibility.ts index b29923a44..51a11a99d 100644 --- a/packages/pyright-internal/src/common/extensibility.ts +++ b/packages/pyright-internal/src/common/extensibility.ts @@ -11,19 +11,20 @@ import { CancellationToken } from 'vscode-languageserver'; import { Declaration } from '../analyzer/declaration'; import { ImportResolver } from '../analyzer/importResolver'; import * as prog from '../analyzer/program'; +import { IPythonMode } from '../analyzer/sourceFile'; import { SourceMapper } from '../analyzer/sourceMapper'; +import { SymbolTable } from '../analyzer/symbol'; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +import { Diagnostic } from '../common/diagnostic'; import { ServerSettings } from '../languageServerBase'; import { ParseNode } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { ConfigOptions } from './configOptions'; import { ConsoleInterface } from './console'; import { ReadOnlyFileSystem } from './fileSystem'; -import { Range } from './textRange'; -import { SymbolTable } from '../analyzer/symbol'; -import { Diagnostic } from '../common/diagnostic'; -import { IPythonMode } from '../analyzer/sourceFile'; import { GroupServiceKey, ServiceKey } from './serviceProvider'; +import { Range } from './textRange'; +import { Uri } from './uri/uri'; export interface SourceFile { // See whether we can convert these to regular properties. @@ -31,9 +32,8 @@ export interface SourceFile { isThirdPartyPyTypedPresent(): boolean; getIPythonMode(): IPythonMode; - getFilePath(): string; + getUri(): Uri; getFileContent(): string | undefined; - getRealFilePath(): string | undefined; getClientVersion(): number | undefined; getOpenFileContents(): string | undefined; getModuleSymbolTable(): SymbolTable | undefined; @@ -73,7 +73,7 @@ export interface ServiceProvider { // Readonly wrapper around a Program. Makes sure it doesn't mutate the program. export interface ProgramView { readonly id: string; - readonly rootPath: string; + readonly rootPath: Uri; readonly console: ConsoleInterface; readonly evaluator: TypeEvaluator | undefined; readonly configOptions: ConfigOptions; @@ -81,21 +81,16 @@ export interface ProgramView { readonly fileSystem: ReadOnlyFileSystem; readonly serviceProvider: ServiceProvider; - owns(file: string): boolean; + owns(uri: Uri): boolean; getSourceFileInfoList(): readonly SourceFileInfo[]; - getParseResults(filePath: string): ParseResults | undefined; - getSourceFileInfo(filePath: string): SourceFileInfo | undefined; - getChainedFilePath(filePath: string): string | undefined; - getSourceMapper( - filePath: string, - token: CancellationToken, - mapCompiled?: boolean, - preferStubs?: boolean - ): SourceMapper; + getParseResults(fileUri: Uri): ParseResults | undefined; + getSourceFileInfo(fileUri: Uri): SourceFileInfo | undefined; + getChainedUri(fileUri: Uri): Uri | undefined; + getSourceMapper(fileUri: Uri, token: CancellationToken, mapCompiled?: boolean, preferStubs?: boolean): SourceMapper; // Consider getDiagnosticsForRange to call `analyzeFile` automatically if the file is not analyzed. - analyzeFile(filePath: string, token: CancellationToken): boolean; - getDiagnosticsForRange(filePath: string, range: Range): Diagnostic[]; + analyzeFile(fileUri: Uri, token: CancellationToken): boolean; + getDiagnosticsForRange(fileUri: Uri, range: Range): Diagnostic[]; // See whether we can get rid of these methods handleMemoryHighUsage(): void; @@ -106,9 +101,9 @@ export interface ProgramView { // and doesn't forward the request to the BG thread. // One can use this when edits are temporary such as `runEditMode` or `test` export interface EditableProgram extends ProgramView { - addInterimFile(file: string): void; - setFileOpened(filePath: string, version: number | null, contents: string, options?: prog.OpenFileOptions): void; - updateChainedFilePath(filePath: string, chainedFilePath: string | undefined): void; + addInterimFile(uri: Uri): void; + setFileOpened(fileUri: Uri, version: number | null, contents: string, options?: prog.OpenFileOptions): void; + updateChainedUri(fileUri: Uri, chainedUri: Uri | undefined): void; } // Mutable wrapper around a program. Allows the FG thread to forward this request to the BG thread @@ -116,7 +111,7 @@ export interface EditableProgram extends ProgramView { export interface ProgramMutator { addInterimFile(file: string): void; setFileOpened( - filePath: string, + fileUri: Uri, version: number | null, contents: string, ipythonMode: IPythonMode, @@ -162,7 +157,7 @@ export interface SymbolUsageProvider { } export interface StatusMutationListener { - fileDirty?: (filePath: string) => void; + fileDirty?: (fileUri: Uri) => void; clearCache?: () => void; updateSettings?: (settings: T) => void; } diff --git a/packages/pyright-internal/src/common/fileSystem.ts b/packages/pyright-internal/src/common/fileSystem.ts index 8ffa3ef9f..f03f454f1 100644 --- a/packages/pyright-internal/src/common/fileSystem.ts +++ b/packages/pyright-internal/src/common/fileSystem.ts @@ -11,6 +11,7 @@ // * NOTE * except tests, this should be only file that import "fs" import type * as fs from 'fs'; import { FileWatcher, FileWatcherEventHandler } from './fileWatcher'; +import { Uri } from './uri/uri'; export interface Stats { size: number; @@ -33,48 +34,47 @@ export interface MkDirOptions { } export interface ReadOnlyFileSystem { - existsSync(path: string): boolean; - chdir(path: string): void; - readdirEntriesSync(path: string): fs.Dirent[]; - readdirSync(path: string): string[]; - readFileSync(path: string, encoding?: null): Buffer; - readFileSync(path: string, encoding: BufferEncoding): string; - readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer; + readonly isCaseSensitive: boolean; + existsSync(uri: Uri): boolean; + chdir(uri: Uri): void; + readdirEntriesSync(uri: Uri): fs.Dirent[]; + readdirSync(uri: Uri): string[]; + readFileSync(uri: Uri, encoding?: null): Buffer; + readFileSync(uri: Uri, encoding: BufferEncoding): string; + readFileSync(uri: Uri, encoding?: BufferEncoding | null): string | Buffer; - statSync(path: string): Stats; - realpathSync(path: string): string; - getModulePath(): string; + statSync(uri: Uri): Stats; + realpathSync(uri: Uri): Uri; + getModulePath(): Uri; // Async I/O - readFile(path: string): Promise; - readFileText(path: string, encoding?: BufferEncoding): Promise; + readFile(uri: Uri): Promise; + readFileText(uri: Uri, encoding?: BufferEncoding): Promise; // Return path in casing on OS. - realCasePath(path: string): string; + realCasePath(uri: Uri): Uri; // See whether the file is mapped to another location. - isMappedFilePath(filepath: string): boolean; + isMappedUri(uri: Uri): boolean; - // Get original filepath if the given filepath is mapped. - getOriginalFilePath(mappedFilePath: string): string; + // Get original uri if the given uri is mapped. + getOriginalUri(mappedUri: Uri): Uri; - // Get mapped filepath if the given filepath is mapped. - getMappedFilePath(originalFilepath: string): string; + // Get mapped uri if the given uri is mapped. + getMappedUri(originalUri: Uri): Uri; - getUri(path: string): string; - - isInZip(path: string): boolean; + isInZip(uri: Uri): boolean; } export interface FileSystem extends ReadOnlyFileSystem { - mkdirSync(path: string, options?: MkDirOptions): void; - writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null): void; + mkdirSync(uri: Uri, options?: MkDirOptions): void; + writeFileSync(uri: Uri, data: string | Buffer, encoding: BufferEncoding | null): void; - unlinkSync(path: string): void; - rmdirSync(path: string): void; + unlinkSync(uri: Uri): void; + rmdirSync(uri: Uri): void; - createFileSystemWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher; - createReadStream(path: string): fs.ReadStream; - createWriteStream(path: string): fs.WriteStream; - copyFileSync(src: string, dst: string): void; + createFileSystemWatcher(uris: Uri[], listener: FileWatcherEventHandler): FileWatcher; + createReadStream(uri: Uri): fs.ReadStream; + createWriteStream(uri: Uri): fs.WriteStream; + copyFileSync(uri: Uri, dst: Uri): void; } export interface TmpfileOptions { @@ -84,8 +84,8 @@ export interface TmpfileOptions { export interface TempFile { // The directory returned by tmpdir must exist and be the same each time tmpdir is called. - tmpdir(): string; - tmpfile(options?: TmpfileOptions): string; + tmpdir(): Uri; + tmpfile(options?: TmpfileOptions): Uri; dispose(): void; } diff --git a/packages/pyright-internal/src/common/fileWatcher.ts b/packages/pyright-internal/src/common/fileWatcher.ts index bc635904e..c385eda16 100644 --- a/packages/pyright-internal/src/common/fileWatcher.ts +++ b/packages/pyright-internal/src/common/fileWatcher.ts @@ -6,16 +6,17 @@ * file watcher related functionality. */ import { Stats } from './fileSystem'; +import { Uri } from './uri/uri'; export type FileWatcherEventType = 'add' | 'addDir' | 'change' | 'unlink' | 'unlinkDir'; -export type FileWatcherEventHandler = (eventName: FileWatcherEventType, path: string, stats?: Stats) => void; +export type FileWatcherEventHandler = (eventName: FileWatcherEventType, uri: Uri, stats?: Stats) => void; export interface FileWatcher { close(): void; } export interface FileWatcherHandler { - onFileChange(eventType: FileWatcherEventType, path: string): void; + onFileChange(eventType: FileWatcherEventType, uri: Uri): void; } export interface FileWatcherProvider { @@ -23,7 +24,7 @@ export interface FileWatcherProvider { } export const nullFileWatcherHandler: FileWatcherHandler = { - onFileChange(_1: FileWatcherEventType, _2: string): void { + onFileChange(_1: FileWatcherEventType, _2: Uri): void { // do nothing }, }; diff --git a/packages/pyright-internal/src/common/fullAccessHost.ts b/packages/pyright-internal/src/common/fullAccessHost.ts index 0429e3f63..de3a59610 100644 --- a/packages/pyright-internal/src/common/fullAccessHost.ts +++ b/packages/pyright-internal/src/common/fullAccessHost.ts @@ -13,10 +13,12 @@ import { PythonPathResult } from '../analyzer/pythonPathUtils'; import { OperationCanceledException, throwIfCancellationRequested } from './cancellationUtils'; import { PythonPlatform } from './configOptions'; import { assertNever } from './debug'; -import { FileSystem } from './fileSystem'; import { HostKind, NoAccessHost, ScriptOutput } from './host'; -import { isDirectory, normalizePath } from './pathUtils'; +import { normalizePath } from './pathUtils'; import { PythonVersion, versionFromMajorMinor } from './pythonVersion'; +import { ServiceProvider } from './serviceProvider'; +import { Uri } from './uri/uri'; +import { isDirectory } from './uri/uriUtils'; // preventLocalImports removes the working directory from sys.path. // The -c flag adds it automatically, which can allow some stdlib @@ -60,7 +62,7 @@ export class LimitedAccessHost extends NoAccessHost { } export class FullAccessHost extends LimitedAccessHost { - constructor(protected fs: FileSystem) { + constructor(protected serviceProvider: ServiceProvider) { super(); } @@ -68,29 +70,29 @@ export class FullAccessHost extends LimitedAccessHost { return HostKind.FullAccess; } - static createHost(kind: HostKind, fs: FileSystem) { + static createHost(kind: HostKind, serviceProvider: ServiceProvider) { switch (kind) { case HostKind.NoAccess: return new NoAccessHost(); case HostKind.LimitedAccess: return new LimitedAccessHost(); case HostKind.FullAccess: - return new FullAccessHost(fs); + return new FullAccessHost(serviceProvider); default: assertNever(kind); } } - override getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult { + override getPythonSearchPaths(pythonPath?: Uri, logInfo?: string[]): PythonPathResult { const importFailureInfo = logInfo ?? []; - let result = this._executePythonInterpreter(pythonPath, (p) => - this._getSearchPathResultFromInterpreter(this.fs, p, importFailureInfo) + let result = this._executePythonInterpreter(pythonPath?.getFilePath(), (p) => + this._getSearchPathResultFromInterpreter(p, importFailureInfo) ); if (!result) { result = { paths: [], - prefix: '', + prefix: undefined, }; } @@ -102,12 +104,12 @@ export class FullAccessHost extends LimitedAccessHost { return result; } - override getPythonVersion(pythonPath?: string, logInfo?: string[]): PythonVersion | undefined { + override getPythonVersion(pythonPath?: Uri, logInfo?: string[]): PythonVersion | undefined { const importFailureInfo = logInfo ?? []; try { const commandLineArgs: string[] = ['-c', extractVersion]; - const execOutput = this._executePythonInterpreter(pythonPath, (p) => + const execOutput = this._executePythonInterpreter(pythonPath?.getFilePath(), (p) => child_process.execFileSync(p, commandLineArgs, { encoding: 'utf8' }) ); @@ -128,8 +130,8 @@ export class FullAccessHost extends LimitedAccessHost { } override runScript( - pythonPath: string | undefined, - script: string, + pythonPath: Uri | undefined, + script: Uri, args: string[], cwd: string, token: CancellationToken @@ -141,9 +143,9 @@ export class FullAccessHost extends LimitedAccessHost { return new Promise<{ stdout: string; stderr: string }>((resolve, reject) => { let stdout = ''; let stderr = ''; - const commandLineArgs = [script, ...args]; + const commandLineArgs = [script.getFilePath(), ...args]; - const child = this._executePythonInterpreter(pythonPath, (p) => + const child = this._executePythonInterpreter(pythonPath?.getFilePath(), (p) => child_process.spawn(p, commandLineArgs, { cwd }) ); const tokenWatch = token.onCancellationRequested(() => { @@ -210,20 +212,19 @@ export class FullAccessHost extends LimitedAccessHost { } private _getSearchPathResultFromInterpreter( - fs: FileSystem, - interpreter: string, + interpreterPath: string, importFailureInfo: string[] ): PythonPathResult | undefined { const result: PythonPathResult = { paths: [], - prefix: '', + prefix: undefined, }; try { const commandLineArgs: string[] = ['-c', extractSys]; - - importFailureInfo.push(`Executing interpreter: '${interpreter}'`); - const execOutput = child_process.execFileSync(interpreter, commandLineArgs, { encoding: 'utf8' }); + importFailureInfo.push(`Executing interpreter: '${interpreterPath}'`); + const execOutput = child_process.execFileSync(interpreterPath, commandLineArgs, { encoding: 'utf8' }); + const isCaseSensitive = this.serviceProvider.fs().isCaseSensitive; // Parse the execOutput. It should be a JSON-encoded array of paths. try { @@ -232,16 +233,20 @@ export class FullAccessHost extends LimitedAccessHost { execSplitEntry = execSplitEntry.trim(); if (execSplitEntry) { const normalizedPath = normalizePath(execSplitEntry); + const normalizedUri = Uri.file(normalizedPath, isCaseSensitive); // Skip non-existent paths and broken zips/eggs. - if (fs.existsSync(normalizedPath) && isDirectory(fs, normalizedPath)) { - result.paths.push(normalizedPath); + if ( + this.serviceProvider.fs().existsSync(normalizedUri) && + isDirectory(this.serviceProvider.fs(), normalizedUri) + ) { + result.paths.push(normalizedUri); } else { importFailureInfo.push(`Skipping '${normalizedPath}' because it is not a valid directory`); } } } - result.prefix = execSplit.prefix; + result.prefix = Uri.file(execSplit.prefix, isCaseSensitive); if (result.paths.length === 0) { importFailureInfo.push(`Found no valid directories`); diff --git a/packages/pyright-internal/src/common/host.ts b/packages/pyright-internal/src/common/host.ts index 133c67c33..ffb70ea06 100644 --- a/packages/pyright-internal/src/common/host.ts +++ b/packages/pyright-internal/src/common/host.ts @@ -11,6 +11,7 @@ import { CancellationToken } from 'vscode-languageserver'; import { PythonPathResult } from '../analyzer/pythonPathUtils'; import { PythonPlatform } from './configOptions'; import { PythonVersion } from './pythonVersion'; +import { Uri } from './uri/uri'; export const enum HostKind { FullAccess, @@ -25,12 +26,12 @@ export interface ScriptOutput { export interface Host { readonly kind: HostKind; - getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult; - getPythonVersion(pythonPath?: string, logInfo?: string[]): PythonVersion | undefined; + getPythonSearchPaths(pythonPath?: Uri, logInfo?: string[]): PythonPathResult; + getPythonVersion(pythonPath?: Uri, logInfo?: string[]): PythonVersion | undefined; getPythonPlatform(logInfo?: string[]): PythonPlatform | undefined; runScript( - pythonPath: string | undefined, - script: string, + pythonPath: Uri | undefined, + script: Uri, args: string[], cwd: string, token: CancellationToken @@ -42,16 +43,16 @@ export class NoAccessHost implements Host { return HostKind.NoAccess; } - getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult { + getPythonSearchPaths(pythonPath?: Uri, logInfo?: string[]): PythonPathResult { logInfo?.push('No access to python executable.'); return { paths: [], - prefix: '', + prefix: undefined, }; } - getPythonVersion(pythonPath?: string, logInfo?: string[]): PythonVersion | undefined { + getPythonVersion(pythonPath?: Uri, logInfo?: string[]): PythonVersion | undefined { return undefined; } @@ -60,8 +61,8 @@ export class NoAccessHost implements Host { } async runScript( - pythonPath: string | undefined, - scriptPath: string, + pythonPath: Uri | undefined, + scriptPath: Uri, args: string[], cwd: string, token: CancellationToken diff --git a/packages/pyright-internal/src/common/logTracker.ts b/packages/pyright-internal/src/common/logTracker.ts index dfcf181ce..24801cee2 100644 --- a/packages/pyright-internal/src/common/logTracker.ts +++ b/packages/pyright-internal/src/common/logTracker.ts @@ -9,16 +9,17 @@ import { ConsoleInterface, LogLevel } from './console'; import { ReadOnlyFileSystem } from './fileSystem'; import { Duration, timingStats } from './timing'; +import { Uri } from './uri/uri'; // Consider an operation "long running" if it goes longer than this. const durationThresholdForInfoInMs = 2000; -export function getPathForLogging(fs: ReadOnlyFileSystem, filepath: string) { - if (fs.isMappedFilePath(filepath)) { - return fs.getOriginalFilePath(filepath); +export function getPathForLogging(fs: ReadOnlyFileSystem, fileUri: Uri) { + if (fs.isMappedUri(fileUri)) { + return fs.getOriginalUri(fileUri); } - return filepath; + return fileUri; } export class LogTracker { diff --git a/packages/pyright-internal/src/common/pathUtils.ts b/packages/pyright-internal/src/common/pathUtils.ts index 5b78735e1..142d4f9cb 100644 --- a/packages/pyright-internal/src/common/pathUtils.ts +++ b/packages/pyright-internal/src/common/pathUtils.ts @@ -7,24 +7,14 @@ * Pathname utility functions. */ -import type { Dirent } from 'fs'; import * as path from 'path'; -import { URI, Utils } from 'vscode-uri'; import { Char } from './charCodes'; import { some } from './collectionUtils'; import { GetCanonicalFileName, identity } from './core'; -import { randomBytesHex } from './crypto'; import * as debug from './debug'; -import { ServiceProvider } from './extensibility'; -import { FileSystem, ReadOnlyFileSystem, Stats, TempFile } from './fileSystem'; -import { ServiceKeys } from './serviceProviderExtensions'; import { equateStringsCaseInsensitive, equateStringsCaseSensitive } from './stringUtils'; -let _fsCaseSensitivity: boolean | undefined = undefined; -let _underTest: boolean = false; -const _uriSchemePattern = /^\w[\w\d+.-]*$/; - export interface FileSpec { // File specs can contain wildcard characters (**, *, ?). This // specifies the first portion of the file spec that contains @@ -72,52 +62,26 @@ export interface FileSystemEntries { directories: string[]; } -export function forEachAncestorDirectory( - directory: string, - callback: (directory: string) => string | undefined -): string | undefined { - while (true) { - const result = callback(directory); - if (result !== undefined) { - return result; - } - - const parentPath = getDirectoryPath(directory); - if (parentPath === directory) { - return undefined; - } - - directory = parentPath; - } -} - export function getDirectoryPath(pathString: string): string { - if (isUri(pathString)) { - return Utils.dirname(URI.parse(pathString).with({ fragment: '', query: '' })).toString(); - } return pathString.substr(0, Math.max(getRootLength(pathString), pathString.lastIndexOf(path.sep))); } -export function isUri(pathString: string) { - return pathString.indexOf(':') > 1 && _uriSchemePattern.test(pathString.split(':')[0]); -} - /** * Returns length of the root part of a path or URL (i.e. length of "/", "x:/", "//server/"). */ -export function getRootLength(pathString: string, checkUri = true): number { - if (pathString.charAt(0) === path.sep) { - if (pathString.charAt(1) !== path.sep) { +export function getRootLength(pathString: string, sep = path.sep): number { + if (pathString.charAt(0) === sep) { + if (pathString.charAt(1) !== sep) { return 1; // POSIX: "/" (or non-normalized "\") } - const p1 = pathString.indexOf(path.sep, 2); + const p1 = pathString.indexOf(sep, 2); if (p1 < 0) { return pathString.length; // UNC: "//server" or "\\server" } return p1 + 1; // UNC: "//server/" or "\\server\" } if (pathString.charAt(1) === ':') { - if (pathString.charAt(2) === path.sep) { + if (pathString.charAt(2) === sep) { return 3; // DOS: "c:/" or "c:\" } if (pathString.length === 2) { @@ -125,25 +89,16 @@ export function getRootLength(pathString: string, checkUri = true): number { } } - if (checkUri && isUri(pathString)) { - const uri = URI.parse(pathString); - if (uri.authority) { - return uri.scheme.length + 3; // URI: "file://" - } else { - return uri.scheme.length + 1; // URI: "untitled:" - } - } - return 0; } export function getPathSeparator(pathString: string) { - return isUri(pathString) ? '/' : path.sep; + return path.sep; } export function getPathComponents(pathString: string) { const normalizedPath = normalizeSlashes(pathString); - const rootLength = getRootLength(normalizedPath, /* checkUri */ isUri(normalizedPath)); + const rootLength = getRootLength(normalizedPath); const root = normalizedPath.substring(0, rootLength); const sep = getPathSeparator(pathString); const rest = normalizedPath.substring(rootLength).split(sep); @@ -211,47 +166,11 @@ export function getRelativePath(dirPath: string, relativeTo: string) { return relativePath; } -// Creates a directory hierarchy for a path, starting from some ancestor path. -export function makeDirectories(fs: FileSystem, dirPath: string, startingFromDirPath: string) { - if (!dirPath.startsWith(startingFromDirPath)) { - return; - } - - const pathComponents = getPathComponents(dirPath); - const relativeToComponents = getPathComponents(startingFromDirPath); - let curPath = startingFromDirPath; - - for (let i = relativeToComponents.length; i < pathComponents.length; i++) { - curPath = combinePaths(curPath, pathComponents[i]); - if (!fs.existsSync(curPath)) { - fs.mkdirSync(curPath); - } - } -} - -export function getFileSize(fs: ReadOnlyFileSystem, path: string) { - const stat = tryStat(fs, path); - if (stat?.isFile()) { - return stat.size; - } - return 0; -} - -export function fileExists(fs: ReadOnlyFileSystem, path: string): boolean { - return fileSystemEntryExists(fs, path, FileSystemEntryKind.File); -} - -export function directoryExists(fs: ReadOnlyFileSystem, path: string): boolean { - return fileSystemEntryExists(fs, path, FileSystemEntryKind.Directory); -} - const getInvalidSeparator = (sep: string) => (sep === '/' ? '\\' : '/'); export function normalizeSlashes(pathString: string, sep = path.sep): string { - if (!isUri(pathString)) { - if (pathString.includes(getInvalidSeparator(sep))) { - const separatorRegExp = /[\\/]/g; - return pathString.replace(separatorRegExp, sep); - } + if (pathString.includes(getInvalidSeparator(sep))) { + const separatorRegExp = /[\\/]/g; + return pathString.replace(separatorRegExp, sep); } return pathString; @@ -271,7 +190,7 @@ export function resolvePaths(path: string, ...paths: (string | undefined)[]): st return normalizePath(some(paths) ? combinePaths(path, ...paths) : normalizeSlashes(path)); } -function combineFilePaths(pathString: string, ...paths: (string | undefined)[]): string { +export function combinePaths(pathString: string, ...paths: (string | undefined)[]): string { if (pathString) { pathString = normalizeSlashes(pathString); } @@ -283,7 +202,7 @@ function combineFilePaths(pathString: string, ...paths: (string | undefined)[]): relativePath = normalizeSlashes(relativePath); - if (!pathString || getRootLength(relativePath, /* checkUri */ false) !== 0) { + if (!pathString || getRootLength(relativePath) !== 0) { pathString = relativePath; } else { pathString = ensureTrailingDirectorySeparator(pathString) + relativePath; @@ -293,29 +212,6 @@ function combineFilePaths(pathString: string, ...paths: (string | undefined)[]): return pathString; } -export function combinePaths(pathString: string, ...paths: (string | undefined)[]): string { - if (!isUri(pathString)) { - // Not a URI, or a URI with a single letter scheme. - return combineFilePaths(pathString, ...paths); - } - - // Go through the paths to see if any are rooted. If so, treat as - // a file path. On linux this might be wrong if a path starts with '/'. - if (some(paths, (p) => !!p && getRootLength(p, /* checkUri */ false) !== 0)) { - return combineFilePaths(pathString, ...paths); - } - - // Otherwise this is a URI - const nonEmptyPaths = paths.filter((p) => !!p) as string[]; - const uri = URI.parse(pathString); - - // Make sure we have a path to append to. - if (uri.path === '' || uri.path === undefined) { - nonEmptyPaths.unshift('/'); - } - return Utils.joinPath(uri.with({ fragment: '', query: '' }), ...nonEmptyPaths).toString(); -} - /** * Determines whether a `parent` path contains a `child` path using the provide case sensitivity. */ @@ -485,9 +381,7 @@ export function getBaseFileName(pathString: string, extensions?: string | readon // return the trailing portion of the path starting after the last (non-terminal) directory // separator but not including any trailing directory separator. pathString = stripTrailingDirectorySeparator(pathString); - const name = isUri(pathString) - ? Utils.basename(URI.parse(pathString).with({ fragment: '', query: '' })) - : pathString.slice(Math.max(getRootLength(pathString), pathString.lastIndexOf(path.sep) + 1)); + const name = pathString.slice(Math.max(getRootLength(pathString), pathString.lastIndexOf(path.sep) + 1)); const extension = extensions !== undefined && ignoreCase !== undefined ? getAnyExtensionFromPath(name, extensions, ignoreCase) @@ -561,7 +455,7 @@ export function stripTrailingDirectorySeparator(pathString: string) { if (!hasTrailingDirectorySeparator(pathString)) { return pathString; } - return pathString.substr(0, pathString.length - 1); + return pathString.slice(0, pathString.length - 1); } export function getFileExtension(fileName: string, multiDotExtension = false) { @@ -571,7 +465,7 @@ export function getFileExtension(fileName: string, multiDotExtension = false) { fileName = getFileName(fileName); const firstDotIndex = fileName.indexOf('.'); - return fileName.substr(firstDotIndex); + return fileName.slice(firstDotIndex); } export function getFileName(pathString: string) { @@ -592,102 +486,8 @@ export function stripFileExtension(fileName: string, multiDotExtension = false) return fileName.substr(0, fileName.length - ext.length); } -export function realCasePath(pathString: string, fileSystem: ReadOnlyFileSystem): string { - return isUri(pathString) ? pathString : fileSystem.realCasePath(pathString); -} - export function normalizePath(pathString: string): string { - if (!isUri(pathString)) { - return normalizeSlashes(path.normalize(pathString)); - } - - // Must be a URI, already normalized. - return pathString; -} - -export function isDirectory(fs: ReadOnlyFileSystem, path: string): boolean { - return tryStat(fs, path)?.isDirectory() ?? false; -} - -export function isFile(fs: ReadOnlyFileSystem, path: string, treatZipDirectoryAsFile = false): boolean { - const stats = tryStat(fs, path); - if (stats?.isFile()) { - return true; - } - - if (!treatZipDirectoryAsFile) { - return false; - } - - return stats?.isZipDirectory?.() ?? false; -} - -export function tryStat(fs: ReadOnlyFileSystem, path: string): Stats | undefined { - try { - if (fs.existsSync(path)) { - return fs.statSync(path); - } - } catch (e: any) { - return undefined; - } - return undefined; -} - -export function tryRealpath(fs: ReadOnlyFileSystem, path: string): string | undefined { - try { - return fs.realCasePath(path); - } catch (e: any) { - return undefined; - } -} - -export function getFileSystemEntries(fs: ReadOnlyFileSystem, path: string): FileSystemEntries { - try { - return getFileSystemEntriesFromDirEntries(fs.readdirEntriesSync(path || '.'), fs, path); - } catch (e: any) { - return { files: [], directories: [] }; - } -} - -// Sorts the entires into files and directories, including any symbolic links. -export function getFileSystemEntriesFromDirEntries( - dirEntries: Dirent[], - fs: ReadOnlyFileSystem, - path: string -): FileSystemEntries { - const entries = dirEntries.sort((a, b) => { - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } else { - return 0; - } - }); - const files: string[] = []; - const directories: string[] = []; - for (const entry of entries) { - // This is necessary because on some file system node fails to exclude - // "." and "..". See https://github.com/nodejs/node/issues/4002 - if (entry.name === '.' || entry.name === '..') { - continue; - } - - if (entry.isFile()) { - files.push(entry.name); - } else if (entry.isDirectory()) { - directories.push(entry.name); - } else if (entry.isSymbolicLink()) { - const entryPath = combinePaths(path, entry.name); - const stat = tryStat(fs, entryPath); - if (stat?.isFile()) { - files.push(entry.name); - } else if (stat?.isDirectory()) { - directories.push(entry.name); - } - } - } - return { files, directories }; + return normalizeSlashes(path.normalize(pathString)); } // Transforms a relative file spec (one that potentially contains @@ -803,25 +603,6 @@ export function hasPythonExtension(path: string) { return path.endsWith('.py') || path.endsWith('.pyi'); } -export function getFileSpec(sp: ServiceProvider, rootPath: string, fileSpec: string): FileSpec { - let regExPattern = getWildcardRegexPattern(rootPath, fileSpec); - const escapedSeparator = getRegexEscapedSeparator(getPathSeparator(rootPath)); - regExPattern = `^(${regExPattern})($|${escapedSeparator})`; - - const fs = sp.get(ServiceKeys.fs); - const tmp = sp.tryGet(ServiceKeys.tempFile); - - const regExp = new RegExp(regExPattern, isFileSystemCaseSensitive(fs, tmp) ? undefined : 'i'); - const wildcardRoot = getWildcardRoot(rootPath, fileSpec); - const hasDirectoryWildcard = isDirectoryWildcardPatternPresent(fileSpec); - - return { - wildcardRoot, - regExp, - hasDirectoryWildcard, - }; -} - export function getRegexEscapedSeparator(pathSep: string = path.sep) { // we don't need to escape "/" in typescript regular expression return pathSep === '/' ? '/' : '\\\\'; @@ -908,168 +689,3 @@ function getPathComponentsRelativeTo( } return ['', ...relative, ...components]; } - -const enum FileSystemEntryKind { - File, - Directory, -} - -function fileSystemEntryExists(fs: ReadOnlyFileSystem, path: string, entryKind: FileSystemEntryKind): boolean { - try { - const stat = fs.statSync(path); - switch (entryKind) { - case FileSystemEntryKind.File: - return stat.isFile(); - case FileSystemEntryKind.Directory: - return stat.isDirectory(); - default: - return false; - } - } catch (e: any) { - return false; - } -} - -export function convertUriToPath(fs: ReadOnlyFileSystem, uriString: string): string { - return realCasePath(fs.getMappedFilePath(extractPathFromUri(uriString)), fs); -} - -export function extractPathFromUri(uriString: string) { - const uri = URI.parse(uriString); - - // Only for file scheme do we actually modify anything. All other uri strings - // maintain the same value they started with. - if (uri.scheme === 'file' && !uri.fragment) { - // When schema is "file", we use fsPath so that we can handle things like UNC paths. - let convertedPath = normalizePath(uri.fsPath); - - // If this is a DOS-style path with a drive letter, remove - // the leading slash. - if (convertedPath.match(/^\\[a-zA-Z]:\\/)) { - convertedPath = convertedPath.slice(1); - } - - return convertedPath; - } - - return uriString; -} - -export function convertPathToUri(fs: ReadOnlyFileSystem, path: string): string { - return fs.getUri(fs.getOriginalFilePath(path)); -} - -export function setTestingMode(underTest: boolean) { - _underTest = underTest; -} - -const isFileSystemCaseSensitiveMap = new WeakMap(); - -export function isFileSystemCaseSensitive(fs: FileSystem, tmp?: TempFile) { - if (!tmp) { - return false; - } - - if (!_underTest && _fsCaseSensitivity !== undefined) { - return _fsCaseSensitivity; - } - - if (!isFileSystemCaseSensitiveMap.has(fs)) { - _fsCaseSensitivity = isFileSystemCaseSensitiveInternal(fs, tmp); - isFileSystemCaseSensitiveMap.set(fs, _fsCaseSensitivity); - } - return !!isFileSystemCaseSensitiveMap.get(fs); -} - -export function isFileSystemCaseSensitiveInternal(fs: FileSystem, tmp: TempFile) { - let filePath: string | undefined = undefined; - try { - // Make unique file name. - let name: string; - let mangledFilePath: string; - do { - name = `${randomBytesHex(21)}-a`; - filePath = path.join(tmp.tmpdir(), name); - mangledFilePath = path.join(tmp.tmpdir(), name.toUpperCase()); - } while (fs.existsSync(filePath) || fs.existsSync(mangledFilePath)); - - fs.writeFileSync(filePath, '', 'utf8'); - - // If file exists, then it is insensitive. - return !fs.existsSync(mangledFilePath); - } catch (e: any) { - return false; - } finally { - if (filePath) { - // remove temp file created - try { - fs.unlinkSync(filePath); - } catch (e: any) { - /* ignored */ - } - } - } -} - -export function getLibraryPathWithoutExtension(libraryFilePath: string) { - let filePathWithoutExtension = stripFileExtension(libraryFilePath); - - // Strip off the '/__init__' if it's present. - if (filePathWithoutExtension.endsWith('__init__')) { - filePathWithoutExtension = filePathWithoutExtension.substr(0, filePathWithoutExtension.length - 9); - } - - return filePathWithoutExtension; -} - -export function getDirectoryChangeKind( - fs: ReadOnlyFileSystem, - oldDirectory: string, - newDirectory: string -): 'Same' | 'Renamed' | 'Moved' { - if (fs.realCasePath(oldDirectory) === fs.realCasePath(newDirectory)) { - return 'Same'; - } - - const relativePaths = getRelativePathComponentsFromDirectory(oldDirectory, newDirectory, (f) => fs.realCasePath(f)); - - // 3 means only last folder name has changed. - if (relativePaths.length === 3 && relativePaths[1] === '..' && relativePaths[2] !== '..') { - return 'Renamed'; - } - - return 'Moved'; -} - -export function deduplicateFolders(listOfFolders: string[][]): string[] { - const foldersToWatch = new Set(); - - listOfFolders.forEach((folders) => { - folders.forEach((p) => { - if (foldersToWatch.has(p)) { - // Bail out on exact match. - return; - } - - for (const existing of foldersToWatch) { - // ex) p: "/user/test" existing: "/user" - if (p.startsWith(existing)) { - // We already have the parent folder in the watch list - return; - } - - // ex) p: "/user" folderToWatch: "/user/test" - if (existing.startsWith(p)) { - // We found better one to watch. replace. - foldersToWatch.delete(existing); - foldersToWatch.add(p); - return; - } - } - - foldersToWatch.add(p); - }); - }); - - return [...foldersToWatch]; -} diff --git a/packages/pyright-internal/src/common/realFileSystem.ts b/packages/pyright-internal/src/common/realFileSystem.ts index 5659f438e..d122fdd7e 100644 --- a/packages/pyright-internal/src/common/realFileSystem.ts +++ b/packages/pyright-internal/src/common/realFileSystem.ts @@ -8,7 +8,6 @@ import { FakeFS, NativePath, PortablePath, PosixFS, ppath, VirtualFS, ZipFS, Zip import { getLibzipSync } from '@yarnpkg/libzip'; import * as fs from 'fs'; import * as tmp from 'tmp'; -import { URI } from 'vscode-uri'; import { isMainThread } from 'worker_threads'; import { ConsoleInterface, NullConsole } from './console'; @@ -21,7 +20,9 @@ import { FileWatcherProvider, nullFileWatcherProvider, } from './fileWatcher'; -import { combinePaths, getRootLength, isUri } from './pathUtils'; +import { getRootLength } from './pathUtils'; +import { Uri } from './uri/uri'; +import { getRootUri, isFileSystemCaseSensitive } from './uri/uriUtils'; // Automatically remove files created by tmp at process exit. tmp.setGracefulCleanup(); @@ -208,9 +209,19 @@ class YarnFS extends PosixFS { const yarnFS = new YarnFS(); class RealFileSystem implements FileSystem { - constructor(private _fileWatcherProvider: FileWatcherProvider, private _console: ConsoleInterface) {} + private _isCaseSensitive = true; + constructor(private _fileWatcherProvider: FileWatcherProvider, private _console: ConsoleInterface) { + this._isCaseSensitive = isFileSystemCaseSensitive(this, new RealTempFile(/* isCaseSensitive */ true)); + } - existsSync(path: string) { + get isCaseSensitive(): boolean { + return this._isCaseSensitive; + } + existsSync(uri: Uri) { + if (uri.isEmpty()) { + return false; + } + const path = uri.getFilePath(); try { // Catch zip open errors. existsSync is assumed to never throw by callers. return yarnFS.existsSync(path); @@ -219,11 +230,13 @@ class RealFileSystem implements FileSystem { } } - mkdirSync(path: string, options?: MkDirOptions) { + mkdirSync(uri: Uri, options?: MkDirOptions) { + const path = uri.getFilePath(); yarnFS.mkdirSync(path, options); } - chdir(path: string) { + chdir(uri: Uri) { + const path = uri.getFilePath(); // If this file system happens to be running in a worker thread, // then we can't call 'chdir'. if (isMainThread) { @@ -231,11 +244,13 @@ class RealFileSystem implements FileSystem { } } - readdirSync(path: string): string[] { + readdirSync(uri: Uri): string[] { + const path = uri.getFilePath(); return yarnFS.readdirSync(path); } - readdirEntriesSync(path: string): fs.Dirent[] { + readdirEntriesSync(uri: Uri): fs.Dirent[] { + const path = uri.getFilePath(); return yarnFS.readdirSync(path, { withFileTypes: true }).map((entry): fs.Dirent => { // Treat zip/egg files as directories. // See: https://github.com/yarnpkg/berry/blob/master/packages/vscode-zipfs/sources/ZipFSProvider.ts @@ -257,21 +272,24 @@ class RealFileSystem implements FileSystem { }); } - readFileSync(path: string, encoding?: null): Buffer; - readFileSync(path: string, encoding: BufferEncoding): string; - readFileSync(path: string, encoding?: BufferEncoding | null): Buffer | string; - readFileSync(path: string, encoding: BufferEncoding | null = null) { + readFileSync(uri: Uri, encoding?: null): Buffer; + readFileSync(uri: Uri, encoding: BufferEncoding): string; + readFileSync(uri: Uri, encoding?: BufferEncoding | null): Buffer | string; + readFileSync(uri: Uri, encoding: BufferEncoding | null = null) { + const path = uri.getFilePath(); if (encoding === 'utf8' || encoding === 'utf-8') { return yarnFS.readFileSync(path, 'utf8'); } return yarnFS.readFileSync(path); } - writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null) { + writeFileSync(uri: Uri, data: string | Buffer, encoding: BufferEncoding | null) { + const path = uri.getFilePath(); yarnFS.writeFileSync(path, data, encoding || undefined); } - statSync(path: string) { + statSync(uri: Uri) { + const path = uri.getFilePath(); const stat = yarnFS.statSync(path); // Treat zip/egg files as directories. // See: https://github.com/yarnpkg/berry/blob/master/packages/vscode-zipfs/sources/ZipFSProvider.ts @@ -286,53 +304,62 @@ class RealFileSystem implements FileSystem { return stat; } - rmdirSync(path: string): void { + rmdirSync(uri: Uri): void { + const path = uri.getFilePath(); yarnFS.rmdirSync(path); } - unlinkSync(path: string) { + unlinkSync(uri: Uri) { + const path = uri.getFilePath(); yarnFS.unlinkSync(path); } - realpathSync(path: string) { + realpathSync(uri: Uri) { try { - return yarnFS.realpathSync(path); + const path = uri.getFilePath(); + return Uri.file(yarnFS.realpathSync(path), this._isCaseSensitive); } catch (e: any) { - return path; + return uri; } } - getModulePath(): string { + getModulePath(): Uri { // The entry point to the tool should have set the __rootDirectory // global variable to point to the directory that contains the // typeshed-fallback directory. - return (global as any).__rootDirectory; + return getRootUri(this._isCaseSensitive) || Uri.empty(); } - createFileSystemWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher { + createFileSystemWatcher(paths: Uri[], listener: FileWatcherEventHandler): FileWatcher { return this._fileWatcherProvider.createFileWatcher( - paths.map((p) => this.realCasePath(p)), + paths.map((p) => p.getFilePath()), listener ); } - createReadStream(path: string): fs.ReadStream { + createReadStream(uri: Uri): fs.ReadStream { + const path = uri.getFilePath(); return yarnFS.createReadStream(path); } - createWriteStream(path: string): fs.WriteStream { + createWriteStream(uri: Uri): fs.WriteStream { + const path = uri.getFilePath(); return yarnFS.createWriteStream(path); } - copyFileSync(src: string, dst: string): void { - yarnFS.copyFileSync(src, dst); + copyFileSync(src: Uri, dst: Uri): void { + const srcPath = src.getFilePath(); + const destPath = dst.getFilePath(); + yarnFS.copyFileSync(srcPath, destPath); } - readFile(path: string): Promise { + readFile(uri: Uri): Promise { + const path = uri.getFilePath(); return yarnFS.readFilePromise(path); } - async readFileText(path: string, encoding: BufferEncoding): Promise { + async readFileText(uri: Uri, encoding: BufferEncoding): Promise { + const path = uri.getFilePath(); if (encoding === 'utf8' || encoding === 'utf-8') { return yarnFS.readFilePromise(path, 'utf8'); } @@ -340,17 +367,18 @@ class RealFileSystem implements FileSystem { return buffer.toString(encoding); } - realCasePath(path: string): string { + realCasePath(uri: Uri): Uri { try { // If it doesn't exist in the real FS, then just use this path. - if (!this.existsSync(path)) { - return this._getNormalizedPath(path); + if (!this.existsSync(uri)) { + return uri; } // If it does exist, skip this for symlinks. + const path = uri.getFilePath(); const stat = fs.lstatSync(path); if (stat.isSymbolicLink()) { - return this._getNormalizedPath(path); + return uri; } // realpathSync.native will return casing as in OS rather than @@ -359,53 +387,34 @@ class RealFileSystem implements FileSystem { // On UNC mapped drives we want to keep the original drive letter. if (getRootLength(realCase) !== getRootLength(path)) { - return path; + return uri; } - return realCase; + return Uri.file(realCase, this._isCaseSensitive); } catch (e: any) { // Return as it is, if anything failed. - this._console.log(`Failed to get real file system casing for ${path}: ${e}`); + this._console.log(`Failed to get real file system casing for ${uri}: ${e}`); - return path; + return uri; } } - isMappedFilePath(filepath: string): boolean { + isMappedUri(uri: Uri): boolean { return false; } - getOriginalFilePath(mappedFilePath: string) { - return mappedFilePath; + getOriginalUri(mappedUri: Uri) { + return mappedUri; } - getMappedFilePath(originalFilepath: string) { - return originalFilepath; + getMappedUri(originalUri: Uri) { + return originalUri; } - getUri(path: string): string { - // If this is not a file path, just return the original path. - if (isUri(path)) { - return path; - } - return URI.file(path).toString(); - } - - isInZip(path: string): boolean { + isInZip(uri: Uri): boolean { + const path = uri.getFilePath(); return /[^\\/]\.(?:egg|zip|jar)[\\/]/.test(path) && yarnFS.isZip(path); } - - private _getNormalizedPath(path: string) { - const driveLength = getRootLength(path); - - if (driveLength === 0) { - return path; - } - - // `vscode` sometimes uses different casing for drive letter. - // Make sure we normalize at least drive letter. - return combinePaths(fs.realpathSync.native(path.substring(0, driveLength)), path.substring(driveLength)); - } } interface WorkspaceFileWatcher extends FileWatcher { @@ -436,15 +445,15 @@ export class WorkspaceFileWatcherProvider implements FileWatcherProvider, FileWa return fileWatcher; } - onFileChange(eventType: FileWatcherEventType, filePath: string): void { + onFileChange(eventType: FileWatcherEventType, fileUri: Uri): void { // Since file watcher is a server wide service, we don't know which watcher is // for which workspace (for multi workspace case), also, we don't know which watcher // is for source or library. so we need to solely rely on paths that can cause us // to raise events both for source and library if .venv is inside of workspace root // for a file change. It is event handler's job to filter those out. this._fileWatchers.forEach((watcher) => { - if (watcher.workspacePaths.some((dirPath) => filePath.startsWith(dirPath))) { - watcher.eventHandler(eventType, filePath); + if (watcher.workspacePaths.some((dirPath) => fileUri.pathStartsWith(dirPath))) { + watcher.eventHandler(eventType, fileUri); } }); } @@ -453,17 +462,15 @@ export class WorkspaceFileWatcherProvider implements FileWatcherProvider, FileWa export class RealTempFile implements TempFile { private _tmpdir?: tmp.DirResult; - tmpdir() { - if (!this._tmpdir) { - this._tmpdir = tmp.dirSync({ prefix: 'pyright' }); - } + constructor(private readonly _isCaseSensitive: boolean) {} - return this._tmpdir.name; + tmpdir(): Uri { + return Uri.file(this._getTmpDir().name, this._isCaseSensitive); } - tmpfile(options?: TmpfileOptions): string { - const f = tmp.fileSync({ dir: this.tmpdir(), discardDescriptor: true, ...options }); - return f.name; + tmpfile(options?: TmpfileOptions): Uri { + const f = tmp.fileSync({ dir: this._getTmpDir().name, discardDescriptor: true, ...options }); + return Uri.file(f.name, this._isCaseSensitive); } dispose(): void { @@ -474,4 +481,12 @@ export class RealTempFile implements TempFile { // ignore } } + + private _getTmpDir(): tmp.DirResult { + if (!this._tmpdir) { + this._tmpdir = tmp.dirSync({ prefix: 'pyright' }); + } + + return this._tmpdir; + } } diff --git a/packages/pyright-internal/src/common/serviceProviderExtensions.ts b/packages/pyright-internal/src/common/serviceProviderExtensions.ts index 61e437071..0f4a99094 100644 --- a/packages/pyright-internal/src/common/serviceProviderExtensions.ts +++ b/packages/pyright-internal/src/common/serviceProviderExtensions.ts @@ -11,20 +11,22 @@ import { IPythonMode, SourceFile, SourceFileEditMode } from '../analyzer/sourceF import { SupportPartialStubs } from '../pyrightFileSystem'; import { ConsoleInterface } from './console'; import { - StatusMutationListener, + DebugInfoInspector, ServiceProvider as ReadOnlyServiceProvider, + StatusMutationListener, SymbolDefinitionProvider, SymbolUsageProviderFactory, - DebugInfoInspector, } from './extensibility'; import { FileSystem, TempFile } from './fileSystem'; import { LogTracker } from './logTracker'; import { GroupServiceKey, ServiceKey, ServiceProvider } from './serviceProvider'; +import { Uri } from './uri/uri'; declare module './serviceProvider' { interface ServiceProvider { fs(): FileSystem; console(): ConsoleInterface; + tmp(): TempFile | undefined; sourceFileFactory(): ISourceFileFactory; partialStubs(): SupportPartialStubs; } @@ -79,6 +81,9 @@ ServiceProvider.prototype.console = function () { ServiceProvider.prototype.partialStubs = function () { return this.get(ServiceKeys.partialStubs); }; +ServiceProvider.prototype.tmp = function () { + return this.tryGet(ServiceKeys.tempFile); +}; ServiceProvider.prototype.sourceFileFactory = function () { const result = this.tryGet(ServiceKeys.sourceFileFactory); return result || DefaultSourceFileFactory; @@ -87,26 +92,24 @@ ServiceProvider.prototype.sourceFileFactory = function () { const DefaultSourceFileFactory: ISourceFileFactory = { createSourceFile( serviceProvider: ReadOnlyServiceProvider, - filePath: string, + fileUri: Uri, moduleName: string, isThirdPartyImport: boolean, isThirdPartyPyTypedPresent: boolean, editMode: SourceFileEditMode, console?: ConsoleInterface, logTracker?: LogTracker, - realFilePath?: string, ipythonMode?: IPythonMode ) { return new SourceFile( serviceProvider, - filePath, + fileUri, moduleName, isThirdPartyImport, isThirdPartyPyTypedPresent, editMode, console, logTracker, - realFilePath, ipythonMode ); }, diff --git a/packages/pyright-internal/src/common/textEditTracker.ts b/packages/pyright-internal/src/common/textEditTracker.ts index 8b8040812..637818845 100644 --- a/packages/pyright-internal/src/common/textEditTracker.ts +++ b/packages/pyright-internal/src/common/textEditTracker.ts @@ -32,11 +32,11 @@ import { } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { appendArray, getOrAdd, removeArrayElements } from './collectionUtils'; -import { isString } from './core'; import * as debug from './debug'; import { FileEditAction } from './editAction'; import { convertOffsetToPosition, convertTextRangeToRange } from './positionUtils'; import { doesRangeContain, doRangesIntersect, extendRange, Range, TextRange } from './textRange'; +import { Uri } from './uri/uri'; export class TextEditTracker { private readonly _nodesRemoved: Map = new Map(); @@ -49,11 +49,11 @@ export class TextEditTracker { } addEdits(...edits: FileEditAction[]) { - edits.forEach((e) => this.addEdit(e.filePath, e.range, e.replacementText)); + edits.forEach((e) => this.addEdit(e.fileUri, e.range, e.replacementText)); } - addEdit(filePath: string, range: Range, replacementText: string) { - const edits = getOrAdd(this._results, filePath, () => []); + addEdit(fileUri: Uri, range: Range, replacementText: string) { + const edits = getOrAdd(this._results, fileUri.key, () => []); // If there is any overlapping edit, see whether we can merge edits. // We can merge edits, if one of them is 'deletion' or 2 edits has the same @@ -70,11 +70,11 @@ export class TextEditTracker { ); } - edits.push({ filePath, range, replacementText }); + edits.push({ fileUri: fileUri, range, replacementText }); } addEditWithTextRange(parseResults: ParseResults, range: TextRange, replacementText: string) { - const filePath = getFileInfo(parseResults.parseTree).filePath; + const filePath = getFileInfo(parseResults.parseTree).fileUri; const existing = parseResults.text.substr(range.start, range.length); if (existing === replacementText) { @@ -93,7 +93,7 @@ export class TextEditTracker { ? (importToDelete.parent as ImportNode).list : (importToDelete.parent as ImportFromNode).imports; - const filePath = getFileInfo(parseResults.parseTree).filePath; + const filePath = getFileInfo(parseResults.parseTree).fileUri; const ranges = getTextRangeForImportNameDeletion( parseResults, imports, @@ -183,7 +183,7 @@ export class TextEditTracker { importGroup: ImportGroup, importNameInfo?: ImportNameInfo[] ) { - const filePath = getFileInfo(parseResults.parseTree).filePath; + const fileUri = getFileInfo(parseResults.parseTree).fileUri; this.addEdits( ...getTextEditsForAutoImportInsertion( @@ -193,7 +193,7 @@ export class TextEditTracker { importGroup, parseResults, convertOffsetToPosition(parseResults.parseTree.length, parseResults.tokenizerOutput.lines) - ).map((e) => ({ filePath, range: e.range, replacementText: e.replacementText })) + ).map((e) => ({ fileUri, range: e.range, replacementText: e.replacementText })) ); } @@ -220,7 +220,7 @@ export class TextEditTracker { return false; } - const filePath = getFileInfo(parseResults.parseTree).filePath; + const fileUri = getFileInfo(parseResults.parseTree).fileUri; const edits = getTextEditsForAutoImportSymbolAddition(importNameInfo, imported, parseResults); if (imported.node !== updateOptions.currentFromImport) { @@ -228,7 +228,7 @@ export class TextEditTracker { // node we are working on. // ex) from xxx import yyy <= we are working on here. // from xxx import zzz <= but we found this. - this.addEdits(...edits.map((e) => ({ filePath, range: e.range, replacementText: e.replacementText }))); + this.addEdits(...edits.map((e) => ({ fileUri, range: e.range, replacementText: e.replacementText }))); return true; } @@ -247,9 +247,9 @@ export class TextEditTracker { return false; } - const deletions = this._getDeletionsForSpan(filePath, edits[0].range); + const deletions = this._getDeletionsForSpan(fileUri, edits[0].range); if (deletions.length === 0) { - this.addEdit(filePath, edits[0].range, edits[0].replacementText); + this.addEdit(fileUri, edits[0].range, edits[0].replacementText); return true; } @@ -265,13 +265,13 @@ export class TextEditTracker { return false; } - this._removeEdits(filePath, deletions); + this._removeEdits(fileUri, deletions); if (importName.alias) { this._nodesRemoved.delete(importName.alias); } this.addEdit( - filePath, + fileUri, convertTextRangeToRange(importName.name, parseResults.tokenizerOutput.lines), newLastModuleName ); @@ -279,17 +279,17 @@ export class TextEditTracker { return true; } - private _getDeletionsForSpan(filePathOrEdit: string | FileEditAction[], range: Range) { - const edits = this._getOverlappingForSpan(filePathOrEdit, range); + private _getDeletionsForSpan(fileUriOrEdit: Uri | FileEditAction[], range: Range) { + const edits = this._getOverlappingForSpan(fileUriOrEdit, range); return edits.filter((e) => e.replacementText === ''); } - private _removeEdits(filePathOrEdit: string | FileEditAction[], edits: FileEditAction[]) { - if (isString(filePathOrEdit)) { - filePathOrEdit = this._results.get(filePathOrEdit) ?? []; + private _removeEdits(fileUriOrEdit: Uri | FileEditAction[], edits: FileEditAction[]) { + if (Uri.isUri(fileUriOrEdit)) { + fileUriOrEdit = this._results.get(fileUriOrEdit.key) ?? []; } - removeArrayElements(filePathOrEdit, (f) => edits.some((e) => FileEditAction.areEqual(f, e))); + removeArrayElements(fileUriOrEdit, (f) => edits.some((e) => FileEditAction.areEqual(f, e))); } private _getEditsToMerge(edits: FileEditAction[], range: Range, replacementText: string) { @@ -319,12 +319,12 @@ export class TextEditTracker { ); } - private _getOverlappingForSpan(filePathOrEdit: string | FileEditAction[], range: Range) { - if (isString(filePathOrEdit)) { - filePathOrEdit = this._results.get(filePathOrEdit) ?? []; + private _getOverlappingForSpan(fileUriOrEdit: Uri | FileEditAction[], range: Range) { + if (Uri.isUri(fileUriOrEdit)) { + fileUriOrEdit = this._results.get(fileUriOrEdit.key) ?? []; } - return filePathOrEdit.filter((e) => doRangesIntersect(e.range, range)); + return fileUriOrEdit.filter((e) => doRangesIntersect(e.range, range)); } private _processNodeRemoved(token: CancellationToken) { @@ -343,7 +343,7 @@ export class TextEditTracker { this._pendingNodeToRemove.pop(); const info = getFileInfo(peekNodeToRemove.parseResults.parseTree); - this.addEdit(info.filePath, convertTextRangeToRange(peekNodeToRemove.node, info.lines), ''); + this.addEdit(info.fileUri, convertTextRangeToRange(peekNodeToRemove.node, info.lines), ''); } } } @@ -370,11 +370,7 @@ export class TextEditTracker { ); if (nameNodes.length === nodesRemoved.length) { - this.addEdit( - info.filePath, - ParseTreeUtils.getFullStatementRange(importNode, nodeToRemove.parseResults), - '' - ); + this.addEdit(info.fileUri, ParseTreeUtils.getFullStatementRange(importNode, nodeToRemove.parseResults), ''); // Remove nodes that are handled from queue. this._removeNodesHandled(nodesRemoved); @@ -397,7 +393,7 @@ export class TextEditTracker { } const editSpans = getTextRangeForImportNameDeletion(nodeToRemove.parseResults, nameNodes, ...indices); - editSpans.forEach((e) => this.addEdit(info.filePath, convertTextRangeToRange(e, info.lines), '')); + editSpans.forEach((e) => this.addEdit(info.fileUri, convertTextRangeToRange(e, info.lines), '')); this._removeNodesHandled(nodesRemoved); return true; diff --git a/packages/pyright-internal/src/common/textRange.ts b/packages/pyright-internal/src/common/textRange.ts index 1f8547f0c..9f4b5c9ea 100644 --- a/packages/pyright-internal/src/common/textRange.ts +++ b/packages/pyright-internal/src/common/textRange.ts @@ -7,6 +7,8 @@ * Specifies the range of text within a larger string. */ +import { Uri } from './uri/uri'; + export interface TextRange { readonly start: number; readonly length: number; @@ -131,7 +133,7 @@ export namespace Range { // Represents a range within a particular document. export interface DocumentRange { - path: string; + uri: Uri; range: Range; } diff --git a/packages/pyright-internal/src/common/uri/baseUri.ts b/packages/pyright-internal/src/common/uri/baseUri.ts new file mode 100644 index 000000000..197703f6f --- /dev/null +++ b/packages/pyright-internal/src/common/uri/baseUri.ts @@ -0,0 +1,269 @@ +/* + * baseUri.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Base URI class for storing and manipulating URIs. + */ + +import { some } from '../collectionUtils'; +import { getShortenedFileName, normalizeSlashes } from '../pathUtils'; +import { Uri } from './uri'; + +export abstract class BaseUri implements Uri { + protected constructor(private readonly _key: string) {} + + // Unique key for storing in maps. + get key() { + return this._key; + } + + // Returns the scheme of the URI. + abstract get scheme(): string; + + // Returns whether the underlying file system is case sensitive or not. + abstract get isCaseSensitive(): boolean; + + // Returns the last segment of the URI, similar to the UNIX basename command. + abstract get fileName(): string; + + // Returns just the fileName without any extensions + get fileNameWithoutExtension(): string { + const fileName = this.fileName; + const index = fileName.lastIndexOf('.'); + if (index > 0) { + return fileName.slice(0, index); + } else { + return fileName; + } + } + + // Returns the extension of the URI, similar to the UNIX extname command. + abstract get lastExtension(): string; + + // Returns a URI where the path just contains the root folder. + abstract get root(): Uri; + + // Returns a URI where the path contains the path with .py appended. + get packageUri(): Uri { + // This is assuming that the current path is a file already. + return this.addExtension('.py'); + } + + // Returns a URI where the path contains the path with .pyi appended. + get packageStubUri(): Uri { + // This is assuming that the current path is a file already. + return this.addExtension('.pyi'); + } + + // Returns a URI where the path has __init__.py appended. + get initPyUri(): Uri { + // This is assuming that the current path is a directory already. + return this.combinePaths('__init__.py'); + } + + // Returns a URI where the path has __init__.pyi appended. + get initPyiUri(): Uri { + // This is assuming that the current path is a directory already. + return this.combinePaths('__init__.pyi'); + } + + // Returns a URI where the path has py.typed appended. + get pytypedUri(): Uri { + // This is assuming that the current path is a directory already. + return this.combinePaths('py.typed'); + } + + isEmpty(): boolean { + return false; + } + + abstract toString(): string; + + abstract toUserVisibleString(): string; + + abstract matchesRegex(regex: RegExp): boolean; + + replaceExtension(ext: string): Uri { + const dir = this.getDirectory(); + const base = this.fileName; + const newBase = base.slice(0, base.length - this.lastExtension.length) + ext; + return dir.combinePaths(newBase); + } + + addExtension(ext: string): Uri { + return this.addPath(ext); + } + + hasExtension(ext: string): boolean { + return this.isCaseSensitive + ? this.lastExtension === ext + : this.lastExtension.toLowerCase() === ext.toLowerCase(); + } + + abstract addPath(extra: string): Uri; + + // Returns a URI where the path is the directory name of the original URI, similar to the UNIX dirname command. + abstract getDirectory(): Uri; + + getRootPathLength(): number { + return this.getRootPath().length; + } + + // Determines whether a path consists only of a path root. + abstract isRoot(): boolean; + + // Determines whether a Uri is a child of some parent Uri. + abstract isChild(parent: Uri, ignoreCase?: boolean): boolean; + + abstract isLocal(): boolean; + + isUntitled(): boolean { + return this.scheme === 'untitled'; + } + + equals(other: Uri | undefined): boolean { + return this.key === other?.key; + } + + abstract startsWith(other: Uri | undefined, ignoreCase?: boolean): boolean; + + pathStartsWith(name: string): boolean { + // ignore path separators. + name = normalizeSlashes(name); + return this.getComparablePath().startsWith(name); + } + + pathEndsWith(name: string): boolean { + // ignore path separators. + name = normalizeSlashes(name); + return this.getComparablePath().endsWith(name); + } + + pathIncludes(include: string): boolean { + // ignore path separators. + include = normalizeSlashes(include); + return this.getComparablePath().includes(include); + } + + // How long the path for this Uri is. + abstract getPathLength(): number; + + // Combines paths to create a new Uri. Any '..' or '.' path components will be normalized. + abstract combinePaths(...paths: string[]): Uri; + + getRelativePath(child: Uri): string | undefined { + if (this.scheme !== child.scheme) { + return undefined; + } + + // Unlike getRelativePathComponents, this function should not return relative path + // markers for non children. + if (child.isChild(this)) { + const relativeToComponents = this.getRelativePathComponents(child); + if (relativeToComponents.length > 0) { + return ['.', ...relativeToComponents].join('/'); + } + } + return undefined; + } + + getPathComponents(): readonly string[] { + // Make sure to freeze the result so that it can't be modified. + return Object.freeze(this.getPathComponentsImpl()); + } + + abstract getPath(): string; + + abstract getFilePath(): string; + + getRelativePathComponents(to: Uri): readonly string[] { + const fromComponents = this.getPathComponents(); + const toComponents = to.getPathComponents(); + + let start: number; + for (start = 0; start < fromComponents.length && start < toComponents.length; start++) { + const fromComponent = fromComponents[start]; + const toComponent = toComponents[start]; + if (fromComponent !== toComponent) { + break; + } + } + + if (start === 0) { + return toComponents; + } + + const components = toComponents.slice(start); + const relative: string[] = []; + for (; start < fromComponents.length; start++) { + relative.push('..'); + } + return [...relative, ...components]; + } + + getShortenedFileName(maxDirLength: number = 15): string { + return getShortenedFileName(this.getPath(), maxDirLength); + } + + stripExtension(): Uri { + const base = this.fileName; + const index = base.lastIndexOf('.'); + if (index > 0) { + const stripped = base.slice(0, index); + return this.getDirectory().combinePaths(stripped); + } else { + return this; + } + } + + stripAllExtensions(): Uri { + const base = this.fileName; + const stripped = base.split('.')[0]; + if (stripped === base) { + return this; + } else { + return this.getDirectory().combinePaths(stripped); + } + } + + protected abstract getRootPath(): string; + + protected normalizeSlashes(path: string): string { + return path.replace(/\\/g, '/'); + } + + protected reducePathComponents(components: string[]): string[] { + if (!some(components)) { + return []; + } + + // Reduce the path components by eliminating + // any '.' or '..'. We start at 1 because the first component is + // always the root. + const reduced = [components[0]]; + for (let i = 1; i < components.length; i++) { + const component = components[i]; + if (!component || component === '.') { + continue; + } + + if (component === '..') { + if (reduced.length > 1) { + if (reduced[reduced.length - 1] !== '..') { + reduced.pop(); + continue; + } + } else if (reduced[0]) { + continue; + } + } + reduced.push(component); + } + + return reduced; + } + + protected abstract getComparablePath(): string; + protected abstract getPathComponentsImpl(): string[]; +} diff --git a/packages/pyright-internal/src/common/uri/emptyUri.ts b/packages/pyright-internal/src/common/uri/emptyUri.ts new file mode 100644 index 000000000..ad76d61aa --- /dev/null +++ b/packages/pyright-internal/src/common/uri/emptyUri.ts @@ -0,0 +1,123 @@ +/* + * emptyUri.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * URI class that represents an empty URI. + */ + +import * as debug from '../debug'; +import { BaseUri } from './baseUri'; +import { Uri } from './uri'; + +const EmptyKey = ''; + +export class EmptyUri extends BaseUri { + private static _instance = new EmptyUri(); + private constructor() { + super(EmptyKey); + } + + static get instance() { + return EmptyUri._instance; + } + + override get scheme(): string { + return ''; + } + + override get fileName(): string { + return ''; + } + + override get lastExtension(): string { + return ''; + } + + override get root(): Uri { + return this; + } + + get isCaseSensitive(): boolean { + return true; + } + + override isEmpty(): boolean { + return true; + } + + override isLocal(): boolean { + return false; + } + + override getPath(): string { + return ''; + } + + override getFilePath(): string { + debug.fail(`EmptyUri.getFilePath() should not be called.`); + } + + override toString(): string { + return ''; + } + + override toUserVisibleString(): string { + return ''; + } + + override matchesRegex(regex: RegExp): boolean { + return false; + } + + override replaceExtension(ext: string): Uri { + return this; + } + override addPath(extra: string): Uri { + return this; + } + + override getDirectory(): Uri { + return this; + } + + override isRoot(): boolean { + return true; + } + + override isChild(parent: Uri): boolean { + return false; + } + + override startsWith(other: Uri | undefined): boolean { + return false; + } + + override getPathLength(): number { + return 0; + } + + override combinePaths(...paths: string[]): Uri { + return this; + } + + override getShortenedFileName(maxDirLength: number): string { + return ''; + } + + override stripExtension(): Uri { + return this; + } + + protected override getPathComponentsImpl(): string[] { + return []; + } + + protected override getRootPath(): string { + return ''; + } + + protected override getComparablePath(): string { + return ''; + } +} diff --git a/packages/pyright-internal/src/common/uri/fileUri.ts b/packages/pyright-internal/src/common/uri/fileUri.ts new file mode 100644 index 000000000..ba44bf9cd --- /dev/null +++ b/packages/pyright-internal/src/common/uri/fileUri.ts @@ -0,0 +1,203 @@ +/* + * fileUri.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * URI class that represents a file path. These URIs are always 'file' schemed. + */ + +import { URI } from 'vscode-uri'; +import { + ensureTrailingDirectorySeparator, + getDirectoryPath, + getFileExtension, + getFileName, + getPathComponents, + getRootLength, + hasTrailingDirectorySeparator, + isDiskPathRoot, + normalizeSlashes, + resolvePaths, +} from '../pathUtils'; +import { BaseUri } from './baseUri'; +import { cacheMethodWithNoArgs, cacheProperty, cacheStaticFunc } from './memoization'; +import { Uri } from './uri'; + +export class FileUri extends BaseUri { + private _formattedString: string | undefined; + private constructor( + key: string, + private readonly _filePath: string, + private readonly _query: string, + private readonly _fragment: string, + private readonly _originalString: string | undefined, + private readonly _isCaseSensitive: boolean + ) { + super(_isCaseSensitive ? key : key.toLowerCase()); + } + + override get scheme(): string { + return 'file'; + } + + @cacheProperty() + override get fileName(): string { + return getFileName(this._filePath); + } + + @cacheProperty() + override get lastExtension(): string { + return getFileExtension(this._filePath); + } + + @cacheProperty() + override get root(): Uri { + const rootPath = this.getRootPath(); + if (rootPath !== this._filePath) { + return FileUri.createFileUri(rootPath, '', '', undefined, this._isCaseSensitive); + } + return this; + } + + get isCaseSensitive(): boolean { + return this._isCaseSensitive; + } + + @cacheStaticFunc() + static createFileUri( + filePath: string, + query: string, + fragment: string, + originalString: string | undefined, + isCaseSensitive: boolean + ): FileUri { + const key = FileUri._createKey(filePath, query, fragment); + return new FileUri(key, filePath, query, fragment, originalString, isCaseSensitive); + } + + static isFileUri(uri: Uri): uri is FileUri { + return uri.scheme === 'file' && (uri as any)._filePath !== undefined; + } + + override matchesRegex(regex: RegExp): boolean { + // Compare the regex to our path but normalize it for comparison. + // The regex assumes it's comparing itself to a URI path. + const path = this.normalizeSlashes(this._filePath); + return regex.test(path); + } + + override toString(): string { + if (!this._formattedString) { + this._formattedString = + this._originalString || + URI.file(this._filePath).with({ query: this._query, fragment: this._fragment }).toString(); + } + return this._formattedString; + } + + override toUserVisibleString(): string { + return this._filePath; + } + + override addPath(extra: string): Uri { + return FileUri.createFileUri(this._filePath + extra, '', '', undefined, this._isCaseSensitive); + } + + override isRoot(): boolean { + return isDiskPathRoot(this._filePath); + } + + override isChild(parent: Uri): boolean { + if (!FileUri.isFileUri(parent)) { + return false; + } + + return parent._filePath.length < this._filePath.length && this.startsWith(parent); + } + + override isLocal(): boolean { + return true; + } + + override startsWith(other: Uri | undefined): boolean { + if (!other || !FileUri.isFileUri(other)) { + return false; + } + if (other.isEmpty() !== this.isEmpty()) { + return false; + } + if (this._filePath.length >= other._filePath.length) { + // Make sure the other ends with a / when comparing longer paths, otherwise we might + // say that /a/food is a child of /a/foo. + const otherPath = + this._filePath.length > other._filePath.length && !hasTrailingDirectorySeparator(other._filePath) + ? ensureTrailingDirectorySeparator(other._filePath) + : other._filePath; + + if (!this.isCaseSensitive) { + return this._filePath.toLowerCase().startsWith(otherPath.toLowerCase()); + } + return this._filePath.startsWith(otherPath); + } + return false; + } + override getPathLength(): number { + return this._filePath.length; + } + override getPath(): string { + return this.normalizeSlashes(this._filePath); + } + override getFilePath(): string { + return this._filePath; + } + + override combinePaths(...paths: string[]): Uri { + // Resolve and combine paths, never want URIs with '..' in the middle. + let combined = resolvePaths(this._filePath, ...paths); + + // Make sure to remove any trailing directory chars. + if (hasTrailingDirectorySeparator(combined) && combined.length > 1) { + combined = combined.slice(0, combined.length - 1); + } + if (combined !== this._filePath) { + return FileUri.createFileUri(combined, '', '', undefined, this._isCaseSensitive); + } + return this; + } + + @cacheMethodWithNoArgs() + override getDirectory(): Uri { + const filePath = this._filePath; + let dir = getDirectoryPath(filePath); + if (hasTrailingDirectorySeparator(dir) && dir.length > 1) { + dir = dir.slice(0, -1); + } + if (dir !== filePath) { + return FileUri.createFileUri(dir, '', '', undefined, this._isCaseSensitive); + } else { + return this; + } + } + + protected override getPathComponentsImpl(): string[] { + const components = getPathComponents(this._filePath); + // Remove the first one if it's empty. The new algorithm doesn't + // expect this to be there. + if (components.length > 0 && components[0] === '') { + components.shift(); + } + return components.map((component) => this.normalizeSlashes(component)); + } + + protected override getRootPath(): string { + return this._filePath.slice(0, getRootLength(this._filePath)); + } + + protected override getComparablePath(): string { + return normalizeSlashes(this._filePath); + } + + private static _createKey(filePath: string, query: string, fragment: string) { + return `${filePath}${query ? '?' + query : ''}${fragment ? '#' + fragment : ''}`; + } +} diff --git a/packages/pyright-internal/src/common/uri/memoization.ts b/packages/pyright-internal/src/common/uri/memoization.ts new file mode 100644 index 000000000..084395bec --- /dev/null +++ b/packages/pyright-internal/src/common/uri/memoization.ts @@ -0,0 +1,70 @@ +/* + * memoization.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Decorators used to memoize the result of a function call. + */ + +// Cache for static method results. +const staticCache = new Map(); + +// Caches the results of a getter property. +export function cacheProperty() { + return function (target: any, functionName: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.get; + descriptor.get = function (this: any, ...args: any) { + // Call the function once to get the result. + const result = originalMethod!.apply(this, args); + + // Then we replace the original function with one that just returns the result. + Object.defineProperty(this, functionName, { + get() { + return result; + }, + }); + return result; + }; + return descriptor; + }; +} + +// Caches the results of method that takes no args. +// This situation can be optimized because the parameters are always the same. +export function cacheMethodWithNoArgs() { + return function (target: any, functionName: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = function (this: any, ...args: any) { + // Call the function once to get the result. + const result = originalMethod.apply(this, args); + + // Then we replace the original function with one that just returns the result. + this[functionName] = () => { + // Note that this poses a risk. The result is passed by reference, so if the caller + // modifies the result, it will modify the cached result. + return result; + }; + return result; + }; + return descriptor; + }; +} + +// Create a decorator to cache the results of a static method. +export function cacheStaticFunc() { + return function cacheStaticFunc_Fast(target: any, functionName: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any) { + const key = `${functionName}+${args.map((a: any) => a?.toString()).join(',')}`; + let cachedResult: any; + if (!staticCache.has(key)) { + cachedResult = originalMethod.apply(this, args); + staticCache.set(key, cachedResult); + } else { + cachedResult = staticCache.get(key); + } + return cachedResult; + }; + return descriptor; + }; +} diff --git a/packages/pyright-internal/src/common/uri/uri.ts b/packages/pyright-internal/src/common/uri/uri.ts new file mode 100644 index 000000000..64dfc1d3d --- /dev/null +++ b/packages/pyright-internal/src/common/uri/uri.ts @@ -0,0 +1,215 @@ +/* + * uri.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * URI namespace for storing and manipulating URIs. + */ + +import { URI, Utils } from 'vscode-uri'; +import { combinePaths, isRootedDiskPath } from '../pathUtils'; +import { EmptyUri } from './emptyUri'; +import { FileUri } from './fileUri'; +import { WebUri } from './webUri'; + +export interface Uri { + // Unique key for storing in maps. + readonly key: string; + + // Returns the scheme of the URI. + readonly scheme: string; + + // Returns the last segment of the URI, similar to the UNIX basename command. + readonly fileName: string; + + // Returns the extension of the URI, similar to the UNIX extname command. This includes '.' on the extension. + readonly lastExtension: string; + + // Returns a URI where the path just contains the root folder. + readonly root: Uri; + + // Returns a URI where the path contains the directory name with .py appended. + readonly packageUri: Uri; + + // Returns a URI where the path contains the directory name with .pyi appended. + readonly packageStubUri: Uri; + + // Returns a URI where the path has __init__.py appended. + readonly initPyUri: Uri; + + // Returns a URI where the path has __init__.pyi appended. + readonly initPyiUri: Uri; + + // Returns a URI where the path has py.typed appended. + readonly pytypedUri: Uri; + + // Returns the filename without any extensions + readonly fileNameWithoutExtension: string; + + // Indicates if the underlying file system for this URI is case sensitive or not. + readonly isCaseSensitive: boolean; + + isEmpty(): boolean; + + toString(): string; + + toUserVisibleString(): string; + + // Determines whether a path consists only of a path root. + isRoot(): boolean; + + // Determines whether a Uri is a child of some parent Uri. + isChild(parent: Uri): boolean; + + isLocal(): boolean; + + isUntitled(): boolean; + + equals(other: Uri | undefined): boolean; + + startsWith(other: Uri | undefined): boolean; + + pathStartsWith(name: string): boolean; + + pathEndsWith(name: string): boolean; + + pathIncludes(include: string): boolean; + matchesRegex(regex: RegExp): boolean; + + addPath(extra: string): Uri; + + // Returns a URI where the path is the directory name of the original URI, similar to the UNIX dirname command. + getDirectory(): Uri; + + getRootPathLength(): number; + + // How long the path for this Uri is. + getPathLength(): number; + + combinePaths(...paths: string[]): Uri; + + getRelativePath(child: Uri): string | undefined; + + getPathComponents(): readonly string[]; + + getPath(): string; + + getFilePath(): string; + + getRelativePathComponents(to: Uri): readonly string[]; + getShortenedFileName(maxDirLength?: number): string; + + stripExtension(): Uri; + + stripAllExtensions(): Uri; + replaceExtension(ext: string): Uri; + + addExtension(ext: string): Uri; + hasExtension(ext: string): boolean; +} + +// Returns just the fsPath path portion of a vscode URI. +function getFilePath(uri: URI): string { + let filePath: string | undefined; + + // Compute the file path ourselves. The vscode.URI class doesn't + // treat UNC shares with a single slash as UNC paths. + // https://github.com/microsoft/vscode-uri/blob/53e4ca6263f2e4ddc35f5360c62bc1b1d30f27dd/src/uri.ts#L567 + if (uri.authority && uri.path[0] === '/' && uri.path.length === 1) { + filePath = `//${uri.authority}${uri.path}`; + } else { + // Otherwise use the vscode.URI version + filePath = uri.fsPath; + } + + // If this is a DOS-style path with a drive letter, remove + // the leading slash. + if (filePath.match(/^\/[a-zA-Z]:\//)) { + filePath = filePath.slice(1); + } + + // vscode.URI normalizes the path to use the correct path separators. + // We need to do the same. + if (process.platform === 'win32') { + filePath = filePath.replace(/\//g, '\\'); + } + + return filePath; +} + +// Function called to normalize input URIs. This gets rid of '..' and '.' in the path. +// It also removes any '/' on the end of the path. +// This is slow but should only be called when the URI is first created. +function normalizeUri(uri: string | URI): { uri: URI; str: string } { + // Make sure the drive letter is lower case. This + // is consistent with what VS code does for URIs. + let originalString = URI.isUri(uri) ? uri.toString() : uri; + const parsed = URI.isUri(uri) ? uri : URI.parse(uri); + if (parsed.scheme === 'file') { + // The Vscode.URI parser makes sure the drive is lower cased. + originalString = parsed.toString(); + } + + // Original URI may not have resolved all the `..` in the path, so remove them. + // Note: this also has the effect of removing any trailing slashes. + const finalURI = Utils.resolvePath(parsed); + const finalString = finalURI.path.length !== parsed.path.length ? finalURI.toString() : originalString; + return { uri: finalURI, str: finalString }; +} + +export namespace Uri { + export function file(path: string, isCaseSensitive = true, checkRelative = false): Uri { + // Fix path if we're checking for relative paths and this is not a rooted path. + path = checkRelative && !isRootedDiskPath(path) ? combinePaths(process.cwd(), path) : path; + + // If this already starts with 'file:', then we can + // parse it normally. It's actually a uri string. Otherwise parse it as a file path. + const normalized = path.startsWith('file:') ? normalizeUri(path) : normalizeUri(URI.file(path)); + + // Turn the path into a file URI. + return FileUri.createFileUri( + getFilePath(normalized.uri), + normalized.uri.query, + normalized.uri.fragment, + normalized.str, + isCaseSensitive + ); + } + + export function empty(): Uri { + return EmptyUri.instance; + } + + export function parse(value: string | undefined, isCaseSensitive: boolean): Uri { + if (!value) { + return Uri.empty(); + } + + // Normalize the value here. This gets rid of '..' and '.' in the path. It also removes any + // '/' on the end of the path. + const normalized = normalizeUri(value); + if (normalized.uri.scheme === 'file') { + return FileUri.createFileUri( + getFilePath(normalized.uri), + normalized.uri.query, + normalized.uri.fragment, + normalized.str, + isCaseSensitive + ); + } + + // Web URIs are always case sensitive. + return WebUri.createWebUri( + normalized.uri.scheme, + normalized.uri.authority, + normalized.uri.path, + normalized.uri.query, + normalized.uri.fragment, + normalized.str + ); + } + + export function isUri(thing: any): thing is Uri { + return !!thing && typeof thing._key === 'string'; + } +} diff --git a/packages/pyright-internal/src/common/uri/uriUtils.ts b/packages/pyright-internal/src/common/uri/uriUtils.ts new file mode 100644 index 000000000..f3f51a6ce --- /dev/null +++ b/packages/pyright-internal/src/common/uri/uriUtils.ts @@ -0,0 +1,406 @@ +/* + * uriUtils.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * Utility functions for manipulating URIs. + */ + +import type { Dirent } from 'fs'; + +import { randomBytesHex } from '../crypto'; +import { FileSystem, ReadOnlyFileSystem, Stats, TempFile } from '../fileSystem'; +import { + getRegexEscapedSeparator, + isDirectoryWildcardPatternPresent, + stripTrailingDirectorySeparator, +} from '../pathUtils'; +import { Uri } from './uri'; + +let _fsCaseSensitivity: boolean | undefined = undefined; +let _underTest: boolean = false; + +export interface FileSpec { + // File specs can contain wildcard characters (**, *, ?). This + // specifies the first portion of the file spec that contains + // no wildcards. + wildcardRoot: Uri; + + // Regular expression that can be used to match against this + // file spec. + regExp: RegExp; + + // Indicates whether the file spec has a directory wildcard (**). + // When present, the search cannot terminate without exploring to + // an arbitrary depth. + hasDirectoryWildcard: boolean; +} + +const _includeFileRegex = /\.pyi?$/; + +export namespace FileSpec { + export function is(value: any): value is FileSpec { + const candidate: FileSpec = value as FileSpec; + return candidate && !!candidate.wildcardRoot && !!candidate.regExp; + } + export function isInPath(uri: Uri, paths: FileSpec[]) { + return !!paths.find((p) => uri.matchesRegex(p.regExp)); + } + + export function matchesIncludeFileRegex(uri: Uri, isFile = true) { + return isFile ? uri.matchesRegex(_includeFileRegex) : true; + } + + export function matchIncludeFileSpec(includeRegExp: RegExp, exclude: FileSpec[], uri: Uri, isFile = true) { + if (uri.matchesRegex(includeRegExp)) { + if (!FileSpec.isInPath(uri, exclude) && FileSpec.matchesIncludeFileRegex(uri, isFile)) { + return true; + } + } + + return false; + } +} + +export interface FileSystemEntries { + files: Uri[]; + directories: Uri[]; +} + +export function forEachAncestorDirectory( + directory: Uri, + callback: (directory: Uri) => Uri | undefined +): Uri | undefined { + while (true) { + const result = callback(directory); + if (result !== undefined) { + return result; + } + + const parentPath = directory.getDirectory(); + if (parentPath.equals(directory)) { + return undefined; + } + + directory = parentPath; + } +} + +// Creates a directory hierarchy for a path, starting from some ancestor path. +export function makeDirectories(fs: FileSystem, dir: Uri, startingFrom: Uri) { + if (!dir.startsWith(startingFrom)) { + return; + } + + const pathComponents = dir.getPathComponents(); + const relativeToComponents = startingFrom.getPathComponents(); + let curPath = startingFrom; + + for (let i = relativeToComponents.length; i < pathComponents.length; i++) { + curPath = curPath.combinePaths(pathComponents[i]); + if (!fs.existsSync(curPath)) { + fs.mkdirSync(curPath); + } + } +} + +export function getFileSize(fs: ReadOnlyFileSystem, uri: Uri) { + const stat = tryStat(fs, uri); + if (stat?.isFile()) { + return stat.size; + } + return 0; +} + +export function fileExists(fs: ReadOnlyFileSystem, uri: Uri): boolean { + return fileSystemEntryExists(fs, uri, FileSystemEntryKind.File); +} + +export function directoryExists(fs: ReadOnlyFileSystem, uri: Uri): boolean { + return fileSystemEntryExists(fs, uri, FileSystemEntryKind.Directory); +} + +export function isDirectory(fs: ReadOnlyFileSystem, uri: Uri): boolean { + return tryStat(fs, uri)?.isDirectory() ?? false; +} + +export function isFile(fs: ReadOnlyFileSystem, uri: Uri, treatZipDirectoryAsFile = false): boolean { + const stats = tryStat(fs, uri); + if (stats?.isFile()) { + return true; + } + + if (!treatZipDirectoryAsFile) { + return false; + } + + return stats?.isZipDirectory?.() ?? false; +} + +export function tryStat(fs: ReadOnlyFileSystem, uri: Uri): Stats | undefined { + try { + if (fs.existsSync(uri)) { + return fs.statSync(uri); + } + } catch (e: any) { + return undefined; + } + return undefined; +} + +export function tryRealpath(fs: ReadOnlyFileSystem, uri: Uri): Uri | undefined { + try { + return fs.realCasePath(uri); + } catch (e: any) { + return undefined; + } +} + +export function getFileSystemEntries(fs: ReadOnlyFileSystem, uri: Uri): FileSystemEntries { + try { + return getFileSystemEntriesFromDirEntries(fs.readdirEntriesSync(uri), fs, uri); + } catch (e: any) { + return { files: [], directories: [] }; + } +} + +// Sorts the entires into files and directories, including any symbolic links. +export function getFileSystemEntriesFromDirEntries( + dirEntries: Dirent[], + fs: ReadOnlyFileSystem, + uri: Uri +): FileSystemEntries { + const entries = dirEntries.sort((a, b) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + }); + const files: Uri[] = []; + const directories: Uri[] = []; + for (const entry of entries) { + // This is necessary because on some file system node fails to exclude + // "." and "..". See https://github.com/nodejs/node/issues/4002 + if (entry.name === '.' || entry.name === '..') { + continue; + } + + const entryUri = uri.combinePaths(entry.name); + if (entry.isFile()) { + files.push(entryUri); + } else if (entry.isDirectory()) { + directories.push(entryUri); + } else if (entry.isSymbolicLink()) { + const stat = tryStat(fs, entryUri); + if (stat?.isFile()) { + files.push(entryUri); + } else if (stat?.isDirectory()) { + directories.push(entryUri); + } + } + } + return { files, directories }; +} + +export function setTestingMode(underTest: boolean) { + _underTest = underTest; +} + +// Transforms a relative file spec (one that potentially contains +// escape characters **, * or ?) and returns a regular expression +// that can be used for matching against. +export function getWildcardRegexPattern(root: Uri, fileSpec: string): string { + const absolutePath = root.combinePaths(fileSpec); + const pathComponents = Array.from(absolutePath.getPathComponents()); + const escapedSeparator = getRegexEscapedSeparator('/'); + const doubleAsteriskRegexFragment = `(${escapedSeparator}[^${escapedSeparator}][^${escapedSeparator}]*)*?`; + const reservedCharacterPattern = new RegExp(`[^\\w\\s${escapedSeparator}]`, 'g'); + + // Strip the directory separator from the root component. + if (pathComponents.length > 0) { + pathComponents[0] = stripTrailingDirectorySeparator(pathComponents[0]); + } + + let regExPattern = ''; + let firstComponent = true; + + for (let component of pathComponents) { + if (component === '**') { + regExPattern += doubleAsteriskRegexFragment; + } else { + if (!firstComponent) { + component = escapedSeparator + component; + } + + regExPattern += component.replace(reservedCharacterPattern, (match) => { + if (match === '*') { + return `[^${escapedSeparator}]*`; + } else if (match === '?') { + return `[^${escapedSeparator}]`; + } else { + // escaping anything that is not reserved characters - word/space/separator + return '\\' + match; + } + }); + + firstComponent = false; + } + } + + return regExPattern; +} + +// Returns the topmost path that contains no wildcard characters. +export function getWildcardRoot(root: Uri, fileSpec: string): Uri { + const absolutePath = root.combinePaths(fileSpec); + // make a copy of the path components so we can modify them. + const pathComponents = Array.from(absolutePath.getPathComponents()); + let wildcardRoot = absolutePath.root; + + // Remove the root component. + if (pathComponents.length > 0) { + pathComponents.shift(); + } + + for (const component of pathComponents) { + if (component === '**') { + break; + } else { + if (/[*?]/.test(component)) { + break; + } + + wildcardRoot = wildcardRoot.combinePaths(component); + } + } + + return wildcardRoot; +} + +export function hasPythonExtension(uri: Uri) { + return uri.hasExtension('.py') || uri.hasExtension('.pyi'); +} + +export function getFileSpec(root: Uri, fileSpec: string): FileSpec { + let regExPattern = getWildcardRegexPattern(root, fileSpec); + const escapedSeparator = getRegexEscapedSeparator('/'); + regExPattern = `^(${regExPattern})($|${escapedSeparator})`; + + const regExp = new RegExp(regExPattern, root.isCaseSensitive ? undefined : 'i'); + const wildcardRoot = getWildcardRoot(root, fileSpec); + const hasDirectoryWildcard = isDirectoryWildcardPatternPresent(fileSpec); + + return { + wildcardRoot, + regExp, + hasDirectoryWildcard, + }; +} + +const enum FileSystemEntryKind { + File, + Directory, +} + +function fileSystemEntryExists(fs: ReadOnlyFileSystem, uri: Uri, entryKind: FileSystemEntryKind): boolean { + try { + const stat = fs.statSync(uri); + switch (entryKind) { + case FileSystemEntryKind.File: + return stat.isFile(); + case FileSystemEntryKind.Directory: + return stat.isDirectory(); + default: + return false; + } + } catch (e: any) { + return false; + } +} + +const isFileSystemCaseSensitiveMap = new WeakMap(); + +export function isFileSystemCaseSensitive(fs: FileSystem, tmp: TempFile | undefined) { + if (!_underTest && _fsCaseSensitivity !== undefined) { + return _fsCaseSensitivity; + } + + if (!isFileSystemCaseSensitiveMap.has(fs)) { + _fsCaseSensitivity = tmp ? isFileSystemCaseSensitiveInternal(fs, tmp) : false; + isFileSystemCaseSensitiveMap.set(fs, _fsCaseSensitivity); + } + return !!isFileSystemCaseSensitiveMap.get(fs); +} + +export function isFileSystemCaseSensitiveInternal(fs: FileSystem, tmp: TempFile) { + let filePath: Uri | undefined = undefined; + try { + // Make unique file name. + let name: string; + let mangledFilePath: Uri; + do { + name = `${randomBytesHex(21)}-a`; + filePath = tmp.tmpdir().combinePaths(name); + mangledFilePath = tmp.tmpdir().combinePaths(name.toUpperCase()); + } while (fs.existsSync(filePath) || fs.existsSync(mangledFilePath)); + + fs.writeFileSync(filePath, '', 'utf8'); + + // If file exists, then it is insensitive. + return !fs.existsSync(mangledFilePath); + } catch (e: any) { + return false; + } finally { + if (filePath) { + // remove temp file created + try { + fs.unlinkSync(filePath); + } catch (e: any) { + /* ignored */ + } + } + } +} + +export function deduplicateFolders(listOfFolders: Uri[][]): Uri[] { + const foldersToWatch = new Map(); + + listOfFolders.forEach((folders) => { + folders.forEach((p) => { + if (foldersToWatch.has(p.key)) { + // Bail out on exact match. + return; + } + + for (const existing of foldersToWatch) { + // ex) p: "/user/test" existing: "/user" + if (p.startsWith(existing[1])) { + // We already have the parent folder in the watch list + return; + } + + // ex) p: "/user" folderToWatch: "/user/test" + if (existing[1].startsWith(p)) { + // We found better one to watch. replace. + foldersToWatch.delete(existing[0]); + foldersToWatch.set(p.key, p); + return; + } + } + + foldersToWatch.set(p.key, p); + }); + }); + + return [...foldersToWatch.values()]; +} + +export function getRootUri(isCaseSensitive: boolean): Uri | undefined { + if ((global as any).__rootDirectory) { + return Uri.file((global as any).__rootDirectory, isCaseSensitive); + } + return undefined; +} diff --git a/packages/pyright-internal/src/common/uri/webUri.ts b/packages/pyright-internal/src/common/uri/webUri.ts new file mode 100644 index 000000000..d363daad4 --- /dev/null +++ b/packages/pyright-internal/src/common/uri/webUri.ts @@ -0,0 +1,207 @@ +/* + * webUri.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * + * URI class that represents a URI that isn't 'file' schemed. + * This can be URIs like: + * - http://www.microsoft.com/file.txt + * - untitled:Untitled-1 + * - vscode:extension/ms-python.python + * - vscode-vfs://github.com/microsoft/debugpy/debugpy/launcher/debugAdapter.py + */ + +import * as debug from '../debug'; +import { getRootLength, hasTrailingDirectorySeparator, normalizeSlashes, resolvePaths } from '../pathUtils'; +import { BaseUri } from './baseUri'; +import { cacheMethodWithNoArgs, cacheProperty, cacheStaticFunc } from './memoization'; +import { Uri } from './uri'; + +export class WebUri extends BaseUri { + private constructor( + key: string, + private readonly _scheme: string, + private readonly _authority: string, + private readonly _path: string, + private readonly _query: string, + private readonly _fragment: string, + private readonly _originalString: string | undefined + ) { + super(key); + } + + override get scheme(): string { + return this._scheme; + } + + get isCaseSensitive(): boolean { + // Web URIs are always case sensitive + return true; + } + + @cacheProperty() + override get root(): Uri { + const rootPath = this.getRootPath(); + if (rootPath !== this._path) { + return WebUri.createWebUri(this._scheme, this._authority, rootPath, '', '', undefined); + } + return this; + } + + @cacheProperty() + override get fileName(): string { + // Path should already be normalized, just get the last on a split of '/'. + const components = this._path.split('/'); + return components[components.length - 1]; + } + + @cacheProperty() + override get lastExtension(): string { + const basename = this.fileName; + const index = basename.lastIndexOf('.'); + if (index >= 0) { + return basename.slice(index); + } + return ''; + } + + @cacheStaticFunc() + static createWebUri( + scheme: string, + authority: string, + path: string, + query: string, + fragment: string, + originalString: string | undefined + ): WebUri { + const key = WebUri._createKey(scheme, authority, path, query, fragment); + return new WebUri(key, scheme, authority, path, query, fragment, originalString); + } + + override toString(): string { + if (!this._originalString) { + return `${this._scheme}://${this._authority}${this._path}${this._query ? '?' + this._query : ''}${ + this._fragment ? '#' + this._fragment : '' + }`; + } + return this._originalString; + } + override toUserVisibleString(): string { + return this.toString(); + } + static isWebUri(uri: Uri): uri is WebUri { + return uri.scheme !== 'file' && (uri as any)._scheme !== undefined; + } + + override matchesRegex(regex: RegExp): boolean { + return regex.test(this._path); + } + + override addPath(extra: string): Uri { + const newPath = this._path + extra; + return WebUri.createWebUri(this._scheme, this._authority, newPath, this._query, this._fragment, undefined); + } + + override isRoot(): boolean { + return this._path === this.getRootPath() && this._path.length > 0; + } + + override isChild(parent: Uri): boolean { + if (!WebUri.isWebUri(parent)) { + return false; + } + + return parent._path.length < this._path.length && this.startsWith(parent); + } + + override isLocal(): boolean { + return false; + } + + override startsWith(other: Uri | undefined): boolean { + if (!other || !WebUri.isWebUri(other)) { + return false; + } + if (other.isEmpty() !== this.isEmpty()) { + return false; + } + if (this.scheme !== other.scheme) { + return false; + } + if (this._path.length >= other._path.length) { + // Make sure the other ends with a / when comparing longer paths, otherwise we might + // say that /a/food is a child of /a/foo. + const otherPath = + this._path.length > other._path.length && !hasTrailingDirectorySeparator(other._path) + ? `${other._path}/` + : other._path; + + return this._path.startsWith(otherPath); + } + return false; + } + override getPathLength(): number { + return this._path.length; + } + + override getPath(): string { + return this._path; + } + + override getFilePath(): string { + debug.fail(`${this} is not a file based URI.`); + } + + override combinePaths(...paths: string[]): Uri { + // Resolve and combine paths, never want URIs with '..' in the middle. + let combined = this.normalizeSlashes(resolvePaths(this._path, ...paths)); + + // Make sure to remove any trailing directory chars. + if (hasTrailingDirectorySeparator(combined) && combined.length > 1) { + combined = combined.slice(0, combined.length - 1); + } + if (combined !== this._path) { + return WebUri.createWebUri(this._scheme, this._authority, combined, '', '', undefined); + } + return this; + } + + @cacheMethodWithNoArgs() + override getDirectory(): Uri { + const index = this._path.lastIndexOf('/'); + if (index > 0) { + return WebUri.createWebUri( + this._scheme, + this._authority, + this._path.slice(0, index), + this._query, + this._fragment, + undefined + ); + } else { + return this; + } + } + + protected override getPathComponentsImpl(): string[] { + // Get the root path and the rest of the path components. + const rootPath = this.getRootPath(); + const otherPaths = this._path.slice(rootPath.length).split('/'); + return this.reducePathComponents([rootPath, ...otherPaths]).map((component) => + this.normalizeSlashes(component) + ); + } + + protected override getRootPath(): string { + const rootLength = getRootLength(this._path, '/'); + return this._path.slice(0, rootLength); + } + + protected override getComparablePath(): string { + return normalizeSlashes(this._path); + } + + private static _createKey(scheme: string, authority: string, path: string, query: string, fragment: string) { + return `${scheme}://${authority}${path}${query ? '?' + query : ''}${fragment ? '#' + fragment : ''}`; + } +} diff --git a/packages/pyright-internal/src/common/uriParser.ts b/packages/pyright-internal/src/common/uriParser.ts deleted file mode 100644 index 33c3f3f12..000000000 --- a/packages/pyright-internal/src/common/uriParser.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * uriParser.ts - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT license. - * - * URI utility functions. - */ - -import { Position } from 'vscode-languageserver'; -import { TextDocumentIdentifier } from 'vscode-languageserver-protocol'; -import { URI } from 'vscode-uri'; - -import { isString } from './core'; -import { FileSystem } from './fileSystem'; -import { convertUriToPath } from './pathUtils'; - -export interface IUriParser { - decodeTextDocumentPosition( - textDocument: TextDocumentIdentifier, - position: Position - ): { filePath: string; position: Position }; - decodeTextDocumentUri(uriString: string): string; - isUntitled(uri: URI | string | undefined): boolean; - isLocal(uri: URI | string | undefined): boolean; -} - -export class UriParser implements IUriParser { - constructor(protected readonly fs: FileSystem) {} - - decodeTextDocumentPosition(textDocument: TextDocumentIdentifier, position: Position) { - const filePath = this.decodeTextDocumentUri(textDocument.uri); - return { filePath, position }; - } - - decodeTextDocumentUri(uriString: string) { - return convertUriToPath(this.fs, uriString); - } - - isUntitled(uri: URI | string | undefined) { - if (!uri) { - return false; - } - - if (isString(uri)) { - uri = URI.parse(uri); - } - - return uri.scheme === 'untitled'; - } - - isLocal(uri: URI | string | undefined) { - if (!uri) { - return false; - } - - if (isString(uri)) { - uri = URI.parse(uri); - } - - return uri.scheme === 'file'; - } -} diff --git a/packages/pyright-internal/src/common/workspaceEditUtils.ts b/packages/pyright-internal/src/common/workspaceEditUtils.ts index 02e669910..047be8d8f 100644 --- a/packages/pyright-internal/src/common/workspaceEditUtils.ts +++ b/packages/pyright-internal/src/common/workspaceEditUtils.ts @@ -19,15 +19,14 @@ import { import { TextDocument } from 'vscode-languageserver-textdocument'; import { AnalyzerService } from '../analyzer/service'; import { FileEditAction, FileEditActions, TextEditAction } from '../common/editAction'; -import { convertPathToUri, convertUriToPath } from '../common/pathUtils'; import { createMapFromItems } from './collectionUtils'; import { isArray } from './core'; import { assertNever } from './debug'; import { EditableProgram, SourceFileInfo } from './extensibility'; -import { ReadOnlyFileSystem } from './fileSystem'; import { convertRangeToTextRange, convertTextRangeToRange } from './positionUtils'; import { TextRange } from './textRange'; import { TextRangeCollection } from './textRangeCollection'; +import { Uri } from './uri/uri'; export function convertToTextEdits(editActions: TextEditAction[]): TextEdit[] { return editActions.map((editAction) => ({ @@ -36,14 +35,13 @@ export function convertToTextEdits(editActions: TextEditAction[]): TextEdit[] { })); } -export function convertToFileTextEdits(filePath: string, editActions: TextEditAction[]): FileEditAction[] { - return editActions.map((a) => ({ filePath, ...a })); +export function convertToFileTextEdits(fileUri: Uri, editActions: TextEditAction[]): FileEditAction[] { + return editActions.map((a) => ({ fileUri, ...a })); } -export function convertToWorkspaceEdit(fs: ReadOnlyFileSystem, edits: FileEditAction[]): WorkspaceEdit; -export function convertToWorkspaceEdit(fs: ReadOnlyFileSystem, edits: FileEditActions): WorkspaceEdit; +export function convertToWorkspaceEdit(edits: FileEditAction[]): WorkspaceEdit; +export function convertToWorkspaceEdit(edits: FileEditActions): WorkspaceEdit; export function convertToWorkspaceEdit( - fs: ReadOnlyFileSystem, edits: FileEditActions, changeAnnotations: { [id: string]: ChangeAnnotation; @@ -51,7 +49,6 @@ export function convertToWorkspaceEdit( defaultAnnotationId: string ): WorkspaceEdit; export function convertToWorkspaceEdit( - fs: ReadOnlyFileSystem, edits: FileEditActions | FileEditAction[], changeAnnotations?: { [id: string]: ChangeAnnotation; @@ -59,17 +56,17 @@ export function convertToWorkspaceEdit( defaultAnnotationId = 'default' ): WorkspaceEdit { if (isArray(edits)) { - return _convertToWorkspaceEditWithChanges(fs, edits); + return _convertToWorkspaceEditWithChanges(edits); } - return _convertToWorkspaceEditWithDocumentChanges(fs, edits, changeAnnotations, defaultAnnotationId); + return _convertToWorkspaceEditWithDocumentChanges(edits, changeAnnotations, defaultAnnotationId); } -export function appendToWorkspaceEdit(fs: ReadOnlyFileSystem, edits: FileEditAction[], workspaceEdit: WorkspaceEdit) { +export function appendToWorkspaceEdit(edits: FileEditAction[], workspaceEdit: WorkspaceEdit) { edits.forEach((edit) => { - const uri = convertPathToUri(fs, edit.filePath); - workspaceEdit.changes![uri] = workspaceEdit.changes![uri] || []; - workspaceEdit.changes![uri].push({ range: edit.range, newText: edit.replacementText }); + const uri = edit.fileUri; + workspaceEdit.changes![uri.toString()] = workspaceEdit.changes![uri.toString()] || []; + workspaceEdit.changes![uri.toString()].push({ range: edit.range, newText: edit.replacementText }); }); } @@ -101,18 +98,18 @@ export function applyTextEditsToString( return current; } -export function applyWorkspaceEdit(program: EditableProgram, edits: WorkspaceEdit, filesChanged: Set) { +export function applyWorkspaceEdit(program: EditableProgram, edits: WorkspaceEdit, filesChanged: Map) { if (edits.changes) { for (const kv of Object.entries(edits.changes)) { - const filePath = convertUriToPath(program.fileSystem, kv[0]); - const fileInfo = program.getSourceFileInfo(filePath); + const fileUri = Uri.parse(kv[0], program.configOptions.projectRoot.isCaseSensitive); + const fileInfo = program.getSourceFileInfo(fileUri); if (!fileInfo || !fileInfo.isTracked) { // We don't allow non user file being modified. continue; } applyDocumentChanges(program, fileInfo, kv[1]); - filesChanged.add(filePath); + filesChanged.set(fileUri.key, fileUri); } } @@ -120,15 +117,15 @@ export function applyWorkspaceEdit(program: EditableProgram, edits: WorkspaceEdi if (edits.documentChanges) { for (const change of edits.documentChanges) { if (TextDocumentEdit.is(change)) { - const filePath = convertUriToPath(program.fileSystem, change.textDocument.uri); - const fileInfo = program.getSourceFileInfo(filePath); + const fileUri = Uri.parse(change.textDocument.uri, program.configOptions.projectRoot.isCaseSensitive); + const fileInfo = program.getSourceFileInfo(fileUri); if (!fileInfo || !fileInfo.isTracked) { // We don't allow non user file being modified. continue; } applyDocumentChanges(program, fileInfo, change.edits); - filesChanged.add(filePath); + filesChanged.set(fileUri.key, fileUri); } // For now, we don't support other kinds of text changes. @@ -140,30 +137,29 @@ export function applyWorkspaceEdit(program: EditableProgram, edits: WorkspaceEdi export function applyDocumentChanges(program: EditableProgram, fileInfo: SourceFileInfo, edits: TextEdit[]) { if (!fileInfo.isOpenByClient) { const fileContent = fileInfo.sourceFile.getFileContent(); - program.setFileOpened(fileInfo.sourceFile.getFilePath(), 0, fileContent ?? '', { + program.setFileOpened(fileInfo.sourceFile.getUri(), 0, fileContent ?? '', { isTracked: fileInfo.isTracked, ipythonMode: fileInfo.sourceFile.getIPythonMode(), - chainedFilePath: fileInfo.chainedSourceFile?.sourceFile.getFilePath(), - realFilePath: fileInfo.sourceFile.getRealFilePath(), + chainedFileUri: fileInfo.chainedSourceFile?.sourceFile.getUri(), }); } const version = fileInfo.sourceFile.getClientVersion() ?? 0; - const filePath = fileInfo.sourceFile.getFilePath(); + const fileUri = fileInfo.sourceFile.getUri(); + const filePath = fileUri.getFilePath(); const sourceDoc = TextDocument.create(filePath, 'python', version, fileInfo.sourceFile.getOpenFileContents() ?? ''); - program.setFileOpened(filePath, version + 1, TextDocument.applyEdits(sourceDoc, edits), { + program.setFileOpened(fileUri, version + 1, TextDocument.applyEdits(sourceDoc, edits), { isTracked: fileInfo.isTracked, ipythonMode: fileInfo.sourceFile.getIPythonMode(), - chainedFilePath: fileInfo.chainedSourceFile?.sourceFile.getFilePath(), - realFilePath: fileInfo.sourceFile.getRealFilePath(), + chainedFileUri: fileInfo.chainedSourceFile?.sourceFile.getUri(), }); } export function generateWorkspaceEdit( originalService: AnalyzerService, clonedService: AnalyzerService, - filesChanged: Set + filesChanged: Map ) { // For now, we won't do text diff to find out minimal text changes. instead, we will // consider whole text of the files are changed. In future, we could consider @@ -171,9 +167,9 @@ export function generateWorkspaceEdit( // to support annotation. const edits: WorkspaceEdit = { changes: {} }; - for (const filePath of filesChanged) { - const original = originalService.backgroundAnalysisProgram.program.getBoundSourceFile(filePath); - const final = clonedService.backgroundAnalysisProgram.program.getBoundSourceFile(filePath); + for (const uri of filesChanged.values()) { + const original = originalService.backgroundAnalysisProgram.program.getBoundSourceFile(uri); + const final = clonedService.backgroundAnalysisProgram.program.getBoundSourceFile(uri); if (!original || !final) { // Both must exist. continue; @@ -184,7 +180,7 @@ export function generateWorkspaceEdit( continue; } - edits.changes![convertPathToUri(originalService.fs, filePath)] = [ + edits.changes![uri.toString()] = [ { range: convertTextRangeToRange(parseResults.parseTree, parseResults.tokenizerOutput.lines), newText: final.getFileContent() ?? '', @@ -195,17 +191,16 @@ export function generateWorkspaceEdit( return edits; } -function _convertToWorkspaceEditWithChanges(fs: ReadOnlyFileSystem, edits: FileEditAction[]) { +function _convertToWorkspaceEditWithChanges(edits: FileEditAction[]) { const workspaceEdit: WorkspaceEdit = { changes: {}, }; - appendToWorkspaceEdit(fs, edits, workspaceEdit); + appendToWorkspaceEdit(edits, workspaceEdit); return workspaceEdit; } function _convertToWorkspaceEditWithDocumentChanges( - fs: ReadOnlyFileSystem, editActions: FileEditActions, changeAnnotations?: { [id: string]: ChangeAnnotation; @@ -223,11 +218,7 @@ function _convertToWorkspaceEditWithDocumentChanges( switch (operation.kind) { case 'create': workspaceEdit.documentChanges!.push( - CreateFile.create( - convertPathToUri(fs, operation.filePath), - /* options */ undefined, - defaultAnnotationId - ) + CreateFile.create(operation.fileUri.toString(), /* options */ undefined, defaultAnnotationId) ); break; case 'rename': @@ -239,11 +230,11 @@ function _convertToWorkspaceEditWithDocumentChanges( } // Text edit's file path must refer to original file paths unless it is a new file just created. - const mapPerFile = createMapFromItems(editActions.edits, (e) => e.filePath); + const mapPerFile = createMapFromItems(editActions.edits, (e) => e.fileUri.key); for (const [key, value] of mapPerFile) { workspaceEdit.documentChanges!.push( TextDocumentEdit.create( - { uri: convertPathToUri(fs, key), version: null }, + { uri: key, version: null }, Array.from( value.map((v) => ({ range: v.range, @@ -262,8 +253,8 @@ function _convertToWorkspaceEditWithDocumentChanges( case 'rename': workspaceEdit.documentChanges!.push( RenameFile.create( - convertPathToUri(fs, operation.oldFilePath), - convertPathToUri(fs, operation.newFilePath), + operation.oldFileUri.toString(), + operation.newFileUri.toString(), /* options */ undefined, defaultAnnotationId ) @@ -271,11 +262,7 @@ function _convertToWorkspaceEditWithDocumentChanges( break; case 'delete': workspaceEdit.documentChanges!.push( - DeleteFile.create( - convertPathToUri(fs, operation.filePath), - /* options */ undefined, - defaultAnnotationId - ) + DeleteFile.create(operation.fileUri.toString(), /* options */ undefined, defaultAnnotationId) ); break; default: diff --git a/packages/pyright-internal/src/languageServerBase.ts b/packages/pyright-internal/src/languageServerBase.ts index 2570d1051..607da4885 100644 --- a/packages/pyright-internal/src/languageServerBase.ts +++ b/packages/pyright-internal/src/languageServerBase.ts @@ -107,18 +107,11 @@ import { FileSystem, ReadOnlyFileSystem } from './common/fileSystem'; import { FileWatcherEventType, FileWatcherHandler } from './common/fileWatcher'; import { Host } from './common/host'; import { fromLSPAny } from './common/lspUtils'; -import { - convertPathToUri, - convertUriToPath, - deduplicateFolders, - getDirectoryPath, - getFileName, - isFile, -} from './common/pathUtils'; import { ProgressReportTracker, ProgressReporter } from './common/progressReporter'; import { ServiceProvider } from './common/serviceProvider'; import { DocumentRange, Position, Range } from './common/textRange'; -import { UriParser } from './common/uriParser'; +import { Uri } from './common/uri/uri'; +import { deduplicateFolders, isFile } from './common/uri/uriUtils'; import { AnalyzerServiceExecutor } from './languageService/analyzerServiceExecutor'; import { CallHierarchyProvider } from './languageService/callHierarchyProvider'; import { CompletionItemData, CompletionProvider } from './languageService/completionProvider'; @@ -137,17 +130,17 @@ import { ParseResults } from './parser/parser'; import { InitStatus, WellKnownWorkspaceKinds, Workspace, WorkspaceFactory } from './workspaceFactory'; export interface ServerSettings { - venvPath?: string | undefined; - pythonPath?: string | undefined; - typeshedPath?: string | undefined; - stubPath?: string | undefined; + venvPath?: Uri | undefined; + pythonPath?: Uri | undefined; + typeshedPath?: Uri | undefined; + stubPath?: Uri | undefined; openFilesOnly?: boolean | undefined; typeCheckingMode?: string | undefined; useLibraryCodeForTypes?: boolean | undefined; disableLanguageServices?: boolean | undefined; disableOrganizeImports?: boolean | undefined; autoSearchPaths?: boolean | undefined; - extraPaths?: string[] | undefined; + extraPaths?: Uri[] | undefined; watchForSourceChanges?: boolean | undefined; watchForLibraryChanges?: boolean | undefined; watchForConfigChanges?: boolean | undefined; @@ -181,23 +174,22 @@ export interface WindowInterface { } export interface LanguageServerInterface { - readonly rootPath: string; + readonly rootUri: Uri; readonly console: ConsoleInterface; readonly window: WindowInterface; readonly supportAdvancedEdits: boolean; getWorkspaces(): Promise; - getWorkspaceForFile(filePath: string): Promise; + getWorkspaceForFile(fileUri: Uri): Promise; getSettings(workspace: Workspace): Promise; createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined; reanalyze(): void; restart(): void; - decodeTextDocumentUri(uriString: string): string; } export interface ServerOptions { productName: string; - rootDirectory: string; + rootDirectory: Uri; version: string; cancellationProvider: CancellationProvider; serviceProvider: ServiceProvider; @@ -323,7 +315,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis private _workspaceFoldersChangedDisposable: Disposable | undefined; // Global root path - the basis for all global settings. - rootPath = ''; + rootUri = Uri.empty(); protected client: ClientCapabilities = { hasConfigurationCapability: false, @@ -358,16 +350,10 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis // The URIs for which diagnostics are reported protected readonly documentsWithDiagnostics = new Set(); - readonly uriParser: UriParser; - - constructor( - protected serverOptions: ServerOptions, - protected connection: Connection, - uriParserFactory = (fs: FileSystem) => new UriParser(fs) - ) { + constructor(protected serverOptions: ServerOptions, protected connection: Connection) { // Stash the base directory into a global variable. // This must happen before fs.getModulePath(). - (global as any).__rootDirectory = serverOptions.rootDirectory; + (global as any).__rootDirectory = serverOptions.rootDirectory.getFilePath(); this.console.info( `${serverOptions.productName} language server ${ @@ -379,16 +365,14 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis this.fs = this.serverOptions.serviceProvider.fs(); - this.uriParser = uriParserFactory(this.fs); - this.workspaceFactory = new WorkspaceFactory( this.console, - this.uriParser, /* isWeb */ false, this.createAnalyzerServiceForWorkspace.bind(this), this.isPythonPathImmutable.bind(this), this.onWorkspaceCreated.bind(this), - this.onWorkspaceRemoved.bind(this) + this.onWorkspaceRemoved.bind(this), + this.fs.isCaseSensitive ); // Set the working directory to a known location within @@ -427,11 +411,6 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis this._workspaceFoldersChangedDisposable?.dispose(); } - // Convert uri to path - decodeTextDocumentUri(uriString: string): string { - return this.uriParser.decodeTextDocumentUri(uriString); - } - abstract createBackgroundAnalysis(serviceId: string): BackgroundAnalysisBase | undefined; abstract getSettings(workspace: Workspace): Promise; @@ -472,12 +451,12 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis return workspaces; } - async getWorkspaceForFile(filePath: string, pythonPath?: string): Promise { - return this.workspaceFactory.getWorkspaceForFile(filePath, pythonPath); + async getWorkspaceForFile(fileUri: Uri, pythonPath?: Uri): Promise { + return this.workspaceFactory.getWorkspaceForFile(fileUri, pythonPath); } - async getContainingWorkspacesForFile(filePath: string): Promise { - return this.workspaceFactory.getContainingWorkspacesForFile(filePath); + async getContainingWorkspacesForFile(fileUri: Uri): Promise { + return this.workspaceFactory.getContainingWorkspacesForFile(fileUri); } reanalyze() { @@ -521,7 +500,10 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis (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); + serverSettings.pythonPath = this.workspaceFactory.applyPythonPath( + workspace, + serverSettings.pythonPath ? serverSettings.pythonPath : undefined + ); // Then use the updated settings to restart the service. this.updateOptionsAndRestartService(workspace, serverSettings); @@ -540,8 +522,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis serverSettings: ServerSettings, typeStubTargetImportName?: string ) { - AnalyzerServiceExecutor.runWithOptions(this.rootPath, workspace, serverSettings, typeStubTargetImportName); - workspace.searchPathsToWatch = workspace.service.librarySearchPathsToWatch ?? []; + AnalyzerServiceExecutor.runWithOptions(this.rootUri, workspace, serverSettings, typeStubTargetImportName); + workspace.searchPathsToWatch = workspace.service.librarySearchUrisToWatch ?? []; } protected abstract executeCommand(params: ExecuteCommandParams, token: CancellationToken): Promise; @@ -553,18 +535,18 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis token: CancellationToken ): Promise<(Command | CodeAction)[] | undefined | null>; - protected isPythonPathImmutable(filePath: string): boolean { + protected isPythonPathImmutable(fileUri: Uri): 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) { + protected async getConfiguration(scopeUri: Uri | undefined, section: string) { if (this.client.hasConfigurationCapability) { const item: ConfigurationItem = {}; if (scopeUri !== undefined) { - item.scopeUri = scopeUri; + item.scopeUri = scopeUri.toString(); } if (section !== undefined) { item.section = section; @@ -691,7 +673,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis setLocaleOverride(params.locale); } - this.rootPath = params.rootPath || ''; + this.rootUri = params.rootUri ? Uri.parse(params.rootUri, this.fs.isCaseSensitive) : Uri.empty(); const capabilities = params.capabilities; this.client.hasConfigurationCapability = !!capabilities.workspace?.configuration; @@ -862,7 +844,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis filter: DefinitionFilter, getDefinitionsFunc: ( workspace: Workspace, - filePath: string, + fileUri: Uri, position: Position, filter: DefinitionFilter, token: CancellationToken @@ -870,20 +852,20 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis ) { this.recordUserInteractionTime(); - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return undefined; } - const locations = getDefinitionsFunc(workspace, filePath, position, filter, token); + const locations = getDefinitionsFunc(workspace, uri, params.position, filter, token); if (!locations) { return undefined; } return locations - .filter((loc) => this.canNavigateToFile(loc.path, workspace.service.fs)) - .map((loc) => Location.create(convertPathToUri(workspace.service.fs, loc.path), loc.range)); + .filter((loc) => this.canNavigateToFile(loc.uri, workspace.service.fs)) + .map((loc) => Location.create(loc.uri.toString(), loc.range)); } protected async onReferences( @@ -891,7 +873,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis token: CancellationToken, workDoneReporter: WorkDoneProgressReporter, resultReporter: ResultProgressReporter | undefined, - createDocumentRange?: (filePath: string, result: CollectionResult, parseResults: ParseResults) => DocumentRange, + createDocumentRange?: (uri: Uri, result: CollectionResult, parseResults: ParseResults) => DocumentRange, convertToLocation?: (fs: ReadOnlyFileSystem, ranges: DocumentRange) => Location | undefined ): Promise { if (this._pendingFindAllRefsCancellationSource) { @@ -912,12 +894,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis this._pendingFindAllRefsCancellationSource = source; try { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition( - params.textDocument, - params.position - ); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return; } @@ -928,7 +907,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis source.token, createDocumentRange, convertToLocation - ).reportReferences(filePath, position, params.context.includeDeclaration, resultReporter); + ).reportReferences(uri, params.position, params.context.includeDeclaration, resultReporter); }, token); } finally { progress.reporter.done(); @@ -942,8 +921,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis ): Promise { this.recordUserInteractionTime(); - const filePath = this.uriParser.decodeTextDocumentUri(params.textDocument.uri); - const workspace = await this.getWorkspaceForFile(filePath); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return undefined; } @@ -951,7 +930,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis return workspace.service.run((program) => { return new DocumentSymbolProvider( program, - filePath, + uri, this.client.hasHierarchicalDocumentSymbolCapability, token ).getSymbols(); @@ -974,11 +953,11 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis } protected async onHover(params: HoverParams, token: CancellationToken) { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); - const workspace = await this.getWorkspaceForFile(filePath); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const workspace = await this.getWorkspaceForFile(uri); return workspace.service.run((program) => { - return new HoverProvider(program, filePath, position, this.client.hoverContentFormat, token).getHover(); + return new HoverProvider(program, uri, params.position, this.client.hoverContentFormat, token).getHover(); }, token); } @@ -986,11 +965,11 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis params: DocumentHighlightParams, token: CancellationToken ): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); - const workspace = await this.getWorkspaceForFile(filePath); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const workspace = await this.getWorkspaceForFile(uri); return workspace.service.run((program) => { - return new DocumentHighlightProvider(program, filePath, position, token).getDocumentHighlight(); + return new DocumentHighlightProvider(program, uri, params.position, token).getDocumentHighlight(); }, token); } @@ -998,9 +977,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis params: SignatureHelpParams, token: CancellationToken ): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return; } @@ -1008,8 +987,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis return workspace.service.run((program) => { return new SignatureHelpProvider( program, - filePath, - position, + uri, + params.position, this.client.signatureDocFormat, this.client.hasSignatureLabelOffsetCapability, this.client.hasActiveParameterCapability, @@ -1040,8 +1019,8 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis } protected async onCompletion(params: CompletionParams, token: CancellationToken): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); - const workspace = await this.getWorkspaceForFile(filePath); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return null; } @@ -1049,9 +1028,9 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis return workspace.service.run((program) => { const completions = new CompletionProvider( program, - workspace.rootPath, - filePath, - position, + workspace.rootUri, + uri, + params.position, { format: this.client.completionDocFormat, snippet: this.client.completionSupportsSnippet, @@ -1074,13 +1053,14 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis // cache that result and never call us back. protected async onCompletionResolve(params: CompletionItem, token: CancellationToken): Promise { const completionItemData = fromLSPAny(params.data); - if (completionItemData && completionItemData.filePath) { - const workspace = await this.getWorkspaceForFile(completionItemData.filePath); + if (completionItemData && completionItemData.uri) { + const uri = Uri.parse(completionItemData.uri, this.fs.isCaseSensitive); + const workspace = await this.getWorkspaceForFile(uri); workspace.service.run((program) => { return new CompletionProvider( program, - workspace.rootPath, - completionItemData.filePath, + workspace.rootUri, + uri, completionItemData.position, { format: this.client.completionDocFormat, @@ -1098,16 +1078,16 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis params: PrepareRenameParams, token: CancellationToken ): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); - const isUntitled = this.uriParser.isUntitled(params.textDocument.uri); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const isUntitled = uri.isUntitled(); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return null; } return workspace.service.run((program) => { - return new RenameProvider(program, filePath, position, token).canRenameSymbol( + return new RenameProvider(program, uri, params.position, token).canRenameSymbol( workspace.kinds.includes(WellKnownWorkspaceKinds.Default), isUntitled ); @@ -1118,16 +1098,16 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis params: RenameParams, token: CancellationToken ): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); - const isUntitled = this.uriParser.isUntitled(params.textDocument.uri); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const isUntitled = uri.isUntitled(); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return; } return workspace.service.run((program) => { - return new RenameProvider(program, filePath, position, token).renameSymbol( + return new RenameProvider(program, uri, params.position, token).renameSymbol( params.newName, workspace.kinds.includes(WellKnownWorkspaceKinds.Default), isUntitled @@ -1139,28 +1119,28 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis params: CallHierarchyPrepareParams, token: CancellationToken ): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.textDocument, params.position); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return null; } return workspace.service.run((program) => { - return new CallHierarchyProvider(program, filePath, position, token).onPrepare(); + return new CallHierarchyProvider(program, uri, params.position, token).onPrepare(); }, token); } protected async onCallHierarchyIncomingCalls(params: CallHierarchyIncomingCallsParams, token: CancellationToken) { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.item, params.item.range.start); + const uri = Uri.parse(params.item.uri, this.fs.isCaseSensitive); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return null; } return workspace.service.run((program) => { - return new CallHierarchyProvider(program, filePath, position, token).getIncomingCalls(); + return new CallHierarchyProvider(program, uri, params.item.range.start, token).getIncomingCalls(); }, token); } @@ -1168,46 +1148,51 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis params: CallHierarchyOutgoingCallsParams, token: CancellationToken ): Promise { - const { filePath, position } = this.uriParser.decodeTextDocumentPosition(params.item, params.item.range.start); + const uri = Uri.parse(params.item.uri, this.fs.isCaseSensitive); - const workspace = await this.getWorkspaceForFile(filePath); + const workspace = await this.getWorkspaceForFile(uri); if (workspace.disableLanguageServices) { return null; } return workspace.service.run((program) => { - return new CallHierarchyProvider(program, filePath, position, token).getOutgoingCalls(); + return new CallHierarchyProvider(program, uri, params.item.range.start, token).getOutgoingCalls(); }, token); } protected async onDidOpenTextDocument(params: DidOpenTextDocumentParams, ipythonMode = IPythonMode.None) { - const filePath = this.uriParser.decodeTextDocumentUri(params.textDocument.uri); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); - let doc = this.openFileMap.get(filePath); + let doc = this.openFileMap.get(uri.key); if (doc) { // We shouldn't get an open text document request for an already-opened doc. - this.console.error(`Received redundant open text document command for ${filePath}`); + this.console.error(`Received redundant open text document command for ${uri}`); TextDocument.update(doc, [{ text: params.textDocument.text }], params.textDocument.version); } else { - doc = TextDocument.create(filePath, 'python', params.textDocument.version, params.textDocument.text); + doc = TextDocument.create( + params.textDocument.uri, + 'python', + params.textDocument.version, + params.textDocument.text + ); } - this.openFileMap.set(filePath, doc); + this.openFileMap.set(uri.key, doc); // Send this open to all the workspaces that might contain this file. - const workspaces = await this.getContainingWorkspacesForFile(filePath); + const workspaces = await this.getContainingWorkspacesForFile(uri); workspaces.forEach((w) => { - w.service.setFileOpened(filePath, params.textDocument.version, params.textDocument.text, ipythonMode); + w.service.setFileOpened(uri, params.textDocument.version, params.textDocument.text, ipythonMode); }); } protected async onDidChangeTextDocument(params: DidChangeTextDocumentParams, ipythonMode = IPythonMode.None) { this.recordUserInteractionTime(); - const filePath = this.uriParser.decodeTextDocumentUri(params.textDocument.uri); - const doc = this.openFileMap.get(filePath); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); + const doc = this.openFileMap.get(uri.key); if (!doc) { // We shouldn't get a change text request for a closed doc. - this.console.error(`Received change text document command for closed file ${filePath}`); + this.console.error(`Received change text document command for closed file ${uri}`); return; } @@ -1215,27 +1200,27 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis const newContents = doc.getText(); // Send this change to all the workspaces that might contain this file. - const workspaces = await this.getContainingWorkspacesForFile(filePath); + const workspaces = await this.getContainingWorkspacesForFile(uri); workspaces.forEach((w) => { - w.service.updateOpenFileContents(filePath, params.textDocument.version, newContents, ipythonMode); + w.service.updateOpenFileContents(uri, params.textDocument.version, newContents, ipythonMode); }); } protected async onDidCloseTextDocument(params: DidCloseTextDocumentParams) { - const filePath = this.uriParser.decodeTextDocumentUri(params.textDocument.uri); + const uri = Uri.parse(params.textDocument.uri, this.fs.isCaseSensitive); // Send this close to all the workspaces that might contain this file. - const workspaces = await this.getContainingWorkspacesForFile(filePath); + const workspaces = await this.getContainingWorkspacesForFile(uri); workspaces.forEach((w) => { - w.service.setFileClosed(filePath); + w.service.setFileClosed(uri); }); - this.openFileMap.delete(filePath); + this.openFileMap.delete(uri.key); } protected onDidChangeWatchedFiles(params: DidChangeWatchedFilesParams) { params.changes.forEach((change) => { - const filePath = this.fs.realCasePath(this.uriParser.decodeTextDocumentUri(change.uri)); + const filePath = this.fs.realCasePath(Uri.parse(change.uri, this.fs.isCaseSensitive)); const eventType: FileWatcherEventType = change.type === 1 ? 'add' : 'change'; this.serverOptions.fileWatcherHandler.onFileChange(eventType, filePath); }); @@ -1302,7 +1287,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis protected convertDiagnostics(fs: FileSystem, fileDiagnostics: FileDiagnostics): PublishDiagnosticsParams[] { return [ { - uri: convertPathToUri(fs, fileDiagnostics.filePath), + uri: fileDiagnostics.fileUri.toString(), version: fileDiagnostics.version, diagnostics: this._convertDiagnostics(fs, fileDiagnostics.diagnostics), }, @@ -1316,7 +1301,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis protected onAnalysisCompletedHandler(fs: FileSystem, results: AnalysisResults): void { // Send the computed diagnostics to the client. results.diagnostics.forEach((fileDiag) => { - if (!this.canNavigateToFile(fileDiag.filePath, fs)) { + if (!this.canNavigateToFile(fileDiag.fileUri, fs)) { return; } @@ -1361,11 +1346,11 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis const otherWorkspaces = this.workspaceFactory.items().filter((w) => w !== workspace); for (const uri of documentsWithDiagnosticsList) { - const filePath = convertUriToPath(workspace.service.fs, uri); + const fileUri = Uri.parse(uri, this.fs.isCaseSensitive); - if (workspace.service.isTracked(filePath)) { + if (workspace.service.isTracked(fileUri)) { // Do not clean up diagnostics for files tracked by multiple workspaces - if (otherWorkspaces.some((w) => w.service.isTracked(filePath))) { + if (otherWorkspaces.some((w) => w.service.isTracked(fileUri))) { continue; } this.sendDiagnostics([ @@ -1380,8 +1365,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis protected createAnalyzerServiceForWorkspace( name: string, - _rootPath: string, - _uri: string, + uri: Uri, kinds: string[], services?: WorkspaceServices ): AnalyzerService { @@ -1422,7 +1406,7 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis protected abstract createProgressReporter(): ProgressReporter; - protected canNavigateToFile(path: string, fs: FileSystem): boolean { + protected canNavigateToFile(path: Uri, fs: FileSystem): boolean { return canNavigateToFile(fs, path); } @@ -1481,13 +1465,13 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis const foldersToWatch = deduplicateFolders( this.workspaceFactory .getNonDefaultWorkspaces() - .map((w) => w.searchPathsToWatch.filter((p) => !p.startsWith(w.rootPath))) + .map((w) => w.searchPathsToWatch.filter((p) => !p.startsWith(w.rootUri))) ); foldersToWatch.forEach((p) => { const globPattern = isFile(this.fs, p, /* treatZipDirectoryAsFile */ true) - ? { baseUri: convertPathToUri(this.fs, getDirectoryPath(p)), pattern: getFileName(p) } - : { baseUri: convertPathToUri(this.fs, p), pattern: '**' }; + ? { baseUri: p.getDirectory().toString(), pattern: p.fileName } + : { baseUri: p.toString(), pattern: '**' }; watchers.push({ globPattern, kind: watchKind }); }); @@ -1582,10 +1566,10 @@ export abstract class LanguageServerBase implements LanguageServerInterface, Dis const relatedInfo = diag.getRelatedInfo(); if (relatedInfo.length > 0) { vsDiag.relatedInformation = relatedInfo - .filter((info) => this.canNavigateToFile(info.filePath, fs)) + .filter((info) => this.canNavigateToFile(info.uri, fs)) .map((info) => DiagnosticRelatedInformation.create( - Location.create(convertPathToUri(fs, info.filePath), info.range), + Location.create(info.uri.toString(), info.range), info.message ) ); diff --git a/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts b/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts index cab3dffe4..70ae135b5 100644 --- a/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts +++ b/packages/pyright-internal/src/languageService/analyzerServiceExecutor.ts @@ -13,7 +13,7 @@ import { AnalyzerService, getNextServiceId } from '../analyzer/service'; import { CommandLineOptions } from '../common/commandLineOptions'; import { LogLevel } from '../common/console'; import { FileSystem } from '../common/fileSystem'; -import { combinePaths } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; import { LanguageServerInterface, ServerSettings } from '../languageServerBase'; import { WellKnownWorkspaceKinds, Workspace, createInitStatus } from '../workspaceFactory'; @@ -25,15 +25,15 @@ export interface CloneOptions { export class AnalyzerServiceExecutor { static runWithOptions( - languageServiceRootPath: string, + languageServiceRootUri: Uri, workspace: Workspace, serverSettings: ServerSettings, typeStubTargetImportName?: string, trackFiles = true ): void { const commandLineOptions = getEffectiveCommandLineOptions( - languageServiceRootPath, - workspace.rootPath, + languageServiceRootUri.getFilePath(), + workspace.rootUri.getFilePath(), serverSettings, trackFiles, typeStubTargetImportName, @@ -58,8 +58,7 @@ export class AnalyzerServiceExecutor { const tempWorkspace: Workspace = { ...workspace, workspaceName: `temp workspace for cloned service`, - rootPath: workspace.rootPath, - uri: workspace.uri, + rootUri: workspace.rootUri, pythonPath: workspace.pythonPath, pythonPathKind: workspace.pythonPathKind, kinds: [...workspace.kinds, WellKnownWorkspaceKinds.Cloned], @@ -78,7 +77,7 @@ export class AnalyzerServiceExecutor { const serverSettings = await ls.getSettings(workspace); AnalyzerServiceExecutor.runWithOptions( - ls.rootPath, + ls.rootUri, tempWorkspace, serverSettings, options.typeStubTargetImportName, @@ -120,21 +119,15 @@ function getEffectiveCommandLineOptions( } if (serverSettings.venvPath) { - commandLineOptions.venvPath = combinePaths( - workspaceRootPath || languageServiceRootPath, - serverSettings.venvPath - ); + commandLineOptions.venvPath = serverSettings.venvPath.getFilePath(); } if (serverSettings.pythonPath) { // The Python VS Code extension treats the value "python" specially. This means // the local python interpreter should be used rather than interpreting the // setting value as a path to the interpreter. We'll simply ignore it in this case. - if (!isPythonBinary(serverSettings.pythonPath)) { - commandLineOptions.pythonPath = combinePaths( - workspaceRootPath || languageServiceRootPath, - serverSettings.pythonPath - ); + if (!isPythonBinary(serverSettings.pythonPath.getFilePath())) { + commandLineOptions.pythonPath = serverSettings.pythonPath.getFilePath(); } } @@ -142,11 +135,11 @@ function getEffectiveCommandLineOptions( // Pyright supports only one typeshed path currently, whereas the // official VS Code Python extension supports multiple typeshed paths. // We'll use the first one specified and ignore the rest. - commandLineOptions.typeshedPath = serverSettings.typeshedPath; + commandLineOptions.typeshedPath = serverSettings.typeshedPath.getFilePath(); } if (serverSettings.stubPath) { - commandLineOptions.stubPath = serverSettings.stubPath; + commandLineOptions.stubPath = serverSettings.stubPath.getFilePath(); } if (serverSettings.logLevel === LogLevel.Log) { @@ -160,7 +153,7 @@ function getEffectiveCommandLineOptions( } commandLineOptions.autoSearchPaths = serverSettings.autoSearchPaths; - commandLineOptions.extraPaths = serverSettings.extraPaths; + commandLineOptions.extraPaths = serverSettings.extraPaths?.map((e) => e.getFilePath()) ?? []; commandLineOptions.diagnosticSeverityOverrides = serverSettings.diagnosticSeverityOverrides; commandLineOptions.includeFileSpecs = serverSettings.includeFileSpecs ?? []; diff --git a/packages/pyright-internal/src/languageService/autoImporter.ts b/packages/pyright-internal/src/languageService/autoImporter.ts index 0fc2701c7..ac2aad56b 100644 --- a/packages/pyright-internal/src/languageService/autoImporter.ts +++ b/packages/pyright-internal/src/languageService/autoImporter.ts @@ -12,15 +12,15 @@ import { DeclarationType } from '../analyzer/declaration'; import { ImportResolver, ModuleNameAndType } from '../analyzer/importResolver'; import { ImportType } from '../analyzer/importResult'; import { + ImportGroup, + ImportNameInfo, + ImportStatements, + ModuleNameInfo, getImportGroup, getImportGroupFromModuleNameAndType, getTextEditsForAutoImportInsertion, getTextEditsForAutoImportSymbolAddition, getTopLevelImports, - ImportGroup, - ImportNameInfo, - ImportStatements, - ModuleNameInfo, } from '../analyzer/importStatementUtils'; import { isUserCode } from '../analyzer/sourceFileInfoUtils'; import { Symbol } from '../analyzer/symbol'; @@ -30,13 +30,14 @@ import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { appendArray } from '../common/collectionUtils'; import { ExecutionEnvironment } from '../common/configOptions'; import { TextEditAction } from '../common/editAction'; -import { combinePaths, getDirectoryPath, getFileName, stripFileExtension } from '../common/pathUtils'; +import { SourceFileInfo } from '../common/extensibility'; +import { stripFileExtension } from '../common/pathUtils'; import * as StringUtils from '../common/stringUtils'; import { Position } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { CompletionMap } from './completionProvider'; -import { SourceFileInfo } from '../common/extensibility'; import { IndexAliasData } from './symbolIndexer'; export interface AutoImportSymbol { @@ -47,6 +48,7 @@ export interface AutoImportSymbol { } export interface ModuleSymbolTable { + uri: Uri; forEach(callbackfn: (symbol: AutoImportSymbol, name: string, library: boolean) => void): void; } @@ -54,8 +56,8 @@ export type ModuleSymbolMap = Map; export interface AutoImportResult { readonly name: string; - readonly declPath: string; - readonly originalDeclPath: string; + readonly declUri: Uri; + readonly originalDeclUri: Uri; readonly insertionText: string; readonly symbol?: Symbol; readonly source?: string; @@ -74,7 +76,7 @@ export interface ImportParts { readonly importName: string; readonly symbolName?: string; readonly importFrom?: string; - readonly filePath: string; + readonly fileUri: Uri; readonly dotCount: number; readonly moduleNameAndType: ModuleNameAndType; } @@ -85,7 +87,7 @@ export interface ImportAliasData { readonly symbol?: Symbol; readonly kind?: SymbolKind; readonly itemKind?: CompletionItemKind; - readonly filePath: string; + readonly fileUri: Uri; } export type AutoImportResultMap = Map; @@ -106,13 +108,13 @@ export function addModuleSymbolsMap(files: readonly SourceFileInfo[], moduleSymb return; } - const filePath = file.sourceFile.getFilePath(); + const uri = file.sourceFile.getUri(); const symbolTable = file.sourceFile.getModuleSymbolTable(); if (!symbolTable) { return; } - const fileName = stripFileExtension(getFileName(filePath)); + const fileName = stripFileExtension(uri.fileName); // Don't offer imports from files that are named with private // naming semantics like "_ast.py". @@ -120,7 +122,8 @@ export function addModuleSymbolsMap(files: readonly SourceFileInfo[], moduleSymb return; } - moduleSymbolMap.set(filePath, { + moduleSymbolMap.set(uri.key, { + uri, forEach(callbackfn: (value: AutoImportSymbol, key: string, library: boolean) => void): void { symbolTable.forEach((symbol, name) => { if (!isVisibleExternally(symbol)) { @@ -206,12 +209,12 @@ export class AutoImporter { results: AutoImportResultMap, token: CancellationToken ) { - this.moduleSymbolMap.forEach((topLevelSymbols, filePath) => { + this.moduleSymbolMap.forEach((topLevelSymbols, key) => { // See if this file should be offered as an implicit import. - const isStubFileOrHasInit = this.isStubFileOrHasInit(this.moduleSymbolMap!, filePath); + const isStubFileOrHasInit = this.isStubFileOrHasInit(this.moduleSymbolMap!, topLevelSymbols.uri); this.processModuleSymbolTable( topLevelSymbols, - filePath, + topLevelSymbols.uri, word, similarityLimit, isStubFileOrHasInit, @@ -244,7 +247,7 @@ export class AutoImporter { // If import statement for the module already exist, then bail out. // ex) import module[.submodule] or from module[.submodule] import symbol - if (this._importStatements.mapByFilePath.has(importAliasData.importParts.filePath)) { + if (this._importStatements.mapByFilePath.has(importAliasData.importParts.fileUri.key)) { return; } @@ -281,7 +284,7 @@ export class AutoImporter { }, importAliasData.importParts.importName, importAliasData.importGroup, - importAliasData.importParts.filePath + importAliasData.importParts.fileUri ); this._addResult(results, { @@ -292,8 +295,8 @@ export class AutoImporter { source: importAliasData.importParts.importFrom, insertionText: autoImportTextEdits.insertionText, edits: autoImportTextEdits.edits, - declPath: importAliasData.importParts.filePath, - originalDeclPath: importAliasData.filePath, + declUri: importAliasData.importParts.fileUri, + originalDeclUri: importAliasData.fileUri, }); }); }); @@ -301,7 +304,7 @@ export class AutoImporter { protected processModuleSymbolTable( topLevelSymbols: ModuleSymbolTable, - moduleFilePath: string, + moduleUri: Uri, word: string, similarityLimit: number, isStubOrHasInit: { isStub: boolean; hasInit: boolean }, @@ -312,7 +315,7 @@ export class AutoImporter { ) { throwIfCancellationRequested(token); - const [importSource, importGroup, moduleNameAndType] = this._getImportPartsForSymbols(moduleFilePath); + const [importSource, importGroup, moduleNameAndType] = this._getImportPartsForSymbols(moduleUri); if (!importSource) { return; } @@ -345,7 +348,7 @@ export class AutoImporter { symbolName: name, importName: name, importFrom: importSource, - filePath: moduleFilePath, + fileUri: moduleUri, dotCount, moduleNameAndType, }, @@ -353,20 +356,20 @@ export class AutoImporter { symbol: autoImportSymbol.symbol, kind: autoImportSymbol.importAlias.kind, itemKind: autoImportSymbol.importAlias.itemKind, - filePath: autoImportSymbol.importAlias.modulePath, + fileUri: autoImportSymbol.importAlias.moduleUri, }, importAliasMap ); return; } - const nameForImportFrom = this.getNameForImportFrom(library, moduleFilePath); + const nameForImportFrom = this.getNameForImportFrom(library, moduleUri); const autoImportTextEdits = this._getTextEditsForAutoImportByFilePath( { name, alias: abbrFromUsers }, { name: importSource, nameForImportFrom }, name, importGroup, - moduleFilePath + moduleUri ); this._addResult(results, { @@ -377,8 +380,8 @@ export class AutoImporter { kind: autoImportSymbol.itemKind ?? convertSymbolKindToCompletionItemKind(autoImportSymbol.kind), insertionText: autoImportTextEdits.insertionText, edits: autoImportTextEdits.edits, - declPath: moduleFilePath, - originalDeclPath: moduleFilePath, + declUri: moduleUri, + originalDeclUri: moduleUri, }); }); @@ -389,7 +392,7 @@ export class AutoImporter { return; } - const importParts = this._getImportParts(moduleFilePath); + const importParts = this._getImportParts(moduleUri); if (!importParts) { return; } @@ -406,7 +409,7 @@ export class AutoImporter { this._addToImportAliasMap( { - modulePath: moduleFilePath, + moduleUri, originalName: importParts.importName, kind: SymbolKind.Module, itemKind: CompletionItemKind.Module, @@ -416,22 +419,22 @@ export class AutoImporter { importGroup, kind: SymbolKind.Module, itemKind: CompletionItemKind.Module, - filePath: moduleFilePath, + fileUri: moduleUri, }, importAliasMap ); } - protected getNameForImportFrom(library: boolean, moduleFilePath: string): string | undefined { + protected getNameForImportFrom(library: boolean, moduleUri: Uri): string | undefined { return undefined; } - protected isStubFileOrHasInit(map: Map, filePath: string) { - const fileDir = getDirectoryPath(filePath); - const initPathPy = combinePaths(fileDir, '__init__.py'); - const initPathPyi = initPathPy + 'i'; - const isStub = filePath.endsWith('.pyi'); - const hasInit = map.has(initPathPy) || map.has(initPathPyi); + protected isStubFileOrHasInit(map: Map, uri: Uri) { + const fileDir = uri.getDirectory(); + const initPathPy = fileDir.combinePaths('__init__.py'); + const initPathPyi = initPathPy.addPath('i'); + const isStub = uri.pathEndsWith('.pyi'); + const hasInit = map.has(initPathPy.key) || map.has(initPathPyi.key); return { isStub, hasInit }; } @@ -462,14 +465,14 @@ export class AutoImporter { // Since we don't resolve alias declaration using type evaluator, there is still a chance // where we show multiple aliases for same symbols. but this should still reduce number of // such cases. - if (!importAliasMap.has(alias.modulePath)) { + if (!importAliasMap.has(alias.moduleUri.key)) { const map = new Map(); map.set(alias.originalName, data); - importAliasMap.set(alias.modulePath, map); + importAliasMap.set(alias.moduleUri.key, map); return; } - const map = importAliasMap.get(alias.modulePath)!; + const map = importAliasMap.get(alias.moduleUri.key)!; if (!map.has(alias.originalName)) { map.set(alias.originalName, data); return; @@ -508,8 +511,8 @@ export class AutoImporter { return StringUtils.getStringComparer()(left.importParts.importName, right.importParts.importName); } - private _getImportPartsForSymbols(filePath: string): [string | undefined, ImportGroup, ModuleNameAndType] { - const localImport = this._importStatements.mapByFilePath.get(filePath); + private _getImportPartsForSymbols(uri: Uri): [string | undefined, ImportGroup, ModuleNameAndType] { + const localImport = this._importStatements.mapByFilePath.get(uri.key); if (localImport) { return [ localImport.moduleName, @@ -521,7 +524,7 @@ export class AutoImporter { }, ]; } else { - const moduleNameAndType = this._getModuleNameAndTypeFromFilePath(filePath); + const moduleNameAndType = this._getModuleNameAndTypeFromFilePath(uri); return [ moduleNameAndType.moduleName, getImportGroupFromModuleNameAndType(moduleNameAndType), @@ -530,15 +533,15 @@ export class AutoImporter { } } - private _getImportParts(filePath: string) { - const name = stripFileExtension(getFileName(filePath)); + private _getImportParts(uri: Uri) { + const name = stripFileExtension(uri.fileName); // See if we can import module as "import xxx" if (name === '__init__') { - return createImportParts(this._getModuleNameAndTypeFromFilePath(getDirectoryPath(filePath))); + return createImportParts(this._getModuleNameAndTypeFromFilePath(uri.getDirectory())); } - return createImportParts(this._getModuleNameAndTypeFromFilePath(filePath)); + return createImportParts(this._getModuleNameAndTypeFromFilePath(uri)); function createImportParts(module: ModuleNameAndType): ImportParts | undefined { const moduleName = module.moduleName; @@ -553,7 +556,7 @@ export class AutoImporter { symbolName: importNamePart, importName: importNamePart ?? moduleName, importFrom, - filePath, + fileUri: uri, dotCount: StringUtils.getCharacterCount(moduleName, '.'), moduleNameAndType: module, }; @@ -601,8 +604,8 @@ export class AutoImporter { // Given the file path of a module that we want to import, // convert to a module name that can be used in an // 'import from' statement. - private _getModuleNameAndTypeFromFilePath(filePath: string): ModuleNameAndType { - return this.importResolver.getModuleNameForImport(filePath, this.execEnvironment); + private _getModuleNameAndTypeFromFilePath(uri: Uri): ModuleNameAndType { + return this.importResolver.getModuleNameForImport(uri, this.execEnvironment); } private _getTextEditsForAutoImportByFilePath( @@ -610,10 +613,10 @@ export class AutoImporter { moduleNameInfo: ModuleNameInfo, insertionText: string, importGroup: ImportGroup, - filePath: string + fileUri: Uri ): { insertionText: string; edits?: TextEditAction[] | undefined } { // If there is no symbolName, there can't be existing import statement. - const importStatement = this._importStatements.mapByFilePath.get(filePath); + const importStatement = this._importStatements.mapByFilePath.get(fileUri.key); if (importStatement) { // Found import for given module. See whether we can use the module as it is. if (importStatement.node.nodeType === ParseNodeType.Import) { @@ -699,7 +702,7 @@ export class AutoImporter { } // Check whether it is one of implicit imports - const importFrom = this._importStatements.implicitImports?.get(filePath); + const importFrom = this._importStatements.implicitImports?.get(fileUri.key); if (importFrom) { // For now, we don't check whether alias or moduleName got overwritten at // given position diff --git a/packages/pyright-internal/src/languageService/callHierarchyProvider.ts b/packages/pyright-internal/src/languageService/callHierarchyProvider.ts index 13298cb79..c21a117cc 100644 --- a/packages/pyright-internal/src/languageService/callHierarchyProvider.ts +++ b/packages/pyright-internal/src/languageService/callHierarchyProvider.ts @@ -29,10 +29,10 @@ import { appendArray } from '../common/collectionUtils'; import { isDefined } from '../common/core'; import { ProgramView, ReferenceUseCase, SymbolUsageProvider } from '../common/extensibility'; import { getSymbolKind } from '../common/lspUtils'; -import { convertPathToUri, getFileName } from '../common/pathUtils'; import { convertOffsetsToRange } from '../common/positionUtils'; import { ServiceKeys } from '../common/serviceProviderExtensions'; import { Position, rangesAreEqual } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ReferencesProvider, ReferencesResult } from '../languageService/referencesProvider'; import { CallNode, MemberAccessNode, NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; @@ -44,11 +44,11 @@ export class CallHierarchyProvider { constructor( private _program: ProgramView, - private _filePath: string, + private _fileUri: Uri, private _position: Position, private _token: CancellationToken ) { - this._parseResults = this._program.getParseResults(this._filePath); + this._parseResults = this._program.getParseResults(this._fileUri); } onPrepare(): CallHierarchyItem[] | null { @@ -86,18 +86,15 @@ export class CallHierarchyProvider { const callItem: CallHierarchyItem = { name: symbolName, kind: getSymbolKind(targetDecl, this._evaluator, symbolName) ?? SymbolKind.Module, - uri: callItemUri, + uri: callItemUri.toString(), range: targetDecl.range, selectionRange: targetDecl.range, }; - if (!canNavigateToFile(this._program.fileSystem, callItem.uri)) { + if (!canNavigateToFile(this._program.fileSystem, Uri.parse(callItem.uri, this._fileUri.isCaseSensitive))) { return null; } - // Convert the file path in the item to proper URI. - callItem.uri = convertPathToUri(this._program.fileSystem, callItem.uri); - return [callItem]; } @@ -117,11 +114,11 @@ export class CallHierarchyProvider { const items: CallHierarchyIncomingCall[] = []; const sourceFiles = targetDecl.type === DeclarationType.Alias - ? [this._program.getSourceFileInfo(this._filePath)!] + ? [this._program.getSourceFileInfo(this._fileUri)!] : this._program.getSourceFileInfoList(); for (const curSourceFileInfo of sourceFiles) { if (isUserCode(curSourceFileInfo) || curSourceFileInfo.isOpenByClient) { - const filePath = curSourceFileInfo.sourceFile.getFilePath(); + const filePath = curSourceFileInfo.sourceFile.getUri(); const itemsToAdd = this._getIncomingCallsForDeclaration(filePath, symbolName, targetDecl); if (itemsToAdd) { @@ -138,14 +135,9 @@ export class CallHierarchyProvider { return null; } - const callItems = items.filter((item) => canNavigateToFile(this._program.fileSystem, item.from.uri)); - - // Convert the file paths in the items to proper URIs. - callItems.forEach((item) => { - item.from.uri = convertPathToUri(this._program.fileSystem, item.from.uri); - }); - - return callItems; + return items.filter((item) => + canNavigateToFile(this._program.fileSystem, Uri.parse(item.from.uri, this._fileUri.isCaseSensitive)) + ); } getOutgoingCalls(): CallHierarchyOutgoingCall[] | null { @@ -209,14 +201,9 @@ export class CallHierarchyProvider { return null; } - const callItems = outgoingCalls.filter((item) => canNavigateToFile(this._program.fileSystem, item.to.uri)); - - // Convert the file paths in the items to proper URIs. - callItems.forEach((item) => { - item.to.uri = convertPathToUri(this._program.fileSystem, item.to.uri); - }); - - return callItems; + return outgoingCalls.filter((item) => + canNavigateToFile(this._program.fileSystem, Uri.parse(item.to.uri, this._fileUri.isCaseSensitive)) + ); } private get _evaluator(): TypeEvaluator { @@ -225,7 +212,7 @@ export class CallHierarchyProvider { private _getTargetDeclaration(referencesResult: ReferencesResult): { targetDecl: Declaration; - callItemUri: string; + callItemUri: Uri; symbolName: string; } { // If there's more than one declaration, pick the target one. @@ -253,32 +240,26 @@ export class CallHierarchyProvider { // Although the LSP specification requires a URI, we are using a file path // here because it is converted to the proper URI by the caller. // This simplifies our code and ensures compatibility with the LSP specification. - let callItemUri; + let callItemUri: Uri; if (targetDecl.type === DeclarationType.Alias) { symbolName = (referencesResult.nodeAtOffset as NameNode).value; - callItemUri = this._filePath; + callItemUri = this._fileUri; } else { symbolName = DeclarationUtils.getNameFromDeclaration(targetDecl) || referencesResult.symbolNames[0]; - callItemUri = targetDecl.path; + callItemUri = targetDecl.uri; } return { targetDecl, callItemUri, symbolName }; } private _getIncomingCallsForDeclaration( - filePath: string, + fileUri: Uri, symbolName: string, declaration: Declaration ): CallHierarchyIncomingCall[] | undefined { throwIfCancellationRequested(this._token); - const callFinder = new FindIncomingCallTreeWalker( - this._program, - filePath, - symbolName, - declaration, - this._token - ); + const callFinder = new FindIncomingCallTreeWalker(this._program, fileUri, symbolName, declaration, this._token); const incomingCalls = callFinder.findCalls(); return incomingCalls.length > 0 ? incomingCalls : undefined; @@ -287,7 +268,7 @@ export class CallHierarchyProvider { private _getDeclaration(): ReferencesResult | undefined { return ReferencesProvider.getDeclarationForPosition( this._program, - this._filePath, + this._fileUri, this._position, /* reporter */ undefined, ReferenceUseCase.References, @@ -394,7 +375,7 @@ class FindOutgoingCallTreeWalker extends ParseTreeWalker { const callDest: CallHierarchyItem = { name: nameNode.value, kind: getSymbolKind(resolvedDecl, this._evaluator, nameNode.value) ?? SymbolKind.Module, - uri: resolvedDecl.path, + uri: resolvedDecl.uri.toString(), range: resolvedDecl.range, selectionRange: resolvedDecl.range, }; @@ -437,14 +418,14 @@ class FindIncomingCallTreeWalker extends ParseTreeWalker { constructor( private readonly _program: ProgramView, - private readonly _filePath: string, + private readonly _fileUri: Uri, private readonly _symbolName: string, private readonly _targetDeclaration: Declaration, private readonly _cancellationToken: CancellationToken ) { super(); - this._parseResults = this._program.getParseResults(this._filePath)!; + this._parseResults = this._program.getParseResults(this._fileUri)!; this._usageProviders = (this._program.serviceProvider.tryGet(ServiceKeys.symbolUsageProviderFactory) ?? []) .map((f) => f.tryCreateProvider(ReferenceUseCase.References, [this._targetDeclaration], this._cancellationToken) @@ -570,12 +551,12 @@ class FindIncomingCallTreeWalker extends ParseTreeWalker { let callSource: CallHierarchyItem; if (executionNode.nodeType === ParseNodeType.Module) { const moduleRange = convertOffsetsToRange(0, 0, this._parseResults.tokenizerOutput.lines); - const fileName = getFileName(this._filePath); + const fileName = this._fileUri.fileName; callSource = { name: `(module) ${fileName}`, kind: SymbolKind.Module, - uri: this._filePath, + uri: this._fileUri.toString(), range: moduleRange, selectionRange: moduleRange, }; @@ -589,7 +570,7 @@ class FindIncomingCallTreeWalker extends ParseTreeWalker { callSource = { name: '(lambda)', kind: SymbolKind.Function, - uri: this._filePath, + uri: this._fileUri.toString(), range: lambdaRange, selectionRange: lambdaRange, }; @@ -603,7 +584,7 @@ class FindIncomingCallTreeWalker extends ParseTreeWalker { callSource = { name: executionNode.name.value, kind: SymbolKind.Function, - uri: this._filePath, + uri: this._fileUri.toString(), range: functionRange, selectionRange: functionRange, }; diff --git a/packages/pyright-internal/src/languageService/codeActionProvider.ts b/packages/pyright-internal/src/languageService/codeActionProvider.ts index 1f73f82a1..14843877f 100644 --- a/packages/pyright-internal/src/languageService/codeActionProvider.ts +++ b/packages/pyright-internal/src/languageService/codeActionProvider.ts @@ -12,8 +12,8 @@ import { Commands } from '../commands/commands'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { ActionKind, CreateTypeStubFileAction, RenameShadowedFileAction } from '../common/diagnostic'; import { FileEditActions } from '../common/editAction'; -import { getShortenedFileName } from '../common/pathUtils'; import { Range } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { convertToWorkspaceEdit } from '../common/workspaceEditUtils'; import { Localizer } from '../localization/localize'; import { Workspace } from '../workspaceFactory'; @@ -29,7 +29,7 @@ export class CodeActionProvider { } static async getCodeActionsForPosition( workspace: Workspace, - filePath: string, + fileUri: Uri, range: Range, kinds: CodeActionKind[] | undefined, token: CancellationToken @@ -44,7 +44,7 @@ export class CodeActionProvider { } if (!workspace.disableLanguageServices) { - const diags = await workspace.service.getDiagnosticsForRange(filePath, range, token); + const diags = await workspace.service.getDiagnosticsForRange(fileUri, range, token); const typeStubDiag = diags.find((d) => { const actions = d.getActions(); return actions && actions.find((a) => a.action === Commands.createTypeStub); @@ -60,9 +60,9 @@ export class CodeActionProvider { Command.create( Localizer.CodeAction.createTypeStub(), Commands.createTypeStub, - workspace.rootPath, + workspace.rootUri.toString(), action.moduleName, - filePath + fileUri.toString() ), CodeActionKind.QuickFix ); @@ -80,21 +80,20 @@ export class CodeActionProvider { .find((a) => a.action === ActionKind.RenameShadowedFileAction) as RenameShadowedFileAction; if (action) { const title = Localizer.CodeAction.renameShadowedFile().format({ - oldFile: getShortenedFileName(action.oldFile), - newFile: getShortenedFileName(action.newFile), + oldFile: action.oldUri.getShortenedFileName(), + newFile: action.newUri.getShortenedFileName(), }); - const fs = workspace.service.getImportResolver().fileSystem; const editActions: FileEditActions = { edits: [], fileOperations: [ { kind: 'rename', - oldFilePath: action.oldFile, - newFilePath: action.newFile, + oldFileUri: action.oldUri, + newFileUri: action.newUri, }, ], }; - const workspaceEdit = convertToWorkspaceEdit(fs, editActions); + const workspaceEdit = convertToWorkspaceEdit(editActions); const renameAction = CodeAction.create(title, workspaceEdit, CodeActionKind.QuickFix); codeActions.push(renameAction); } diff --git a/packages/pyright-internal/src/languageService/completionProvider.ts b/packages/pyright-internal/src/languageService/completionProvider.ts index 650dd030a..e901d35b1 100644 --- a/packages/pyright-internal/src/languageService/completionProvider.ts +++ b/packages/pyright-internal/src/languageService/completionProvider.ts @@ -41,7 +41,7 @@ import { Symbol, SymbolTable } from '../analyzer/symbol'; import * as SymbolNameUtils from '../analyzer/symbolNameUtils'; import { getLastTypedDeclaredForSymbol, isVisibleExternally } from '../analyzer/symbolUtils'; import { getTypedDictMembersForClass } from '../analyzer/typedDicts'; -import { getModuleDocStringFromPaths } from '../analyzer/typeDocStringUtils'; +import { getModuleDocStringFromUris } from '../analyzer/typeDocStringUtils'; import { CallSignatureInfo, TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; import { printLiteralValue } from '../analyzer/typePrinter'; import { @@ -83,6 +83,7 @@ import { PythonVersion } from '../common/pythonVersion'; import * as StringUtils from '../common/stringUtils'; import { comparePositions, Position, TextRange } from '../common/textRange'; import { TextRangeCollection } from '../common/textRangeCollection'; +import { Uri } from '../common/uri/uri'; import { convertToTextEdits } from '../common/workspaceEditUtils'; import { Localizer } from '../localization/localize'; import { @@ -231,13 +232,13 @@ enum SortCategory { // This data allows the resolve handling to disambiguate // which item was selected. export interface CompletionItemData { - filePath: string; - workspacePath: string; + uri: string; // Have to be strings because this data is passed across the LSP boundary. + workspaceUri: string; position: Position; autoImportText?: string; symbolLabel?: string; funcParensDisabled?: boolean; - modulePath?: string; + moduleUri?: string; } export interface CompletionOptions { @@ -287,20 +288,20 @@ export class CompletionProvider { constructor( protected readonly program: ProgramView, - private readonly _workspacePath: string, - protected readonly filePath: string, + private readonly _workspaceRootUri: Uri, + protected readonly fileUri: Uri, protected readonly position: Position, protected readonly options: CompletionOptions, protected readonly cancellationToken: CancellationToken ) { - this.execEnv = this.configOptions.findExecEnvironment(this.filePath); + this.execEnv = this.configOptions.findExecEnvironment(this.fileUri); - this.parseResults = this.program.getParseResults(this.filePath)!; - this.sourceMapper = this.program.getSourceMapper(this.filePath, this.cancellationToken, /* mapCompiled */ true); + this.parseResults = this.program.getParseResults(this.fileUri)!; + this.sourceMapper = this.program.getSourceMapper(this.fileUri, this.cancellationToken, /* mapCompiled */ true); } getCompletions(): CompletionList | null { - if (!this.program.getSourceFileInfo(this.filePath)) { + if (!this.program.getSourceFileInfo(this.fileUri)) { return null; } @@ -347,10 +348,15 @@ export class CompletionProvider { } if ( - completionItemData.modulePath && - ImportResolver.isSupportedImportSourceFile(completionItemData.modulePath) + completionItemData.moduleUri && + ImportResolver.isSupportedImportSourceFile( + Uri.parse(completionItemData.moduleUri, this.importResolver.fileSystem.isCaseSensitive) + ) ) { - const documentation = getModuleDocStringFromPaths([completionItemData.modulePath], this.sourceMapper); + const documentation = getModuleDocStringFromUris( + [Uri.parse(completionItemData.moduleUri, this.importResolver.fileSystem.isCaseSensitive)], + this.sourceMapper + ); if (!documentation) { return; } @@ -499,7 +505,7 @@ export class CompletionProvider { const methodSignature = this._printMethodSignature(classResults.classType, decl); let text: string; - if (isStubFile(this.filePath)) { + if (isStubFile(this.fileUri)) { text = `${methodSignature}: ...`; } else { const methodBody = this.printOverriddenMethodBody( @@ -826,7 +832,7 @@ export class CompletionProvider { return; } - const currentFile = this.program.getSourceFileInfo(this.filePath); + const currentFile = this.program.getSourceFileInfo(this.fileUri); const moduleSymbolMap = buildModuleSymbolsMap( this.program.getSourceFileInfoList().filter((s) => s !== currentFile) ); @@ -923,8 +929,8 @@ export class CompletionProvider { } const completionItemData: CompletionItemData = { - workspacePath: this._workspacePath, - filePath: this.filePath, + workspaceUri: this._workspaceRootUri.toString(), + uri: this.fileUri.toString(), position: this.position, }; @@ -932,8 +938,8 @@ export class CompletionProvider { completionItemData.funcParensDisabled = true; } - if (detail?.modulePath) { - completionItemData.modulePath = detail.modulePath; + if (detail?.moduleUri) { + completionItemData.moduleUri = detail.moduleUri.toString(); } completionItem.data = toLSPAny(completionItemData); @@ -1689,7 +1695,7 @@ export class CompletionProvider { return; } - const printFlags = isStubFile(this.filePath) + const printFlags = isStubFile(this.fileUri) ? ParseTreeUtils.PrintExpressionFlags.ForwardDeclarations | ParseTreeUtils.PrintExpressionFlags.DoNotLimitStringLength : ParseTreeUtils.PrintExpressionFlags.DoNotLimitStringLength; @@ -1822,7 +1828,7 @@ export class CompletionProvider { const node = decl.node; let ellipsisForDefault: boolean | undefined; - if (isStubFile(this.filePath)) { + if (isStubFile(this.fileUri)) { // In stubs, always use "...". ellipsisForDefault = true; } else if (classType.details.moduleName === decl.moduleName) { @@ -1830,7 +1836,7 @@ export class CompletionProvider { ellipsisForDefault = false; } - const printFlags = isStubFile(this.filePath) + const printFlags = isStubFile(this.fileUri) ? ParseTreeUtils.PrintExpressionFlags.ForwardDeclarations | ParseTreeUtils.PrintExpressionFlags.DoNotLimitStringLength : ParseTreeUtils.PrintExpressionFlags.DoNotLimitStringLength; @@ -2190,7 +2196,7 @@ export class CompletionProvider { return []; } - if (declaration.path !== this.filePath) { + if (!declaration.uri.equals(this.fileUri)) { return []; } @@ -2200,8 +2206,8 @@ export class CompletionProvider { // Find the lowest tree to search the symbol. if ( - ParseTreeUtils.getFileInfoFromNode(startingNode)?.filePath === - ParseTreeUtils.getFileInfoFromNode(scopeRoot)?.filePath + ParseTreeUtils.getFileInfoFromNode(startingNode)?.fileUri === + ParseTreeUtils.getFileInfoFromNode(scopeRoot)?.fileUri ) { startingNode = scopeRoot; } @@ -2731,7 +2737,9 @@ export class CompletionProvider { const completionMap = new CompletionMap(); const resolvedPath = - importInfo.resolvedPaths.length > 0 ? importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1] : ''; + importInfo.resolvedUris.length > 0 + ? importInfo.resolvedUris[importInfo.resolvedUris.length - 1] + : Uri.empty(); const parseResults = this.program.getParseResults(resolvedPath); if (!parseResults) { @@ -2775,7 +2783,7 @@ export class CompletionProvider { importInfo.implicitImports.forEach((implImport) => { if (!importFromNode.imports.find((imp) => imp.name.value === implImport.name)) { this.addNameToCompletions(implImport.name, CompletionItemKind.Module, priorWord, completionMap, { - modulePath: implImport.path, + moduleUri: implImport.uri, }); } }); @@ -2817,8 +2825,8 @@ export class CompletionProvider { completionItem.kind = CompletionItemKind.Variable; const completionItemData: CompletionItemData = { - workspacePath: this._workspacePath, - filePath: this.filePath, + workspaceUri: this._workspaceRootUri.toString(), + uri: this.fileUri.toString(), position: this.position, }; completionItem.data = toLSPAny(completionItemData); @@ -2914,8 +2922,7 @@ export class CompletionProvider { // exported from this scope, don't include it in the // suggestion list unless we are in the same file. const hidden = - !isVisibleExternally(symbol) && - !symbol.getDeclarations().some((d) => isDefinedInFile(d, this.filePath)); + !isVisibleExternally(symbol) && !symbol.getDeclarations().some((d) => isDefinedInFile(d, this.fileUri)); if (!hidden && includeSymbolCallback(symbol, name)) { // Don't add a symbol more than once. It may have already been // added from an inner scope's symbol table. @@ -3098,7 +3105,7 @@ export class CompletionProvider { importedSymbols: new Set(), }; - const completions = this.importResolver.getCompletionSuggestions(this.filePath, this.execEnv, moduleDescriptor); + const completions = this.importResolver.getCompletionSuggestions(this.fileUri, this.execEnv, moduleDescriptor); const completionMap = new CompletionMap(); @@ -3120,7 +3127,7 @@ export class CompletionProvider { completions.forEach((modulePath, completionName) => { this.addNameToCompletions(completionName, CompletionItemKind.Module, '', completionMap, { sortText: this._makeSortText(SortCategory.ImportModuleName, completionName), - modulePath, + moduleUri: modulePath, }); }); diff --git a/packages/pyright-internal/src/languageService/completionProviderUtils.ts b/packages/pyright-internal/src/languageService/completionProviderUtils.ts index 7727e9a22..9e26317c5 100644 --- a/packages/pyright-internal/src/languageService/completionProviderUtils.ts +++ b/packages/pyright-internal/src/languageService/completionProviderUtils.ts @@ -11,21 +11,22 @@ import { InsertTextFormat, MarkupContent, MarkupKind, TextEdit } from 'vscode-la import { Declaration, DeclarationType } from '../analyzer/declaration'; import { convertDocStringToMarkdown, convertDocStringToPlainText } from '../analyzer/docStringConversion'; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; +import { isProperty } from '../analyzer/typeUtils'; import { ClassType, + Type, + TypeBase, + TypeCategory, + UnknownType, getTypeAliasInfo, isClassInstance, isFunction, isModule, isOverloadedFunction, - Type, - TypeBase, - TypeCategory, - UnknownType, } from '../analyzer/types'; -import { isProperty } from '../analyzer/typeUtils'; import { SignatureDisplayType } from '../common/configOptions'; import { TextEditAction } from '../common/editAction'; +import { Uri } from '../common/uri/uri'; import { getToolTipForType } from './tooltipUtils'; export interface Edits { @@ -55,7 +56,7 @@ export interface CompletionDetail extends CommonDetail { }; sortText?: string; itemDetail?: string; - modulePath?: string; + moduleUri?: Uri; } export function getTypeDetail( diff --git a/packages/pyright-internal/src/languageService/definitionProvider.ts b/packages/pyright-internal/src/languageService/definitionProvider.ts index 32ae95762..a63a212ad 100644 --- a/packages/pyright-internal/src/languageService/definitionProvider.ts +++ b/packages/pyright-internal/src/languageService/definitionProvider.ts @@ -24,10 +24,11 @@ import { appendArray } from '../common/collectionUtils'; import { isDefined } from '../common/core'; import { ProgramView, ServiceProvider } from '../common/extensibility'; import { convertPositionToOffset } from '../common/positionUtils'; +import { ServiceKeys } from '../common/serviceProviderExtensions'; import { DocumentRange, Position, rangesAreEqual } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ParseNode, ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; -import { ServiceKeys } from '../common/serviceProviderExtensions'; export enum DefinitionFilter { All = 'all', @@ -50,7 +51,7 @@ export function addDeclarationsToDefinitions( allowExternallyHiddenAccess: true, }); - if (!resolvedDecl || !resolvedDecl.path) { + if (!resolvedDecl || resolvedDecl.uri.isEmpty()) { return; } @@ -66,13 +67,13 @@ export function addDeclarationsToDefinitions( resolvedDecl.type === DeclarationType.Alias && resolvedDecl.symbolName && resolvedDecl.submoduleFallback && - resolvedDecl.submoduleFallback.path + !resolvedDecl.submoduleFallback.uri.isEmpty() ) { resolvedDecl = resolvedDecl.submoduleFallback; } _addIfUnique(definitions, { - path: resolvedDecl.path, + uri: resolvedDecl.uri, range: resolvedDecl.range, }); @@ -82,22 +83,22 @@ export function addDeclarationsToDefinitions( if (functionType && isOverloadedFunction(functionType)) { for (const overloadDecl of functionType.overloads.map((o) => o.details.declaration).filter(isDefined)) { _addIfUnique(definitions, { - path: overloadDecl.path, + uri: overloadDecl.uri, range: overloadDecl.range, }); } } } - if (!isStubFile(resolvedDecl.path)) { + if (!isStubFile(resolvedDecl.uri)) { return; } if (resolvedDecl.type === DeclarationType.Alias) { // Add matching source module sourceMapper - .findModules(resolvedDecl.path) - .map((m) => getFileInfo(m)?.filePath) + .findModules(resolvedDecl.uri) + .map((m) => getFileInfo(m)?.fileUri) .filter(isDefined) .forEach((f) => _addIfUnique(definitions, _createModuleEntry(f))); return; @@ -105,9 +106,9 @@ export function addDeclarationsToDefinitions( const implDecls = sourceMapper.findDeclarations(resolvedDecl); for (const implDecl of implDecls) { - if (implDecl && implDecl.path) { + if (implDecl && !implDecl.uri.isEmpty()) { _addIfUnique(definitions, { - path: implDecl.path, + uri: implDecl.uri, range: implDecl.range, }); } @@ -123,7 +124,7 @@ export function filterDefinitions(filter: DefinitionFilter, definitions: Documen // If go-to-declaration is supported, attempt to only show only pyi files in go-to-declaration // and none in go-to-definition, unless filtering would produce an empty list. const preferStubs = filter === DefinitionFilter.PreferStubs; - const wantedFile = (v: DocumentRange) => preferStubs === isStubFile(v.path); + const wantedFile = (v: DocumentRange) => preferStubs === isStubFile(v.uri); if (definitions.find(wantedFile)) { return definitions.filter(wantedFile); } @@ -181,13 +182,13 @@ class DefinitionProviderBase { export class DefinitionProvider extends DefinitionProviderBase { constructor( program: ProgramView, - filePath: string, + fileUri: Uri, position: Position, filter: DefinitionFilter, token: CancellationToken ) { - const sourceMapper = program.getSourceMapper(filePath, token); - const parseResults = program.getParseResults(filePath); + const sourceMapper = program.getSourceMapper(fileUri, token); + const parseResults = program.getParseResults(fileUri); const { node, offset } = _tryGetNode(parseResults, position); super(sourceMapper, program.evaluator!, program.serviceProvider, node, offset, filter, token); @@ -222,15 +223,15 @@ export class DefinitionProvider extends DefinitionProviderBase { } export class TypeDefinitionProvider extends DefinitionProviderBase { - private readonly _filePath: string; + private readonly _fileUri: Uri; - constructor(program: ProgramView, filePath: string, position: Position, token: CancellationToken) { - const sourceMapper = program.getSourceMapper(filePath, token, /*mapCompiled*/ false, /*preferStubs*/ true); - const parseResults = program.getParseResults(filePath); + constructor(program: ProgramView, fileUri: Uri, position: Position, token: CancellationToken) { + const sourceMapper = program.getSourceMapper(fileUri, token, /*mapCompiled*/ false, /*preferStubs*/ true); + const parseResults = program.getParseResults(fileUri); const { node, offset } = _tryGetNode(parseResults, position); super(sourceMapper, program.evaluator!, program.serviceProvider, node, offset, DefinitionFilter.All, token); - this._filePath = filePath; + this._fileUri = fileUri; } getDefinitions(): DocumentRange[] | undefined { @@ -251,7 +252,7 @@ export class TypeDefinitionProvider extends DefinitionProviderBase { if (subtype?.category === TypeCategory.Class) { appendArray( declarations, - this.sourceMapper.findClassDeclarationsByType(this._filePath, subtype) + this.sourceMapper.findClassDeclarationsByType(this._fileUri, subtype) ); } }); @@ -290,9 +291,9 @@ function _tryGetNode(parseResults: ParseResults | undefined, position: Position) return { node: ParseTreeUtils.findNodeByOffset(parseResults.parseTree, offset), offset }; } -function _createModuleEntry(filePath: string): DocumentRange { +function _createModuleEntry(uri: Uri): DocumentRange { return { - path: filePath, + uri, range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 }, @@ -302,7 +303,7 @@ function _createModuleEntry(filePath: string): DocumentRange { function _addIfUnique(definitions: DocumentRange[], itemToAdd: DocumentRange) { for (const def of definitions) { - if (def.path === itemToAdd.path && rangesAreEqual(def.range, itemToAdd.range)) { + if (def.uri.equals(itemToAdd.uri) && rangesAreEqual(def.range, itemToAdd.range)) { return; } } diff --git a/packages/pyright-internal/src/languageService/documentHighlightProvider.ts b/packages/pyright-internal/src/languageService/documentHighlightProvider.ts index 77c187ef0..6d45a74d2 100644 --- a/packages/pyright-internal/src/languageService/documentHighlightProvider.ts +++ b/packages/pyright-internal/src/languageService/documentHighlightProvider.ts @@ -15,6 +15,7 @@ import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { ProgramView, ReferenceUseCase } from '../common/extensibility'; import { convertOffsetsToRange, convertPositionToOffset } from '../common/positionUtils'; import { Position, TextRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { DocumentSymbolCollector } from './documentSymbolCollector'; @@ -24,11 +25,11 @@ export class DocumentHighlightProvider { constructor( private _program: ProgramView, - private _filePath: string, + private _fileUri: Uri, private _position: Position, private _token: CancellationToken ) { - this._parseResults = this._program.getParseResults(this._filePath); + this._parseResults = this._program.getParseResults(this._fileUri); } getDocumentHighlight(): DocumentHighlight[] | undefined { diff --git a/packages/pyright-internal/src/languageService/documentSymbolCollector.ts b/packages/pyright-internal/src/languageService/documentSymbolCollector.ts index dd3543415..ee9979c55 100644 --- a/packages/pyright-internal/src/languageService/documentSymbolCollector.ts +++ b/packages/pyright-internal/src/languageService/documentSymbolCollector.ts @@ -29,12 +29,12 @@ import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; import { TypeCategory } from '../analyzer/types'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { appendArray } from '../common/collectionUtils'; +import { isDefined } from '../common/core'; import { assert } from '../common/debug'; import { ProgramView, ReferenceUseCase, SymbolUsageProvider } from '../common/extensibility'; +import { ServiceKeys } from '../common/serviceProviderExtensions'; import { TextRange } from '../common/textRange'; import { ImportAsNode, NameNode, ParseNode, ParseNodeType, StringListNode, StringNode } from '../parser/parseNodes'; -import { ServiceKeys } from '../common/serviceProviderExtensions'; -import { isDefined } from '../common/core'; export type CollectionResult = { node: NameNode | StringNode; @@ -184,17 +184,18 @@ export class DocumentSymbolCollector extends ParseTreeWalker { const declarations = getDeclarationsForNameNode(evaluator, node, /* skipUnreachableCode */ false); const fileInfo = AnalyzerNodeInfo.getFileInfo(node); + const fileUri = fileInfo.fileUri; const resolvedDeclarations: Declaration[] = []; - const sourceMapper = program.getSourceMapper(fileInfo.filePath, token); + const sourceMapper = program.getSourceMapper(fileUri, token); declarations.forEach((decl) => { const resolvedDecl = evaluator.resolveAliasDeclaration(decl, resolveLocalName); if (resolvedDecl) { addDeclarationIfUnique(resolvedDeclarations, resolvedDecl); - if (sourceMapper && isStubFile(resolvedDecl.path)) { + if (sourceMapper && isStubFile(resolvedDecl.uri)) { const implDecls = sourceMapper.findDeclarations(resolvedDecl); for (const implDecl of implDecls) { - if (implDecl && implDecl.path) { + if (implDecl && !implDecl.uri.isEmpty()) { addDeclarationIfUnique(resolvedDeclarations, implDecl); } } @@ -202,7 +203,7 @@ export class DocumentSymbolCollector extends ParseTreeWalker { } }); - const sourceFileInfo = program.getSourceFileInfo(fileInfo.filePath); + const sourceFileInfo = program.getSourceFileInfo(fileUri); if (sourceFileInfo && sourceFileInfo.sourceFile.getIPythonMode() === IPythonMode.CellDocs) { // Add declarations from chained source files let builtinsScope = fileInfo.builtinsScope; @@ -215,7 +216,7 @@ export class DocumentSymbolCollector extends ParseTreeWalker { // Add declarations from files that implicitly import the target file. const implicitlyImportedBy = collectImportedByCells(program, sourceFileInfo); implicitlyImportedBy.forEach((implicitImport) => { - const parseTree = program.getParseResults(implicitImport.sourceFile.getFilePath())?.parseTree; + const parseTree = program.getParseResults(implicitImport.sourceFile.getUri())?.parseTree; if (parseTree) { const scope = AnalyzerNodeInfo.getScope(parseTree); const symbol = scope?.lookUpSymbol(node.value); @@ -449,7 +450,7 @@ function _getDeclarationsForNonModuleNameNode( const type = evaluator.getType(node); if (type?.category === TypeCategory.Module) { // Synthesize decl for the module. - return [createSynthesizedAliasDeclaration(type.filePath)]; + return [createSynthesizedAliasDeclaration(type.fileUri)]; } } diff --git a/packages/pyright-internal/src/languageService/documentSymbolProvider.ts b/packages/pyright-internal/src/languageService/documentSymbolProvider.ts index e72fb6e50..e081aca1f 100644 --- a/packages/pyright-internal/src/languageService/documentSymbolProvider.ts +++ b/packages/pyright-internal/src/languageService/documentSymbolProvider.ts @@ -10,23 +10,22 @@ import { CancellationToken, DocumentSymbol, Location, SymbolInformation } from 'vscode-languageserver'; +import { getFileInfo } from '../analyzer/analyzerNodeInfo'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; +import { ProgramView } from '../common/extensibility'; +import { Uri } from '../common/uri/uri'; import { ParseResults } from '../parser/parser'; import { IndexSymbolData, SymbolIndexer } from './symbolIndexer'; -import { ProgramView } from '../common/extensibility'; -import { getFileInfo } from '../analyzer/analyzerNodeInfo'; -import { convertPathToUri } from '../common/pathUtils'; export function convertToFlatSymbols( program: ProgramView, - filePath: string, + uri: Uri, symbolList: DocumentSymbol[] ): SymbolInformation[] { const flatSymbols: SymbolInformation[] = []; - const documentUri = convertPathToUri(program.fileSystem, filePath); for (const symbol of symbolList) { - _appendToFlatSymbolsRecursive(flatSymbols, documentUri, symbol); + _appendToFlatSymbolsRecursive(flatSymbols, uri, symbol); } return flatSymbols; @@ -37,11 +36,11 @@ export class DocumentSymbolProvider { constructor( protected readonly program: ProgramView, - protected readonly filePath: string, + protected readonly uri: Uri, private readonly _supportHierarchicalDocumentSymbol: boolean, private readonly _token: CancellationToken ) { - this._parseResults = this.program.getParseResults(this.filePath); + this._parseResults = this.program.getParseResults(this.uri); } getSymbols(): DocumentSymbol[] | SymbolInformation[] { @@ -54,12 +53,12 @@ export class DocumentSymbolProvider { return symbolList; } - return convertToFlatSymbols(this.program, this.filePath, symbolList); + return convertToFlatSymbols(this.program, this.uri, symbolList); } protected getHierarchicalSymbols() { const symbolList: DocumentSymbol[] = []; - const parseResults = this.program.getParseResults(this.filePath); + const parseResults = this.program.getParseResults(this.uri); if (!parseResults) { return symbolList; } @@ -115,14 +114,14 @@ export class DocumentSymbolProvider { function _appendToFlatSymbolsRecursive( flatSymbols: SymbolInformation[], - documentUri: string, + documentUri: Uri, symbol: DocumentSymbol, parent?: DocumentSymbol ) { const flatSymbol: SymbolInformation = { name: symbol.name, kind: symbol.kind, - location: Location.create(documentUri, symbol.range), + location: Location.create(documentUri.toString(), symbol.range), }; if (symbol.tags) { diff --git a/packages/pyright-internal/src/languageService/hoverProvider.ts b/packages/pyright-internal/src/languageService/hoverProvider.ts index e116bd740..cb7b1d28b 100644 --- a/packages/pyright-internal/src/languageService/hoverProvider.ts +++ b/packages/pyright-internal/src/languageService/hoverProvider.ts @@ -30,10 +30,12 @@ import { isTypeVar, } from '../analyzer/types'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; +import { SignatureDisplayType } from '../common/configOptions'; import { assertNever, fail } from '../common/debug'; import { ProgramView } from '../common/extensibility'; import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils'; import { Position, Range, TextRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ExpressionNode, NameNode, ParseNode, ParseNodeType, StringNode } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { @@ -43,7 +45,6 @@ import { getToolTipForType, getTypeForToolTip, } from './tooltipUtils'; -import { SignatureDisplayType } from '../common/configOptions'; export interface HoverTextPart { python?: boolean; @@ -151,13 +152,13 @@ export class HoverProvider { constructor( private readonly _program: ProgramView, - private readonly _filePath: string, + private readonly _fileUri: Uri, private readonly _position: Position, private readonly _format: MarkupKind, private readonly _token: CancellationToken ) { - this._parseResults = this._program.getParseResults(this._filePath); - this._sourceMapper = this._program.getSourceMapper(this._filePath, this._token, /* mapCompiled */ true); + this._parseResults = this._program.getParseResults(this._fileUri); + this._sourceMapper = this._program.getSourceMapper(this._fileUri, this._token, /* mapCompiled */ true); } getHover(): Hover | null { diff --git a/packages/pyright-internal/src/languageService/navigationUtils.ts b/packages/pyright-internal/src/languageService/navigationUtils.ts index a7a266fb8..a5a486548 100644 --- a/packages/pyright-internal/src/languageService/navigationUtils.ts +++ b/packages/pyright-internal/src/languageService/navigationUtils.ts @@ -7,10 +7,10 @@ */ import { Location } from 'vscode-languageserver-types'; import { ReadOnlyFileSystem } from '../common/fileSystem'; -import { convertPathToUri } from '../common/pathUtils'; import { DocumentRange } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; -export function canNavigateToFile(fs: ReadOnlyFileSystem, path: string): boolean { +export function canNavigateToFile(fs: ReadOnlyFileSystem, path: Uri): boolean { return !fs.isInZip(path); } @@ -23,9 +23,9 @@ export function convertDocumentRangesToLocation( } export function convertDocumentRangeToLocation(fs: ReadOnlyFileSystem, range: DocumentRange): Location | undefined { - if (!canNavigateToFile(fs, range.path)) { + if (!canNavigateToFile(fs, range.uri)) { return undefined; } - return Location.create(convertPathToUri(fs, range.path), range.range); + return Location.create(range.uri.toString(), range.range); } diff --git a/packages/pyright-internal/src/languageService/quickActions.ts b/packages/pyright-internal/src/languageService/quickActions.ts index 52a03e10a..d11b731b6 100644 --- a/packages/pyright-internal/src/languageService/quickActions.ts +++ b/packages/pyright-internal/src/languageService/quickActions.ts @@ -11,16 +11,17 @@ import { CancellationToken } from 'vscode-languageserver'; import { Commands } from '../commands/commands'; import { ProgramView } from '../common/extensibility'; +import { Uri } from '../common/uri/uri'; import { ImportSorter } from './importSorter'; export function performQuickAction( programView: ProgramView, - filePath: string, + uri: Uri, command: string, args: any[], token: CancellationToken ) { - const sourceFileInfo = programView.getSourceFileInfo(filePath); + const sourceFileInfo = programView.getSourceFileInfo(uri); // This command should be called only for open files, in which // case we should have the file contents already loaded. @@ -29,7 +30,7 @@ export function performQuickAction( } // If we have no completed analysis job, there's nothing to do. - const parseResults = programView.getParseResults(filePath); + const parseResults = programView.getParseResults(uri); if (!parseResults) { return []; } diff --git a/packages/pyright-internal/src/languageService/referencesProvider.ts b/packages/pyright-internal/src/languageService/referencesProvider.ts index c486910f0..daf2d1422 100644 --- a/packages/pyright-internal/src/languageService/referencesProvider.ts +++ b/packages/pyright-internal/src/languageService/referencesProvider.ts @@ -19,18 +19,19 @@ import { isVisibleExternally } from '../analyzer/symbolUtils'; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; import { maxTypeRecursionCount } from '../analyzer/types'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; -import { isDefined } from '../common/core'; import { appendArray } from '../common/collectionUtils'; +import { isDefined } from '../common/core'; import { assertNever } from '../common/debug'; import { ProgramView, ReferenceUseCase, SymbolUsageProvider } from '../common/extensibility'; +import { ReadOnlyFileSystem } from '../common/fileSystem'; import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils'; +import { ServiceKeys } from '../common/serviceProviderExtensions'; import { DocumentRange, Position, TextRange, doesRangeContain } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { CollectionResult, DocumentSymbolCollector } from './documentSymbolCollector'; -import { ReadOnlyFileSystem } from '../common/fileSystem'; import { convertDocumentRangesToLocation } from './navigationUtils'; -import { ServiceKeys } from '../common/serviceProviderExtensions'; export type ReferenceCallback = (locations: DocumentRange[]) => void; @@ -103,17 +104,17 @@ export class FindReferencesTreeWalker { constructor( private _program: ProgramView, - private _filePath: string, + private _fileUri: Uri, private _referencesResult: ReferencesResult, private _includeDeclaration: boolean, private _cancellationToken: CancellationToken, private readonly _createDocumentRange: ( - filePath: string, + fileUri: Uri, result: CollectionResult, parseResults: ParseResults ) => DocumentRange = FindReferencesTreeWalker.createDocumentRange ) { - this._parseResults = this._program.getParseResults(this._filePath); + this._parseResults = this._program.getParseResults(this._fileUri); } findReferences(rootNode = this._parseResults?.parseTree) { @@ -139,16 +140,16 @@ export class FindReferencesTreeWalker { for (const result of collector.collect()) { // Is it the same symbol? if (this._includeDeclaration || result.node !== this._referencesResult.nodeAtOffset) { - results.push(this._createDocumentRange(this._filePath, result, this._parseResults)); + results.push(this._createDocumentRange(this._fileUri, result, this._parseResults)); } } return results; } - static createDocumentRange(filePath: string, result: CollectionResult, parseResults: ParseResults): DocumentRange { + static createDocumentRange(fileUri: Uri, result: CollectionResult, parseResults: ParseResults): DocumentRange { return { - path: filePath, + uri: fileUri, range: { start: convertOffsetToPosition(result.range.start, parseResults.tokenizerOutput.lines), end: convertOffsetToPosition(TextRange.getEnd(result.range), parseResults.tokenizerOutput.lines), @@ -162,7 +163,7 @@ export class ReferencesProvider { private _program: ProgramView, private _token: CancellationToken, private readonly _createDocumentRange?: ( - filePath: string, + fileUri: Uri, result: CollectionResult, parseResults: ParseResults ) => DocumentRange, @@ -172,17 +173,17 @@ export class ReferencesProvider { } reportReferences( - filePath: string, + fileUri: Uri, position: Position, includeDeclaration: boolean, resultReporter?: ResultProgressReporter ) { - const sourceFileInfo = this._program.getSourceFileInfo(filePath); + const sourceFileInfo = this._program.getSourceFileInfo(fileUri); if (!sourceFileInfo) { return; } - const parseResults = this._program.getParseResults(filePath); + const parseResults = this._program.getParseResults(fileUri); if (!parseResults) { return; } @@ -202,7 +203,7 @@ export class ReferencesProvider { const invokedFromUserFile = isUserCode(sourceFileInfo); const referencesResult = ReferencesProvider.getDeclarationForPosition( this._program, - filePath, + fileUri, position, reporter, ReferenceUseCase.References, @@ -214,7 +215,7 @@ export class ReferencesProvider { // Do we need to do a global search as well? if (!referencesResult.requiresGlobalSearch) { - this.addReferencesToResult(sourceFileInfo.sourceFile.getFilePath(), includeDeclaration, referencesResult); + this.addReferencesToResult(sourceFileInfo.sourceFile.getUri(), includeDeclaration, referencesResult); } for (const curSourceFileInfo of this._program.getSourceFileInfoList()) { @@ -228,7 +229,7 @@ export class ReferencesProvider { const fileContents = curSourceFileInfo.sourceFile.getFileContent(); if (!fileContents || referencesResult.symbolNames.some((s) => fileContents.search(s) >= 0)) { this.addReferencesToResult( - curSourceFileInfo.sourceFile.getFilePath(), + curSourceFileInfo.sourceFile.getUri(), includeDeclaration, referencesResult ); @@ -246,12 +247,12 @@ export class ReferencesProvider { for (const decl of referencesResult.declarations) { throwIfCancellationRequested(this._token); - if (referencesResult.locations.some((l) => l.path === decl.path)) { + if (referencesResult.locations.some((l) => l.uri.equals(decl.uri))) { // Already included. continue; } - const declFileInfo = this._program.getSourceFileInfo(decl.path); + const declFileInfo = this._program.getSourceFileInfo(decl.uri); if (!declFileInfo) { // The file the declaration belongs to doesn't belong to the program. continue; @@ -266,10 +267,10 @@ export class ReferencesProvider { referencesResult.providers ); - this.addReferencesToResult(declFileInfo.sourceFile.getFilePath(), includeDeclaration, tempResult); + this.addReferencesToResult(declFileInfo.sourceFile.getUri(), includeDeclaration, tempResult); for (const loc of tempResult.locations) { // Include declarations only. And throw away any references - if (loc.path === decl.path && doesRangeContain(decl.range, loc.range)) { + if (loc.uri.equals(decl.uri) && doesRangeContain(decl.range, loc.range)) { referencesResult.addLocations(loc); } } @@ -279,15 +280,15 @@ export class ReferencesProvider { return locations; } - addReferencesToResult(filePath: string, includeDeclaration: boolean, referencesResult: ReferencesResult): void { - const parseResults = this._program.getParseResults(filePath); + addReferencesToResult(fileUri: Uri, includeDeclaration: boolean, referencesResult: ReferencesResult): void { + const parseResults = this._program.getParseResults(fileUri); if (!parseResults) { return; } const refTreeWalker = new FindReferencesTreeWalker( this._program, - filePath, + fileUri, referencesResult, includeDeclaration, this._token, @@ -299,7 +300,7 @@ export class ReferencesProvider { static getDeclarationForNode( program: ProgramView, - filePath: string, + fileUri: Uri, node: NameNode, reporter: ReferenceCallback | undefined, useCase: ReferenceUseCase, @@ -318,7 +319,7 @@ export class ReferencesProvider { return undefined; } - const requiresGlobalSearch = isVisibleOutside(program.evaluator!, filePath, node, declarations); + const requiresGlobalSearch = isVisibleOutside(program.evaluator!, fileUri, node, declarations); const symbolNames = new Set(declarations.map((d) => getNameFromDeclaration(d)!).filter((n) => !!n)); symbolNames.add(node.value); @@ -345,14 +346,14 @@ export class ReferencesProvider { static getDeclarationForPosition( program: ProgramView, - filePath: string, + fileUri: Uri, position: Position, reporter: ReferenceCallback | undefined, useCase: ReferenceUseCase, token: CancellationToken ): ReferencesResult | undefined { throwIfCancellationRequested(token); - const parseResults = program.getParseResults(filePath); + const parseResults = program.getParseResults(fileUri); if (!parseResults) { return undefined; } @@ -372,16 +373,11 @@ export class ReferencesProvider { return undefined; } - return this.getDeclarationForNode(program, filePath, node, reporter, useCase, token); + return this.getDeclarationForNode(program, fileUri, node, reporter, useCase, token); } } -function isVisibleOutside( - evaluator: TypeEvaluator, - currentFilePath: string, - node: NameNode, - declarations: Declaration[] -) { +function isVisibleOutside(evaluator: TypeEvaluator, currentUri: Uri, node: NameNode, declarations: Declaration[]) { const result = evaluator.lookUpSymbolRecursive(node, node.value, /* honorCodeFlow */ false); if (result && !isExternallyVisible(result.symbol)) { return false; @@ -394,7 +390,7 @@ function isVisibleOutside( // that is within the current file and cannot be imported directly from other modules. return declarations.some((decl) => { // If the declaration is outside of this file, a global search is needed. - if (decl.path !== currentFilePath) { + if (!decl.uri.equals(currentUri)) { return true; } diff --git a/packages/pyright-internal/src/languageService/renameProvider.ts b/packages/pyright-internal/src/languageService/renameProvider.ts index 74a222059..14c4a1407 100644 --- a/packages/pyright-internal/src/languageService/renameProvider.ts +++ b/packages/pyright-internal/src/languageService/renameProvider.ts @@ -9,27 +9,28 @@ import { CancellationToken, WorkspaceEdit } from 'vscode-languageserver'; +import { isUserCode } from '../analyzer/sourceFileInfoUtils'; +import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { assertNever } from '../common/debug'; import { FileEditAction } from '../common/editAction'; import { ProgramView, ReferenceUseCase } from '../common/extensibility'; import { convertTextRangeToRange } from '../common/positionUtils'; import { Position, Range } from '../common/textRange'; -import { ReferencesProvider, ReferencesResult } from '../languageService/referencesProvider'; -import { isUserCode } from '../analyzer/sourceFileInfoUtils'; -import { throwIfCancellationRequested } from '../common/cancellationUtils'; -import { ParseResults } from '../parser/parser'; +import { Uri } from '../common/uri/uri'; import { convertToWorkspaceEdit } from '../common/workspaceEditUtils'; +import { ReferencesProvider, ReferencesResult } from '../languageService/referencesProvider'; +import { ParseResults } from '../parser/parser'; export class RenameProvider { private readonly _parseResults: ParseResults | undefined; constructor( private _program: ProgramView, - private _filePath: string, + private _fileUri: Uri, private _position: Position, private _token: CancellationToken ) { - this._parseResults = this._program.getParseResults(this._filePath); + this._parseResults = this._program.getParseResults(this._fileUri); } canRenameSymbol(isDefaultWorkspace: boolean, isUntitled: boolean): Range | null { @@ -45,7 +46,7 @@ export class RenameProvider { const renameMode = RenameProvider.getRenameSymbolMode( this._program, - this._filePath, + this._fileUri, referencesResult, isDefaultWorkspace, isUntitled @@ -72,7 +73,7 @@ export class RenameProvider { const referenceProvider = new ReferencesProvider(this._program, this._token); const renameMode = RenameProvider.getRenameSymbolMode( this._program, - this._filePath, + this._fileUri, referencesResult, isDefaultWorkspace, isUntitled @@ -80,11 +81,7 @@ export class RenameProvider { switch (renameMode) { case 'singleFileMode': - referenceProvider.addReferencesToResult( - this._filePath, - /* includeDeclaration */ true, - referencesResult - ); + referenceProvider.addReferencesToResult(this._fileUri, /* includeDeclaration */ true, referencesResult); break; case 'multiFileMode': { @@ -99,7 +96,7 @@ export class RenameProvider { } referenceProvider.addReferencesToResult( - curSourceFileInfo.sourceFile.getFilePath(), + curSourceFileInfo.sourceFile.getUri(), /* includeDeclaration */ true, referencesResult ); @@ -124,23 +121,23 @@ export class RenameProvider { const edits: FileEditAction[] = []; referencesResult.locations.forEach((loc) => { edits.push({ - filePath: loc.path, + fileUri: loc.uri, range: loc.range, replacementText: newName, }); }); - return convertToWorkspaceEdit(this._program.fileSystem, { edits, fileOperations: [] }); + return convertToWorkspaceEdit({ edits, fileOperations: [] }); } static getRenameSymbolMode( program: ProgramView, - filePath: string, + fileUri: Uri, referencesResult: ReferencesResult, isDefaultWorkspace: boolean, isUntitled: boolean ) { - const sourceFileInfo = program.getSourceFileInfo(filePath)!; + const sourceFileInfo = program.getSourceFileInfo(fileUri)!; // We have 2 different cases // Single file mode. @@ -156,12 +153,12 @@ export class RenameProvider { (userFile && !referencesResult.requiresGlobalSearch) || (!userFile && sourceFileInfo.isOpenByClient && - referencesResult.declarations.every((d) => program.getSourceFileInfo(d.path) === sourceFileInfo)) + referencesResult.declarations.every((d) => program.getSourceFileInfo(d.uri) === sourceFileInfo)) ) { return 'singleFileMode'; } - if (!isUntitled && referencesResult.declarations.every((d) => isUserCode(program.getSourceFileInfo(d.path)))) { + if (!isUntitled && referencesResult.declarations.every((d) => isUserCode(program.getSourceFileInfo(d.uri)))) { return 'multiFileMode'; } @@ -173,7 +170,7 @@ export class RenameProvider { private _getReferenceResult() { const referencesResult = ReferencesProvider.getDeclarationForPosition( this._program, - this._filePath, + this._fileUri, this._position, /* reporter */ undefined, ReferenceUseCase.Rename, diff --git a/packages/pyright-internal/src/languageService/signatureHelpProvider.ts b/packages/pyright-internal/src/languageService/signatureHelpProvider.ts index 79ed9800b..d580c060f 100644 --- a/packages/pyright-internal/src/languageService/signatureHelpProvider.ts +++ b/packages/pyright-internal/src/languageService/signatureHelpProvider.ts @@ -31,6 +31,7 @@ import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { ProgramView } from '../common/extensibility'; import { convertPositionToOffset } from '../common/positionUtils'; import { Position } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { CallNode, NameNode, ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; import { getDocumentationPartsForTypeAndDecl, getFunctionDocStringFromType } from './tooltipUtils'; @@ -41,7 +42,7 @@ export class SignatureHelpProvider { constructor( private _program: ProgramView, - private _filePath: string, + private _fileUri: Uri, private _position: Position, private _format: MarkupKind, private _hasSignatureLabelOffsetCapability: boolean, @@ -49,8 +50,8 @@ export class SignatureHelpProvider { private _context: SignatureHelpContext | undefined, private _token: CancellationToken ) { - this._parseResults = this._program.getParseResults(this._filePath); - this._sourceMapper = this._program.getSourceMapper(this._filePath, this._token, /* mapCompiled */ true); + this._parseResults = this._program.getParseResults(this._fileUri); + this._sourceMapper = this._program.getSourceMapper(this._fileUri, this._token, /* mapCompiled */ true); } getSignatureHelp(): SignatureHelp | undefined { diff --git a/packages/pyright-internal/src/languageService/symbolIndexer.ts b/packages/pyright-internal/src/languageService/symbolIndexer.ts index b66027a68..24e15d361 100644 --- a/packages/pyright-internal/src/languageService/symbolIndexer.ts +++ b/packages/pyright-internal/src/languageService/symbolIndexer.ts @@ -13,15 +13,16 @@ import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { Declaration, DeclarationType } from '../analyzer/declaration'; import { getLastTypedDeclaredForSymbol, isVisibleExternally } from '../analyzer/symbolUtils'; import { throwIfCancellationRequested } from '../common/cancellationUtils'; +import { getSymbolKind } from '../common/lspUtils'; import { convertOffsetsToRange } from '../common/positionUtils'; import { Range } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { ParseResults } from '../parser/parser'; import { convertSymbolKindToCompletionItemKind } from './autoImporter'; -import { getSymbolKind } from '../common/lspUtils'; export interface IndexAliasData { readonly originalName: string; - readonly modulePath: string; + readonly moduleUri: Uri; readonly kind: SymbolKind; readonly itemKind?: CompletionItemKind | undefined; } diff --git a/packages/pyright-internal/src/languageService/tooltipUtils.ts b/packages/pyright-internal/src/languageService/tooltipUtils.ts index 031f2fc9b..133edda36 100644 --- a/packages/pyright-internal/src/languageService/tooltipUtils.ts +++ b/packages/pyright-internal/src/languageService/tooltipUtils.ts @@ -16,7 +16,7 @@ import { getClassDocString, getFunctionDocStringInherited, getModuleDocString, - getModuleDocStringFromPaths, + getModuleDocStringFromUris, getOverloadedFunctionDocStringsInherited, getPropertyDocStringInherited, getVariableDocString, @@ -323,7 +323,7 @@ export function getDocumentationPartsForTypeAndDecl( } } - typeDoc = getModuleDocStringFromPaths([resolvedDecl.path], sourceMapper); + typeDoc = getModuleDocStringFromUris([resolvedDecl.uri], sourceMapper); } typeDoc = diff --git a/packages/pyright-internal/src/languageService/workspaceSymbolProvider.ts b/packages/pyright-internal/src/languageService/workspaceSymbolProvider.ts index 7aff4cf10..982568f40 100644 --- a/packages/pyright-internal/src/languageService/workspaceSymbolProvider.ts +++ b/packages/pyright-internal/src/languageService/workspaceSymbolProvider.ts @@ -7,15 +7,15 @@ */ import { CancellationToken, Location, ResultProgressReporter, SymbolInformation } from 'vscode-languageserver'; -import { throwIfCancellationRequested } from '../common/cancellationUtils'; -import * as StringUtils from '../common/stringUtils'; -import { IndexSymbolData, SymbolIndexer } from './symbolIndexer'; -import { ProgramView } from '../common/extensibility'; -import { isUserCode } from '../analyzer/sourceFileInfoUtils'; import { getFileInfo } from '../analyzer/analyzerNodeInfo'; -import { convertPathToUri } from '../common/pathUtils'; -import { Workspace } from '../workspaceFactory'; +import { isUserCode } from '../analyzer/sourceFileInfoUtils'; +import { throwIfCancellationRequested } from '../common/cancellationUtils'; import { appendArray } from '../common/collectionUtils'; +import { ProgramView } from '../common/extensibility'; +import * as StringUtils from '../common/stringUtils'; +import { Uri } from '../common/uri/uri'; +import { Workspace } from '../workspaceFactory'; +import { IndexSymbolData, SymbolIndexer } from './symbolIndexer'; type WorkspaceSymbolCallback = (symbols: SymbolInformation[]) => void; @@ -55,10 +55,10 @@ export class WorkspaceSymbolProvider { return this._allSymbols; } - protected getSymbolsForDocument(program: ProgramView, filePath: string): SymbolInformation[] { + protected getSymbolsForDocument(program: ProgramView, fileUri: Uri): SymbolInformation[] { const symbolList: SymbolInformation[] = []; - const parseResults = program.getParseResults(filePath); + const parseResults = program.getParseResults(fileUri); if (!parseResults) { return symbolList; } @@ -69,7 +69,7 @@ export class WorkspaceSymbolProvider { } const indexSymbolData = SymbolIndexer.indexSymbols(fileInfo, parseResults, this._token); - this.appendWorkspaceSymbolsRecursive(indexSymbolData, program, filePath, '', symbolList); + this.appendWorkspaceSymbolsRecursive(indexSymbolData, program, fileUri, '', symbolList); return symbolList; } @@ -77,7 +77,7 @@ export class WorkspaceSymbolProvider { protected appendWorkspaceSymbolsRecursive( indexSymbolData: IndexSymbolData[] | undefined, program: ProgramView, - filePath: string, + fileUri: Uri, container: string, symbolList: SymbolInformation[] ) { @@ -94,7 +94,7 @@ export class WorkspaceSymbolProvider { if (StringUtils.isPatternInSymbol(this._query, symbolData.name)) { const location: Location = { - uri: convertPathToUri(program.fileSystem, filePath), + uri: fileUri.toString(), range: symbolData.selectionRange!, }; @@ -114,7 +114,7 @@ export class WorkspaceSymbolProvider { this.appendWorkspaceSymbolsRecursive( symbolData.children, program, - filePath, + fileUri, this._getContainerName(container, symbolData.name), symbolList ); @@ -134,7 +134,7 @@ export class WorkspaceSymbolProvider { continue; } - const symbolList = this.getSymbolsForDocument(program, sourceFileInfo.sourceFile.getFilePath()); + const symbolList = this.getSymbolsForDocument(program, sourceFileInfo.sourceFile.getUri()); if (symbolList.length > 0) { this._reporter(symbolList); } diff --git a/packages/pyright-internal/src/pyright.ts b/packages/pyright-internal/src/pyright.ts index b1b3fe9cc..21fc410c4 100644 --- a/packages/pyright-internal/src/pyright.ts +++ b/packages/pyright-internal/src/pyright.ts @@ -28,12 +28,14 @@ import { createDeferred } from './common/deferred'; import { Diagnostic, DiagnosticCategory } from './common/diagnostic'; import { FileDiagnostics } from './common/diagnosticSink'; import { FullAccessHost } from './common/fullAccessHost'; -import { combinePaths, getFileSpec, normalizePath, tryStat } from './common/pathUtils'; +import { combinePaths, normalizePath } from './common/pathUtils'; import { versionFromString } from './common/pythonVersion'; import { RealTempFile, createFromRealFileSystem } from './common/realFileSystem'; import { ServiceProvider } from './common/serviceProvider'; import { createServiceProvider } from './common/serviceProviderExtensions'; 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'; @@ -64,11 +66,11 @@ interface PyrightSymbolCount { interface PyrightTypeCompletenessReport { packageName: string; - packageRootDirectory?: string | undefined; + packageRootDirectory?: Uri | undefined; moduleName: string; - moduleRootDirectory?: string | undefined; + moduleRootDirectory?: Uri | undefined; ignoreUnknownTypesFromImports: boolean; - pyTypedPath?: string | undefined; + pyTypedPath?: Uri | undefined; exportedSymbolCounts: PyrightSymbolCount; otherSymbolCounts: PyrightSymbolCount; missingFunctionDocStringCount: number; @@ -95,7 +97,7 @@ interface PyrightPublicSymbolReport { } interface PyrightJsonDiagnostic { - file: string; + uri: Uri; severity: SeverityLevel; message: string; range?: Range | undefined; @@ -244,10 +246,9 @@ async function processArgs(): Promise { // Verify the specified file specs to make sure their wildcard roots exist. const tempFileSystem = new PyrightFileSystem(createFromRealFileSystem()); - const tempServiceProvider = createServiceProvider(tempFileSystem, console); for (const fileDesc of options.includeFileSpecsOverride) { - const includeSpec = getFileSpec(tempServiceProvider, '', fileDesc); + const includeSpec = getFileSpec(Uri.file(process.cwd(), tempFileSystem.isCaseSensitive), fileDesc); try { const stat = tryStat(tempFileSystem, includeSpec.wildcardRoot); if (!stat) { @@ -361,7 +362,7 @@ async function processArgs(): Promise { // up the JSON output, which goes to stdout. const output = args.outputjson ? new StderrConsole(logLevel) : new StandardConsole(logLevel); const fileSystem = new PyrightFileSystem(createFromRealFileSystem(output, new ChokidarFileWatcherProvider(output))); - const tempFile = new RealTempFile(); + const tempFile = new RealTempFile(fileSystem.isCaseSensitive); const serviceProvider = createServiceProvider(fileSystem, output, tempFile); // The package type verification uses a different path. @@ -386,7 +387,7 @@ async function processArgs(): Promise { // Refresh service after 2 seconds after the last library file change is detected. const service = new AnalyzerService('', serviceProvider, { console: output, - hostFactory: () => new FullAccessHost(fileSystem), + hostFactory: () => new FullAccessHost(serviceProvider), libraryReanalysisTimeProvider: () => 2 * 1000, }); const exitStatus = createDeferred(); @@ -540,7 +541,7 @@ function buildTypeCompletenessReport( // Add the general diagnostics. completenessReport.generalDiagnostics.forEach((diag) => { - const jsonDiag = convertDiagnosticToJson('', diag); + const jsonDiag = convertDiagnosticToJson(Uri.empty(), diag); if (isDiagnosticIncluded(jsonDiag.severity, minSeverityLevel)) { report.generalDiagnostics.push(jsonDiag); } @@ -549,11 +550,11 @@ function buildTypeCompletenessReport( report.typeCompleteness = { packageName, - packageRootDirectory: completenessReport.packageRootDirectory, + packageRootDirectory: completenessReport.packageRootDirectoryUri, moduleName: completenessReport.moduleName, - moduleRootDirectory: completenessReport.moduleRootDirectory, + moduleRootDirectory: completenessReport.moduleRootDirectoryUri, ignoreUnknownTypesFromImports: completenessReport.ignoreExternal, - pyTypedPath: completenessReport.pyTypedPath, + pyTypedPath: completenessReport.pyTypedPathUri, exportedSymbolCounts: { withKnownType: 0, withAmbiguousType: 0, @@ -587,7 +588,7 @@ function buildTypeCompletenessReport( // Convert and filter the diagnostics. symbol.diagnostics.forEach((diag) => { - const jsonDiag = convertDiagnosticToJson(diag.filePath, diag.diagnostic); + const jsonDiag = convertDiagnosticToJson(diag.uri, diag.diagnostic); if (isDiagnosticIncluded(jsonDiag.severity, minSeverityLevel)) { diagnostics.push(jsonDiag); } @@ -811,7 +812,7 @@ function reportDiagnosticsAsJson( diag.category === DiagnosticCategory.Warning || diag.category === DiagnosticCategory.Information ) { - const jsonDiag = convertDiagnosticToJson(fileDiag.filePath, diag); + const jsonDiag = convertDiagnosticToJson(fileDiag.fileUri, diag); if (isDiagnosticIncluded(jsonDiag.severity, minSeverityLevel)) { report.generalDiagnostics.push(jsonDiag); } @@ -862,9 +863,9 @@ function convertDiagnosticCategoryToSeverity(category: DiagnosticCategory): Seve } } -function convertDiagnosticToJson(filePath: string, diag: Diagnostic): PyrightJsonDiagnostic { +function convertDiagnosticToJson(uri: Uri, diag: Diagnostic): PyrightJsonDiagnostic { return { - file: filePath, + uri, severity: convertDiagnosticCategoryToSeverity(diag.category), message: diag.message, range: isEmptyRange(diag.range) ? undefined : diag.range, @@ -891,9 +892,9 @@ function reportDiagnosticsAsText( ); if (fileErrorsAndWarnings.length > 0) { - console.info(`${fileDiagnostics.filePath}`); + console.info(`${fileDiagnostics.fileUri.toUserVisibleString()}`); fileErrorsAndWarnings.forEach((diag) => { - const jsonDiag = convertDiagnosticToJson(fileDiagnostics.filePath, diag); + const jsonDiag = convertDiagnosticToJson(fileDiagnostics.fileUri, diag); logDiagnosticToConsole(jsonDiag); if (diag.category === DiagnosticCategory.Error) { @@ -923,8 +924,8 @@ function reportDiagnosticsAsText( function logDiagnosticToConsole(diag: PyrightJsonDiagnostic, prefix = ' ') { let message = prefix; - if (diag.file) { - message += `${diag.file}:`; + if (!diag.uri.isEmpty()) { + message += `${diag.uri.toUserVisibleString()}:`; } if (diag.range && !isEmptyRange(diag.range)) { message += diff --git a/packages/pyright-internal/src/pyrightFileSystem.ts b/packages/pyright-internal/src/pyrightFileSystem.ts index 78436e1f3..51a62b2b4 100644 --- a/packages/pyright-internal/src/pyrightFileSystem.ts +++ b/packages/pyright-internal/src/pyrightFileSystem.ts @@ -15,13 +15,14 @@ import { getPyTypedInfo } from './analyzer/pyTypedUtils'; import { ExecutionEnvironment } from './common/configOptions'; import { FileSystem, MkDirOptions } from './common/fileSystem'; import { stubsSuffix } from './common/pathConsts'; -import { combinePaths, ensureTrailingDirectorySeparator, isDirectory, tryStat } from './common/pathUtils'; +import { Uri } from './common/uri/uri'; +import { isDirectory, tryStat } from './common/uri/uriUtils'; import { ReadOnlyAugmentedFileSystem } from './readonlyAugmentedFileSystem'; export interface SupportPartialStubs { isPartialStubPackagesScanned(execEnv: ExecutionEnvironment): boolean; - isPathScanned(path: string): boolean; - processPartialStubPackages(paths: string[], roots: string[], bundledStubPath?: string): void; + isPathScanned(path: Uri): boolean; + processPartialStubPackages(paths: Uri[], roots: Uri[], bundledStubPath?: Uri): void; clearPartialStubs(): void; } @@ -49,49 +50,45 @@ export class PyrightFileSystem extends ReadOnlyAugmentedFileSystem implements IP super(realFS); } - override mkdirSync(path: string, options?: MkDirOptions): void { - this.realFS.mkdirSync(path, options); + override mkdirSync(uri: Uri, options?: MkDirOptions): void { + this.realFS.mkdirSync(uri, options); } - override chdir(path: string): void { - this.realFS.chdir(path); + override chdir(uri: Uri): void { + this.realFS.chdir(uri); } - override writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null): void { - this.realFS.writeFileSync(this.getOriginalPath(path), data, encoding); + override writeFileSync(uri: Uri, data: string | Buffer, encoding: BufferEncoding | null): void { + this.realFS.writeFileSync(this.getOriginalPath(uri), data, encoding); } - override rmdirSync(path: string): void { - this.realFS.rmdirSync(this.getOriginalPath(path)); + override rmdirSync(uri: Uri): void { + this.realFS.rmdirSync(this.getOriginalPath(uri)); } - override unlinkSync(path: string): void { - this.realFS.unlinkSync(this.getOriginalPath(path)); + override unlinkSync(uri: Uri): void { + this.realFS.unlinkSync(this.getOriginalPath(uri)); } - override createWriteStream(path: string): fs.WriteStream { - return this.realFS.createWriteStream(this.getOriginalPath(path)); + override createWriteStream(uri: Uri): fs.WriteStream { + return this.realFS.createWriteStream(this.getOriginalPath(uri)); } - override copyFileSync(src: string, dst: string): void { + override copyFileSync(src: Uri, dst: Uri): void { this.realFS.copyFileSync(this.getOriginalPath(src), this.getOriginalPath(dst)); } - override getUri(originalPath: string): string { - return this.realFS.getUri(originalPath); - } - isPartialStubPackagesScanned(execEnv: ExecutionEnvironment): boolean { - return this.isPathScanned(execEnv.root ?? ''); + return execEnv.root ? this.isPathScanned(execEnv.root) : false; } - isPathScanned(path: string): boolean { - return this._rootSearched.has(path); + isPathScanned(uri: Uri): boolean { + return this._rootSearched.has(uri.key); } - processPartialStubPackages(paths: string[], roots: string[], bundledStubPath?: string) { + processPartialStubPackages(paths: Uri[], roots: Uri[], bundledStubPath?: Uri) { for (const path of paths) { - this._rootSearched.add(path); + this._rootSearched.add(path.key); if (!this.realFS.existsSync(path) || !isDirectory(this.realFS, path)) { continue; @@ -105,9 +102,9 @@ export class PyrightFileSystem extends ReadOnlyAugmentedFileSystem implements IP // Leave empty set of dir entries to process. } - const isBundledStub = path === bundledStubPath; + const isBundledStub = path.equals(bundledStubPath); for (const entry of dirEntries) { - const partialStubPackagePath = combinePaths(path, entry.name); + const partialStubPackagePath = path.combinePaths(entry.name); const isDirectory = !entry.isSymbolicLink() ? entry.isDirectory() : !!tryStat(this.realFS, partialStubPackagePath)?.isDirectory(); @@ -123,13 +120,13 @@ export class PyrightFileSystem extends ReadOnlyAugmentedFileSystem implements IP } // We found partially typed stub-packages. - this._partialStubPackagePaths.add(partialStubPackagePath); + this._partialStubPackagePaths.add(partialStubPackagePath.key); // Search the root to see whether we have matching package installed. let partialStubs: string[] | undefined; const packageName = entry.name.substr(0, entry.name.length - stubsSuffix.length); for (const root of roots) { - const packagePath = combinePaths(root, packageName); + const packagePath = root.combinePaths(packageName); try { const stat = tryStat(this.realFS, packagePath); if (!stat?.isDirectory()) { @@ -149,8 +146,8 @@ export class PyrightFileSystem extends ReadOnlyAugmentedFileSystem implements IP // Merge partial stub packages to the library. partialStubs = partialStubs ?? this._getRelativePathPartialStubs(partialStubPackagePath); for (const partialStub of partialStubs) { - const originalPyiFile = combinePaths(partialStubPackagePath, partialStub); - const mappedPyiFile = combinePaths(packagePath, partialStub); + const originalPyiFile = partialStubPackagePath.combinePaths(partialStub); + const mappedPyiFile = packagePath.combinePaths(partialStub); this.recordMovedEntry(mappedPyiFile, originalPyiFile, packagePath); } } catch { @@ -168,17 +165,15 @@ export class PyrightFileSystem extends ReadOnlyAugmentedFileSystem implements IP this._partialStubPackagePaths.clear(); } - protected override isMovedEntry(path: string) { - return this._partialStubPackagePaths.has(path) || super.isMovedEntry(path); + protected override isMovedEntry(uri: Uri) { + return this._partialStubPackagePaths.has(uri.key) || super.isMovedEntry(uri); } - private _getRelativePathPartialStubs(path: string) { - const paths: string[] = []; - - const partialStubPathLength = ensureTrailingDirectorySeparator(path).length; - const searchAllStubs = (path: string) => { - for (const entry of this.realFS.readdirEntriesSync(path)) { - const filePath = combinePaths(path, entry.name); + private _getRelativePathPartialStubs(partialStubPath: Uri) { + const relativePaths: string[] = []; + const searchAllStubs = (uri: Uri) => { + for (const entry of this.realFS.readdirEntriesSync(uri)) { + const filePath = uri.combinePaths(entry.name); let isDirectory = entry.isDirectory(); let isFile = entry.isFile(); @@ -195,15 +190,15 @@ export class PyrightFileSystem extends ReadOnlyAugmentedFileSystem implements IP } if (isFile && entry.name.endsWith('.pyi')) { - const relative = filePath.substring(partialStubPathLength); + const relative = partialStubPath.getRelativePathComponents(filePath).join('/'); if (relative) { - paths.push(relative); + relativePaths.push(relative); } } } }; - searchAllStubs(path); - return paths; + searchAllStubs(partialStubPath); + return relativePaths; } } diff --git a/packages/pyright-internal/src/readonlyAugmentedFileSystem.ts b/packages/pyright-internal/src/readonlyAugmentedFileSystem.ts index 170ea4936..5a6855014 100644 --- a/packages/pyright-internal/src/readonlyAugmentedFileSystem.ts +++ b/packages/pyright-internal/src/readonlyAugmentedFileSystem.ts @@ -12,57 +12,53 @@ import type * as fs from 'fs'; import { appendArray, getOrAdd } from './common/collectionUtils'; import { FileSystem, MkDirOptions, Stats, VirtualDirent } from './common/fileSystem'; import { FileWatcher, FileWatcherEventHandler } from './common/fileWatcher'; -import { - combinePaths, - ensureTrailingDirectorySeparator, - getDirectoryPath, - getFileName, - getRelativePathComponentsFromDirectory, -} from './common/pathUtils'; +import { Uri } from './common/uri/uri'; export class ReadOnlyAugmentedFileSystem implements FileSystem { // Mapped file to original file map - private readonly _entryMap = new Map(); + private readonly _entryMap = new Map(); // Original file to mapped file map - private readonly _reverseEntryMap = new Map(); + private readonly _reverseEntryMap = new Map(); // Mapped files per a containing folder map private readonly _folderMap = new Map(); constructor(protected realFS: FileSystem) {} - existsSync(path: string): boolean { - if (this.isMovedEntry(path)) { + get isCaseSensitive(): boolean { + return this.realFS.isCaseSensitive; + } + + existsSync(uri: Uri): boolean { + if (this.isMovedEntry(uri)) { // Pretend partial stub folder and its files not exist return false; } - return this.realFS.existsSync(this.getOriginalPath(path)); + return this.realFS.existsSync(this.getOriginalPath(uri)); } - mkdirSync(path: string, options?: MkDirOptions): void { + mkdirSync(uri: Uri, options?: MkDirOptions): void { throw new Error('Operation is not allowed.'); } - chdir(path: string): void { + chdir(uri: Uri): void { throw new Error('Operation is not allowed.'); } - readdirEntriesSync(path: string): fs.Dirent[] { - const maybeDirectory = ensureTrailingDirectorySeparator(path); - + readdirEntriesSync(uri: Uri): fs.Dirent[] { const entries: fs.Dirent[] = []; - const movedEntries = this._folderMap.get(maybeDirectory); - if (!movedEntries || this.realFS.existsSync(path)) { + const movedEntries = this._folderMap.get(uri.key); + if (!movedEntries || this.realFS.existsSync(uri)) { appendArray( entries, - this.realFS.readdirEntriesSync(path).filter((item) => { + this.realFS.readdirEntriesSync(uri).filter((item) => { // Filter out the stub package directory and any // entries that will be overwritten by stub package // virtual items. return ( - !this.isMovedEntry(combinePaths(path, item.name)) && + !this.isMovedEntry(uri.combinePaths(item.name)) && !movedEntries?.some((movedEntry) => movedEntry.name === item.name) ); }) @@ -76,129 +72,124 @@ export class ReadOnlyAugmentedFileSystem implements FileSystem { return entries.concat(movedEntries.map((e) => new VirtualDirent(e.name, e.isFile))); } - readdirSync(path: string): string[] { - return this.readdirEntriesSync(path).map((p) => p.name); + readdirSync(uri: Uri): string[] { + return this.readdirEntriesSync(uri).map((p) => p.name); } - readFileSync(path: string, encoding?: null): Buffer; - readFileSync(path: string, encoding: BufferEncoding): string; - readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer { - return this.realFS.readFileSync(this.getOriginalPath(path), encoding); + readFileSync(uri: Uri, encoding?: null): Buffer; + readFileSync(uri: Uri, encoding: BufferEncoding): string; + readFileSync(uri: Uri, encoding?: BufferEncoding | null): string | Buffer { + return this.realFS.readFileSync(this.getOriginalPath(uri), encoding); } - writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null): void { + writeFileSync(uri: Uri, data: string | Buffer, encoding: BufferEncoding | null): void { throw new Error('Operation is not allowed.'); } - statSync(path: string): Stats { - return this.realFS.statSync(this.getOriginalPath(path)); + statSync(uri: Uri): Stats { + return this.realFS.statSync(this.getOriginalPath(uri)); } - rmdirSync(path: string): void { + rmdirSync(uri: Uri): void { throw new Error('Operation is not allowed.'); } - unlinkSync(path: string): void { + unlinkSync(uri: Uri): void { throw new Error('Operation is not allowed.'); } - realpathSync(path: string): string { - if (this._entryMap.has(path)) { - return path; + realpathSync(uri: Uri): Uri { + if (this._entryMap.has(uri.key)) { + return uri; } - return this.realFS.realpathSync(path); + return this.realFS.realpathSync(uri); } - getModulePath(): string { + getModulePath(): Uri { return this.realFS.getModulePath(); } - createFileSystemWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher { + createFileSystemWatcher(paths: Uri[], listener: FileWatcherEventHandler): FileWatcher { return this.realFS.createFileSystemWatcher(paths, listener); } - createReadStream(path: string): fs.ReadStream { - return this.realFS.createReadStream(this.getOriginalPath(path)); + createReadStream(uri: Uri): fs.ReadStream { + return this.realFS.createReadStream(this.getOriginalPath(uri)); } - createWriteStream(path: string): fs.WriteStream { + createWriteStream(uri: Uri): fs.WriteStream { throw new Error('Operation is not allowed.'); } - copyFileSync(src: string, dst: string): void { + copyFileSync(src: Uri, dst: Uri): void { throw new Error('Operation is not allowed.'); } // Async I/O - readFile(path: string): Promise { - return this.realFS.readFile(this.getOriginalPath(path)); + readFile(uri: Uri): Promise { + return this.realFS.readFile(this.getOriginalPath(uri)); } - readFileText(path: string, encoding?: BufferEncoding): Promise { - return this.realFS.readFileText(this.getOriginalPath(path), encoding); + readFileText(uri: Uri, encoding?: BufferEncoding): Promise { + return this.realFS.readFileText(this.getOriginalPath(uri), encoding); } - realCasePath(path: string): string { - return this.realFS.realCasePath(path); - } - - getUri(originalPath: string): string { - return this.realFS.getUri(originalPath); + realCasePath(uri: Uri): Uri { + return this.realFS.realCasePath(uri); } // See whether the file is mapped to another location. - isMappedFilePath(filepath: string): boolean { - return this._entryMap.has(filepath) || this.realFS.isMappedFilePath(filepath); + isMappedUri(fileUri: Uri): boolean { + return this._entryMap.has(fileUri.key) || this.realFS.isMappedUri(fileUri); } // Get original filepath if the given filepath is mapped. - getOriginalFilePath(mappedFilePath: string) { - return this.realFS.getOriginalFilePath(this.getOriginalPath(mappedFilePath)); + getOriginalUri(mappedFileUri: Uri) { + return this.realFS.getOriginalUri(this.getOriginalPath(mappedFileUri)); } // Get mapped filepath if the given filepath is mapped. - getMappedFilePath(originalFilepath: string) { - const mappedFilePath = this.realFS.getMappedFilePath(originalFilepath); - return this._reverseEntryMap.get(mappedFilePath) ?? mappedFilePath; + getMappedUri(originalFileUri: Uri) { + const mappedFileUri = this.realFS.getMappedUri(originalFileUri); + return this._reverseEntryMap.get(mappedFileUri.key) ?? mappedFileUri; } - isInZip(path: string): boolean { - return this.realFS.isInZip(path); + isInZip(uri: Uri): boolean { + return this.realFS.isInZip(uri); } - protected recordMovedEntry(mappedPath: string, originalPath: string, rootPath: string) { - this._entryMap.set(mappedPath, originalPath); - this._reverseEntryMap.set(originalPath, mappedPath); + protected recordMovedEntry(mappedUri: Uri, originalUri: Uri, rootPath: Uri) { + this._entryMap.set(mappedUri.key, originalUri); + this._reverseEntryMap.set(originalUri.key, mappedUri); - const directory = ensureTrailingDirectorySeparator(getDirectoryPath(mappedPath)); - const folderInfo = getOrAdd(this._folderMap, directory, () => []); + const directory = mappedUri.getDirectory(); + const folderInfo = getOrAdd(this._folderMap, directory.key, () => []); - const name = getFileName(mappedPath); + const name = mappedUri.fileName; if (!folderInfo.some((entry) => entry.name === name)) { folderInfo.push({ name, isFile: true }); } - // Add the directory entries for the sub paths as well. We should ignoreCase here because - // the paths are just combining of already known paths. - const subPathEntries = getRelativePathComponentsFromDirectory(rootPath, directory, /* ignoreCase */ false); - for (let i = 1; i < subPathEntries.length; i++) { - const subdir = combinePaths(rootPath, ...subPathEntries.slice(1, i + 1)); - const parent = ensureTrailingDirectorySeparator(getDirectoryPath(subdir)); + // Add the directory entries for the sub paths as well. + const subPathEntries = rootPath.getRelativePathComponents(directory); + for (let i = 0; i < subPathEntries.length; i++) { + const subdir = rootPath.combinePaths(...subPathEntries.slice(0, i + 1)); + const parent = subdir.getDirectory().key; const dirInfo = getOrAdd(this._folderMap, parent, () => []); - const dirName = getFileName(subdir); + const dirName = subdir.fileName; if (!dirInfo.some((entry) => entry.name === dirName)) { dirInfo.push({ name: dirName, isFile: false }); } } } - protected getOriginalPath(mappedFilePath: string) { - return this._entryMap.get(mappedFilePath) ?? mappedFilePath; + protected getOriginalPath(mappedFileUri: Uri) { + return this._entryMap.get(mappedFileUri.key) ?? mappedFileUri; } - protected isMovedEntry(path: string) { - return this._reverseEntryMap.has(path); + protected isMovedEntry(uri: Uri) { + return this._reverseEntryMap.has(uri.key); } protected clear() { diff --git a/packages/pyright-internal/src/server.ts b/packages/pyright-internal/src/server.ts index 9b34e0b9a..61f16397e 100644 --- a/packages/pyright-internal/src/server.ts +++ b/packages/pyright-internal/src/server.ts @@ -30,11 +30,12 @@ import { expandPathVariables } from './common/envVarUtils'; import { FileBasedCancellationProvider } from './common/fileBasedCancellationUtils'; import { FullAccessHost } from './common/fullAccessHost'; import { Host } from './common/host'; -import { realCasePath, resolvePaths } from './common/pathUtils'; import { ProgressReporter } from './common/progressReporter'; import { RealTempFile, WorkspaceFileWatcherProvider, createFromRealFileSystem } from './common/realFileSystem'; import { ServiceProvider } from './common/serviceProvider'; import { createServiceProvider } from './common/serviceProviderExtensions'; +import { Uri } from './common/uri/uri'; +import { getRootUri } from './common/uri/uriUtils'; import { LanguageServerBase, ServerSettings } from './languageServerBase'; import { CodeActionProvider } from './languageService/codeActionProvider'; import { PyrightFileSystem } from './pyrightFileSystem'; @@ -49,20 +50,21 @@ export class PyrightServer extends LanguageServerBase { // eslint-disable-next-line @typescript-eslint/no-var-requires const version = require('../package.json').version || ''; - // When executed from CLI command (pyright-langserver), __rootDirectory is - // already defined. When executed from VSCode extension, rootDirectory should - // be __dirname. - const rootDirectory = (global as any).__rootDirectory || __dirname; - const console = new ConsoleWithLogLevel(connection.console); const fileWatcherProvider = new WorkspaceFileWatcherProvider(); const fileSystem = createFromRealFileSystem(console, fileWatcherProvider); const pyrightFs = new PyrightFileSystem(fileSystem); - const tempFile = new RealTempFile(); + const tempFile = new RealTempFile(pyrightFs.isCaseSensitive); const cacheManager = new CacheManager(); const serviceProvider = createServiceProvider(pyrightFs, tempFile, console, cacheManager); - const realPathRoot = realCasePath(rootDirectory, pyrightFs); + + // When executed from CLI command (pyright-langserver), __rootDirectory is + // already defined. When executed from VSCode extension, rootDirectory should + // be __dirname. + const rootDirectory: Uri = + getRootUri(pyrightFs.isCaseSensitive) || Uri.file(__dirname, pyrightFs.isCaseSensitive); + const realPathRoot = pyrightFs.realCasePath(rootDirectory); super( { @@ -98,44 +100,40 @@ export class PyrightServer extends LanguageServerBase { }; try { - const pythonSection = await this.getConfiguration(workspace.uri, 'python'); + const pythonSection = await this.getConfiguration(workspace.rootUri, 'python'); if (pythonSection) { const pythonPath = pythonSection.pythonPath; if (pythonPath && isString(pythonPath) && !isPythonBinary(pythonPath)) { - serverSettings.pythonPath = resolvePaths( - workspace.rootPath, - expandPathVariables(workspace.rootPath, pythonPath) + serverSettings.pythonPath = workspace.rootUri.combinePaths( + expandPathVariables(workspace.rootUri, pythonPath) ); } const venvPath = pythonSection.venvPath; if (venvPath && isString(venvPath)) { - serverSettings.venvPath = resolvePaths( - workspace.rootPath, - expandPathVariables(workspace.rootPath, venvPath) + serverSettings.venvPath = workspace.rootUri.combinePaths( + expandPathVariables(workspace.rootUri, venvPath) ); } } - const pythonAnalysisSection = await this.getConfiguration(workspace.uri, 'python.analysis'); + const pythonAnalysisSection = await this.getConfiguration(workspace.rootUri, 'python.analysis'); if (pythonAnalysisSection) { const typeshedPaths = pythonAnalysisSection.typeshedPaths; if (typeshedPaths && Array.isArray(typeshedPaths) && typeshedPaths.length > 0) { const typeshedPath = typeshedPaths[0]; if (typeshedPath && isString(typeshedPath)) { - serverSettings.typeshedPath = resolvePaths( - workspace.rootPath, - expandPathVariables(workspace.rootPath, typeshedPath) + serverSettings.typeshedPath = workspace.rootUri.combinePaths( + expandPathVariables(workspace.rootUri, typeshedPath) ); } } const stubPath = pythonAnalysisSection.stubPath; if (stubPath && isString(stubPath)) { - serverSettings.stubPath = resolvePaths( - workspace.rootPath, - expandPathVariables(workspace.rootPath, stubPath) + serverSettings.stubPath = workspace.rootUri.combinePaths( + expandPathVariables(workspace.rootUri, stubPath) ); } @@ -167,7 +165,7 @@ export class PyrightServer extends LanguageServerBase { if (extraPaths && Array.isArray(extraPaths) && extraPaths.length > 0) { serverSettings.extraPaths = extraPaths .filter((p) => p && isString(p)) - .map((p) => resolvePaths(workspace.rootPath, expandPathVariables(workspace.rootPath, p))); + .map((p) => workspace.rootUri.combinePaths(expandPathVariables(workspace.rootUri, p))); } serverSettings.includeFileSpecs = this._getStringValues(pythonAnalysisSection.include); @@ -196,7 +194,7 @@ export class PyrightServer extends LanguageServerBase { serverSettings.autoSearchPaths = true; } - const pyrightSection = await this.getConfiguration(workspace.uri, 'pyright'); + const pyrightSection = await this.getConfiguration(workspace.rootUri, 'pyright'); if (pyrightSection) { if (pyrightSection.openFilesOnly !== undefined) { serverSettings.openFilesOnly = !!pyrightSection.openFilesOnly; @@ -227,11 +225,11 @@ export class PyrightServer extends LanguageServerBase { return undefined; } - return new BackgroundAnalysis(this.console); + return new BackgroundAnalysis(this.serverOptions.serviceProvider); } protected override createHost() { - return new FullAccessHost(this.fs); + return new FullAccessHost(this.serverOptions.serviceProvider); } protected override createImportResolver( @@ -262,15 +260,9 @@ export class PyrightServer extends LanguageServerBase { ): Promise<(Command | CodeAction)[] | undefined | null> { this.recordUserInteractionTime(); - const filePath = this.uriParser.decodeTextDocumentUri(params.textDocument.uri); - const workspace = await this.getWorkspaceForFile(filePath); - return CodeActionProvider.getCodeActionsForPosition( - workspace, - filePath, - params.range, - params.context.only, - token - ); + const uri = Uri.parse(params.textDocument.uri, this.serverOptions.serviceProvider.fs().isCaseSensitive); + const workspace = await this.getWorkspaceForFile(uri); + return CodeActionProvider.getCodeActionsForPosition(workspace, uri, params.range, params.context.only, token); } protected createProgressReporter(): ProgressReporter { diff --git a/packages/pyright-internal/src/tests/chainedSourceFiles.test.ts b/packages/pyright-internal/src/tests/chainedSourceFiles.test.ts index 878fff26a..bd5cc1859 100644 --- a/packages/pyright-internal/src/tests/chainedSourceFiles.test.ts +++ b/packages/pyright-internal/src/tests/chainedSourceFiles.test.ts @@ -17,13 +17,14 @@ import { ConfigOptions } from '../common/configOptions'; import { NullConsole } from '../common/console'; import { normalizeSlashes } from '../common/pathUtils'; import { convertOffsetsToRange, convertOffsetToPosition } from '../common/positionUtils'; +import { ServiceProvider } from '../common/serviceProvider'; +import { Uri } from '../common/uri/uri'; +import { CompletionProvider } from '../languageService/completionProvider'; import { parseTestData } from './harness/fourslash/fourSlashParser'; import { TestAccessHost } from './harness/testAccessHost'; import * as host from './harness/testHost'; import { createFromFileSystem, distlibFolder, libFolder } from './harness/vfs/factory'; import * as vfs from './harness/vfs/filesystem'; -import { CompletionProvider } from '../languageService/completionProvider'; -import { ServiceProvider } from '../common/serviceProvider'; test('check chained files', () => { const code = ` @@ -40,16 +41,17 @@ test('check chained files', () => { //// [|foo/*marker*/|] `; - const basePath = normalizeSlashes('/'); + const basePath = Uri.file(normalizeSlashes('/')); const { data, service } = createServiceWithChainedSourceFiles(basePath, code); const marker = data.markerPositions.get('marker')!; + const markerUri = Uri.file(marker.fileName); - const parseResult = service.getParseResult(marker.fileName)!; + const parseResult = service.getParseResult(markerUri)!; const result = new CompletionProvider( service.test_program, basePath, - marker.fileName, + markerUri, convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines), { format: MarkupKind.Markdown, @@ -80,15 +82,16 @@ test('modify chained files', () => { //// [|foo/*marker*/|] `; - const basePath = normalizeSlashes('/'); + const basePath = Uri.file(normalizeSlashes('/')); const { data, service } = createServiceWithChainedSourceFiles(basePath, code); // Make sure files are all realized. const marker = data.markerPositions.get('marker')!; - const parseResult = service.getParseResult(marker.fileName)!; + const markerUri = Uri.file(marker.fileName); + const parseResult = service.getParseResult(markerUri)!; // Close file in the middle of the chain - service.setFileClosed(data.markerPositions.get('delete')!.fileName); + service.setFileClosed(Uri.file(data.markerPositions.get('delete')!.fileName)); // Make sure we don't get suggestion from auto import but from chained files. service.test_program.configOptions.autoImportCompletions = false; @@ -96,7 +99,7 @@ test('modify chained files', () => { const result = new CompletionProvider( service.test_program, basePath, - marker.fileName, + markerUri, convertOffsetToPosition(marker.position, parseResult.tokenizerOutput.lines), { format: MarkupKind.Markdown, @@ -129,18 +132,19 @@ test('modify chained files', async () => { //// [|/*marker*/foo1()|] `; - const basePath = normalizeSlashes('/'); + const basePath = Uri.file(normalizeSlashes('/')); const { data, service } = createServiceWithChainedSourceFiles(basePath, code); const marker = data.markerPositions.get('marker')!; + const markerUri = Uri.file(marker.fileName); const range = data.ranges.find((r) => r.marker === marker)!; - const parseResults = service.getParseResult(marker.fileName)!; + const parseResults = service.getParseResult(markerUri)!; analyze(service.test_program); // Initially, there should be no error. const initialDiags = await service.getDiagnosticsForRange( - marker.fileName, + markerUri, convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), CancellationToken.None ); @@ -148,11 +152,11 @@ test('modify chained files', async () => { assert.strictEqual(initialDiags.length, 0); // Change test1 content - service.updateOpenFileContents(data.markerPositions.get('changed')!.fileName, 2, 'def foo5(): pass'); + service.updateOpenFileContents(Uri.file(data.markerPositions.get('changed')!.fileName), 2, 'def foo5(): pass'); analyze(service.test_program); const finalDiags = await service.getDiagnosticsForRange( - marker.fileName, + markerUri, convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), CancellationToken.None ); @@ -178,17 +182,18 @@ test('chained files with 1000s of files', async () => { //// [|/*marker*/foo1()|] `; const code = generateChainedFiles(1000, lastFile); - const basePath = normalizeSlashes('/'); + const basePath = Uri.file(normalizeSlashes('/')); const { data, service } = createServiceWithChainedSourceFiles(basePath, code); const marker = data.markerPositions.get('marker')!; + const markerUri = Uri.file(marker.fileName); const range = data.ranges.find((r) => r.marker === marker)!; - const parseResults = service.getParseResult(marker.fileName)!; + const parseResults = service.getParseResult(markerUri)!; analyze(service.test_program); // There should be no error as it should find the foo1 in the first chained file. const initialDiags = await service.getDiagnosticsForRange( - marker.fileName, + markerUri, convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), CancellationToken.None ); @@ -205,16 +210,17 @@ test('imported by files', async () => { //// os.path.join() `; - const basePath = normalizeSlashes('/'); + const basePath = Uri.file(normalizeSlashes('/')); const { data, service } = createServiceWithChainedSourceFiles(basePath, code); analyze(service.test_program); const marker = data.markerPositions.get('marker')!; + const markerUri = Uri.file(marker.fileName); const range = data.ranges.find((r) => r.marker === marker)!; - const parseResults = service.getParseResult(marker.fileName)!; + const parseResults = service.getParseResult(markerUri)!; const diagnostics = await service.getDiagnosticsForRange( - marker.fileName, + markerUri, convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), CancellationToken.None ); @@ -231,22 +237,24 @@ test('re ordering cells', async () => { //// /*bottom*/os.path.join() `; - const basePath = normalizeSlashes('/'); + const basePath = Uri.file(normalizeSlashes('/')); const { data, service } = createServiceWithChainedSourceFiles(basePath, code); analyze(service.test_program); const marker = data.markerPositions.get('marker')!; + const markerUri = Uri.file(marker.fileName); const range = data.ranges.find((r) => r.marker === marker)!; const bottom = data.markerPositions.get('bottom')!; + const bottomUri = Uri.file(bottom.fileName); - service.updateChainedFilePath(bottom.fileName, undefined); - service.updateChainedFilePath(marker.fileName, bottom.fileName); + service.updateChainedUri(bottomUri, undefined); + service.updateChainedUri(markerUri, bottomUri); analyze(service.test_program); - const parseResults = service.getParseResult(marker.fileName)!; + const parseResults = service.getParseResult(markerUri)!; const diagnostics = await service.getDiagnosticsForRange( - marker.fileName, + markerUri, convertOffsetsToRange(range.pos, range.end, parseResults.tokenizerOutput.lines), CancellationToken.None ); @@ -254,22 +262,23 @@ test('re ordering cells', async () => { assert.strictEqual(diagnostics.length, 1); }); -function createServiceWithChainedSourceFiles(basePath: string, code: string) { - const fs = createFromFileSystem(host.HOST, /*ignoreCase*/ false, { cwd: basePath }); +function createServiceWithChainedSourceFiles(basePath: Uri, code: string) { + const fs = createFromFileSystem(host.HOST, /*ignoreCase*/ false, { cwd: basePath.getFilePath() }); const service = new AnalyzerService('test service', new ServiceProvider(), { console: new NullConsole(), - hostFactory: () => new TestAccessHost(vfs.MODULE_PATH, [libFolder, distlibFolder]), + hostFactory: () => new TestAccessHost(Uri.file(vfs.MODULE_PATH), [libFolder, distlibFolder]), importResolverFactory: AnalyzerService.createImportResolver, configOptions: new ConfigOptions(basePath), fileSystem: fs, }); - const data = parseTestData(basePath, code, ''); + const data = parseTestData(basePath.getFilePath(), code, ''); - let chainedFilePath: string | undefined; + let chainedFilePath: Uri | undefined; for (const file of data.files) { - service.setFileOpened(file.fileName, 1, file.content, IPythonMode.CellDocs, chainedFilePath); - chainedFilePath = file.fileName; + const uri = Uri.file(file.fileName); + service.setFileOpened(uri, 1, file.content, IPythonMode.CellDocs, chainedFilePath); + chainedFilePath = uri; } return { data, service }; } diff --git a/packages/pyright-internal/src/tests/checker.test.ts b/packages/pyright-internal/src/tests/checker.test.ts index 9da325f29..4ad58f1a9 100644 --- a/packages/pyright-internal/src/tests/checker.test.ts +++ b/packages/pyright-internal/src/tests/checker.test.ts @@ -11,6 +11,7 @@ import { ConfigOptions } from '../common/configOptions'; import { PythonVersion } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; test('BadToken1', () => { @@ -34,7 +35,7 @@ test('CircularBaseClass', () => { }); test('Private1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, optional diagnostics are ignored. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['private1.py'], configOptions); @@ -47,7 +48,7 @@ test('Private1', () => { }); test('Constant1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, optional diagnostics are ignored. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['constant1.py'], configOptions); @@ -168,7 +169,7 @@ test('With3', () => { }); test('With4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_8; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['with4.py'], configOptions); @@ -216,7 +217,7 @@ test('Mro4', () => { }); test('DefaultInitializer1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, the reportCallInDefaultInitializer is disabled. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['defaultInitializer1.py'], configOptions); @@ -229,7 +230,7 @@ test('DefaultInitializer1', () => { }); test('UnnecessaryIsInstance1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsInstance1.py'], configOptions); TestUtils.validateResults(analysisResults, 1); @@ -241,7 +242,7 @@ test('UnnecessaryIsInstance1', () => { }); test('UnnecessaryIsSubclass1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryIsSubclass1.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -253,7 +254,7 @@ test('UnnecessaryIsSubclass1', () => { }); test('UnnecessaryCast1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryCast1.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -265,7 +266,7 @@ test('UnnecessaryCast1', () => { }); test('UnnecessaryContains1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unnecessaryContains1.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -277,7 +278,7 @@ test('UnnecessaryContains1', () => { }); test('TypeIgnore1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore1.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -289,7 +290,7 @@ test('TypeIgnore1', () => { }); test('TypeIgnore2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore2.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -301,7 +302,7 @@ test('TypeIgnore2', () => { }); test('TypeIgnore3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore3.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -313,7 +314,7 @@ test('TypeIgnore3', () => { }); test('TypeIgnore4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore4.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -324,7 +325,7 @@ test('TypeIgnore4', () => { }); test('TypeIgnore5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeIgnore5.py'], configOptions); TestUtils.validateResults(analysisResults, 0); @@ -335,14 +336,14 @@ test('TypeIgnore5', () => { }); test('PyrightIgnore1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults = TestUtils.typeAnalyzeSampleFiles(['pyrightIgnore1.py'], configOptions); TestUtils.validateResults(analysisResults, 1); }); test('PyrightIgnore2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['pyrightIgnore2.py'], configOptions); TestUtils.validateResults(analysisResults, 2); @@ -353,14 +354,14 @@ test('PyrightIgnore2', () => { }); test('PyrightComment1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults = TestUtils.typeAnalyzeSampleFiles(['pyrightComment1.py'], configOptions); TestUtils.validateResults(analysisResults, 9); }); test('DuplicateImports1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, optional diagnostics are ignored. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['duplicateImports1.py'], configOptions); @@ -373,7 +374,7 @@ test('DuplicateImports1', () => { }); test('ParamNames1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); let analysisResults = TestUtils.typeAnalyzeSampleFiles(['paramNames1.py'], configOptions); TestUtils.validateResults(analysisResults, 0, 7); @@ -423,7 +424,7 @@ test('DuplicateDeclaration2', () => { }); test('Strings1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['strings1.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -433,7 +434,7 @@ test('Strings1', () => { }); test('UnusedExpression1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, this is a warning. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedExpression1.py'], configOptions); @@ -451,7 +452,7 @@ test('UnusedExpression1', () => { }); test('UnusedImport1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Enabled it configOptions.diagnosticRuleSet.reportUnusedImport = 'warning'; @@ -470,7 +471,7 @@ test('UnusedImport1', () => { }); test('UninitializedVariable1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, this is off. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable1.py'], configOptions); @@ -483,7 +484,7 @@ test('UninitializedVariable1', () => { }); test('UninitializedVariable2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, this is off. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['uninitializedVariable2.py'], configOptions); @@ -502,7 +503,7 @@ test('RegionComments1', () => { }); test('Deprecated1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_8; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated1.py'], configOptions); @@ -548,7 +549,7 @@ test('Deprecated1', () => { }); test('Deprecated2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated2.py'], configOptions); TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 12); @@ -559,7 +560,7 @@ test('Deprecated2', () => { }); test('Deprecated3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated3.py'], configOptions); TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 5); @@ -570,7 +571,7 @@ test('Deprecated3', () => { }); test('Deprecated4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated4.py'], configOptions); TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 6); @@ -581,7 +582,7 @@ test('Deprecated4', () => { }); test('Deprecated5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['deprecated5.py'], configOptions); TestUtils.validateResults(analysisResults1, 0, 0, 0, undefined, undefined, 2); diff --git a/packages/pyright-internal/src/tests/completions.test.ts b/packages/pyright-internal/src/tests/completions.test.ts index 47cb19700..0f1d8b140 100644 --- a/packages/pyright-internal/src/tests/completions.test.ts +++ b/packages/pyright-internal/src/tests/completions.test.ts @@ -8,6 +8,7 @@ import assert from 'assert'; import { CancellationToken } from 'vscode-languageserver'; import { CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; +import { Uri } from '../common/uri/uri'; import { CompletionOptions, CompletionProvider } from '../languageService/completionProvider'; import { parseAndGetTestState } from './harness/fourslash/testState'; @@ -798,6 +799,7 @@ test('completion quote trigger', async () => { const state = parseAndGetTestState(code).state; const marker = state.getMarkerByName('marker'); const filePath = marker.fileName; + const uri = Uri.file(filePath); const position = state.convertOffsetToPosition(filePath, marker.position); const options: CompletionOptions = { @@ -809,8 +811,8 @@ test('completion quote trigger', async () => { const result = new CompletionProvider( state.program, - state.workspace.rootPath, - filePath, + state.workspace.rootUri, + uri, position, options, CancellationToken.None @@ -836,6 +838,7 @@ test('completion quote trigger - middle', async () => { const state = parseAndGetTestState(code).state; const marker = state.getMarkerByName('marker'); const filePath = marker.fileName; + const uri = Uri.file(filePath); const position = state.convertOffsetToPosition(filePath, marker.position); const options: CompletionOptions = { @@ -847,8 +850,8 @@ test('completion quote trigger - middle', async () => { const result = new CompletionProvider( state.program, - state.workspace.rootPath, - filePath, + state.workspace.rootUri, + uri, position, options, CancellationToken.None @@ -882,6 +885,7 @@ test('auto import sort text', async () => { while (state.workspace.service.test_program.analyze()); const filePath = marker.fileName; + const uri = Uri.file(filePath); const position = state.convertOffsetToPosition(filePath, marker.position); const options: CompletionOptions = { @@ -892,8 +896,8 @@ test('auto import sort text', async () => { const result = new CompletionProvider( state.program, - state.workspace.rootPath, - filePath, + state.workspace.rootUri, + uri, position, options, CancellationToken.None diff --git a/packages/pyright-internal/src/tests/config.test.ts b/packages/pyright-internal/src/tests/config.test.ts index 87bff940f..2fcd4a6ab 100644 --- a/packages/pyright-internal/src/tests/config.test.ts +++ b/packages/pyright-internal/src/tests/config.test.ts @@ -15,10 +15,11 @@ import { CommandLineOptions } from '../common/commandLineOptions'; import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { ConsoleInterface, NullConsole } from '../common/console'; import { NoAccessHost } from '../common/host'; -import { combinePaths, getBaseFileName, normalizePath, normalizeSlashes, realCasePath } from '../common/pathUtils'; +import { combinePaths, normalizePath, normalizeSlashes } from '../common/pathUtils'; import { PythonVersion } from '../common/pythonVersion'; import { createFromRealFileSystem } from '../common/realFileSystem'; import { createServiceProvider } from '../common/serviceProviderExtensions'; +import { Uri } from '../common/uri/uri'; import { TestAccessHost } from './harness/testAccessHost'; import { TestFileSystem } from './harness/vfs/filesystem'; @@ -41,8 +42,8 @@ test('FindFilesWithConfigFile', () => { // The config file specifies a single file spec (a directory). assert.strictEqual(configOptions.include.length, 1, `failed creating options from ${cwd}`); assert.strictEqual( - normalizeSlashes(configOptions.projectRoot), - realCasePath(combinePaths(cwd, commandLineOptions.configFilePath), service.fs) + configOptions.projectRoot.key, + service.fs.realCasePath(Uri.file(combinePaths(cwd, commandLineOptions.configFilePath))).key ); const fileList = service.test_getFileNamesFromFileSpecs(); @@ -67,7 +68,7 @@ test('FindFilesVirtualEnvAutoDetectExclude', () => { // There are 3 python files in the workspace, outside of myvenv // There is 1 python file in myvenv, which should be excluded - const fileNames = fileList.map((p) => getBaseFileName(p)).sort(); + const fileNames = fileList.map((p) => p.fileName).sort(); assert.deepStrictEqual(fileNames, ['sample1.py', 'sample2.py', 'sample3.py']); }); @@ -85,7 +86,7 @@ test('FindFilesVirtualEnvAutoDetectInclude', () => { // There are 3 python files in the workspace, outside of myvenv // There is 1 more python file in excluded folder // There is 1 python file in myvenv, which should be included - const fileNames = fileList.map((p) => getBaseFileName(p)).sort(); + const fileNames = fileList.map((p) => p.fileName).sort(); assert.deepStrictEqual(fileNames, ['library1.py', 'sample1.py', 'sample2.py', 'sample3.py']); }); @@ -132,8 +133,8 @@ test('SomeFileSpecsAreInvalid', () => { assert.strictEqual(configOptions.include.length, 4, `failed creating options from ${cwd}`); assert.strictEqual(configOptions.exclude.length, 1); assert.strictEqual( - normalizeSlashes(configOptions.projectRoot), - realCasePath(combinePaths(cwd, commandLineOptions.configFilePath), service.fs) + configOptions.projectRoot.getFilePath(), + service.fs.realCasePath(Uri.file(combinePaths(cwd, commandLineOptions.configFilePath))).getFilePath() ); const fileList = service.test_getFileNamesFromFileSpecs(); @@ -157,13 +158,13 @@ test('ConfigBadJson', () => { }); test('FindExecEnv1', () => { - const cwd = normalizePath(process.cwd()); + const cwd = Uri.file(normalizePath(process.cwd())); const configOptions = new ConfigOptions(cwd); // Build a config option with three execution environments. const execEnv1 = new ExecutionEnvironment( 'python', - 'src/foo', + cwd.combinePaths('src/foo'), /* defaultPythonVersion */ undefined, /* defaultPythonPlatform */ undefined, /* defaultExtraPaths */ undefined @@ -171,28 +172,29 @@ test('FindExecEnv1', () => { configOptions.executionEnvironments.push(execEnv1); const execEnv2 = new ExecutionEnvironment( 'python', - 'src', + cwd.combinePaths('src'), /* defaultPythonVersion */ undefined, /* defaultPythonPlatform */ undefined, /* defaultExtraPaths */ undefined ); configOptions.executionEnvironments.push(execEnv2); - const file1 = normalizeSlashes(combinePaths(cwd, 'src/foo/bar.py')); + const file1 = cwd.combinePaths('src/foo/bar.py'); assert.strictEqual(configOptions.findExecEnvironment(file1), execEnv1); - const file2 = normalizeSlashes(combinePaths(cwd, 'src/foo2/bar.py')); + const file2 = cwd.combinePaths('src/foo2/bar.py'); assert.strictEqual(configOptions.findExecEnvironment(file2), execEnv2); // If none of the execution environments matched, we should get // a default environment with the root equal to that of the config. - const file4 = '/nothing/bar.py'; + const file4 = Uri.file('/nothing/bar.py'); const defaultExecEnv = configOptions.findExecEnvironment(file4); assert(defaultExecEnv.root); - assert.strictEqual(normalizeSlashes(defaultExecEnv.root), normalizeSlashes(configOptions.projectRoot)); + const rootFilePath = Uri.isUri(defaultExecEnv.root) ? defaultExecEnv.root.getFilePath() : defaultExecEnv.root; + assert.strictEqual(normalizeSlashes(rootFilePath), normalizeSlashes(configOptions.projectRoot.getFilePath())); }); test('PythonPlatform', () => { - const cwd = normalizePath(process.cwd()); + const cwd = Uri.file(normalizePath(process.cwd())); const configOptions = new ConfigOptions(cwd); @@ -216,16 +218,16 @@ test('PythonPlatform', () => { }); test('AutoSearchPathsOn', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src')); + const cwd = Uri.file(normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src'))); const nullConsole = new NullConsole(); const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromVsCodeExtension */ false); + const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromVsCodeExtension */ false); commandLineOptions.autoSearchPaths = true; service.setOptions(commandLineOptions); const configOptions = service.test_getConfigOptions(commandLineOptions); - const expectedExtraPaths = [realCasePath(combinePaths(cwd, 'src'), service.fs)]; + const expectedExtraPaths = [service.fs.realCasePath(cwd.combinePaths('src'))]; assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); }); @@ -274,19 +276,22 @@ test('AutoSearchPathsOnWithConfigExecEnv', () => { }); test('AutoSearchPathsOnAndExtraPaths', () => { - const cwd = normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_no_extra_paths')); const nullConsole = new NullConsole(); const service = createAnalyzer(nullConsole); - const commandLineOptions = new CommandLineOptions(cwd, /* fromVsCodeExtension */ false); + const cwd = Uri.file( + normalizePath(combinePaths(process.cwd(), 'src/tests/samples/project_src_with_config_no_extra_paths')), + service.fs.isCaseSensitive + ); + const commandLineOptions = new CommandLineOptions(cwd.getFilePath(), /* fromVsCodeExtension */ false); commandLineOptions.autoSearchPaths = true; commandLineOptions.extraPaths = ['src/_vendored']; service.setOptions(commandLineOptions); const configOptions = service.test_getConfigOptions(commandLineOptions); - const expectedExtraPaths: string[] = [ - realCasePath(combinePaths(cwd, 'src'), service.fs), - realCasePath(combinePaths(cwd, 'src', '_vendored'), service.fs), + const expectedExtraPaths: Uri[] = [ + service.fs.realCasePath(cwd.combinePaths('src')), + service.fs.realCasePath(cwd.combinePaths('src', '_vendored')), ]; assert.deepStrictEqual(configOptions.defaultExtraPaths, expectedExtraPaths); @@ -314,11 +319,11 @@ test('FindFilesInMemoryOnly', () => { service.setOptions(commandLineOptions); // Open a file that is not backed by the file system. - const untitled = 'untitled:Untitled-1.py'; + const untitled = Uri.parse('untitled:Untitled-1.py', true); service.setFileOpened(untitled, 1, '# empty'); const fileList = service.test_getFileNamesFromFileSpecs(); - assert(fileList.filter((f) => f === untitled)); + assert(fileList.filter((f) => f.equals(untitled))); }); test('verify config fileSpecs after cloning', () => { @@ -327,7 +332,7 @@ test('verify config fileSpecs after cloning', () => { ignore: ['**/node_modules/**'], }; - const config = new ConfigOptions(process.cwd()); + const config = new ConfigOptions(Uri.file(process.cwd())); const sp = createServiceProvider(fs, new NullConsole()); config.initializeFromJson(configFile, undefined, sp, new TestAccessHost()); const cloned = createConfigOptionsFrom(config); diff --git a/packages/pyright-internal/src/tests/documentSymbolCollector.test.ts b/packages/pyright-internal/src/tests/documentSymbolCollector.test.ts index 4bf83a5ad..beafc270a 100644 --- a/packages/pyright-internal/src/tests/documentSymbolCollector.test.ts +++ b/packages/pyright-internal/src/tests/documentSymbolCollector.test.ts @@ -6,6 +6,7 @@ * Tests documentSymbolCollector */ +import { Uri } from '../common/uri/uri'; import { parseAndGetTestState } from './harness/fourslash/testState'; import { verifyReferencesAtPosition } from './testStateUtils'; @@ -225,7 +226,7 @@ test('use localName import alias', () => { const references = state .getRangesByText() .get('tools')! - .map((r) => ({ path: r.fileName, range: state.convertPositionRange(r) })); + .map((r) => ({ uri: Uri.file(r.fileName), range: state.convertPositionRange(r) })); state.verifyFindAllReferences({ marker1: { references }, @@ -288,7 +289,7 @@ test('use localName import module', () => { const references = state .getRangesByText() .get('tools')! - .map((r) => ({ path: r.fileName, range: state.convertPositionRange(r) })); + .map((r) => ({ uri: Uri.file(r.fileName), range: state.convertPositionRange(r) })); state.verifyFindAllReferences({ marker1: { references }, diff --git a/packages/pyright-internal/src/tests/filesystem.test.ts b/packages/pyright-internal/src/tests/filesystem.test.ts index 8961631cc..d08470c66 100644 --- a/packages/pyright-internal/src/tests/filesystem.test.ts +++ b/packages/pyright-internal/src/tests/filesystem.test.ts @@ -9,6 +9,7 @@ import assert from 'assert'; import { combinePaths, normalizeSlashes } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; import * as host from './harness/testHost'; import * as factory from './harness/vfs/factory'; import * as vfs from './harness/vfs/filesystem'; @@ -20,54 +21,55 @@ test('CreateVFS', () => { }); test('Folders', () => { - const cwd = normalizeSlashes('/'); - const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); + const cwd = Uri.file(normalizeSlashes('/')); + const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd: cwd.getFilePath() }); // no such dir exist assert.throws(() => { - fs.chdir('a'); + fs.chdir(cwd.combinePaths('a')); }); - fs.mkdirSync('a'); - fs.chdir('a'); + fs.mkdirSync(cwd.combinePaths('a')); + fs.chdir(cwd.combinePaths('a')); assert.equal(fs.cwd(), normalizeSlashes('/a')); - fs.chdir('..'); - fs.rmdirSync('a'); + fs.chdir(cwd.combinePaths('..')); + fs.rmdirSync(cwd.combinePaths('a')); // no such dir exist assert.throws(() => { - fs.chdir('a'); + fs.chdir(cwd.combinePaths('a')); }); }); test('Folders Recursive', () => { - const cwd = normalizeSlashes('/'); - const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); + const cwd = Uri.file(normalizeSlashes('/')); + const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd: cwd.getFilePath() }); // no such dir exist assert.throws(() => { - fs.chdir('a'); + fs.chdir(cwd.combinePaths('a')); }); - const path = combinePaths('/', 'a', 'b', 'c'); + const path = cwd.combinePaths('a', 'b', 'c'); fs.mkdirSync(path, { recursive: true }); assert(fs.existsSync(path)); }); test('Files', () => { - const cwd = normalizeSlashes('/'); - const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); + const cwd = Uri.file(normalizeSlashes('/')); + const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd: cwd.getFilePath() }); - fs.writeFileSync('1.txt', 'hello', 'utf8'); - const buffer1 = fs.readFileSync('1.txt'); + const uri = cwd.combinePaths('1.txt'); + fs.writeFileSync(uri, 'hello', 'utf8'); + const buffer1 = fs.readFileSync(uri); assert.equal(buffer1.toString(), 'hello'); - const p = normalizeSlashes('a/b/c'); - fs.mkdirpSync(p); + const p = cwd.combinePaths('a/b/c'); + fs.mkdirpSync(p.getFilePath()); - const f = combinePaths(p, '2.txt'); + const f = p.combinePaths('2.txt'); fs.writeFileSync(f, 'hi'); const str = fs.readFileSync(f, 'utf8'); @@ -90,11 +92,11 @@ test('CreateRich', () => { // files + directory + root assert.equal(entries.length, 10); - assert.equal(fs.readFileSync(normalizeSlashes('/a/b/c/1.txt'), 'ascii'), 'hello1'); - assert.equal(fs.readFileSync(normalizeSlashes('/a/b/2.txt'), 'utf8'), 'hello2'); - assert.equal(fs.readFileSync(normalizeSlashes('/a/3.txt'), 'utf-8'), 'hello3'); - assert.equal(fs.readFileSync(normalizeSlashes('/4.txt'), 'utf16le'), 'hello4'); - assert.equal(fs.readFileSync(normalizeSlashes('/a/c/5.txt'), 'ucs2'), 'hello5'); + assert.equal(fs.readFileSync(Uri.file(normalizeSlashes('/a/b/c/1.txt')), 'ascii'), 'hello1'); + assert.equal(fs.readFileSync(Uri.file(normalizeSlashes('/a/b/2.txt')), 'utf8'), 'hello2'); + assert.equal(fs.readFileSync(Uri.file(normalizeSlashes('/a/3.txt')), 'utf-8'), 'hello3'); + assert.equal(fs.readFileSync(Uri.file(normalizeSlashes('/4.txt')), 'utf16le'), 'hello4'); + assert.equal(fs.readFileSync(Uri.file(normalizeSlashes('/a/c/5.txt')), 'ucs2'), 'hello5'); }); test('Shadow', () => { @@ -124,19 +126,19 @@ test('Shadow', () => { }); test('Diffing', () => { - const cwd = normalizeSlashes('/'); - const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); + const cwd = Uri.file(normalizeSlashes('/')); + const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd: cwd.getFilePath() }); // first snapshot fs.snapshot(); - fs.writeFileSync('test1.txt', 'hello1'); + fs.writeFileSync(cwd.combinePaths('test1.txt'), 'hello1'); // compared with original assert.equal(countFile(fs.diff()!), 1); // second snapshot fs.snapshot(); - fs.writeFileSync('test2.txt', 'hello2'); + fs.writeFileSync(cwd.combinePaths('test2.txt'), 'hello2'); // compared with first snapshot assert.equal(countFile(fs.diff()!), 1); @@ -148,11 +150,11 @@ test('Diffing', () => { const s = fs.shadowRoot!.shadow(); // "test2.txt" only exist in first snapshot - assert(!s.existsSync('test2.txt')); + assert(!s.existsSync(cwd.combinePaths('test2.txt'))); // create parallel universe where it has another version of test2.txt with different content // compared to second snapshot which forked from same first snapshot - s.writeFileSync('test2.txt', 'hello3'); + s.writeFileSync(cwd.combinePaths('test2.txt'), 'hello3'); // diff between non direct snapshots // diff gives test2.txt even though it exist in both snapshot @@ -170,16 +172,16 @@ test('createFromFileSystem1', () => { }); // check existing typeshed folder on virtual path inherited from base snapshot from physical file system - const entries = fs.readdirSync(factory.typeshedFolder); + const entries = fs.readdirSync(Uri.file(factory.typeshedFolder)); assert(entries.length > 0); // confirm file - assert.equal(fs.readFileSync(filepath, 'utf8'), content); + assert.equal(fs.readFileSync(Uri.file(filepath), 'utf8'), content); }); test('createFromFileSystem2', () => { const fs = factory.createFromFileSystem(host.HOST, /* ignoreCase */ true, { cwd: factory.srcFolder }); - const entries = fs.readdirSync(factory.typeshedFolder.toUpperCase()); + const entries = fs.readdirSync(Uri.file(factory.typeshedFolder.toUpperCase())); assert(entries.length > 0); }); @@ -190,7 +192,7 @@ test('createFromFileSystemWithCustomTypeshedPath', () => { meta: { [factory.typeshedFolder]: invalidpath }, }); - const entries = fs.readdirSync(factory.typeshedFolder); + const entries = fs.readdirSync(Uri.file(factory.typeshedFolder)); assert(entries.filter((e) => e.endsWith('.md')).length > 0); }); @@ -200,7 +202,7 @@ test('createFromFileSystemWithMetadata', () => { meta: { unused: 'unused' }, }); - assert(fs.existsSync(factory.srcFolder)); + assert(fs.existsSync(Uri.file(factory.srcFolder))); }); function countFile(files: vfs.FileSet): number { diff --git a/packages/pyright-internal/src/tests/fourSlashParser.test.ts b/packages/pyright-internal/src/tests/fourSlashParser.test.ts index de53f3112..77dbc7919 100644 --- a/packages/pyright-internal/src/tests/fourSlashParser.test.ts +++ b/packages/pyright-internal/src/tests/fourSlashParser.test.ts @@ -9,8 +9,9 @@ import assert from 'assert'; -import { combinePaths, getBaseFileName, normalizeSlashes } from '../common/pathUtils'; +import { getBaseFileName, normalizeSlashes } from '../common/pathUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; +import { Uri } from '../common/uri/uri'; import { parseTestData } from './harness/fourslash/fourSlashParser'; import { CompilerSettings } from './harness/fourslash/fourSlashTypes'; import * as host from './harness/testHost'; @@ -89,7 +90,7 @@ test('Library options', () => { const data = parseTestData('.', code, 'test.py'); - assert.equal(data.files[0].fileName, normalizeSlashes(combinePaths(factory.libFolder, 'file1.py'))); + assert.equal(data.files[0].fileName, factory.libFolder.combinePaths('file1.py').getFilePath()); }); test('Range', () => { @@ -222,8 +223,7 @@ test('Multiple Files', () => { assert.equal(data.files.filter((f) => f.fileName === normalizeSlashes('./src/A.py'))[0].content, getContent('A')); assert.equal( - data.files.filter((f) => f.fileName === normalizeSlashes(combinePaths(factory.libFolder, 'src/B.py')))[0] - .content, + data.files.filter((f) => f.fileName === factory.libFolder.combinePaths('src/B.py').getFilePath())[0].content, getContent('B') ); assert.equal(data.files.filter((f) => f.fileName === normalizeSlashes('./src/C.py'))[0].content, getContent('C')); @@ -312,7 +312,10 @@ test('fourSlashWithFileSystem', () => { }); for (const file of data.files) { - assert.equal(fs.readFileSync(file.fileName, 'utf8'), getContent(getBaseFileName(file.fileName, '.py', false))); + assert.equal( + fs.readFileSync(Uri.file(file.fileName), 'utf8'), + getContent(getBaseFileName(file.fileName, '.py', false)) + ); } }); diff --git a/packages/pyright-internal/src/tests/harness/fourslash/fourSlashParser.ts b/packages/pyright-internal/src/tests/harness/fourslash/fourSlashParser.ts index 62badb436..59f7185b8 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/fourSlashParser.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/fourSlashParser.ts @@ -17,13 +17,13 @@ import { } from '../../../common/pathUtils'; import { distlibFolder, libFolder } from '../vfs/factory'; import { - fileMetadataNames, FourSlashData, FourSlashFile, GlobalMetadataOptionNames, Marker, MetadataOptionNames, Range, + fileMetadataNames, } from './fourSlashTypes'; /** @@ -70,13 +70,13 @@ export function parseTestData(basePath: string, contents: string, fileName: stri if (toBoolean(currentFileOptions[MetadataOptionNames.library])) { currentFileName = normalizePath( - combinePaths(libFolder, getRelativePath(currentFileName, normalizedBasePath)) + combinePaths(libFolder.getFilePath(), getRelativePath(currentFileName, normalizedBasePath)) ); } if (toBoolean(currentFileOptions[MetadataOptionNames.distLibrary])) { currentFileName = normalizePath( - combinePaths(distlibFolder, getRelativePath(currentFileName, normalizedBasePath)) + combinePaths(distlibFolder.getFilePath(), getRelativePath(currentFileName, normalizedBasePath)) ); } diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts b/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts index 26d4598ee..73cc2b578 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testLanguageService.ts @@ -6,7 +6,6 @@ * Test mock that implements LanguageServiceInterface */ -import * as path from 'path'; import { CancellationToken, CodeAction, ExecuteCommandParams } from 'vscode-languageserver'; import { @@ -22,8 +21,9 @@ import { ConfigOptions } from '../../../common/configOptions'; import { ConsoleInterface } from '../../../common/console'; import * as debug from '../../../common/debug'; import { FileSystem } from '../../../common/fileSystem'; +import { ServiceProvider } from '../../../common/serviceProvider'; import { Range } from '../../../common/textRange'; -import { UriParser } from '../../../common/uriParser'; +import { Uri } from '../../../common/uri/uri'; import { LanguageServerInterface, MessageAction, ServerSettings, WindowInterface } from '../../../languageServerBase'; import { CodeActionProvider } from '../../../languageService/codeActionProvider'; import { @@ -34,7 +34,6 @@ import { } from '../../../workspaceFactory'; import { TestAccessHost } from '../testAccessHost'; import { HostSpecificFeatures } from './testState'; -import { ServiceProvider } from '../../../common/serviceProvider'; export class TestFeatures implements HostSpecificFeatures { importResolverFactory: ImportResolverFactory = AnalyzerService.createImportResolver; @@ -62,11 +61,11 @@ export class TestFeatures implements HostSpecificFeatures { getCodeActionsForPosition( workspace: Workspace, - filePath: string, + fileUri: Uri, range: Range, token: CancellationToken ): Promise { - return CodeActionProvider.getCodeActionsForPosition(workspace, filePath, range, undefined, token); + return CodeActionProvider.getCodeActionsForPosition(workspace, fileUri, range, undefined, token); } execute(ls: LanguageServerInterface, params: ExecuteCommandParams, token: CancellationToken): Promise { const controller = new CommandController(ls); @@ -75,21 +74,18 @@ export class TestFeatures implements HostSpecificFeatures { } export class TestLanguageService implements LanguageServerInterface { - readonly rootPath = path.sep; + readonly rootUri = Uri.file('/'); readonly window = new TestWindow(); readonly supportAdvancedEdits = true; private readonly _workspace: Workspace; private readonly _defaultWorkspace: Workspace; - private readonly _uriParser: UriParser; constructor(workspace: Workspace, readonly console: ConsoleInterface, readonly fs: FileSystem) { this._workspace = workspace; - this._uriParser = new UriParser(this.fs); this._defaultWorkspace = { workspaceName: '', - rootPath: '', - uri: '', + rootUri: Uri.empty(), pythonPath: undefined, pythonPathKind: WorkspacePythonPathKind.Mutable, kinds: [WellKnownWorkspaceKinds.Test], @@ -97,7 +93,7 @@ export class TestLanguageService implements LanguageServerInterface { console: this.console, hostFactory: () => new TestAccessHost(), importResolverFactory: AnalyzerService.createImportResolver, - configOptions: new ConfigOptions('.'), + configOptions: new ConfigOptions(Uri.empty()), fileSystem: this.fs, }), disableLanguageServices: false, @@ -109,16 +105,12 @@ export class TestLanguageService implements LanguageServerInterface { }; } - decodeTextDocumentUri(uriString: string): string { - return this._uriParser.decodeTextDocumentUri(uriString); - } - getWorkspaces(): Promise { return Promise.resolve([this._workspace, this._defaultWorkspace]); } - getWorkspaceForFile(filePath: string): Promise { - if (filePath.startsWith(this._workspace.rootPath)) { + getWorkspaceForFile(uri: Uri): Promise { + if (uri.startsWith(this._workspace.rootUri)) { return Promise.resolve(this._workspace); } diff --git a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts index 06ba5d2a1..4798c5beb 100644 --- a/packages/pyright-internal/src/tests/harness/fourslash/testState.ts +++ b/packages/pyright-internal/src/tests/harness/fourslash/testState.ts @@ -39,22 +39,15 @@ import * as debug from '../../../common/debug'; import { DiagnosticCategory } from '../../../common/diagnostic'; import { FileEditAction } from '../../../common/editAction'; import { ReadOnlyFileSystem } from '../../../common/fileSystem'; -import { - combinePaths, - convertPathToUri, - getDirectoryPath, - getFileExtension, - getFileSpec, - normalizePath, - normalizeSlashes, - setTestingMode, -} from '../../../common/pathUtils'; +import { getFileExtension, normalizePath, normalizeSlashes } from '../../../common/pathUtils'; import { convertOffsetToPosition, convertPositionToOffset } from '../../../common/positionUtils'; import { ServiceProvider } from '../../../common/serviceProvider'; import { createServiceProvider } from '../../../common/serviceProviderExtensions'; import { compareStringsCaseInsensitive, compareStringsCaseSensitive } from '../../../common/stringUtils'; import { DocumentRange, Position, Range as PositionRange, TextRange, rangesAreEqual } from '../../../common/textRange'; import { TextRangeCollection } from '../../../common/textRangeCollection'; +import { Uri } from '../../../common/uri/uri'; +import { getFileSpec, setTestingMode } from '../../../common/uri/uriUtils'; import { convertToWorkspaceEdit } from '../../../common/workspaceEditUtils'; import { LanguageServerInterface } from '../../../languageServerBase'; import { CallHierarchyProvider } from '../../../languageService/callHierarchyProvider'; @@ -113,7 +106,7 @@ export interface HostSpecificFeatures { runIndexer(workspace: Workspace, noStdLib: boolean, options?: string): void; getCodeActionsForPosition( workspace: Workspace, - filePath: string, + fileUri: Uri, range: PositionRange, token: CancellationToken ): Promise; @@ -121,7 +114,7 @@ export interface HostSpecificFeatures { execute(ls: LanguageServerInterface, params: ExecuteCommandParams, token: CancellationToken): Promise; } -const testAccessHost = new TestAccessHost(vfs.MODULE_PATH, [libFolder, distlibFolder]); +const testAccessHost = new TestAccessHost(Uri.file(vfs.MODULE_PATH), [libFolder, distlibFolder]); export class TestState { private readonly _cancellationToken: TestCancellationToken; @@ -195,10 +188,9 @@ export class TestState { this.workspace = { workspaceName: 'test workspace', - rootPath: vfsInfo.projectRoot, + rootUri: Uri.file(vfsInfo.projectRoot), pythonPath: undefined, pythonPathKind: WorkspacePythonPathKind.Mutable, - uri: convertPathToUri(this.fs, vfsInfo.projectRoot), kinds: [WellKnownWorkspaceKinds.Test], service: service, disableLanguageServices: false, @@ -255,7 +247,7 @@ export class TestState { for (const filePath of this.files) { const file = this._vfsFiles[filePath] as vfs.File; if (file.meta?.[MetadataOptionNames.ipythonMode]) { - this.program.getSourceFile(filePath)?.test_enableIPythonMode(true); + this.program.getSourceFile(Uri.file(filePath))?.test_enableIPythonMode(true); } } } @@ -294,8 +286,9 @@ export class TestState { } getMappedFilePath(path: string): string { - this.importResolver.ensurePartialStubPackages(this.configOptions.findExecEnvironment(path)); - return this.fs.getMappedFilePath(path); + const uri = Uri.file(path); + this.importResolver.ensurePartialStubPackages(this.configOptions.findExecEnvironment(uri)); + return this.fs.getMappedUri(uri).getFilePath(); } getMarkerName(m: Marker): string { @@ -346,14 +339,6 @@ export class TestState { return this.convertOffsetsToRange(range.fileName, range.pos, range.end); } - convertPathToUri(path: string) { - return convertPathToUri(this.fs, path); - } - - getDirectoryPath(path: string) { - return getDirectoryPath(path); - } - getPathSep() { return path.sep; } @@ -461,7 +446,7 @@ export class TestState { fileToOpen.fileName = normalizeSlashes(fileToOpen.fileName); this.activeFile = fileToOpen; - this.program.setFileOpened(this.activeFile.fileName, 1, fileToOpen.content); + this.program.setFileOpened(Uri.file(this.activeFile.fileName), 1, fileToOpen.content); return fileToOpen; } @@ -671,7 +656,7 @@ export class TestState { resultPerFile: Map< string, { - filePath: string; + fileUri: Uri; parseResults: ParseResults | undefined; errors: Diagnostic[]; warnings: Diagnostic[]; @@ -757,6 +742,11 @@ export class TestState { function convertToString(args: any[] | undefined): string[] | undefined { return args?.map((a) => { if (isString(a)) { + // Might be a URI. For comparison purposes in a test, convert it into a + // file path. + if (a.startsWith('file://')) { + return normalizeSlashes(Uri.parse(a, true).getFilePath()); + } return normalizeSlashes(a); } @@ -768,6 +758,15 @@ export class TestState { async verifyCommand(command: Command, files: { [filePath: string]: string }): Promise { this.analyze(); + // Convert command arguments to file Uri strings. That's the expected input for command arguments. + const convertedArgs = command.arguments?.map((arg) => { + if (typeof arg === 'string' && (arg.endsWith('.py') || arg.endsWith('.pyi'))) { + return Uri.file(arg).toString(); + } + return arg; + }); + command.arguments = convertedArgs; + const commandResult = await this._hostSpecificFeatures.execute( new TestLanguageService(this.workspace, this.console, this.fs), { command: command.command, arguments: command.arguments || [] }, @@ -874,7 +873,7 @@ export class TestState { const rangePos = this.convertOffsetsToRange(range.fileName, range.pos, range.end); const provider = new HoverProvider( this.program, - range.fileName, + Uri.file(range.fileName), rangePos.start, kind, CancellationToken.None @@ -1091,7 +1090,7 @@ export class TestState { const actual = new SignatureHelpProvider( this.program, - fileName, + Uri.file(fileName), position, docFormat, /* hasSignatureLabelOffsetCapability */ true, @@ -1154,7 +1153,7 @@ export class TestState { references: DocumentRange[]; }; }, - createDocumentRange?: (filePath: string, result: CollectionResult, parseResults: ParseResults) => DocumentRange, + createDocumentRange?: (fileUri: Uri, result: CollectionResult, parseResults: ParseResults) => DocumentRange, convertToLocation?: (fs: ReadOnlyFileSystem, ranges: DocumentRange) => Location | undefined ) { this.analyze(); @@ -1167,7 +1166,13 @@ export class TestState { continue; } - const expected = map[name].references; + let expected = map[name].references; + expected = expected.map((c) => { + return { + ...c, + uri: c.uri ?? Uri.file((c as any).path), + }; + }); const position = this.convertOffsetToPosition(fileName, marker.position); @@ -1176,7 +1181,7 @@ export class TestState { CancellationToken.None, createDocumentRange, convertToLocation - ).reportReferences(fileName, position, /* includeDeclaration */ true); + ).reportReferences(Uri.file(fileName), position, /* includeDeclaration */ true); assert.strictEqual(actual?.length ?? 0, expected.length, `${name} has failed`); for (const r of convertDocumentRangesToLocation(this.program.fileSystem, expected, convertToLocation)) { @@ -1207,7 +1212,7 @@ export class TestState { const position = this.convertOffsetToPosition(fileName, marker.position); const actual = new CallHierarchyProvider( this.program, - fileName, + Uri.file(fileName), position, CancellationToken.None ).getIncomingCalls(); @@ -1221,9 +1226,7 @@ export class TestState { assert.strictEqual(expectedRange?.filter((e) => this._deepEqual(a.from.range, e)).length, 1); assert.strictEqual(expectedName?.filter((e) => this._deepEqual(a.from.name, e)).length, 1); assert.ok( - expectedFilePath?.filter((e) => - this._deepEqual(a.from.uri, convertPathToUri(this.program.fileSystem, e)) - ).length >= 1 + expectedFilePath?.filter((e) => this._deepEqual(a.from.uri, Uri.file(e).toString())).length >= 1 ); } } @@ -1252,7 +1255,7 @@ export class TestState { const position = this.convertOffsetToPosition(fileName, marker.position); const actual = new CallHierarchyProvider( this.program, - fileName, + Uri.file(fileName), position, CancellationToken.None ).getOutgoingCalls(); @@ -1265,9 +1268,7 @@ export class TestState { assert.strictEqual(expectedRange?.filter((e) => this._deepEqual(a.to.range, e)).length, 1); assert.strictEqual(expectedName?.filter((e) => this._deepEqual(a.to.name, e)).length, 1); assert.ok( - expectedFilePath?.filter((e) => - this._deepEqual(a.to.uri, convertPathToUri(this.program.fileSystem, e)) - ).length >= 1 + expectedFilePath?.filter((e) => this._deepEqual(a.to.uri, Uri.file(e).toString())).length >= 1 ); } } @@ -1304,7 +1305,7 @@ export class TestState { const position = this.convertOffsetToPosition(fileName, marker.position); const actual = new DocumentHighlightProvider( this.program, - fileName, + Uri.file(fileName), position, CancellationToken.None ).getDocumentHighlight(); @@ -1322,6 +1323,16 @@ export class TestState { } } + fixupDefinitionsToMatchExpected(actual: DocumentRange[] | undefined): any { + return actual?.map((a) => { + const { uri, ...restOfActual } = a; + return { + ...restOfActual, + path: uri.getFilePath(), + }; + }); + } + verifyFindDefinitions( map: { [marker: string]: { @@ -1341,25 +1352,26 @@ export class TestState { } const expected = map[name].definitions; - + const uri = Uri.file(fileName); // If we're going to def from a file, act like it's open. - if (!this.program.getSourceFileInfo(fileName)) { + if (!this.program.getSourceFileInfo(uri)) { const file = this.testData.files.find((v) => v.fileName === fileName); if (file) { - this.program.setFileOpened(fileName, file.version, file.content); + this.program.setFileOpened(uri, file.version, file.content); } } const position = this.convertOffsetToPosition(fileName, marker.position); - const actual = new DefinitionProvider( + let actual = new DefinitionProvider( this.program, - fileName, + uri, position, filter, CancellationToken.None ).getDefinitions(); assert.equal(actual?.length ?? 0, expected.length, `No definitions found for marker "${name}"`); + actual = this.fixupDefinitionsToMatchExpected(actual!); for (const r of expected) { assert.equal( @@ -1389,12 +1401,13 @@ export class TestState { const expected = map[name].definitions; const position = this.convertOffsetToPosition(fileName, marker.position); - const actual = new TypeDefinitionProvider( + let actual = new TypeDefinitionProvider( this.program, - fileName, + Uri.file(fileName), position, CancellationToken.None ).getDefinitions(); + actual = this.fixupDefinitionsToMatchExpected(actual!); assert.strictEqual(actual?.length ?? 0, expected.length, name); @@ -1424,16 +1437,23 @@ export class TestState { } const expected = map[name]; + expected.changes = expected.changes.map((c) => { + return { + ...c, + fileUri: c.fileUri ?? Uri.file((c as any).filePath), + }; + }); const position = this.convertOffsetToPosition(fileName, marker.position); - const actual = new RenameProvider(this.program, fileName, position, CancellationToken.None).renameSymbol( - expected.newName, - /* isDefaultWorkspace */ false, - isUntitled - ); + const actual = new RenameProvider( + this.program, + Uri.file(fileName), + position, + CancellationToken.None + ).renameSymbol(expected.newName, /* isDefaultWorkspace */ false, isUntitled); verifyWorkspaceEdit( - convertToWorkspaceEdit(this.program.fileSystem, { edits: expected.changes, fileOperations: [] }), + convertToWorkspaceEdit({ edits: expected.changes, fileOperations: [] }), actual ?? { documentChanges: [] } ); } @@ -1510,8 +1530,8 @@ export class TestState { const provider = new CompletionProvider( this.program, - this.workspace.rootPath, - filePath, + this.workspace.rootUri, + Uri.file(filePath), completionPosition, options, CancellationToken.None @@ -1606,7 +1626,7 @@ export class TestState { } private _convertGlobalOptionsToConfigOptions(projectRoot: string, mountPaths?: Map): ConfigOptions { - const configOptions = new ConfigOptions(projectRoot); + const configOptions = new ConfigOptions(Uri.file(projectRoot)); // add more global options as we need them const newConfigOptions = this._applyTestConfigOptions(configOptions, mountPaths); @@ -1626,17 +1646,17 @@ export class TestState { // make sure we set typing path if (configOptions.stubPath === undefined) { - configOptions.stubPath = normalizePath(combinePaths(vfs.MODULE_PATH, 'typings')); + configOptions.stubPath = Uri.file(vfs.MODULE_PATH).combinePaths('typings'); } - configOptions.include.push(getFileSpec(this.serviceProvider, configOptions.projectRoot, '.')); - configOptions.exclude.push(getFileSpec(this.serviceProvider, configOptions.projectRoot, typeshedFolder)); - configOptions.exclude.push(getFileSpec(this.serviceProvider, configOptions.projectRoot, distlibFolder)); - configOptions.exclude.push(getFileSpec(this.serviceProvider, configOptions.projectRoot, libFolder)); + configOptions.include.push(getFileSpec(configOptions.projectRoot, '.')); + configOptions.exclude.push(getFileSpec(configOptions.projectRoot, typeshedFolder)); + configOptions.exclude.push(getFileSpec(configOptions.projectRoot, distlibFolder.getFilePath())); + configOptions.exclude.push(getFileSpec(configOptions.projectRoot, libFolder.getFilePath())); if (mountPaths) { for (const mountPath of mountPaths.keys()) { - configOptions.exclude.push(getFileSpec(this.serviceProvider, configOptions.projectRoot, mountPath)); + configOptions.exclude.push(getFileSpec(configOptions.projectRoot, mountPath)); } } @@ -1648,7 +1668,7 @@ export class TestState { } private _getParseResult(fileName: string) { - const file = this.program.getBoundSourceFile(fileName)!; + const file = this.program.getBoundSourceFile(Uri.file(fileName))!; return file?.getParseResults(); } @@ -1661,7 +1681,7 @@ export class TestState { } // slow path - const fileContents = this.fs.readFileSync(fileName, 'utf8'); + const fileContents = this.fs.readFileSync(Uri.file(fileName), 'utf8'); const tokenizer = new Tokenizer(); return tokenizer.tokenize(fileContents).lines; } @@ -1680,10 +1700,11 @@ export class TestState { private _editScriptAndUpdateMarkers(fileName: string, editStart: number, editEnd: number, newText: string) { let fileContent = this.getFileContent(fileName); fileContent = fileContent.slice(0, editStart) + newText + fileContent.slice(editEnd); + const uri = Uri.file(fileName); - this.testFS.writeFileSync(fileName, fileContent, 'utf8'); - const newVersion = (this.program.getSourceFile(fileName)?.getClientVersion() ?? -1) + 1; - this.program.setFileOpened(fileName, newVersion, fileContent); + this.testFS.writeFileSync(uri, fileContent, 'utf8'); + const newVersion = (this.program.getSourceFile(uri)?.getClientVersion() ?? -1) + 1; + this.program.setFileOpened(uri, newVersion, fileContent); for (const marker of this.testData.markers) { if (marker.fileName === fileName) { @@ -1834,20 +1855,23 @@ export class TestState { } private _getDiagnosticsPerFile() { - const sourceFiles = this.files.map((f) => this.program.getSourceFile(f)); + const sourceFiles = this.files.map((f) => this.program.getSourceFile(Uri.file(f))); const results = sourceFiles.map((sourceFile, index) => { if (sourceFile) { const diagnostics = sourceFile.getDiagnostics(this.configOptions) || []; - const filePath = sourceFile.getFilePath(); + const fileUri = sourceFile.getUri(); const value = { - filePath, + fileUri, parseResults: sourceFile.getParseResults(), errors: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Error), warnings: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Warning), information: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Information), unused: diagnostics.filter((diag) => diag.category === DiagnosticCategory.UnusedCode), }; - return [filePath, value] as [string, typeof value]; + + // Don't use the uri key, but rather the file name, because other spots + // in the test data assume file paths. + return [this.files[index], value] as [string, typeof value]; } else { this.raiseError(`Source file not found for ${this.files[index]}`); } @@ -1880,6 +1904,7 @@ export class TestState { const fileExtension = getFileExtension(path).toLowerCase(); return fileExtension === '.py' || fileExtension === '.pyi'; }) + .map((path) => Uri.file(path)) .filter((path) => service.isTracked(path)) ); @@ -1898,7 +1923,8 @@ export class TestState { } private async _waitForFile(filePath: string) { - while (!this.fs.existsSync(filePath)) { + const uri = Uri.file(filePath); + while (!this.fs.existsSync(uri)) { await new Promise((res) => setTimeout(() => { res(); @@ -1916,7 +1942,7 @@ export class TestState { return this._hostSpecificFeatures.getCodeActionsForPosition( this.workspace, - file, + Uri.file(file), textRange, CancellationToken.None ); @@ -1930,7 +1956,7 @@ export class TestState { // wait until the file exists await this._waitForFile(normalizedFilePath); - const actual = this.fs.readFileSync(normalizedFilePath, 'utf8'); + const actual = this.fs.readFileSync(Uri.file(normalizedFilePath), 'utf8'); if (actual !== expected) { this.raiseError( `doesn't contain expected result: ${stringify(expected)}, actual: ${stringify(actual)}` @@ -2020,7 +2046,7 @@ export function getNodeAtMarker(codeOrState: string | TestState, markerName = 'm const state = isString(codeOrState) ? parseAndGetTestState(codeOrState).state : codeOrState; const marker = state.getMarkerByName(markerName); - const sourceFile = state.program.getBoundSourceFile(marker.fileName); + const sourceFile = state.program.getBoundSourceFile(Uri.file(marker.fileName)); assert(sourceFile); const parserResults = sourceFile.getParseResults(); diff --git a/packages/pyright-internal/src/tests/harness/testAccessHost.ts b/packages/pyright-internal/src/tests/harness/testAccessHost.ts index 2e845ad49..c93aa2c42 100644 --- a/packages/pyright-internal/src/tests/harness/testAccessHost.ts +++ b/packages/pyright-internal/src/tests/harness/testAccessHost.ts @@ -8,13 +8,14 @@ import { PythonPathResult } from '../../analyzer/pythonPathUtils'; import { NoAccessHost } from '../../common/host'; +import { Uri } from '../../common/uri/uri'; export class TestAccessHost extends NoAccessHost { - constructor(private _modulePath = '', private _searchPaths: string[] = []) { + constructor(private _modulePath = Uri.empty(), private _searchPaths: Uri[] = []) { super(); } - override getPythonSearchPaths(pythonPath?: string, logInfo?: string[]): PythonPathResult { + override getPythonSearchPaths(pythonPath?: Uri, logInfo?: string[]): PythonPathResult { return { paths: this._searchPaths, prefix: this._modulePath, diff --git a/packages/pyright-internal/src/tests/harness/testHost.ts b/packages/pyright-internal/src/tests/harness/testHost.ts index c6802e06d..52454f040 100644 --- a/packages/pyright-internal/src/tests/harness/testHost.ts +++ b/packages/pyright-internal/src/tests/harness/testHost.ts @@ -8,16 +8,11 @@ import * as os from 'os'; import * as pathModule from 'path'; import { NullConsole } from '../../common/console'; -import { - combinePaths, - directoryExists, - fileExists, - FileSystemEntries, - getFileSize, - resolvePaths, -} from '../../common/pathUtils'; +import { combinePaths, FileSystemEntries, resolvePaths } from '../../common/pathUtils'; import { createFromRealFileSystem } from '../../common/realFileSystem'; import { compareStringsCaseInsensitive, compareStringsCaseSensitive } from '../../common/stringUtils'; +import { Uri } from '../../common/uri/uri'; +import { directoryExists, fileExists, getFileSize } from '../../common/uri/uriUtils'; export const HOST: TestHost = createHost(); @@ -57,7 +52,7 @@ function createHost(): TestHost { return false; } // If this file exists under a different case, we must be case-insensitve. - return !vfs.existsSync(swapCase(__filename)); + return !vfs.existsSync(Uri.file(swapCase(__filename))); /** Convert all lowercase chars to uppercase, and vice-versa */ function swapCase(s: string): string { @@ -72,9 +67,9 @@ function createHost(): TestHost { function filesInFolder(folder: string): string[] { let paths: string[] = []; - for (const file of vfs.readdirSync(folder)) { + for (const file of vfs.readdirSync(Uri.file(folder))) { const pathToFile = pathModule.join(folder, file); - const stat = vfs.statSync(pathToFile); + const stat = vfs.statSync(Uri.file(pathToFile)); if (options.recursive && stat.isDirectory()) { paths = paths.concat(filesInFolder(pathToFile)); } else if (stat.isFile() && (!spec || file.match(spec))) { @@ -91,7 +86,7 @@ function createHost(): TestHost { function getAccessibleFileSystemEntries(dirname: string): FileSystemEntries { try { const entries: string[] = vfs - .readdirSync(dirname || '.') + .readdirSync(Uri.file(dirname || '.')) .sort(useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive); const files: string[] = []; const directories: string[] = []; @@ -101,7 +96,7 @@ function createHost(): TestHost { } const name = combinePaths(dirname, entry); try { - const stat = vfs.statSync(name); + const stat = vfs.statSync(Uri.file(name)); if (!stat) { continue; } @@ -121,10 +116,10 @@ function createHost(): TestHost { } function readFile(fileName: string, _encoding?: string): string | undefined { - if (!fileExists(vfs, fileName)) { + if (!fileExists(vfs, Uri.file(fileName))) { return undefined; } - const buffer = vfs.readFileSync(fileName); + const buffer = vfs.readFileSync(Uri.file(fileName)); let len = buffer.length; if (len >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) { // Big endian UTF-16 byte order mark detected. Since big endian is not supported by node.js, @@ -155,18 +150,18 @@ function createHost(): TestHost { data = byteOrderMarkIndicator + data; } - vfs.writeFileSync(fileName, data, 'utf8'); + vfs.writeFileSync(Uri.file(fileName), data, 'utf8'); } return { useCaseSensitiveFileNames: () => useCaseSensitiveFileNames, - getFileSize: (path: string) => getFileSize(vfs, path), + getFileSize: (path: string) => getFileSize(vfs, Uri.file(path)), readFile: (path) => readFile(path), writeFile: (path, content) => { writeFile(path, content); }, - fileExists: (path) => fileExists(vfs, path), - directoryExists: (path) => directoryExists(vfs, path), + fileExists: (path) => fileExists(vfs, Uri.file(path)), + directoryExists: (path) => directoryExists(vfs, Uri.file(path)), listFiles, log: (s) => { console.log(s); diff --git a/packages/pyright-internal/src/tests/harness/vfs/factory.ts b/packages/pyright-internal/src/tests/harness/vfs/factory.ts index da65e7763..8d89ffb61 100644 --- a/packages/pyright-internal/src/tests/harness/vfs/factory.ts +++ b/packages/pyright-internal/src/tests/harness/vfs/factory.ts @@ -8,6 +8,7 @@ import * as pathConsts from '../../../common/pathConsts'; import { combinePaths, getDirectoryPath, normalizeSlashes, resolvePaths } from '../../../common/pathUtils'; +import { Uri } from '../../../common/uri/uri'; import { GlobalMetadataOptionNames } from '../fourslash/fourSlashTypes'; import { TestHost } from '../testHost'; import { bufferFrom } from '../utils'; @@ -39,13 +40,11 @@ export interface FileSystemCreateOptions extends FileSystemOptions { documents?: readonly TextDocument[]; } -export const libFolder = combinePaths( - MODULE_PATH, - normalizeSlashes(combinePaths(pathConsts.lib, pathConsts.sitePackages)) +export const libFolder = Uri.file( + combinePaths(MODULE_PATH, normalizeSlashes(combinePaths(pathConsts.lib, pathConsts.sitePackages))) ); -export const distlibFolder = combinePaths( - MODULE_PATH, - normalizeSlashes(combinePaths(pathConsts.lib, pathConsts.distPackages)) +export const distlibFolder = Uri.file( + combinePaths(MODULE_PATH, normalizeSlashes(combinePaths(pathConsts.lib, pathConsts.distPackages))) ); export const typeshedFolder = combinePaths(MODULE_PATH, normalizeSlashes(pathConsts.typeshedFallback)); export const srcFolder = normalizeSlashes('/.src'); @@ -88,12 +87,12 @@ export function createFromFileSystem( } if (cwd) { fs.mkdirpSync(cwd); - fs.chdir(cwd); + fs.chdir(Uri.file(cwd)); } if (documents) { for (const document of documents) { fs.mkdirpSync(getDirectoryPath(document.file)); - fs.writeFileSync(document.file, document.text, 'utf8'); + fs.writeFileSync(Uri.file(document.file), document.text, 'utf8'); fs.filemeta(document.file).set('document', document); // Add symlinks const symlink = document.meta.get('symlink'); diff --git a/packages/pyright-internal/src/tests/harness/vfs/filesystem.ts b/packages/pyright-internal/src/tests/harness/vfs/filesystem.ts index 471dd5891..92f689c3b 100644 --- a/packages/pyright-internal/src/tests/harness/vfs/filesystem.ts +++ b/packages/pyright-internal/src/tests/harness/vfs/filesystem.ts @@ -8,15 +8,15 @@ /* eslint-disable no-dupe-class-members */ import { Dirent, ReadStream, WriteStream } from 'fs'; -import { URI } from 'vscode-uri'; import { FileSystem, MkDirOptions, TempFile, TmpfileOptions } from '../../../common/fileSystem'; import { FileWatcher, FileWatcherEventHandler, FileWatcherEventType } from '../../../common/fileWatcher'; import * as pathUtil from '../../../common/pathUtils'; +import { compareStringsCaseInsensitive, compareStringsCaseSensitive } from '../../../common/stringUtils'; +import { Uri } from '../../../common/uri/uri'; import { bufferFrom, createIOError } from '../utils'; import { Metadata, SortedMap, closeIterator, getIterator, nextResult } from './../utils'; import { ValidationFlags, validate } from './pathValidation'; -import { compareStringsCaseInsensitive, compareStringsCaseSensitive } from '../../../common/stringUtils'; export const MODULE_PATH = pathUtil.normalizeSlashes('/'); @@ -28,18 +28,14 @@ export interface DiffOptions { } export class TestFileSystemWatcher implements FileWatcher { - private _paths: string[] = []; - constructor(paths: string[], private _listener: FileWatcherEventHandler) { - this._paths = paths.map((p) => pathUtil.normalizePath(p)); - } + constructor(private _paths: Uri[], private _listener: FileWatcherEventHandler) {} close() { // Do nothing. } - fireFileChange(path: string, eventType: FileWatcherEventType): boolean { - const normalized = pathUtil.normalizePath(path); - if (this._paths.some((p) => normalized.startsWith(p))) { - this._listener(eventType, normalized); + fireFileChange(path: Uri, eventType: FileWatcherEventType): boolean { + if (this._paths.some((p) => path.startsWith(p))) { + this._listener(eventType, path); return true; } return false; @@ -90,7 +86,7 @@ export class TestFileSystem implements FileSystem, TempFile { } let cwd = options.cwd; - if ((!cwd || (!pathUtil.isDiskPathRoot(cwd) && !pathUtil.isUri(cwd))) && this._lazy.links) { + if ((!cwd || !pathUtil.isDiskPathRoot(cwd)) && this._lazy.links) { const iterator = getIterator(this._lazy.links.keys()); try { for (let i = nextResult(iterator); i; i = nextResult(iterator)) { @@ -111,6 +107,10 @@ export class TestFileSystem implements FileSystem, TempFile { this._cwd = cwd || ''; } + get isCaseSensitive() { + return !this.ignoreCase; + } + /** * Gets metadata for this `FileSystem`. */ @@ -242,7 +242,8 @@ export class TestFileSystem implements FileSystem, TempFile { * * @link http://pubs.opengroup.org/onlinepubs/9699919799/functions/chdir.html */ - chdir(path: string) { + chdir(uri: Uri) { + let path = uri.getFilePath(); if (this.isReadonly) { throw createIOError('EPERM'); } @@ -274,7 +275,7 @@ export class TestFileSystem implements FileSystem, TempFile { this._dirStack.push(this._cwd); } if (path && path !== this._cwd) { - this.chdir(path); + this.chdir(Uri.file(path)); } } @@ -287,7 +288,7 @@ export class TestFileSystem implements FileSystem, TempFile { } const path = this._dirStack && this._dirStack.pop(); if (path) { - this.chdir(path); + this.chdir(Uri.file(path)); } } @@ -331,7 +332,7 @@ export class TestFileSystem implements FileSystem, TempFile { return results; } - createFileSystemWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher { + createFileSystemWatcher(paths: Uri[], listener: FileWatcherEventHandler): FileWatcher { const watcher = new TestFileSystemWatcher(paths, listener); this._watchers.push(watcher); return watcher; @@ -339,58 +340,49 @@ export class TestFileSystem implements FileSystem, TempFile { fireFileWatcherEvent(path: string, event: FileWatcherEventType) { for (const watcher of this._watchers) { - if (watcher.fireFileChange(path, event)) { + if (watcher.fireFileChange(Uri.file(path), event)) { break; } } } - getModulePath(): string { - return MODULE_PATH; + getModulePath(): Uri { + return Uri.file(MODULE_PATH); } - tmpdir(): string { + tmpdir(): Uri { this.mkdirpSync('/tmp'); - return pathUtil.normalizeSlashes('/tmp'); + return Uri.parse('file:///tmp', true); } - tmpfile(options?: TmpfileOptions): string { + tmpfile(options?: TmpfileOptions): Uri { // Use an algorithm similar to tmp's. const prefix = options?.prefix || 'tmp'; const postfix = options?.prefix ? '-' + options.prefix : ''; const name = `${prefix}-${this._tmpfileCounter++}${postfix}`; - const path = pathUtil.combinePaths(this.tmpdir(), name); + const path = this.tmpdir().combinePaths(name); this.writeFileSync(path, ''); return path; } - realCasePath(path: string): string { + realCasePath(path: Uri): Uri { return path; } - isMappedFilePath(filepath: string): boolean { + isMappedUri(filepath: Uri): boolean { return false; } // Get original filepath if the given filepath is mapped. - getOriginalFilePath(mappedFilePath: string) { + getOriginalUri(mappedFilePath: Uri) { return mappedFilePath; } // Get mapped filepath if the given filepath is mapped. - getMappedFilePath(originalFilepath: string) { + getMappedUri(originalFilepath: Uri) { return originalFilepath; } - getUri(path: string): string { - // If this is not a file path, just return the original path. - if (pathUtil.isUri(path)) { - return path; - } - - return URI.file(path).toString(); - } - /** * Mounts a physical or virtual file system at a location in this virtual file system. * @@ -424,12 +416,12 @@ export class TestFileSystem implements FileSystem, TempFile { try { const stats = this.lstatSync(path); if (stats.isFile() || stats.isSymbolicLink()) { - this.unlinkSync(path); + this.unlinkSync(Uri.file(path)); } else if (stats.isDirectory()) { - for (const file of this.readdirSync(path)) { + for (const file of this.readdirSync(Uri.file(path))) { this.rimrafSync(pathUtil.combinePaths(path, file)); } - this.rmdirSync(path); + this.rmdirSync(Uri.file(path)); } } catch (e: any) { if (e.code === 'ENOENT') { @@ -506,8 +498,11 @@ export class TestFileSystem implements FileSystem, TempFile { /** * Determines whether a path exists. */ - existsSync(path: string) { - const result = this._walk(this._resolve(path), /* noFollow */ true, () => 'stop'); + existsSync(path: Uri) { + if (path.isEmpty()) { + return false; + } + const result = this._walk(this._resolve(path.getFilePath()), /* noFollow */ true, () => 'stop'); return result !== undefined && result.node !== undefined; } @@ -518,8 +513,8 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - statSync(path: string) { - return this._stat(this._walk(this._resolve(path))); + statSync(path: Uri) { + return this._stat(this._walk(this._resolve(path.getFilePath()))); } /** @@ -562,8 +557,8 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - readdirSync(path: string) { - const { node } = this._walk(this._resolve(path)); + readdirSync(path: Uri) { + const { node } = this._walk(this._resolve(path.getFilePath())); if (!node) { throw createIOError('ENOENT'); } @@ -580,8 +575,8 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - readdirEntriesSync(path: string): Dirent[] { - const { node } = this._walk(this._resolve(path)); + readdirEntriesSync(path: Uri): Dirent[] { + const { node } = this._walk(this._resolve(path.getFilePath())); if (!node) { throw createIOError('ENOENT'); } @@ -599,17 +594,17 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - mkdirSync(path: string, options?: MkDirOptions) { + mkdirSync(path: Uri, options?: MkDirOptions) { if (this.isReadonly) { throw createIOError('EROFS'); } if (options?.recursive) { - this.mkdirpSync(path); + this.mkdirpSync(path.getFilePath()); return; } - this._mkdir(this._walk(this._resolve(path), /* noFollow */ true)); + this._mkdir(this._walk(this._resolve(path.getFilePath()), /* noFollow */ true)); } /** @@ -619,11 +614,11 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - rmdirSync(path: string) { + rmdirSync(uri: Uri) { if (this.isReadonly) { throw createIOError('EROFS'); } - path = this._resolve(path); + const path = this._resolve(uri.getFilePath()); const { parent, links, node, basename } = this._walk(path, /* noFollow */ true); if (!parent) { @@ -677,12 +672,12 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - unlinkSync(path: string) { + unlinkSync(path: Uri) { if (this.isReadonly) { throw createIOError('EROFS'); } - const { parent, links, node, basename } = this._walk(this._resolve(path), /* noFollow */ true); + const { parent, links, node, basename } = this._walk(this._resolve(path.getFilePath()), /* noFollow */ true); if (!parent) { throw createIOError('EPERM'); } @@ -791,10 +786,10 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - realpathSync(path: string) { + realpathSync(path: Uri) { try { - const { realpath } = this._walk(this._resolve(path)); - return realpath; + const { realpath } = this._walk(this._resolve(path.getFilePath())); + return Uri.file(realpath); } catch (e: any) { return path; } @@ -805,21 +800,21 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - readFileSync(path: string, encoding?: null): Buffer; + readFileSync(path: Uri, encoding?: null): Buffer; /** * Read from a file. * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - readFileSync(path: string, encoding: BufferEncoding): string; + readFileSync(path: Uri, encoding: BufferEncoding): string; /** * Read from a file. * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer; - readFileSync(path: string, encoding: BufferEncoding | null = null) { - const { node } = this._walk(this._resolve(path)); + readFileSync(path: Uri, encoding?: BufferEncoding | null): string | Buffer; + readFileSync(path: Uri, encoding: BufferEncoding | null = null) { + const { node } = this._walk(this._resolve(path.getFilePath())); if (!node) { throw createIOError('ENOENT'); } @@ -839,12 +834,17 @@ export class TestFileSystem implements FileSystem, TempFile { * * NOTE: do not rename this method as it is intended to align with the same named export of the "fs" module. */ - writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null = null) { + writeFileSync(uri: Uri, data: string | Buffer, encoding: BufferEncoding | null = null) { if (this.isReadonly) { throw createIOError('EROFS'); } - const { parent, links, node: existingNode, basename } = this._walk(this._resolve(path), /* noFollow */ false); + const { + parent, + links, + node: existingNode, + basename, + } = this._walk(this._resolve(uri.getFilePath()), /* noFollow */ false); if (!parent) { throw createIOError('EPERM'); } @@ -870,21 +870,21 @@ export class TestFileSystem implements FileSystem, TempFile { node.ctimeMs = time; } - readFile(filePath: string): Promise { - return Promise.resolve(this.readFileSync(filePath)); + readFile(fileUri: Uri): Promise { + return Promise.resolve(this.readFileSync(fileUri)); } - readFileText(filePath: string, encoding?: BufferEncoding): Promise { - return Promise.resolve(this.readFileSync(filePath, encoding || 'utf8')); + readFileText(fileUri: Uri, encoding?: BufferEncoding): Promise { + return Promise.resolve(this.readFileSync(fileUri, encoding || 'utf8')); } - createReadStream(path: string): ReadStream { + createReadStream(path: Uri): ReadStream { throw new Error('Not implemented in test file system.'); } - createWriteStream(path: string): WriteStream { + createWriteStream(path: Uri): WriteStream { throw new Error('Not implemented in test file system.'); } - copyFileSync(src: string, dst: string): void { + copyFileSync(src: Uri, dst: Uri): void { throw new Error('Not implemented in test file system.'); } @@ -908,7 +908,7 @@ export class TestFileSystem implements FileSystem, TempFile { return TestFileSystem._rootDiff(differences, changed, base, options) ? differences : undefined; } - isInZip(path: string): boolean { + isInZip(path: Uri): boolean { return false; } @@ -954,7 +954,7 @@ export class TestFileSystem implements FileSystem, TempFile { } if (axis === 'descendants-or-self' || axis === 'descendants') { if (stats.isDirectory() && (!traversal.traverse || traversal.traverse(path, stats))) { - for (const file of this.readdirSync(path)) { + for (const file of this.readdirSync(Uri.file(path))) { try { const childpath = pathUtil.combinePaths(path, file); const stats = this._stat(this._walk(childpath, noFollow)); @@ -1588,7 +1588,7 @@ export class TestFileSystem implements FileSystem, TempFile { throw new TypeError('Roots cannot be files.'); } this.mkdirpSync(pathUtil.getDirectoryPath(path)); - this.writeFileSync(path, value.data, value.encoding); + this.writeFileSync(Uri.file(path), value.data, value.encoding); this._applyFileExtendedOptions(path, value); } else if (value instanceof Directory) { this.mkdirpSync(path); diff --git a/packages/pyright-internal/src/tests/harness/vfs/pathValidation.ts b/packages/pyright-internal/src/tests/harness/vfs/pathValidation.ts index 1af7b87f7..f32bfdbc8 100644 --- a/packages/pyright-internal/src/tests/harness/vfs/pathValidation.ts +++ b/packages/pyright-internal/src/tests/harness/vfs/pathValidation.ts @@ -8,7 +8,6 @@ import { sep } from 'path'; import * as pu from '../../../common/pathUtils'; import { createIOError } from '../utils'; -import { URI } from 'vscode-uri'; const invalidRootComponentRegExp = getInvalidRootComponentRegExp(); const invalidNavigableComponentRegExp = /[:*?"<>|]/; @@ -124,13 +123,7 @@ function validateComponents(components: string[], flags: ValidationFlags, hasTra } // Validate component strings - if (pu.isUri(components[0])) { - try { - URI.parse(components[0]); - } catch { - return false; - } - } else if (invalidRootComponentRegExp.test(components[0])) { + if (invalidRootComponentRegExp.test(components[0])) { return false; } for (let i = 1; i < components.length; i++) { diff --git a/packages/pyright-internal/src/tests/importResolver.test.ts b/packages/pyright-internal/src/tests/importResolver.test.ts index 5c310f794..5dacaf2d6 100644 --- a/packages/pyright-internal/src/tests/importResolver.test.ts +++ b/packages/pyright-internal/src/tests/importResolver.test.ts @@ -17,7 +17,8 @@ import { lib, sitePackages, typeshedFallback } from '../common/pathConsts'; import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; import { createFromRealFileSystem } from '../common/realFileSystem'; import { ServiceProvider } from '../common/serviceProvider'; -import { createServiceProvider } from '../common/serviceProviderExtensions'; +import { ServiceKeys, createServiceProvider } from '../common/serviceProviderExtensions'; +import { Uri } from '../common/uri/uri'; import { PyrightFileSystem } from '../pyrightFileSystem'; import { TestAccessHost } from './harness/testAccessHost'; import { TestFileSystem } from './harness/vfs/filesystem'; @@ -51,8 +52,9 @@ if (!usingTrueVenv()) { assert(importResult.isStubFile); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi')) - .length + importResult.resolvedUris.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.pyi') + ).length ); }); @@ -77,8 +79,9 @@ if (!usingTrueVenv()) { assert(importResult.isStubFile); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')) - .length + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length ); }); @@ -120,13 +123,14 @@ if (!usingTrueVenv()) { }, ]; - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = typingFolder)); + const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = Uri.file(typingFolder))); assert(importResult.isImportFound); assert(importResult.isStubFile); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')) - .length + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length ); }); @@ -152,13 +156,14 @@ if (!usingTrueVenv()) { ]; // Stub packages win over typeshed. - const importResult = getImportResult(files, ['myLib'], (c) => (c.typeshedPath = typeshedFolder)); + const importResult = getImportResult(files, ['myLib'], (c) => (c.typeshedPath = Uri.file(typeshedFolder))); assert(importResult.isImportFound); assert(importResult.isStubFile); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')) - .length + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length ); }); @@ -188,8 +193,9 @@ if (!usingTrueVenv()) { assert(importResult.isStubFile); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')) - .length + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length ); }); @@ -237,7 +243,10 @@ if (!usingTrueVenv()) { const importResult = getImportResult(files, ['os']); assert(importResult.isImportFound); - assert.strictEqual(files[0].path, importResult.resolvedPaths[importResult.resolvedPaths.length - 1]); + assert.strictEqual( + files[0].path, + importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() + ); }); test('import side by side file sub under lib folder', () => { @@ -292,15 +301,16 @@ describe('Import tests that can run with or without a true venv', () => { ]; const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(normalizeSlashes('/')); + const configOptions = new ConfigOptions(Uri.file('/')); const importResolver = new ImportResolver( sp, configOptions, - new TestAccessHost(sp.fs().getModulePath(), [libraryRoot]) + new TestAccessHost(sp.fs().getModulePath(), [Uri.file(libraryRoot)]) ); // Stub package wins over original package (per PEP 561 rules). - const sideBySideResult = importResolver.resolveImport(myFile, configOptions.findExecEnvironment(myFile), { + const myUri = Uri.file(myFile); + const sideBySideResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { leadingDots: 0, nameParts: ['myLib', 'partialStub'], importedSymbols: new Set(), @@ -309,12 +319,12 @@ describe('Import tests that can run with or without a true venv', () => { assert(sideBySideResult.isImportFound); assert(sideBySideResult.isStubFile); - const sideBySideStubFile = combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'); - assert.strictEqual(1, sideBySideResult.resolvedPaths.filter((f) => f === sideBySideStubFile).length); + const sideBySideStubFile = Uri.file(combinePaths(libraryRoot, 'myLib', 'partialStub.pyi')); + assert.strictEqual(1, sideBySideResult.resolvedUris.filter((f) => f.key === sideBySideStubFile.key).length); assert.strictEqual('def test(): ...', sp.fs().readFileSync(sideBySideStubFile, 'utf8')); // Side by side stub doesn't completely disable partial stub. - const partialStubResult = importResolver.resolveImport(myFile, configOptions.findExecEnvironment(myFile), { + const partialStubResult = importResolver.resolveImport(myUri, configOptions.findExecEnvironment(myUri), { leadingDots: 0, nameParts: ['myLib', 'partialStub2'], importedSymbols: new Set(), @@ -323,8 +333,8 @@ describe('Import tests that can run with or without a true venv', () => { assert(partialStubResult.isImportFound); assert(partialStubResult.isStubFile); - const partialStubFile = combinePaths(libraryRoot, 'myLib', 'partialStub2.pyi'); - assert.strictEqual(1, partialStubResult.resolvedPaths.filter((f) => f === partialStubFile).length); + const partialStubFile = Uri.file(combinePaths(libraryRoot, 'myLib', 'partialStub2.pyi')); + assert.strictEqual(1, partialStubResult.resolvedUris.filter((f) => f.key === partialStubFile.key).length); }); test('stub namespace package', () => { @@ -345,7 +355,9 @@ describe('Import tests that can run with or without a true venv', () => { assert(!importResult.isStubFile); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', 'partialStub.py')).length + importResult.resolvedUris.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths(libraryRoot, 'myLib', 'partialStub.py') + ).length ); }); @@ -371,12 +383,14 @@ describe('Import tests that can run with or without a true venv', () => { ]; // If the package exists in typing folder, that gets picked up first. - const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = typingFolder)); + const importResult = getImportResult(files, ['myLib'], (c) => (c.stubPath = Uri.file(typingFolder))); assert(importResult.isImportFound); assert(importResult.isStubFile); assert.strictEqual( 0, - importResult.resolvedPaths.filter((f) => f === combinePaths(libraryRoot, 'myLib', '__init__.pyi')).length + importResult.resolvedUris.filter( + (f) => f.getFilePath() === combinePaths(libraryRoot, 'myLib', '__init__.pyi') + ).length ); }); @@ -394,16 +408,19 @@ describe('Import tests that can run with or without a true venv', () => { const importResult = getImportResult(files, ['os']); assert(importResult.isImportFound); - assert.strictEqual(files[1].path, importResult.resolvedPaths[importResult.resolvedPaths.length - 1]); + assert.strictEqual( + files[1].path, + importResult.resolvedUris[importResult.resolvedUris.length - 1].getFilePath() + ); }); test('no empty import roots', () => { const sp = createServiceProviderFromFiles([]); - const configOptions = new ConfigOptions(''); // Empty, like open-file mode. + const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. const importResolver = new ImportResolver( sp, configOptions, - new TestAccessHost(sp.fs().getModulePath(), [libraryRoot]) + new TestAccessHost(sp.fs().getModulePath(), [Uri.file(libraryRoot)]) ); importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()).forEach((path) => assert(path)); }); @@ -421,21 +438,25 @@ describe('Import tests that can run with or without a true venv', () => { ]; const sp = createServiceProviderFromFiles(files); - const configOptions = new ConfigOptions(''); // Empty, like open-file mode. + const configOptions = new ConfigOptions(Uri.empty()); // Empty, like open-file mode. const importResolver = new ImportResolver( sp, configOptions, - new TestAccessHost(sp.fs().getModulePath(), [libraryRoot]) + new TestAccessHost(sp.fs().getModulePath(), [Uri.file(libraryRoot)]) ); const importRoots = importResolver.getImportRoots(configOptions.getDefaultExecEnvironment()); assert.strictEqual( 1, - importRoots.filter((f) => f === combinePaths('/', typeshedFallback, 'stubs', 'aLib')).length + importRoots.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'aLib') + ).length ); assert.strictEqual( 1, - importRoots.filter((f) => f === combinePaths('/', typeshedFallback, 'stubs', 'bLib')).length + importRoots.filter( + (f) => !f.isEmpty() && f.getFilePath() === combinePaths('/', typeshedFallback, 'stubs', 'bLib') + ).length ); }); @@ -453,7 +474,10 @@ describe('Import tests that can run with or without a true venv', () => { const importResult = getImportResult(files, ['file1']); assert(importResult.isImportFound); - assert.strictEqual(1, importResult.resolvedPaths.filter((f) => f === combinePaths('/', 'file1.py')).length); + assert.strictEqual( + 1, + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/', 'file1.py')).length + ); }); test('import side by side file sub folder', () => { @@ -470,7 +494,10 @@ describe('Import tests that can run with or without a true venv', () => { const importResult = getImportResult(files, ['file1']); assert(importResult.isImportFound); - assert.strictEqual(1, importResult.resolvedPaths.filter((f) => f === combinePaths('/test', 'file1.py')).length); + assert.strictEqual( + 1, + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/test', 'file1.py')).length + ); }); test('import side by side file sub under src folder', () => { @@ -489,7 +516,7 @@ describe('Import tests that can run with or without a true venv', () => { assert(importResult.isImportFound); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths('/src/nested', 'file1.py')).length + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')).length ); }); @@ -509,7 +536,7 @@ describe('Import tests that can run with or without a true venv', () => { assert(importResult.isImportFound); assert.strictEqual( 1, - importResult.resolvedPaths.filter((f) => f === combinePaths('/src/nested', 'file1.py')).length + importResult.resolvedUris.filter((f) => f.getFilePath() === combinePaths('/src/nested', 'file1.py')).length ); }); @@ -521,7 +548,7 @@ describe('Import tests that can run with or without a true venv', () => { }, ]; - const importResult = getImportResult(files, ['notExist'], (c) => (c.projectRoot = '')); + const importResult = getImportResult(files, ['notExist'], (c) => (c.projectRoot = Uri.empty())); assert(!importResult.isImportFound); }); @@ -542,7 +569,10 @@ describe('Import tests that can run with or without a true venv', () => { ]; const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [combinePaths('/', 'packages1'), combinePaths('/', 'packages2')]; + config.defaultExtraPaths = [ + Uri.file(combinePaths('/', 'packages1')), + Uri.file(combinePaths('/', 'packages2')), + ]; }); assert(importResult.isImportFound); }); @@ -564,7 +594,10 @@ describe('Import tests that can run with or without a true venv', () => { ]; const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [combinePaths('/', 'packages1'), combinePaths('/', 'packages2')]; + config.defaultExtraPaths = [ + Uri.file(combinePaths('/', 'packages1')), + Uri.file(combinePaths('/', 'packages2')), + ]; }); assert(importResult.isImportFound); }); @@ -582,7 +615,10 @@ describe('Import tests that can run with or without a true venv', () => { ]; const importResult = getImportResult(files, ['a', 'b', 'c', 'd'], (config) => { - config.defaultExtraPaths = [combinePaths('/', 'packages1'), combinePaths('/', 'packages2')]; + config.defaultExtraPaths = [ + Uri.file(combinePaths('/', 'packages1')), + Uri.file(combinePaths('/', 'packages2')), + ]; }); assert(!importResult.isImportFound); }); @@ -608,7 +644,10 @@ describe('Import tests that can run with or without a true venv', () => { ]; const importResult = getImportResult(files, ['a', 'b', 'c'], (config) => { - config.defaultExtraPaths = [combinePaths('/', 'packages1'), combinePaths('/', 'packages2')]; + config.defaultExtraPaths = [ + Uri.file(combinePaths('/', 'packages1')), + Uri.file(combinePaths('/', 'packages2')), + ]; }); assert(!importResult.isImportFound); }); @@ -635,7 +674,8 @@ function getImportResult( nameParts: string[], setup?: (c: ConfigOptions) => void ) { - const defaultHostFactory = (sp: ServiceProvider) => new TestAccessHost(sp.fs().getModulePath(), [libraryRoot]); + const defaultHostFactory = (sp: ServiceProvider) => + new TestAccessHost(sp.fs().getModulePath(), [Uri.file(libraryRoot)]); const defaultSetup = setup ?? ((c) => { @@ -652,16 +692,24 @@ function getImportResult( if (process.env.CI_IMPORT_TEST_VENVPATH) { configModifier = (c: ConfigOptions) => { defaultSetup(c); - c.venvPath = process.env.CI_IMPORT_TEST_VENVPATH; + c.venvPath = Uri.file( + process.env.CI_IMPORT_TEST_VENVPATH!, + /* isCaseSensitive */ true, + /* checkRelative */ true + ); c.venv = process.env.CI_IMPORT_TEST_VENV; }; spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); } else if (process.env.CI_IMPORT_TEST_PYTHONPATH) { configModifier = (c: ConfigOptions) => { defaultSetup(c); - c.pythonPath = process.env.CI_IMPORT_TEST_PYTHONPATH; + c.pythonPath = Uri.file( + process.env.CI_IMPORT_TEST_PYTHONPATH!, + /* isCaseSensitive */ true, + /* checkRelative */ true + ); }; - hostFactory = (sp: ServiceProvider) => new TruePythonTestAccessHost(); + hostFactory = (sp: ServiceProvider) => new TruePythonTestAccessHost(sp); spFactory = (files: { path: string; content: string }[]) => createServiceProviderWithCombinedFs(files); } @@ -676,7 +724,7 @@ function getImportResultImpl( hostFactory: (sp: ServiceProvider) => Host ) { const sp = spFactory(files); - const configOptions = new ConfigOptions(normalizeSlashes('/')); + const configOptions = new ConfigOptions(Uri.file('/')); configModifier(configOptions); const file = files.length > 0 ? files[files.length - 1].path : combinePaths('src', 'file.py'); @@ -687,13 +735,20 @@ function getImportResultImpl( }); } + const uri = Uri.file(file); const importResolver = new ImportResolver(sp, configOptions, hostFactory(sp)); - const importResult = importResolver.resolveImport(file, configOptions.findExecEnvironment(file), { + const importResult = importResolver.resolveImport(uri, configOptions.findExecEnvironment(uri), { leadingDots: 0, nameParts: nameParts, importedSymbols: new Set(), }); + // Add the config venvpath to the import result so we can output it on failure. + if (!importResult.isImportFound) { + importResult.importFailureInfo = importResult.importFailureInfo ?? []; + importResult.importFailureInfo.push(`venvPath: ${configOptions.venvPath}`); + } + return importResult; } function createTestFileSystem(files: { path: string; content: string }[]): TestFileSystem { @@ -704,7 +759,7 @@ function createTestFileSystem(files: { path: string; content: string }[]): TestF const dir = getDirectoryPath(path); fs.mkdirpSync(dir); - fs.writeFileSync(path, file.content); + fs.writeFileSync(Uri.file(path), file.content); } return fs; @@ -720,8 +775,11 @@ function createServiceProviderWithCombinedFs(files: { path: string; content: str } class TruePythonTestAccessHost extends FullAccessHost { - constructor() { - super(createFromRealFileSystem()); + constructor(sp: ServiceProvider) { + // Make sure the service provide in use is using a real file system + const clone = sp.clone(); + clone.add(ServiceKeys.fs, createFromRealFileSystem()); + super(clone); } } @@ -730,123 +788,123 @@ class CombinedFileSystem implements FileSystem { constructor(private _testFS: FileSystem) {} - mkdirSync(path: string, options?: MkDirOptions | undefined): void { + get isCaseSensitive(): boolean { + return this._testFS.isCaseSensitive; + } + + mkdirSync(path: Uri, options?: MkDirOptions | undefined): void { this._testFS.mkdirSync(path, options); } - writeFileSync(path: string, data: string | Buffer, encoding: BufferEncoding | null): void { + writeFileSync(path: Uri, data: string | Buffer, encoding: BufferEncoding | null): void { this._testFS.writeFileSync(path, data, encoding); } - unlinkSync(path: string): void { + unlinkSync(path: Uri): void { this._testFS.unlinkSync(path); } - rmdirSync(path: string): void { + rmdirSync(path: Uri): void { this._testFS.rmdirSync(path); } - createFileSystemWatcher(paths: string[], listener: FileWatcherEventHandler): FileWatcher { + createFileSystemWatcher(paths: Uri[], listener: FileWatcherEventHandler): FileWatcher { return this._testFS.createFileSystemWatcher(paths, listener); } - createReadStream(path: string): ReadStream { + createReadStream(path: Uri): ReadStream { return this._testFS.createReadStream(path); } - createWriteStream(path: string): WriteStream { + createWriteStream(path: Uri): WriteStream { return this._testFS.createWriteStream(path); } - copyFileSync(src: string, dst: string): void { + copyFileSync(src: Uri, dst: Uri): void { this._testFS.copyFileSync(src, dst); } - existsSync(path: string): boolean { + existsSync(path: Uri): boolean { return this._testFS.existsSync(path) || this._realFS.existsSync(path); } - chdir(path: string): void { + chdir(path: Uri): void { this._testFS.chdir(path); } - readdirEntriesSync(path: string): Dirent[] { + readdirEntriesSync(path: Uri): Dirent[] { if (this._testFS.existsSync(path)) { return this._testFS.readdirEntriesSync(path); } return this._realFS.readdirEntriesSync(path); } - readdirSync(path: string): string[] { + readdirSync(path: Uri): string[] { if (this._testFS.existsSync(path)) { return this._testFS.readdirSync(path); } return this._realFS.readdirSync(path); } - readFileSync(path: string, encoding?: null): Buffer; - readFileSync(path: string, encoding: BufferEncoding): string; - readFileSync(path: string, encoding?: BufferEncoding | null): string | Buffer; - readFileSync(path: string, encoding: BufferEncoding | null = null) { + readFileSync(path: Uri, encoding?: null): Buffer; + readFileSync(path: Uri, encoding: BufferEncoding): string; + readFileSync(path: Uri, encoding?: BufferEncoding | null): string | Buffer; + readFileSync(path: Uri, encoding: BufferEncoding | null = null) { if (this._testFS.existsSync(path)) { return this._testFS.readFileSync(path, encoding); } return this._realFS.readFileSync(path, encoding); } - statSync(path: string): Stats { + statSync(path: Uri): Stats { if (this._testFS.existsSync(path)) { return this._testFS.statSync(path); } return this._realFS.statSync(path); } - realpathSync(path: string): string { + realpathSync(path: Uri): Uri { if (this._testFS.existsSync(path)) { return this._testFS.realpathSync(path); } return this._realFS.realpathSync(path); } - getModulePath(): string { + getModulePath(): Uri { return this._testFS.getModulePath(); } - readFile(path: string): Promise { + readFile(path: Uri): Promise { if (this._testFS.existsSync(path)) { return this._testFS.readFile(path); } return this._realFS.readFile(path); } - readFileText(path: string, encoding?: BufferEncoding | undefined): Promise { + readFileText(path: Uri, encoding?: BufferEncoding | undefined): Promise { if (this._testFS.existsSync(path)) { return this._testFS.readFileText(path, encoding); } return this._realFS.readFileText(path, encoding); } - realCasePath(path: string): string { + realCasePath(path: Uri): Uri { return this._testFS.realCasePath(path); } - isMappedFilePath(filepath: string): boolean { - return this._testFS.isMappedFilePath(filepath); + isMappedUri(filepath: Uri): boolean { + return this._testFS.isMappedUri(filepath); } - getOriginalFilePath(mappedFilePath: string): string { - return this._testFS.getOriginalFilePath(mappedFilePath); + getOriginalUri(mappedFilePath: Uri): Uri { + return this._testFS.getOriginalUri(mappedFilePath); } - getMappedFilePath(originalFilepath: string): string { - return this._testFS.getMappedFilePath(originalFilepath); + getMappedUri(originalFilePath: Uri): Uri { + return this._testFS.getMappedUri(originalFilePath); } - getUri(path: string): string { - return this._testFS.getUri(path); - } - - isInZip(path: string): boolean { + isInZip(path: Uri): boolean { return this._testFS.isInZip(path); } } diff --git a/packages/pyright-internal/src/tests/importStatementUtils.test.ts b/packages/pyright-internal/src/tests/importStatementUtils.test.ts index 5e2369724..3141a4a02 100644 --- a/packages/pyright-internal/src/tests/importStatementUtils.test.ts +++ b/packages/pyright-internal/src/tests/importStatementUtils.test.ts @@ -21,9 +21,9 @@ import { import { findNodeByOffset } from '../analyzer/parseTreeUtils'; import { isArray } from '../common/core'; import { TextEditAction } from '../common/editAction'; -import { combinePaths, getDirectoryPath } from '../common/pathUtils'; import { convertOffsetToPosition } from '../common/positionUtils'; import { rangesAreEqual } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { NameNode } from '../parser/parseNodes'; import { Range } from './harness/fourslash/fourSlashTypes'; import { parseAndGetTestState, TestState } from './harness/fourslash/testState'; @@ -417,12 +417,12 @@ test('getRelativeModuleName over fake file', () => { `; const state = parseAndGetTestState(code).state; - const dest = state.getMarkerByName('dest')!.fileName; + const dest = Uri.file(state.getMarkerByName('dest')!.fileName); assert.strictEqual( getRelativeModuleName( state.fs, - combinePaths(getDirectoryPath(dest), 'source.py'), + dest.getDirectory().combinePaths('source.py'), dest, /*ignoreFolderStructure*/ false, /*sourceIsFile*/ true @@ -453,12 +453,13 @@ test('resolve alias of not needed file', () => { const evaluator = state.workspace.service.getEvaluator()!; state.openFile(marker.fileName); - const parseResults = state.workspace.service.getParseResult(marker.fileName)!; + const markerUri = Uri.file(marker.fileName); + const parseResults = state.workspace.service.getParseResult(markerUri)!; const nameNode = findNodeByOffset(parseResults.parseTree, marker.position) as NameNode; const aliasDecls = evaluator.getDeclarationsForNameNode(nameNode)!; // Unroot the file. we can't explicitly close the file since it will unload the file from test program. - state.workspace.service.test_program.getSourceFileInfo(marker.fileName)!.isOpenByClient = false; + state.workspace.service.test_program.getSourceFileInfo(markerUri)!.isOpenByClient = false; const unresolved = evaluator.resolveAliasDeclaration(aliasDecls[0], /*resolveLocalNames*/ false); assert(!unresolved); @@ -473,8 +474,8 @@ test('resolve alias of not needed file', () => { function testRelativeModuleName(code: string, expected: string, ignoreFolderStructure = false) { const state = parseAndGetTestState(code).state; - const src = state.getMarkerByName('src')!.fileName; - const dest = state.getMarkerByName('dest')!.fileName; + const src = Uri.file(state.getMarkerByName('src')!.fileName); + const dest = Uri.file(state.getMarkerByName('dest')!.fileName); assert.strictEqual(getRelativeModuleName(state.fs, src, dest, ignoreFolderStructure), expected); } @@ -487,7 +488,7 @@ function testAddition( ) { const state = parseAndGetTestState(code).state; const marker = state.getMarkerByName(markerName)!; - const parseResults = state.program.getBoundSourceFile(marker!.fileName)!.getParseResults()!; + const parseResults = state.program.getBoundSourceFile(Uri.file(marker!.fileName))!.getParseResults()!; const importStatement = getTopLevelImports(parseResults.parseTree).orderedImports.find( (i) => i.moduleName === moduleName @@ -507,7 +508,7 @@ function testInsertions( ) { const state = parseAndGetTestState(code).state; const marker = state.getMarkerByName(markerName)!; - const parseResults = state.program.getBoundSourceFile(marker!.fileName)!.getParseResults()!; + const parseResults = state.program.getBoundSourceFile(Uri.file(marker!.fileName))!.getParseResults()!; const importStatements = getTopLevelImports(parseResults.parseTree); const edits = getTextEditsForAutoImportInsertions( diff --git a/packages/pyright-internal/src/tests/ipythonMode.test.ts b/packages/pyright-internal/src/tests/ipythonMode.test.ts index adf8d3eaf..d93ca989e 100644 --- a/packages/pyright-internal/src/tests/ipythonMode.test.ts +++ b/packages/pyright-internal/src/tests/ipythonMode.test.ts @@ -12,6 +12,7 @@ import { CompletionItemKind, MarkupKind } from 'vscode-languageserver-types'; import { DiagnosticRule } from '../common/diagnosticRules'; import { TextRange } from '../common/textRange'; import { TextRangeCollection } from '../common/textRangeCollection'; +import { Uri } from '../common/uri/uri'; import { Localizer } from '../localization/localize'; import { Comment, CommentType, Token } from '../parser/tokenizerTypes'; import { parseAndGetTestState } from './harness/fourslash/testState'; @@ -263,7 +264,7 @@ test('top level await raises errors in regular mode', () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(diagnostics?.some((d) => d.message === Localizer.Diagnostic.awaitNotInAsync())); @@ -281,7 +282,7 @@ test('top level await raises no errors in ipython mode', () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(!diagnostics?.some((d) => d.message === Localizer.Diagnostic.awaitNotInAsync())); @@ -300,7 +301,7 @@ test('await still raises errors when used in wrong context in ipython mode', () const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(diagnostics?.some((d) => d.message === Localizer.Diagnostic.awaitNotInAsync())); @@ -319,7 +320,7 @@ test('top level async for raises errors in regular mode', () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(diagnostics?.some((d) => d.message === Localizer.Diagnostic.asyncNotInAsyncFunction())); @@ -339,7 +340,7 @@ test('top level async for raises no errors in ipython mode', () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(!diagnostics?.some((d) => d.message === Localizer.Diagnostic.asyncNotInAsyncFunction())); @@ -357,7 +358,7 @@ test('top level async for in list comprehension raises errors in regular mode', const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(diagnostics?.some((d) => d.message === Localizer.Diagnostic.asyncNotInAsyncFunction())); @@ -376,7 +377,7 @@ test('top level async for in list comprehension raises no errors in ipython mode const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(!diagnostics?.some((d) => d.message === Localizer.Diagnostic.asyncNotInAsyncFunction())); @@ -395,7 +396,7 @@ test('top level async with raises errors in regular mode', () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(diagnostics?.some((d) => d.message === Localizer.Diagnostic.asyncNotInAsyncFunction())); @@ -415,7 +416,7 @@ test('top level async with raises no errors in ipython mode', () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert(!diagnostics?.some((d) => d.message === Localizer.Diagnostic.asyncNotInAsyncFunction())); @@ -474,7 +475,7 @@ function testIPython(code: string, expectMagic = true) { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const results = state.program.getBoundSourceFile(range.fileName)!.getParseResults()!; + const results = state.program.getBoundSourceFile(Uri.file(range.fileName))!.getParseResults()!; const text = results.text.substring(range.pos, range.end); const type = getCommentType(text); @@ -567,7 +568,7 @@ function verifyAnalysisDiagnosticCount(code: string, expectedCount: number, expe state.analyze(); const range = state.getRangeByMarkerName('marker')!; - const source = state.program.getBoundSourceFile(range.fileName)!; + const source = state.program.getBoundSourceFile(Uri.file(range.fileName))!; const diagnostics = source.getDiagnostics(state.configOptions); assert.strictEqual(diagnostics?.length, expectedCount); diff --git a/packages/pyright-internal/src/tests/logger.test.ts b/packages/pyright-internal/src/tests/logger.test.ts index 82835280e..ed6573a3a 100644 --- a/packages/pyright-internal/src/tests/logger.test.ts +++ b/packages/pyright-internal/src/tests/logger.test.ts @@ -12,6 +12,7 @@ import { ConfigOptions } from '../common/configOptions'; import { ConsoleInterface, ConsoleWithLogLevel, LogLevel } from '../common/console'; import { test_setDebugMode } from '../common/core'; import { timingStats } from '../common/timing'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; class TestConsole implements ConsoleInterface { @@ -44,7 +45,7 @@ class TestConsole implements ConsoleInterface { describe('TypeEvaluatorWithTracker tests', () => { const consoleInterface = new TestConsole(); const console = new ConsoleWithLogLevel(consoleInterface); - const config = new ConfigOptions('.'); + const config = new ConfigOptions(Uri.empty()); beforeEach(() => { consoleInterface.clear(); diff --git a/packages/pyright-internal/src/tests/parseTreeUtils.test.ts b/packages/pyright-internal/src/tests/parseTreeUtils.test.ts index 8c93db78a..32d758b5b 100644 --- a/packages/pyright-internal/src/tests/parseTreeUtils.test.ts +++ b/packages/pyright-internal/src/tests/parseTreeUtils.test.ts @@ -25,6 +25,7 @@ import { printExpression, } from '../analyzer/parseTreeUtils'; import { TextRange, rangesAreEqual } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; import { MemberAccessNode, NameNode, ParseNodeType, StringNode, isExpressionNode } from '../parser/parseNodes'; import { TestState, getNodeAtMarker, getNodeForRange, parseAndGetTestState } from './harness/fourslash/testState'; @@ -320,7 +321,7 @@ test('printExpression', () => { function testNodeRange(state: TestState, markerName: string, type: ParseNodeType, includeTrailingBlankLines = false) { const range = state.getRangeByMarkerName(markerName)!; - const sourceFile = state.program.getBoundSourceFile(range.marker!.fileName)!; + const sourceFile = state.program.getBoundSourceFile(Uri.file(range.marker!.fileName))!; const statementNode = getFirstAncestorOrSelfOfKind(getNodeAtMarker(state, markerName), type)!; const statementRange = getFullStatementRange(statementNode, sourceFile.getParseResults()!, { diff --git a/packages/pyright-internal/src/tests/pathUtils.test.ts b/packages/pyright-internal/src/tests/pathUtils.test.ts index 871f2064e..fefaeda7b 100644 --- a/packages/pyright-internal/src/tests/pathUtils.test.ts +++ b/packages/pyright-internal/src/tests/pathUtils.test.ts @@ -8,44 +8,31 @@ */ import assert from 'assert'; -import * as nodefs from 'fs-extra'; -import * as os from 'os'; import * as path from 'path'; -import { expandPathVariables } from '../common/envVarUtils'; import { - changeAnyExtension, combinePathComponents, combinePaths, containsPath, - convertUriToPath, - deduplicateFolders, ensureTrailingDirectorySeparator, getAnyExtensionFromPath, getBaseFileName, - getDirectoryPath, getFileExtension, getFileName, getPathComponents, getRelativePath, - getRelativePathFromDirectory, getRootLength, getWildcardRegexPattern, getWildcardRoot, hasTrailingDirectorySeparator, isDirectoryWildcardPatternPresent, - isFileSystemCaseSensitiveInternal, isRootedDiskPath, - isUri, normalizeSlashes, - realCasePath, reducePathComponents, resolvePaths, stripFileExtension, stripTrailingDirectorySeparator, } from '../common/pathUtils'; -import { createFromRealFileSystem } from '../common/realFileSystem'; -import * as vfs from './harness/vfs/filesystem'; test('getPathComponents1', () => { const components = getPathComponents(''); @@ -94,8 +81,8 @@ test('getPathComponents6', () => { test('getPathComponents7', () => { const components = getPathComponents('ab:cdef/test'); assert.equal(components.length, 3); - assert.equal(components[0], 'ab:'); - assert.equal(components[1], 'cdef'); + assert.equal(components[0], ''); + assert.equal(components[1], 'ab:cdef'); assert.equal(components[2], 'test'); }); @@ -111,7 +98,7 @@ test('combinePaths2', () => { test('combinePaths3', () => { const p = combinePaths('untitled:foo', 'ab:c'); - assert.equal(p, normalizeSlashes('untitled:foo/ab%3Ac')); + assert.equal(p, normalizeSlashes('untitled:foo/ab:c')); }); test('ensureTrailingDirectorySeparator1', () => { @@ -267,32 +254,6 @@ test('resolvePath2', () => { assert.equal(resolvePaths('/path', 'to', '..', 'from', 'file.ext/'), normalizeSlashes('/path/from/file.ext/')); }); -test('resolvePath3 ~ escape', () => { - const homedir = os.homedir(); - assert.equal( - resolvePaths(expandPathVariables('', '~/path'), 'to', '..', 'from', 'file.ext/'), - normalizeSlashes(`${homedir}/path/from/file.ext/`) - ); -}); - -test('resolvePath4 ~ escape in middle', () => { - const homedir = os.homedir(); - assert.equal( - resolvePaths('/path', expandPathVariables('', '~/file.ext/')), - normalizeSlashes(`${homedir}/file.ext/`) - ); -}); - -test('invalid ~ without root', () => { - const path = combinePaths('Library', 'Mobile Documents', 'com~apple~CloudDocs', 'Development', 'mysuperproject'); - assert.equal(resolvePaths(expandPathVariables('/src', path)), path); -}); - -test('invalid ~ with root', () => { - const path = combinePaths('/', 'Library', 'com~apple~CloudDocs', 'Development', 'mysuperproject'); - assert.equal(resolvePaths(expandPathVariables('/src', path)), path); -}); - test('containsPath1', () => { assert.equal(containsPath('/a/b/c/', '/a/d/../b/c/./d'), true); }); @@ -305,22 +266,6 @@ test('containsPath3', () => { assert.equal(containsPath('/a', '/A/B', true), true); }); -test('changeAnyExtension1', () => { - assert.equal(changeAnyExtension('/path/to/file.ext', '.js', ['.ext', '.ts'], true), '/path/to/file.js'); -}); - -test('changeAnyExtension2', () => { - assert.equal(changeAnyExtension('/path/to/file.ext', '.js'), '/path/to/file.js'); -}); - -test('changeAnyExtension3', () => { - assert.equal(changeAnyExtension('/path/to/file.ext', '.js', '.ts', false), '/path/to/file.ext'); -}); - -test('changeAnyExtension1', () => { - assert.equal(getAnyExtensionFromPath('/path/to/file.ext'), '.ext'); -}); - test('changeAnyExtension2', () => { assert.equal(getAnyExtensionFromPath('/path/to/file.ext', '.ts', true), ''); }); @@ -345,14 +290,6 @@ test('getBaseFileName4', () => { assert.equal(getBaseFileName('/path/to/file.ext', ['.ext'], true), 'file'); }); -test('getRelativePathFromDirectory1', () => { - assert.equal(getRelativePathFromDirectory('/a', '/a/b/c/d', true), normalizeSlashes('b/c/d')); -}); - -test('getRelativePathFromDirectory2', () => { - assert.equal(getRelativePathFromDirectory('/a', '/b/c/d', true), normalizeSlashes('../b/c/d')); -}); - test('getRootLength1', () => { assert.equal(getRootLength('a'), 0); }); @@ -381,14 +318,6 @@ test('getRootLength7', () => { assert.equal(getRootLength(fixSeparators('//server/share')), 9); }); -test('getRootLength8', () => { - assert.equal(getRootLength('scheme:/no/authority'), 7); -}); - -test('getRootLength9', () => { - assert.equal(getRootLength('scheme://with/authority'), 9); -}); - test('isRootedDiskPath1', () => { assert(isRootedDiskPath(normalizeSlashes('C:/a/b'))); }); @@ -423,121 +352,3 @@ test('getRelativePath', () => { normalizeSlashes('./d/e/f') ); }); - -test('getDirectoryPath', () => { - assert.equal(getDirectoryPath(normalizeSlashes('/a/b/c/d/e/f')), normalizeSlashes('/a/b/c/d/e')); - assert.equal( - getDirectoryPath(normalizeSlashes('untitled:/a/b/c/d/e/f?query#frag')), - normalizeSlashes('untitled:/a/b/c/d/e') - ); -}); - -test('isUri', () => { - assert.ok(isUri('untitled:/a/b/c/d/e/f?query#frag')); - assert.ok(isUri('untitled:/a/b/c/d/e/f')); - assert.ok(isUri('untitled:a/b/c/d/e/f')); - assert.ok(isUri('untitled:/a/b/c/d/e/f?query')); - assert.ok(isUri('untitled:/a/b/c/d/e/f#frag')); - assert.ok(!isUri('c:/foo/bar')); - assert.ok(!isUri('c:/foo#/bar')); - assert.ok(!isUri('222/dd:/foo/bar')); -}); -test('CaseSensitivity', () => { - const cwd = normalizeSlashes('/'); - - const fsCaseInsensitive = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); - assert.equal(isFileSystemCaseSensitiveInternal(fsCaseInsensitive, fsCaseInsensitive), false); - - const fsCaseSensitive = new vfs.TestFileSystem(/*ignoreCase*/ false, { cwd }); - assert.equal(isFileSystemCaseSensitiveInternal(fsCaseSensitive, fsCaseSensitive), true); -}); - -test('deduplicateFolders', () => { - const listOfFolders = [ - ['/user', '/user/temp', '/xuser/app', '/lib/python', '/home/p/.venv/lib/site-packages'], - ['/user', '/user/temp', '/xuser/app', '/lib/python/Python310.zip', '/home/z/.venv/lib/site-packages'], - ['/main/python/lib/site-packages', '/home/p'], - ]; - - const folders = deduplicateFolders(listOfFolders); - - const expected = [ - '/user', - '/xuser/app', - '/lib/python', - '/home/z/.venv/lib/site-packages', - '/main/python/lib/site-packages', - '/home/p', - ]; - - assert.deepStrictEqual(folders.sort(), expected.sort()); -}); - -test('convert UNC path', () => { - const cwd = normalizeSlashes('/'); - const fs = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); - - const path = convertUriToPath(fs, 'file://server/c$/folder/file.py'); - - // When converting UNC path, server part shouldn't be removed. - assert(path.indexOf('server') > 0); -}); - -test('Realcase', () => { - const fs = createFromRealFileSystem(); - const cwd = process.cwd(); - const dir = path.join(cwd, 'src', 'tests', '..', 'tests'); - const entries = nodefs.readdirSync(dir).map((entry) => path.basename(nodefs.realpathSync(path.join(dir, entry)))); - const fsentries = fs.readdirSync(dir); - assert.deepStrictEqual(entries, fsentries); - - const paths = entries.map((entry) => nodefs.realpathSync(path.join(dir, entry))); - const fspaths = fsentries.map((entry) => fs.realCasePath(path.join(dir, entry))); - assert.deepStrictEqual(paths, fspaths); - - // Check that the '..' has been removed. - assert.ok(!fspaths.some((p) => p.indexOf('..') >= 0)); - - // If windows, check that the case is correct. - if (process.platform === 'win32') { - for (const p of fspaths) { - const upper = p.toUpperCase(); - const real = fs.realCasePath(upper); - assert.strictEqual(p, real); - } - } -}); - -test('Realcase use cwd implicitly', () => { - const fs = createFromRealFileSystem(); - const empty = realCasePath('', fs); - assert.deepStrictEqual(empty, ''); - const cwd = process.cwd(); - const dir = path.join(cwd, 'src', 'tests'); - - const entries = nodefs.readdirSync(dir).map((entry) => path.basename(nodefs.realpathSync(path.join(dir, entry)))); - const fsentries = fs.readdirSync(path.join('src', 'tests')); - const paths = entries.map((entry) => nodefs.realpathSync(path.join(dir, entry))); - const fspaths = fsentries.map((entry) => fs.realCasePath(path.join(dir, entry))); - assert.deepStrictEqual(paths, fspaths); -}); - -test('Realcase drive letter', () => { - const fs = createFromRealFileSystem(); - - const cwd = process.cwd(); - - assert.strictEqual( - getDriveLetter(fs.realCasePath(cwd)), - getDriveLetter(fs.realCasePath(combinePaths(cwd.toLowerCase(), 'notExist.txt'))) - ); - - function getDriveLetter(path: string) { - const driveLetter = getRootLength(path); - if (driveLetter === 0) { - return ''; - } - - return path.substring(0, driveLetter); - } -}); diff --git a/packages/pyright-internal/src/tests/pyrightFileSystem.test.ts b/packages/pyright-internal/src/tests/pyrightFileSystem.test.ts index 05710d4f2..c1d807982 100644 --- a/packages/pyright-internal/src/tests/pyrightFileSystem.test.ts +++ b/packages/pyright-internal/src/tests/pyrightFileSystem.test.ts @@ -8,10 +8,12 @@ import assert from 'assert'; import { lib, sitePackages } from '../common/pathConsts'; import { combinePaths, getDirectoryPath, normalizeSlashes } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; import { PyrightFileSystem } from '../pyrightFileSystem'; import { TestFileSystem } from './harness/vfs/filesystem'; const libraryRoot = combinePaths(normalizeSlashes('/'), lib, sitePackages); +const libraryRootUri = Uri.file(libraryRoot); test('virtual file exists', () => { const files = [ @@ -34,24 +36,24 @@ test('virtual file exists', () => { ]; const fs = createFileSystem(files); - fs.processPartialStubPackages([libraryRoot], [libraryRoot]); + fs.processPartialStubPackages([libraryRootUri], [libraryRootUri]); - const stubFile = combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'); + const stubFile = libraryRootUri.combinePaths('myLib', 'partialStub.pyi'); assert(fs.existsSync(stubFile)); - assert(fs.isMappedFilePath(stubFile)); + assert(fs.isMappedUri(stubFile)); - const myLib = combinePaths(libraryRoot, 'myLib'); + const myLib = libraryRootUri.combinePaths('myLib'); const entries = fs.readdirEntriesSync(myLib); assert.strictEqual(3, entries.length); - const subDirFile = combinePaths(libraryRoot, 'myLib', 'subdir', '__init__.pyi'); + const subDirFile = libraryRootUri.combinePaths('myLib', 'subdir', '__init__.pyi'); assert(fs.existsSync(subDirFile)); - assert(fs.isMappedFilePath(subDirFile)); + assert(fs.isMappedUri(subDirFile)); const fakeFile = entries.filter((e) => e.name.endsWith('.pyi'))[0]; assert(fakeFile.isFile()); - assert(!fs.existsSync(combinePaths(libraryRoot, 'myLib-stubs'))); + assert(!fs.existsSync(libraryRootUri.combinePaths('myLib-stubs'))); }); test('virtual file coexists with real', () => { @@ -79,26 +81,27 @@ test('virtual file coexists with real', () => { ]; const fs = createFileSystem(files); - fs.processPartialStubPackages([libraryRoot], [libraryRoot]); + fs.processPartialStubPackages([libraryRootUri], [libraryRootUri]); - const stubFile = combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'); + const stubFile = libraryRootUri.combinePaths('myLib', 'partialStub.pyi'); assert(fs.existsSync(stubFile)); - assert(fs.isMappedFilePath(stubFile)); + assert(fs.isMappedUri(stubFile)); - const myLib = combinePaths(libraryRoot, 'myLib'); + const myLib = libraryRootUri.combinePaths('myLib'); const entries = fs.readdirEntriesSync(myLib); assert.strictEqual(3, entries.length); - const subDirFile = combinePaths(libraryRoot, 'myLib', 'subdir', '__init__.py'); + const subDirFile = libraryRootUri.combinePaths('myLib', 'subdir', '__init__.pyi'); assert(fs.existsSync(subDirFile)); - assert(!fs.isMappedFilePath(subDirFile)); - const subDirPyiFile = combinePaths(libraryRoot, 'myLib', 'subdir', '__init__.pyi'); + assert(fs.isMappedUri(subDirFile)); + + const subDirPyiFile = libraryRootUri.combinePaths('myLib', 'subdir', '__init__.pyi'); assert(fs.existsSync(subDirPyiFile)); const fakeFile = entries.filter((e) => e.name.endsWith('.pyi'))[0]; assert(fakeFile.isFile()); - assert(!fs.existsSync(combinePaths(libraryRoot, 'myLib-stubs'))); + assert(!fs.existsSync(libraryRootUri.combinePaths('myLib-stubs'))); }); test('virtual file not exist', () => { @@ -114,17 +117,17 @@ test('virtual file not exist', () => { ]; const fs = createFileSystem(files); - fs.processPartialStubPackages([libraryRoot], [libraryRoot]); + fs.processPartialStubPackages([libraryRootUri], [libraryRootUri]); - assert(!fs.existsSync(combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'))); + assert(!fs.existsSync(libraryRootUri.combinePaths('myLib', 'partialStub.pyi'))); - const myLib = combinePaths(libraryRoot, 'myLib'); + const myLib = libraryRootUri.combinePaths('myLib'); const entries = fs.readdirEntriesSync(myLib); assert.strictEqual(1, entries.length); assert.strictEqual(0, entries.filter((e) => e.name.endsWith('.pyi')).length); - assert(fs.existsSync(combinePaths(libraryRoot, 'myLib-stubs'))); + assert(fs.existsSync(libraryRootUri.combinePaths('myLib-stubs'))); }); test('existing stub file', () => { @@ -148,22 +151,23 @@ test('existing stub file', () => { ]; const fs = createFileSystem(files); - fs.processPartialStubPackages([libraryRoot], [libraryRoot]); + fs.processPartialStubPackages([libraryRootUri], [libraryRootUri]); - const stubFile = combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'); + const stubFile = libraryRootUri.combinePaths('myLib', 'partialStub.pyi'); assert(fs.existsSync(stubFile)); - const myLib = combinePaths(libraryRoot, 'myLib'); + const myLib = libraryRootUri.combinePaths('myLib'); const entries = fs.readdirEntriesSync(myLib); assert.strictEqual(2, entries.length); assert.strictEqual('def test(): ...', fs.readFileSync(stubFile, 'utf8')); - assert(!fs.existsSync(combinePaths(libraryRoot, 'myLib-stubs'))); + assert(!fs.existsSync(libraryRootUri.combinePaths('myLib-stubs'))); }); test('multiple package installed', () => { const extraRoot = combinePaths(normalizeSlashes('/'), lib, 'extra'); + const extraRootUri = Uri.file(extraRoot); const files = [ { path: combinePaths(libraryRoot, 'myLib-stubs', 'partialStub.pyi'), @@ -184,20 +188,21 @@ test('multiple package installed', () => { ]; const fs = createFileSystem(files); - fs.processPartialStubPackages([libraryRoot, extraRoot], [libraryRoot, extraRoot]); + fs.processPartialStubPackages([libraryRootUri, extraRootUri], [libraryRootUri, extraRootUri]); - assert(fs.isPathScanned(libraryRoot)); - assert(fs.isPathScanned(extraRoot)); + assert(fs.isPathScanned(libraryRootUri)); + assert(fs.isPathScanned(extraRootUri)); - assert(fs.existsSync(combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'))); - assert(fs.existsSync(combinePaths(extraRoot, 'myLib', 'partialStub.pyi'))); + assert(fs.existsSync(libraryRootUri.combinePaths('myLib', 'partialStub.pyi'))); + assert(fs.existsSync(extraRootUri.combinePaths('myLib', 'partialStub.pyi'))); - assert.strictEqual(2, fs.readdirEntriesSync(combinePaths(libraryRoot, 'myLib')).length); - assert.strictEqual(2, fs.readdirEntriesSync(combinePaths(extraRoot, 'myLib')).length); + assert.strictEqual(2, fs.readdirEntriesSync(libraryRootUri.combinePaths('myLib')).length); + assert.strictEqual(2, fs.readdirEntriesSync(extraRootUri.combinePaths('myLib')).length); }); test('bundled partial stubs', () => { const bundledPath = combinePaths(normalizeSlashes('/'), 'bundled'); + const bundledPathUri = Uri.file(bundledPath); const files = [ { @@ -219,12 +224,12 @@ test('bundled partial stubs', () => { ]; const fs = createFileSystem(files); - fs.processPartialStubPackages([bundledPath], [libraryRoot], bundledPath); + fs.processPartialStubPackages([bundledPathUri], [libraryRootUri], bundledPathUri); - const stubFile = combinePaths(libraryRoot, 'myLib', 'partialStub.pyi'); + const stubFile = libraryRootUri.combinePaths('myLib', 'partialStub.pyi'); assert(!fs.existsSync(stubFile)); - const myLib = combinePaths(libraryRoot, 'myLib'); + const myLib = libraryRootUri.combinePaths('myLib'); const entries = fs.readdirEntriesSync(myLib); assert.strictEqual(2, entries.length); }); @@ -237,7 +242,7 @@ function createFileSystem(files: { path: string; content: string }[]): PyrightFi const dir = getDirectoryPath(path); fs.mkdirpSync(dir); - fs.writeFileSync(path, file.content); + fs.writeFileSync(Uri.file(path), file.content); } return new PyrightFileSystem(fs); diff --git a/packages/pyright-internal/src/tests/service.test.ts b/packages/pyright-internal/src/tests/service.test.ts index f6870f80a..cf12685f3 100644 --- a/packages/pyright-internal/src/tests/service.test.ts +++ b/packages/pyright-internal/src/tests/service.test.ts @@ -6,16 +6,19 @@ import assert from 'assert'; -import { combinePaths, getDirectoryPath } from '../common/pathUtils'; -import { parseAndGetTestState } from './harness/fourslash/testState'; import { CancellationToken } from 'vscode-jsonrpc'; import { IPythonMode } from '../analyzer/sourceFile'; +import { combinePaths, getDirectoryPath } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; +import { parseAndGetTestState } from './harness/fourslash/testState'; test('random library file changed', () => { const state = parseAndGetTestState('', '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/site-packages/test.py', ['/site-packages']), + state.workspace.service.test_shouldHandleLibraryFileWatchChanges(Uri.file('/site-packages/test.py'), [ + Uri.file('/site-packages'), + ]), true ); }); @@ -24,7 +27,9 @@ test('random library file starting with . changed', () => { const state = parseAndGetTestState('', '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/site-packages/.test.py', ['/site-packages']), + state.workspace.service.test_shouldHandleLibraryFileWatchChanges(Uri.file('/site-packages/.test.py'), [ + Uri.file('/site-packages'), + ]), false ); }); @@ -33,10 +38,10 @@ test('random library file changed, nested search paths', () => { const state = parseAndGetTestState('', '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/lib/.venv/site-packages/myFile.py', [ - '/lib', - '/lib/.venv/site-packages', - ]), + state.workspace.service.test_shouldHandleLibraryFileWatchChanges( + Uri.file('/lib/.venv/site-packages/myFile.py'), + [Uri.file('/lib'), Uri.file('/lib/.venv/site-packages')] + ), true ); }); @@ -49,10 +54,10 @@ test('random library file changed, nested search paths, fs is not case sensitive const state = parseAndGetTestState(code, '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/lib/.venv/site-packages/myFile.py', [ - '/lib', - '/LIB/.venv/site-packages', - ]), + state.workspace.service.test_shouldHandleLibraryFileWatchChanges( + Uri.file('/lib/.venv/site-packages/myFile.py', false), + [Uri.file('/lib', false), Uri.file('/LIB/.venv/site-packages', false)] + ), true ); }); @@ -65,10 +70,10 @@ test('random library file changed, nested search paths, fs is case sensitive', ( const state = parseAndGetTestState(code, '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/lib/.venv/site-packages/myFile.py', [ - '/lib', - '/LIB/.venv/site-packages', - ]), + state.workspace.service.test_shouldHandleLibraryFileWatchChanges( + Uri.file('/lib/.venv/site-packages/myFile.py'), + [Uri.file('/lib'), Uri.file('/LIB/.venv/site-packages')] + ), false ); }); @@ -81,9 +86,9 @@ test('random library file starting with . changed, fs is not case sensitive', () const state = parseAndGetTestState(code, '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/lib/.test.py', [ - '/LIB', - '/lib/site-packages', + state.workspace.service.test_shouldHandleLibraryFileWatchChanges(Uri.file('/lib/.test.py', false), [ + Uri.file('/LIB', false), + Uri.file('/lib/site-packages', false), ]), false ); @@ -97,9 +102,9 @@ test('random library file starting with . changed, fs is case sensitive', () => const state = parseAndGetTestState(code, '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/lib/.test.py', [ - '/LIB', - '/lib/site-packages', + state.workspace.service.test_shouldHandleLibraryFileWatchChanges(Uri.file('/lib/.test.py'), [ + Uri.file('/LIB'), + Uri.file('/lib/site-packages'), ]), true ); @@ -109,9 +114,10 @@ test('random library file under a folder starting with . changed', () => { const state = parseAndGetTestState('', '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleLibraryFileWatchChanges('/site-packages/.testFolder/test.py', [ - '/site-packages', - ]), + state.workspace.service.test_shouldHandleLibraryFileWatchChanges( + Uri.file('/site-packages/.testFolder/test.py'), + [Uri.file('/site-packages')] + ), false ); }); @@ -180,7 +186,7 @@ test('excluded but still part of program', () => { while (state.workspace.service.test_program.analyze()); assert.strictEqual( - state.workspace.service.test_shouldHandleSourceFileWatchChanges(marker.fileName, /* isFile */ true), + state.workspace.service.test_shouldHandleSourceFileWatchChanges(Uri.file(marker.fileName), /* isFile */ true), true ); }); @@ -194,7 +200,7 @@ test('random folder changed', () => { const state = parseAndGetTestState(code, '/projectRoot').state; assert.strictEqual( - state.workspace.service.test_shouldHandleSourceFileWatchChanges('/randomFolder', /* isFile */ false), + state.workspace.service.test_shouldHandleSourceFileWatchChanges(Uri.file('/randomFolder'), /* isFile */ false), false ); }); @@ -296,7 +302,7 @@ test('program containsSourceFileIn', () => { `; const state = parseAndGetTestState(code, '/projectRoot').state; - assert(state.workspace.service.test_program.containsSourceFileIn(state.activeFile.fileName)); + assert(state.workspace.service.test_program.containsSourceFileIn(Uri.file(state.activeFile.fileName))); }); test('service runEditMode', () => { @@ -311,15 +317,16 @@ test('service runEditMode', () => { const state = parseAndGetTestState(code, '/projectRoot').state; const open = state.getMarkerByName('open'); const closed = state.getMarkerByName('closed'); + const openUri = Uri.file(open.fileName); + const closedUri = Uri.file(closed.fileName); - const newFilePath = combinePaths(getDirectoryPath(open.fileName), 'interimFile.py'); - state.testFS.writeFileSync(newFilePath, '# empty', 'utf8'); + const newFileUri = Uri.file(combinePaths(getDirectoryPath(open.fileName), 'interimFile.py')); + state.testFS.writeFileSync(newFileUri, '# empty', 'utf8'); const options = { isTracked: true, ipythonMode: IPythonMode.None, - chainedFilePath: newFilePath, - realFilePath: closed.fileName, + chainedFileUri: newFileUri, }; // try run edit mode @@ -330,34 +337,34 @@ test('service runEditMode', () => { function verifyRunEditMode(value: string) { state.workspace.service.runEditMode((p) => { - p.addInterimFile(newFilePath); - p.setFileOpened(open.fileName, 0, value, options); - p.setFileOpened(closed.fileName, 0, value, options); + p.addInterimFile(newFileUri); + p.setFileOpened(openUri, 0, value, options); + p.setFileOpened(closedUri, 0, value, options); - const interim = p.getSourceFileInfo(newFilePath); + const interim = p.getSourceFileInfo(newFileUri); assert(interim); - const openFile = p.getSourceFileInfo(open.fileName); + const openFile = p.getSourceFileInfo(openUri); assert(openFile); assert(openFile.isOpenByClient); assert.strictEqual(value, openFile.sourceFile.getFileContent()); - const closedFile = p.getSourceFileInfo(closed.fileName); + const closedFile = p.getSourceFileInfo(closedUri); assert(closedFile); assert(closedFile.isOpenByClient); assert.strictEqual(value, closedFile.sourceFile.getFileContent()); }, CancellationToken.None); - const interim = state.workspace.service.test_program.getSourceFileInfo(newFilePath); + const interim = state.workspace.service.test_program.getSourceFileInfo(newFileUri); assert(!interim); - const openFile = state.workspace.service.test_program.getSourceFileInfo(open.fileName); + const openFile = state.workspace.service.test_program.getSourceFileInfo(openUri); assert(openFile); assert(openFile.isOpenByClient); assert.strictEqual('', openFile.sourceFile.getFileContent()?.trim()); - const closedFile = state.workspace.service.test_program.getSourceFileInfo(closed.fileName); + const closedFile = state.workspace.service.test_program.getSourceFileInfo(closedUri); assert(closedFile); assert(!closedFile.isOpenByClient); @@ -371,5 +378,8 @@ function testSourceFileWatchChange(code: string, expected = true, isFile = true) const marker = state.getMarkerByName('marker'); const path = isFile ? marker.fileName : getDirectoryPath(marker.fileName); - assert.strictEqual(state.workspace.service.test_shouldHandleSourceFileWatchChanges(path, isFile), expected); + assert.strictEqual( + state.workspace.service.test_shouldHandleSourceFileWatchChanges(Uri.file(path), isFile), + expected + ); } diff --git a/packages/pyright-internal/src/tests/signatureHelp.test.ts b/packages/pyright-internal/src/tests/signatureHelp.test.ts index 1c7319fd2..8bea82530 100644 --- a/packages/pyright-internal/src/tests/signatureHelp.test.ts +++ b/packages/pyright-internal/src/tests/signatureHelp.test.ts @@ -10,6 +10,7 @@ import assert from 'assert'; import { CancellationToken, MarkupKind } from 'vscode-languageserver'; import { convertOffsetToPosition } from '../common/positionUtils'; +import { Uri } from '../common/uri/uri'; import { SignatureHelpProvider } from '../languageService/signatureHelpProvider'; import { parseAndGetTestState } from './harness/fourslash/testState'; @@ -77,12 +78,12 @@ function checkSignatureHelp(code: string, expects: boolean) { const state = parseAndGetTestState(code).state; const marker = state.getMarkerByName('marker'); - const parseResults = state.workspace.service.getParseResult(marker.fileName)!; + const parseResults = state.workspace.service.getParseResult(Uri.file(marker.fileName))!; const position = convertOffsetToPosition(marker.position, parseResults.tokenizerOutput.lines); const actual = new SignatureHelpProvider( state.workspace.service.test_program, - marker.fileName, + Uri.file(marker.fileName), position, MarkupKind.Markdown, /*hasSignatureLabelOffsetCapability*/ true, diff --git a/packages/pyright-internal/src/tests/sourceFile.test.ts b/packages/pyright-internal/src/tests/sourceFile.test.ts index b709cf744..03d425f58 100644 --- a/packages/pyright-internal/src/tests/sourceFile.test.ts +++ b/packages/pyright-internal/src/tests/sourceFile.test.ts @@ -14,17 +14,18 @@ import { ConfigOptions } from '../common/configOptions'; import { FullAccessHost } from '../common/fullAccessHost'; import { combinePaths } from '../common/pathUtils'; import { createFromRealFileSystem } from '../common/realFileSystem'; -import { parseAndGetTestState } from './harness/fourslash/testState'; import { createServiceProvider } from '../common/serviceProviderExtensions'; +import { Uri } from '../common/uri/uri'; +import { parseAndGetTestState } from './harness/fourslash/testState'; test('Empty', () => { const filePath = combinePaths(process.cwd(), 'tests/samples/test_file1.py'); const fs = createFromRealFileSystem(); const serviceProvider = createServiceProvider(fs); - const sourceFile = new SourceFile(serviceProvider, filePath, '', false, false, { isEditMode: false }); - const configOptions = new ConfigOptions(process.cwd()); + const sourceFile = new SourceFile(serviceProvider, Uri.file(filePath), '', false, false, { isEditMode: false }); + const configOptions = new ConfigOptions(Uri.file(process.cwd())); const sp = createServiceProvider(fs); - const importResolver = new ImportResolver(sp, configOptions, new FullAccessHost(fs)); + const importResolver = new ImportResolver(sp, configOptions, new FullAccessHost(sp)); sourceFile.parse(configOptions, importResolver); }); @@ -39,10 +40,13 @@ test('Empty Open file', () => { const marker = state.getMarkerByName('marker'); assert.strictEqual( - state.workspace.service.test_program.getSourceFile(marker.fileName)?.getFileContent(), + state.workspace.service.test_program.getSourceFile(Uri.file(marker.fileName))?.getFileContent(), '# Content' ); - state.workspace.service.updateOpenFileContents(marker.fileName, 1, ''); - assert.strictEqual(state.workspace.service.test_program.getSourceFile(marker.fileName)?.getFileContent(), ''); + state.workspace.service.updateOpenFileContents(Uri.file(marker.fileName), 1, ''); + assert.strictEqual( + state.workspace.service.test_program.getSourceFile(Uri.file(marker.fileName))?.getFileContent(), + '' + ); }); diff --git a/packages/pyright-internal/src/tests/sourceMapperUtils.test.ts b/packages/pyright-internal/src/tests/sourceMapperUtils.test.ts index 3c85c0be9..6588368aa 100644 --- a/packages/pyright-internal/src/tests/sourceMapperUtils.test.ts +++ b/packages/pyright-internal/src/tests/sourceMapperUtils.test.ts @@ -6,9 +6,27 @@ * Unit tests for pyright sourceMapperUtils module. */ import * as assert from 'assert'; -import { CancellationTokenSource } from 'vscode-jsonrpc'; +import { CancellationToken, CancellationTokenSource } from 'vscode-jsonrpc'; -import { buildImportTree } from '../analyzer/sourceMapperUtils'; +import { buildImportTree as buildImportTreeImpl } from '../analyzer/sourceMapperUtils'; +import { Uri } from '../common/uri/uri'; + +function buildImportTree( + sourceFile: string, + targetFile: string, + importResolver: (f: string) => string[], + token: CancellationToken +): string[] { + return buildImportTreeImpl( + Uri.file(sourceFile), + Uri.file(targetFile), + (from) => { + const resolved = importResolver(from.getFilePath().slice(1)); + return resolved.map((f) => Uri.file(f)); + }, + token + ).map((u) => u.getFilePath().slice(1)); +} describe('BuildImportTree', () => { const tokenSource = new CancellationTokenSource(); diff --git a/packages/pyright-internal/src/tests/testState.test.ts b/packages/pyright-internal/src/tests/testState.test.ts index 2836ea2e8..546414c2e 100644 --- a/packages/pyright-internal/src/tests/testState.test.ts +++ b/packages/pyright-internal/src/tests/testState.test.ts @@ -10,6 +10,7 @@ import assert from 'assert'; import { combinePaths, getFileName, normalizeSlashes } from '../common/pathUtils'; import { compareStringsCaseSensitive } from '../common/stringUtils'; +import { Uri } from '../common/uri/uri'; import { Range } from './harness/fourslash/fourSlashTypes'; import { runFourSlashTestContent } from './harness/fourslash/runner'; import { parseAndGetTestState } from './harness/fourslash/testState'; @@ -44,7 +45,7 @@ test('Multiple files', () => { const state = parseAndGetTestState(code, factory.srcFolder).state; assert.equal(state.cwd(), normalizeSlashes('/')); - assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')))); + assert(state.fs.existsSync(Uri.file(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py'))))); }); test('Configuration', () => { @@ -114,11 +115,11 @@ test('Configuration', () => { const state = parseAndGetTestState(code, factory.srcFolder).state; assert.equal(state.cwd(), normalizeSlashes('/')); - assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py')))); + assert(state.fs.existsSync(Uri.file(normalizeSlashes(combinePaths(factory.srcFolder, 'file1.py'))))); assert.equal(state.configOptions.diagnosticRuleSet.reportMissingImports, 'error'); assert.equal(state.configOptions.diagnosticRuleSet.reportMissingModuleSource, 'warning'); - assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs')); + assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/src/typestubs')); }); test('stubPath configuration', () => { @@ -130,7 +131,7 @@ test('stubPath configuration', () => { `; const state = parseAndGetTestState(code).state; - assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs')); + assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/src/typestubs')); }); test('Duplicated stubPath configuration', () => { @@ -143,7 +144,7 @@ test('Duplicated stubPath configuration', () => { `; const state = parseAndGetTestState(code).state; - assert.equal(state.configOptions.stubPath, normalizeSlashes('/src/typestubs2')); + assert.equal(state.configOptions.stubPath?.getFilePath(), normalizeSlashes('/src/typestubs2')); }); test('ProjectRoot', () => { @@ -159,9 +160,9 @@ test('ProjectRoot', () => { const state = parseAndGetTestState(code).state; assert.equal(state.cwd(), normalizeSlashes('/root')); - assert(state.fs.existsSync(normalizeSlashes('/root/file1.py'))); + assert(state.fs.existsSync(Uri.file(normalizeSlashes('/root/file1.py')))); - assert.equal(state.configOptions.projectRoot, normalizeSlashes('/root')); + assert.equal(state.configOptions.projectRoot.getFilePath(), normalizeSlashes('/root')); }); test('CustomTypeshedFolder', () => { @@ -177,7 +178,7 @@ test('CustomTypeshedFolder', () => { // mount the folder this file is in as typeshed folder and check whether // in typeshed folder in virtual file system, this file exists. const state = parseAndGetTestState(code).state; - assert(state.fs.existsSync(combinePaths(factory.typeshedFolder, getFileName(__filename)))); + assert(state.fs.existsSync(Uri.file(combinePaths(factory.typeshedFolder, getFileName(__filename))))); }); test('IgnoreCase', () => { @@ -192,7 +193,7 @@ test('IgnoreCase', () => { const state = parseAndGetTestState(code, factory.srcFolder).state; - assert(state.fs.existsSync(normalizeSlashes(combinePaths(factory.srcFolder, 'FILE1.py')))); + assert(state.fs.existsSync(Uri.file(normalizeSlashes(combinePaths(factory.srcFolder, 'FILE1.py'))))); }); test('GoToMarker', () => { diff --git a/packages/pyright-internal/src/tests/testStateUtils.ts b/packages/pyright-internal/src/tests/testStateUtils.ts index fdd1dd85f..ff5e87bf6 100644 --- a/packages/pyright-internal/src/tests/testStateUtils.ts +++ b/packages/pyright-internal/src/tests/testStateUtils.ts @@ -7,27 +7,26 @@ */ import assert from 'assert'; -import { WorkspaceEdit } from 'vscode-languageserver-protocol'; -import { createMapFromItems } from '../common/collectionUtils'; -import { assertNever } from '../common/debug'; -import { FileEditAction, FileEditActions, FileOperations } from '../common/editAction'; -import { FileSystem } from '../common/fileSystem'; -import { convertUriToPath, getDirectoryPath, isFile } from '../common/pathUtils'; -import { TextRange, rangesAreEqual } from '../common/textRange'; -import { applyTextEditsToString } from '../common/workspaceEditUtils'; -import { Range } from './harness/fourslash/fourSlashTypes'; -import { TestState } from './harness/fourslash/testState'; -import { CancellationToken, CreateFile, DeleteFile, RenameFile, TextDocumentEdit } from 'vscode-languageserver'; -import { Program } from '../analyzer/program'; -import { ConfigOptions } from '../common/configOptions'; +import { CancellationToken } from 'vscode-languageserver'; import { findNodeByOffset } from '../analyzer/parseTreeUtils'; +import { Program } from '../analyzer/program'; +import { createMapFromItems } from '../common/collectionUtils'; +import { ConfigOptions } from '../common/configOptions'; +import { isArray } from '../common/core'; +import { assertNever } from '../common/debug'; +import { FileEditAction, FileEditActions } from '../common/editAction'; +import { TextRange, rangesAreEqual } from '../common/textRange'; +import { Uri } from '../common/uri/uri'; +import { isFile } from '../common/uri/uriUtils'; +import { applyTextEditsToString } from '../common/workspaceEditUtils'; import { DocumentSymbolCollector } from '../languageService/documentSymbolCollector'; import { NameNode } from '../parser/parseNodes'; -import { isArray } from '../common/core'; +import { Range } from './harness/fourslash/fourSlashTypes'; +import { TestState } from './harness/fourslash/testState'; export function convertFileEditActionToString(edit: FileEditAction): string { - return `'${edit.replacementText.replace(/\n/g, '!n!')}'@'${edit.filePath}:(${edit.range.start.line},${ + return `'${edit.replacementText.replace(/\n/g, '!n!')}'@'${edit.fileUri}:(${edit.range.start.line},${ edit.range.start.character })-(${edit.range.end.line},${edit.range.end.character})'`; } @@ -35,7 +34,7 @@ export function convertFileEditActionToString(edit: FileEditAction): string { export function convertRangeToFileEditAction(state: TestState, range: Range, replacementText?: string): FileEditAction { const data = range.marker?.data as { r: string } | undefined; return { - filePath: range.fileName, + fileUri: Uri.file(range.fileName), replacementText: (replacementText ?? data?.r ?? 'N/A').replace(/!n!/g, '\n'), range: state.convertPositionRange(range), }; @@ -52,7 +51,7 @@ export function verifyEdits( assert( expected.some((a) => { return ( - a.filePath === edit.filePath && + a.fileUri === edit.fileUri && rangesAreEqual(a.range, edit.range) && a.replacementText === edit.replacementText ); @@ -67,24 +66,24 @@ export function verifyEdits( export function applyFileEditActions(state: TestState, fileEditActions: FileEditActions) { // Apply changes // First, apply text changes - const editsPerFileMap = createMapFromItems(fileEditActions.edits, (e) => e.filePath); + const editsPerFileMap = createMapFromItems(fileEditActions.edits, (e) => e.fileUri.key); for (const [editFileName, editsPerFile] of editsPerFileMap) { const result = _applyEdits(state, editFileName, editsPerFile); - state.testFS.writeFileSync(editFileName, result.text, 'utf8'); + state.testFS.writeFileSync(Uri.file(editFileName), result.text, 'utf8'); // Update open file content if the file is in opened state. if (result.version) { let openedFilePath = editFileName; const renamed = fileEditActions.fileOperations.find( - (o) => o.kind === 'rename' && o.oldFilePath === editFileName + (o) => o.kind === 'rename' && o.oldFileUri.getFilePath() === editFileName ); if (renamed?.kind === 'rename') { - openedFilePath = renamed.newFilePath; - state.program.setFileClosed(renamed.oldFilePath); + openedFilePath = renamed.newFileUri.getFilePath(); + state.program.setFileClosed(renamed.oldFileUri); } - state.program.setFileOpened(openedFilePath, result.version + 1, result.text); + state.program.setFileOpened(Uri.file(openedFilePath), result.version + 1, result.text); } } @@ -92,24 +91,30 @@ export function applyFileEditActions(state: TestState, fileEditActions: FileEdit for (const fileOperation of fileEditActions.fileOperations) { switch (fileOperation.kind) { case 'create': { - state.testFS.mkdirpSync(getDirectoryPath(fileOperation.filePath)); - state.testFS.writeFileSync(fileOperation.filePath, ''); + state.testFS.mkdirpSync(fileOperation.fileUri.getDirectory().getFilePath()); + state.testFS.writeFileSync(fileOperation.fileUri, ''); break; } case 'rename': { - if (isFile(state.testFS, fileOperation.oldFilePath)) { - state.testFS.mkdirpSync(getDirectoryPath(fileOperation.newFilePath)); - state.testFS.renameSync(fileOperation.oldFilePath, fileOperation.newFilePath); + if (isFile(state.testFS, fileOperation.oldFileUri)) { + state.testFS.mkdirpSync(fileOperation.newFileUri.getDirectory().getFilePath()); + state.testFS.renameSync( + fileOperation.oldFileUri.getFilePath(), + fileOperation.newFileUri.getFilePath() + ); // Add new file as tracked file - state.program.addTrackedFile(fileOperation.newFilePath); + state.program.addTrackedFile(fileOperation.newFileUri); } else { - state.testFS.renameSync(fileOperation.oldFilePath, fileOperation.newFilePath); + state.testFS.renameSync( + fileOperation.oldFileUri.getFilePath(), + fileOperation.newFileUri.getFilePath() + ); } break; } case 'delete': { - state.testFS.rimrafSync(fileOperation.filePath); + state.testFS.rimrafSync(fileOperation.fileUri.getFilePath()); break; } default: @@ -123,11 +128,11 @@ export function applyFileEditActions(state: TestState, fileEditActions: FileEdit } function _applyEdits(state: TestState, filePath: string, edits: FileEditAction[]) { - const sourceFile = state.program.getBoundSourceFile(filePath)!; + const sourceFile = state.program.getBoundSourceFile(Uri.file(filePath))!; const parseResults = sourceFile.getParseResults()!; const current = applyTextEditsToString( - edits.filter((e) => e.filePath === filePath), + edits.filter((e) => e.fileUri.getFilePath() === filePath), parseResults.tokenizerOutput.lines, parseResults.text ); @@ -135,43 +140,6 @@ function _applyEdits(state: TestState, filePath: string, edits: FileEditAction[] return { version: sourceFile.getClientVersion(), text: current }; } -export function convertWorkspaceEditToFileEditActions(fs: FileSystem, edit: WorkspaceEdit): FileEditActions { - const edits: FileEditAction[] = []; - const fileOperations: FileOperations[] = []; - - if (edit.changes) { - for (const kv of Object.entries(edit.changes)) { - const filePath = convertUriToPath(fs, kv[0]); - kv[1].forEach((e) => edits.push({ filePath, range: e.range, replacementText: e.newText })); - } - } - - if (edit.documentChanges) { - for (const change of edit.documentChanges) { - if (TextDocumentEdit.is(change)) { - for (const e of change.edits) { - edits.push({ - filePath: convertUriToPath(fs, change.textDocument.uri), - range: e.range, - replacementText: e.newText, - }); - } - } else if (CreateFile.is(change)) { - fileOperations.push({ kind: 'create', filePath: convertUriToPath(fs, change.uri) }); - } else if (RenameFile.is(change)) { - fileOperations.push({ - kind: 'rename', - oldFilePath: convertUriToPath(fs, change.oldUri), - newFilePath: convertUriToPath(fs, change.newUri), - }); - } else if (DeleteFile.is(change)) { - fileOperations.push({ kind: 'delete', filePath: convertUriToPath(fs, change.uri) }); - } - } - } - return { edits, fileOperations: fileOperations }; -} - export function verifyReferencesAtPosition( program: Program, configOption: ConfigOptions, @@ -180,7 +148,7 @@ export function verifyReferencesAtPosition( position: number, ranges: Range[] ) { - const sourceFile = program.getBoundSourceFile(fileName); + const sourceFile = program.getBoundSourceFile(Uri.file(fileName)); assert(sourceFile); const node = findNodeByOffset(sourceFile.getParseResults()!.parseTree, position); @@ -197,7 +165,7 @@ export function verifyReferencesAtPosition( program, isArray(symbolNames) ? symbolNames : [symbolNames], decls, - program.getBoundSourceFile(rangeFileName)!.getParseResults()!.parseTree, + program.getBoundSourceFile(Uri.file(rangeFileName))!.getParseResults()!.parseTree, CancellationToken.None, { treatModuleInImportAndFromImportSame: true, diff --git a/packages/pyright-internal/src/tests/testUtils.ts b/packages/pyright-internal/src/tests/testUtils.ts index 46ec0c540..8872b965b 100644 --- a/packages/pyright-internal/src/tests/testUtils.ts +++ b/packages/pyright-internal/src/tests/testUtils.ts @@ -11,22 +11,20 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as path from 'path'; -import { AnalyzerFileInfo } from '../analyzer/analyzerFileInfo'; -import { Binder } from '../analyzer/binder'; import { ImportResolver } from '../analyzer/importResolver'; import { Program } from '../analyzer/program'; -import { IPythonMode } from '../analyzer/sourceFile'; import { NameTypeWalker } from '../analyzer/testWalker'; import { TypeEvaluator } from '../analyzer/typeEvaluatorTypes'; -import { cloneDiagnosticRuleSet, ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; +import { ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { ConsoleWithLogLevel, NullConsole } from '../common/console'; import { fail } from '../common/debug'; import { Diagnostic, DiagnosticCategory } from '../common/diagnostic'; -import { DiagnosticSink, TextRangeDiagnosticSink } from '../common/diagnosticSink'; +import { DiagnosticSink } from '../common/diagnosticSink'; import { FullAccessHost } from '../common/fullAccessHost'; import { createFromRealFileSystem } from '../common/realFileSystem'; import { createServiceProvider } from '../common/serviceProviderExtensions'; -import { ParseOptions, Parser, ParseResults } from '../parser/parser'; +import { Uri } from '../common/uri/uri'; +import { ParseOptions, ParseResults, Parser } from '../parser/parser'; // This is a bit gross, but it's necessary to allow the fallback typeshed // directory to be located when running within the jest environment. This @@ -35,7 +33,7 @@ import { ParseOptions, Parser, ParseResults } from '../parser/parser'; (global as any).__rootDirectory = path.resolve(); export interface FileAnalysisResult { - filePath: string; + fileUri: Uri; parseResults?: ParseResults | undefined; errors: Diagnostic[]; warnings: Diagnostic[]; @@ -79,7 +77,7 @@ export function parseSampleFile( diagSink: DiagnosticSink, execEnvironment = new ExecutionEnvironment( 'python', - '.', + Uri.file('.'), /* defaultPythonVersion */ undefined, /* defaultPythonPlatform */ undefined, /* defaultExtraPaths */ undefined @@ -98,65 +96,9 @@ export function parseSampleFile( }; } -export function buildAnalyzerFileInfo( - filePath: string, - fileContents: string, - parseResults: ParseResults, - configOptions: ConfigOptions -): AnalyzerFileInfo { - const analysisDiagnostics = new TextRangeDiagnosticSink(parseResults.tokenizerOutput.lines); - - const fileInfo: AnalyzerFileInfo = { - importLookup: (_) => undefined, - futureImports: new Set(), - builtinsScope: undefined, - diagnosticSink: analysisDiagnostics, - executionEnvironment: configOptions.findExecEnvironment(filePath), - diagnosticRuleSet: cloneDiagnosticRuleSet(configOptions.diagnosticRuleSet), - definedConstants: configOptions.defineConstant, - fileContents, - lines: parseResults.tokenizerOutput.lines, - filePath, - moduleName: '', - isStubFile: filePath.endsWith('.pyi'), - isTypingStubFile: false, - isInPyTypedPackage: false, - isTypingExtensionsStubFile: false, - isTypeshedStubFile: false, - isBuiltInStubFile: false, - ipythonMode: IPythonMode.None, - accessedSymbolSet: new Set(), - typingSymbolAliases: new Map(), - }; - - return fileInfo; -} - -export function bindSampleFile(fileName: string, configOptions = new ConfigOptions('.')): FileAnalysisResult { - const diagSink = new DiagnosticSink(); - const filePath = resolveSampleFilePath(fileName); - const execEnvironment = configOptions.findExecEnvironment(filePath); - const parseInfo = parseSampleFile(fileName, diagSink, execEnvironment); - - const fileInfo = buildAnalyzerFileInfo(filePath, parseInfo.fileContents, parseInfo.parseResults, configOptions); - const binder = new Binder(fileInfo); - binder.bindModule(parseInfo.parseResults.parseTree); - - return { - filePath, - parseResults: parseInfo.parseResults, - errors: fileInfo.diagnosticSink.getErrors(), - warnings: fileInfo.diagnosticSink.getWarnings(), - infos: fileInfo.diagnosticSink.getInformation(), - unusedCodes: fileInfo.diagnosticSink.getUnusedCode(), - unreachableCodes: fileInfo.diagnosticSink.getUnreachableCode(), - deprecateds: fileInfo.diagnosticSink.getDeprecated(), - }; -} - export function typeAnalyzeSampleFiles( fileNames: string[], - configOptions = new ConfigOptions('.'), + configOptions = new ConfigOptions(Uri.empty()), console?: ConsoleWithLogLevel ): FileAnalysisResult[] { // Always enable "test mode". @@ -164,11 +106,11 @@ export function typeAnalyzeSampleFiles( const fs = createFromRealFileSystem(); const serviceProvider = createServiceProvider(fs, console || new NullConsole()); - const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(fs)); + const importResolver = new ImportResolver(serviceProvider, configOptions, new FullAccessHost(serviceProvider)); const program = new Program(importResolver, configOptions, serviceProvider); - const filePaths = fileNames.map((name) => resolveSampleFilePath(name)); - program.setTrackedFiles(filePaths); + const fileUris = fileNames.map((name) => Uri.file(resolveSampleFilePath(name))); + program.setTrackedFiles(fileUris); // Set a "pre-check callback" so we can evaluate the types of each NameNode // prior to checking the full document. This will exercise the contextual @@ -178,7 +120,7 @@ export function typeAnalyzeSampleFiles( nameTypeWalker.walk(parseResults.parseTree); }); - const results = getAnalysisResults(program, filePaths, configOptions); + const results = getAnalysisResults(program, fileUris, configOptions); program.dispose(); return results; @@ -186,8 +128,8 @@ export function typeAnalyzeSampleFiles( export function getAnalysisResults( program: Program, - filePaths: string[], - configOptions = new ConfigOptions('.') + fileUris: Uri[], + configOptions = new ConfigOptions(Uri.empty()) ): FileAnalysisResult[] { // Always enable "test mode". configOptions.internalTestMode = true; @@ -197,12 +139,12 @@ export function getAnalysisResults( // specifying a timeout, it should complete the first time. } - const sourceFiles = filePaths.map((filePath) => program.getSourceFile(filePath)); + const sourceFiles = fileUris.map((filePath) => program.getSourceFile(filePath)); return sourceFiles.map((sourceFile, index) => { if (sourceFile) { const diagnostics = sourceFile.getDiagnostics(configOptions) || []; const analysisResult: FileAnalysisResult = { - filePath: sourceFile.getFilePath(), + fileUri: sourceFile.getUri(), parseResults: sourceFile.getParseResults(), errors: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Error), warnings: diagnostics.filter((diag) => diag.category === DiagnosticCategory.Warning), @@ -213,10 +155,10 @@ export function getAnalysisResults( }; return analysisResult; } else { - fail(`Source file not found for ${filePaths[index]}`); + fail(`Source file not found for ${fileUris[index]}`); const analysisResult: FileAnalysisResult = { - filePath: '', + fileUri: Uri.empty(), parseResults: undefined, errors: [], warnings: [], @@ -232,14 +174,14 @@ export function getAnalysisResults( export function printDiagnostics(fileResults: FileAnalysisResult) { if (fileResults.errors.length > 0) { - console.error(`Errors in ${fileResults.filePath}:`); + console.error(`Errors in ${fileResults.fileUri}:`); for (const diag of fileResults.errors) { console.error(` ${diag.message}`); } } if (fileResults.warnings.length > 0) { - console.error(`Warnings in ${fileResults.filePath}:`); + console.error(`Warnings in ${fileResults.fileUri}:`); for (const diag of fileResults.warnings) { console.error(` ${diag.message}`); } diff --git a/packages/pyright-internal/src/tests/textEditUtil.test.ts b/packages/pyright-internal/src/tests/textEditUtil.test.ts index a6416e7e7..38eb0cc97 100644 --- a/packages/pyright-internal/src/tests/textEditUtil.test.ts +++ b/packages/pyright-internal/src/tests/textEditUtil.test.ts @@ -7,12 +7,13 @@ import assert from 'assert'; import { CancellationToken } from 'vscode-jsonrpc'; +import { findNodeByOffset } from '../analyzer/parseTreeUtils'; import { FileEditAction } from '../common/editAction'; import { TextEditTracker } from '../common/textEditTracker'; +import { Uri } from '../common/uri/uri'; import { Range } from './harness/fourslash/fourSlashTypes'; import { parseAndGetTestState, TestState } from './harness/fourslash/testState'; import { convertRangeToFileEditAction } from './testStateUtils'; -import { findNodeByOffset } from '../analyzer/parseTreeUtils'; test('simple add', () => { const code = ` @@ -114,7 +115,7 @@ function verifyRemoveNodes(code: string) { const ranges = state.getRanges(); const changeRanges = _getChangeRanges(ranges); for (const range of changeRanges) { - const parseResults = state.program.getParseResults(range.fileName)!; + const parseResults = state.program.getParseResults(Uri.file(range.fileName))!; const node = findNodeByOffset(parseResults.parseTree, range.pos)!; tracker.removeNodes({ node, parseResults }); } @@ -139,7 +140,7 @@ function verifyEdits(code: string, mergeOnlyDuplications = true) { const changeRanges = _getChangeRanges(ranges); for (const range of changeRanges) { const edit = convertRangeToFileEditAction(state, range); - tracker.addEdit(edit.filePath, edit.range, edit.replacementText); + tracker.addEdit(edit.fileUri, edit.range, edit.replacementText); } const edits = tracker.getEdits(CancellationToken.None); diff --git a/packages/pyright-internal/src/tests/typeEvaluator1.test.ts b/packages/pyright-internal/src/tests/typeEvaluator1.test.ts index 64c07be3b..260df47b3 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator1.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator1.test.ts @@ -14,6 +14,7 @@ import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { ScopeType } from '../analyzer/scope'; import { ConfigOptions } from '../common/configOptions'; import { PythonVersion } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; test('Unreachable1', () => { @@ -628,7 +629,7 @@ test('Unpack2', () => { }); test('Unpack3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.7 settings. configOptions.defaultPythonVersion = PythonVersion.V3_7; @@ -642,7 +643,7 @@ test('Unpack3', () => { }); test('Unpack4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.8 settings. configOptions.defaultPythonVersion = PythonVersion.V3_8; @@ -656,7 +657,7 @@ test('Unpack4', () => { }); test('Unpack4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['unpack5.py'], configOptions); @@ -760,7 +761,7 @@ test('Call2', () => { }); test('Call3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.7 settings. This will generate more errors. configOptions.defaultPythonVersion = PythonVersion.V3_7; @@ -888,7 +889,7 @@ test('KwargsUnpack1', () => { }); test('FunctionMember1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportFunctionMemberAccess = 'none'; const analysisResult1 = TestUtils.typeAnalyzeSampleFiles(['functionMember1.py'], configOptions); @@ -978,7 +979,7 @@ test('AnnotatedVar6', () => { }); test('AnnotatedVar7', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['annotatedVar7.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -1079,7 +1080,7 @@ test('Property5', () => { }); test('Property6', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with reportPropertyTypeMismatch enabled. configOptions.diagnosticRuleSet.reportPropertyTypeMismatch = 'error'; @@ -1219,7 +1220,7 @@ test('Operator11', () => { }); test('Optional1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Disable diagnostics. configOptions.diagnosticRuleSet.reportOptionalSubscript = 'none'; @@ -1253,7 +1254,7 @@ test('Optional1', () => { }); test('Optional2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Disable diagnostics. configOptions.diagnosticRuleSet.reportOptionalOperand = 'none'; @@ -1441,7 +1442,7 @@ test('Slots3', () => { }); test('Parameters1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportMissingParameterType = 'none'; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['parameters1.py'], configOptions); @@ -1513,7 +1514,7 @@ test('Self10', () => { }); test('UnusedVariable1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportUnusedVariable = 'none'; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['unusedVariable1.py'], configOptions); @@ -1585,7 +1586,7 @@ test('TupleUnpack1', () => { }); test('TupleUnpack2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['tupleUnpack2.py'], configOptions); @@ -1597,7 +1598,7 @@ test('TupleUnpack2', () => { }); test('TupleUnpack3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['tupleUnpack3.py'], configOptions); @@ -1677,7 +1678,7 @@ test('Dictionary4', () => { }); test('StaticExpression1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_8; configOptions.defaultPythonPlatform = 'windows'; diff --git a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts index c561fdbb9..717569bd5 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator2.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator2.test.ts @@ -10,6 +10,7 @@ import { ConfigOptions } from '../common/configOptions'; import { PythonVersion } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; test('CallbackProtocol1', () => { @@ -133,7 +134,7 @@ test('Assignment11', () => { }); test('Assignment12', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['assignment12.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -228,7 +229,7 @@ test('Super11', () => { }); test('MissingSuper1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['missingSuper1.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -287,7 +288,7 @@ test('isInstance2', () => { }); test('isInstance3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_9; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['isinstance3.py'], configOptions); @@ -335,7 +336,7 @@ test('Unbound5', () => { }); test('Assert1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, this is reported as a warning. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['assert1.py'], configOptions); @@ -479,7 +480,7 @@ test('ConstrainedTypeVar14', () => { }); test('ConstrainedTypeVar15', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.disableBytesTypePromotions = true; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['constrainedTypeVar15.py'], configOptions); @@ -506,7 +507,7 @@ test('ConstrainedTypeVar18', () => { }); test('MissingTypeArg1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, reportMissingTypeArgument is disabled. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['missingTypeArg1.py']); @@ -1185,7 +1186,7 @@ test('Protocol16', () => { }); test('Protocol17', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportInvalidTypeVarUse = 'error'; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol17.py']); @@ -1217,7 +1218,7 @@ test('Protocol21', () => { }); test('Protocol22', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportInvalidTypeVarUse = 'error'; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['protocol22.py']); @@ -1519,7 +1520,7 @@ test('TypedDict24', () => { }); test('TypedDictInline1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.enableExperimentalFeatures = true; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictInline1.py'], configOptions); diff --git a/packages/pyright-internal/src/tests/typeEvaluator3.test.ts b/packages/pyright-internal/src/tests/typeEvaluator3.test.ts index eebd2bbbf..2f4b7c999 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator3.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator3.test.ts @@ -10,6 +10,7 @@ import { ConfigOptions } from '../common/configOptions'; import { PythonVersion } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; test('Module1', () => { @@ -139,7 +140,7 @@ test('Await2', () => { }); test('Coroutines1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // This functionality is deprecated in Python 3.11, so the type no longer // exists in typing.pyi after that point. @@ -156,7 +157,7 @@ test('Coroutines2', () => { }); test('Coroutines3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // This functionality is deprecated in Python 3.11, so the type no longer // exists in typing.pyi after that point. @@ -551,7 +552,7 @@ test('TypeAlias3', () => { }); test('TypeAlias4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_9; const analysisResults3_9 = TestUtils.typeAnalyzeSampleFiles(['typeAlias4.py'], configOptions); @@ -635,7 +636,7 @@ test('TypeAlias16', () => { }); test('TypeAlias17', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['typeAlias17.py'], configOptions); TestUtils.validateResults(analysisResults1, 4); @@ -682,7 +683,7 @@ test('RecursiveTypeAlias2', () => { }); test('RecursiveTypeAlias3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['recursiveTypeAlias3.py'], configOptions); @@ -775,7 +776,7 @@ test('Classes4', () => { }); test('Classes5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportIncompatibleVariableOverride = 'none'; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['classes5.py'], configOptions); @@ -829,7 +830,7 @@ test('Methods1', () => { }); test('MethodOverride1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportIncompatibleMethodOverride = 'none'; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['methodOverride1.py'], configOptions); @@ -841,7 +842,7 @@ test('MethodOverride1', () => { }); test('MethodOverride2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportIncompatibleMethodOverride = 'none'; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['methodOverride2.py'], configOptions); @@ -853,7 +854,7 @@ test('MethodOverride2', () => { }); test('MethodOverride3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportIncompatibleMethodOverride = 'none'; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['methodOverride3.py'], configOptions); @@ -870,7 +871,7 @@ test('MethodOverride4', () => { }); test('MethodOverride5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['methodOverride5.py'], configOptions); @@ -878,7 +879,7 @@ test('MethodOverride5', () => { }); test('MethodOverride6', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportIncompatibleMethodOverride = 'none'; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['methodOverride6.py'], configOptions); @@ -962,7 +963,7 @@ test('TypeGuard2', () => { }); test('TypeGuard3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.enableExperimentalFeatures = true; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeGuard3.py'], configOptions); @@ -970,7 +971,7 @@ test('TypeGuard3', () => { }); test('TypeGuard4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.enableExperimentalFeatures = true; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeGuard4.py'], configOptions); @@ -1014,7 +1015,7 @@ test('ProtocolModule4', () => { }); test('VariadicTypeVar1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar1.py'], configOptions); @@ -1022,7 +1023,7 @@ test('VariadicTypeVar1', () => { }); test('VariadicTypeVar2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar2.py'], configOptions); @@ -1030,7 +1031,7 @@ test('VariadicTypeVar2', () => { }); test('VariadicTypeVar3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar3.py'], configOptions); @@ -1038,7 +1039,7 @@ test('VariadicTypeVar3', () => { }); test('VariadicTypeVar4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar4.py'], configOptions); @@ -1046,7 +1047,7 @@ test('VariadicTypeVar4', () => { }); test('VariadicTypeVar5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar5.py'], configOptions); @@ -1054,7 +1055,7 @@ test('VariadicTypeVar5', () => { }); test('VariadicTypeVar6', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar6.py'], configOptions); @@ -1062,7 +1063,7 @@ test('VariadicTypeVar6', () => { }); test('VariadicTypeVar7', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar7.py'], configOptions); @@ -1070,7 +1071,7 @@ test('VariadicTypeVar7', () => { }); test('VariadicTypeVar8', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar8.py'], configOptions); @@ -1078,7 +1079,7 @@ test('VariadicTypeVar8', () => { }); test('VariadicTypeVar9', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar9.py'], configOptions); @@ -1086,7 +1087,7 @@ test('VariadicTypeVar9', () => { }); test('VariadicTypeVar10', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar10.py'], configOptions); @@ -1094,7 +1095,7 @@ test('VariadicTypeVar10', () => { }); test('VariadicTypeVar11', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar11.py'], configOptions); @@ -1102,7 +1103,7 @@ test('VariadicTypeVar11', () => { }); test('VariadicTypeVar12', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar12.py'], configOptions); @@ -1110,7 +1111,7 @@ test('VariadicTypeVar12', () => { }); test('VariadicTypeVar13', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar13.py'], configOptions); @@ -1118,7 +1119,7 @@ test('VariadicTypeVar13', () => { }); test('VariadicTypeVar14', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar14.py'], configOptions); @@ -1126,7 +1127,7 @@ test('VariadicTypeVar14', () => { }); test('VariadicTypeVar15', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar15.py'], configOptions); @@ -1134,7 +1135,7 @@ test('VariadicTypeVar15', () => { }); test('VariadicTypeVar16', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar16.py'], configOptions); @@ -1142,7 +1143,7 @@ test('VariadicTypeVar16', () => { }); test('VariadicTypeVar17', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar17.py'], configOptions); @@ -1150,7 +1151,7 @@ test('VariadicTypeVar17', () => { }); test('VariadicTypeVar18', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar18.py'], configOptions); @@ -1158,7 +1159,7 @@ test('VariadicTypeVar18', () => { }); test('VariadicTypeVar19', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar19.py'], configOptions); @@ -1166,7 +1167,7 @@ test('VariadicTypeVar19', () => { }); test('VariadicTypeVar20', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar20.py'], configOptions); @@ -1174,7 +1175,7 @@ test('VariadicTypeVar20', () => { }); test('VariadicTypeVar21', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar21.py'], configOptions); @@ -1182,7 +1183,7 @@ test('VariadicTypeVar21', () => { }); test('VariadicTypeVar22', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar22.py'], configOptions); @@ -1190,7 +1191,7 @@ test('VariadicTypeVar22', () => { }); test('VariadicTypeVar23', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar23.py'], configOptions); @@ -1198,7 +1199,7 @@ test('VariadicTypeVar23', () => { }); test('VariadicTypeVar24', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar24.py'], configOptions); @@ -1206,7 +1207,7 @@ test('VariadicTypeVar24', () => { }); test('VariadicTypeVar25', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar25.py'], configOptions); @@ -1214,7 +1215,7 @@ test('VariadicTypeVar25', () => { }); test('VariadicTypeVar26', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['variadicTypeVar26.py'], configOptions); @@ -1222,7 +1223,7 @@ test('VariadicTypeVar26', () => { }); test('Match1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['match1.py'], configOptions); @@ -1230,7 +1231,7 @@ test('Match1', () => { }); test('Match2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['match2.py'], configOptions); @@ -1238,7 +1239,7 @@ test('Match2', () => { }); test('Match3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['match3.py'], configOptions); @@ -1246,7 +1247,7 @@ test('Match3', () => { }); test('MatchSequence1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchSequence1.py'], configOptions); @@ -1254,7 +1255,7 @@ test('MatchSequence1', () => { }); test('MatchClass1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass1.py'], configOptions); @@ -1262,7 +1263,7 @@ test('MatchClass1', () => { }); test('MatchClass2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass2.py'], configOptions); @@ -1270,7 +1271,7 @@ test('MatchClass2', () => { }); test('MatchClass3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass3.py'], configOptions); @@ -1278,7 +1279,7 @@ test('MatchClass3', () => { }); test('MatchClass4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass4.py'], configOptions); @@ -1286,7 +1287,7 @@ test('MatchClass4', () => { }); test('MatchClass5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass5.py'], configOptions); @@ -1294,7 +1295,7 @@ test('MatchClass5', () => { }); test('MatchClass6', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchClass6.py'], configOptions); @@ -1302,7 +1303,7 @@ test('MatchClass6', () => { }); test('MatchValue1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchValue1.py'], configOptions); @@ -1310,7 +1311,7 @@ test('MatchValue1', () => { }); test('MatchMapping1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchMapping1.py'], configOptions); @@ -1318,7 +1319,7 @@ test('MatchMapping1', () => { }); test('MatchLiteral1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['matchLiteral1.py'], configOptions); @@ -1326,7 +1327,7 @@ test('MatchLiteral1', () => { }); test('MatchExhaustion1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; configOptions.diagnosticRuleSet.reportMatchNotExhaustive = 'none'; @@ -1339,7 +1340,7 @@ test('MatchExhaustion1', () => { }); test('MatchUnnecessary1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['matchUnnecessary1.py'], configOptions); @@ -1366,7 +1367,7 @@ test('List3', () => { }); test('Comparison1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['comparison1.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -1377,7 +1378,7 @@ test('Comparison1', () => { }); test('Comparison2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['comparison2.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -1555,7 +1556,7 @@ test('Constructor23', () => { }); test('Constructor24', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.strictParameterNoneValue = false; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['constructor24.py'], configOptions); @@ -1597,7 +1598,7 @@ test('Constructor29', () => { }); test('InconsistentConstructor1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportInconsistentConstructor = 'none'; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['inconsistentConstructor1.py'], configOptions); @@ -1616,7 +1617,7 @@ test('ClassGetItem1', () => { }); test('UnusedCallResult1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, this is disabled. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['unusedCallResult1.py'], configOptions); @@ -1652,7 +1653,7 @@ test('FunctionAnnotation3', () => { }); test('FunctionAnnotation4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['functionAnnotation4.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -1663,7 +1664,7 @@ test('FunctionAnnotation4', () => { }); test('Subscript1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.8 settings. configOptions.defaultPythonVersion = PythonVersion.V3_8; @@ -1682,7 +1683,7 @@ test('Subscript2', () => { }); test('Subscript3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.9 settings. configOptions.defaultPythonVersion = PythonVersion.V3_9; @@ -1714,7 +1715,7 @@ test('Decorator2', () => { }); test('Decorator3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.8 settings. configOptions.defaultPythonVersion = PythonVersion.V3_8; diff --git a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts index 3e63d38df..ad1f12fa8 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts @@ -12,11 +12,12 @@ import * as assert from 'assert'; import { ConfigOptions } from '../common/configOptions'; import { PythonVersion } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; test('Required1', () => { // Analyze with Python 3.10 settings. - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['required1.py'], configOptions); @@ -25,7 +26,7 @@ test('Required1', () => { test('Required2', () => { // Analyze with Python 3.10 settings. - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['required2.py'], configOptions); @@ -34,7 +35,7 @@ test('Required2', () => { test('Required3', () => { // Analyze with Python 3.10 settings. - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['required3.py'], configOptions); @@ -182,7 +183,7 @@ test('Import11', () => { }); test('Import12', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, optional diagnostics are ignored. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['import12.py'], configOptions); @@ -223,7 +224,7 @@ test('Import18', () => { }); test('DunderAll1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, reportUnsupportedDunderAll is a warning. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['dunderAll1.py'], configOptions); @@ -241,7 +242,7 @@ test('DunderAll1', () => { }); test('DunderAll2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // By default, reportUnsupportedDunderAll is a warning. let analysisResults = TestUtils.typeAnalyzeSampleFiles(['dunderAll2.py'], configOptions); @@ -259,7 +260,7 @@ test('DunderAll2', () => { }); test('DunderAll3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Turn on error. configOptions.diagnosticRuleSet.reportUnsupportedDunderAll = 'error'; @@ -288,7 +289,7 @@ test('Overload4', () => { }); test('Overload5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.reportOverlappingOverload = 'none'; let analysisResults = TestUtils.typeAnalyzeSampleFiles(['overload5.py'], configOptions); @@ -400,7 +401,7 @@ test('CallSite2', () => { }); test('FString1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['fstring1.py'], configOptions); @@ -427,7 +428,7 @@ test('FString4', () => { }); test('FString5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.7 settings. This will generate errors. configOptions.defaultPythonVersion = PythonVersion.V3_7; @@ -675,7 +676,7 @@ test('DataClassFrozen1', () => { }); test('DataClassKwOnly1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclassKwOnly1.py'], configOptions); @@ -683,7 +684,7 @@ test('DataClassKwOnly1', () => { }); test('DataClassSlots1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclassSlots1.py'], configOptions); @@ -793,7 +794,7 @@ test('Generic3', () => { }); test('Unions1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.disableBytesTypePromotions = true; // Analyze with Python 3.9 settings. This will generate errors. @@ -808,7 +809,7 @@ test('Unions1', () => { }); test('Unions2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.8 settings. configOptions.defaultPythonVersion = PythonVersion.V3_8; @@ -817,7 +818,7 @@ test('Unions2', () => { }); test('Unions3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); // Analyze with Python 3.9 settings. This will generate errors. configOptions.defaultPythonVersion = PythonVersion.V3_9; @@ -854,7 +855,7 @@ test('ParamSpec1', () => { }); test('ParamSpec2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_9; const analysisResults39 = TestUtils.typeAnalyzeSampleFiles(['paramSpec2.py'], configOptions); @@ -1197,7 +1198,7 @@ test('TypeVar12', () => { }); test('Annotated1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_8; const analysisResults38 = TestUtils.typeAnalyzeSampleFiles(['annotated1.py'], configOptions); @@ -1263,7 +1264,7 @@ test('TryExcept6', () => { }); test('TryExcept7', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_10; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['tryExcept7.py'], configOptions); @@ -1275,7 +1276,7 @@ test('TryExcept7', () => { }); test('TryExcept8', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['tryExcept8.py'], configOptions); diff --git a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts index ada9f897a..e295d4505 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator5.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator5.test.ts @@ -10,10 +10,11 @@ import { ConfigOptions } from '../common/configOptions'; import { PythonVersion } from '../common/pythonVersion'; +import { Uri } from '../common/uri/uri'; import * as TestUtils from './testUtils'; test('TypeParams1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams1.py'], configOptions); @@ -21,7 +22,7 @@ test('TypeParams1', () => { }); test('TypeParams2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['typeParams2.py'], configOptions); @@ -33,7 +34,7 @@ test('TypeParams2', () => { }); test('TypeParams3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams3.py'], configOptions); @@ -41,7 +42,7 @@ test('TypeParams3', () => { }); test('TypeParams4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams4.py'], configOptions); @@ -49,7 +50,7 @@ test('TypeParams4', () => { }); test('TypeParams5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams5.py'], configOptions); @@ -57,7 +58,7 @@ test('TypeParams5', () => { }); test('TypeParams6', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams6.py'], configOptions); @@ -65,7 +66,7 @@ test('TypeParams6', () => { }); test('TypeParams7', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeParams7.py'], configOptions); @@ -73,7 +74,7 @@ test('TypeParams7', () => { }); test('AutoVariance1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['autoVariance1.py'], configOptions); @@ -81,7 +82,7 @@ test('AutoVariance1', () => { }); test('AutoVariance2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['autoVariance2.py'], configOptions); @@ -89,7 +90,7 @@ test('AutoVariance2', () => { }); test('AutoVariance3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['autoVariance3.py'], configOptions); @@ -97,7 +98,7 @@ test('AutoVariance3', () => { }); test('AutoVariance4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['autoVariance4.py'], configOptions); @@ -110,7 +111,7 @@ test('AutoVariance5', () => { }); test('TypeAliasStatement1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeAliasStatement1.py'], configOptions); @@ -118,7 +119,7 @@ test('TypeAliasStatement1', () => { }); test('TypeAliasStatement2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_11; const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['typeAliasStatement2.py'], configOptions); @@ -130,7 +131,7 @@ test('TypeAliasStatement2', () => { }); test('TypeAliasStatement3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeAliasStatement3.py'], configOptions); @@ -138,7 +139,7 @@ test('TypeAliasStatement3', () => { }); test('TypeAliasStatement4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeAliasStatement4.py'], configOptions); @@ -166,7 +167,7 @@ test('Override1', () => { }); test('Override2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); const analysisResults1 = TestUtils.typeAnalyzeSampleFiles(['override2.py'], configOptions); TestUtils.validateResults(analysisResults1, 0); @@ -182,7 +183,7 @@ test('TypeVarDefault1', () => { }); test('TypeVarDefault2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefault2.py'], configOptions); @@ -195,7 +196,7 @@ test('TypeVarDefault3', () => { }); test('TypeVarDefault4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefault4.py'], configOptions); @@ -203,7 +204,7 @@ test('TypeVarDefault4', () => { }); test('TypeVarDefault5', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefault5.py'], configOptions); @@ -216,7 +217,7 @@ test('TypeVarDefaultClass1', () => { }); test('TypeVarDefaultClass2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefaultClass2.py'], configOptions); @@ -224,7 +225,7 @@ test('TypeVarDefaultClass2', () => { }); test('TypeVarDefaultClass3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefaultClass3.py'], configOptions); @@ -232,7 +233,7 @@ test('TypeVarDefaultClass3', () => { }); test('TypeVarDefaultClass4', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefaultClass4.py'], configOptions); @@ -250,7 +251,7 @@ test('TypeVarDefaultTypeAlias2', () => { }); test('TypeVarDefaultTypeAlias3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefaultTypeAlias3.py'], configOptions); @@ -268,7 +269,7 @@ test('TypeVarDefaultFunction2', () => { }); test('TypeVarDefaultFunction3', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_13; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeVarDefaultFunction3.py'], configOptions); @@ -306,7 +307,7 @@ test('TypePrinter3', () => { }); test('TypeAliasType1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.defaultPythonVersion = PythonVersion.V3_12; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typeAliasType1.py'], configOptions); @@ -319,7 +320,7 @@ test('TypeAliasType2', () => { }); test('TypedDictReadOnly1', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.enableExperimentalFeatures = true; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictReadOnly1.py'], configOptions); @@ -327,7 +328,7 @@ test('TypedDictReadOnly1', () => { }); test('TypedDictReadOnly2', () => { - const configOptions = new ConfigOptions('.'); + const configOptions = new ConfigOptions(Uri.empty()); configOptions.diagnosticRuleSet.enableExperimentalFeatures = true; const analysisResults = TestUtils.typeAnalyzeSampleFiles(['typedDictReadOnly2.py'], configOptions); diff --git a/packages/pyright-internal/src/tests/typePrinter.test.ts b/packages/pyright-internal/src/tests/typePrinter.test.ts index c7c881aad..c389b2274 100644 --- a/packages/pyright-internal/src/tests/typePrinter.test.ts +++ b/packages/pyright-internal/src/tests/typePrinter.test.ts @@ -23,6 +23,7 @@ import { UnboundType, UnknownType, } from '../analyzer/types'; +import { Uri } from '../common/uri/uri'; import { ParameterCategory } from '../parser/parseNodes'; function returnTypeCallback(type: FunctionType) { @@ -45,7 +46,7 @@ test('SimpleTypes', () => { assert.strictEqual(printType(unboundType, PrintTypeFlags.None, returnTypeCallback), 'Unbound'); assert.strictEqual(printType(unboundType, PrintTypeFlags.PythonSyntax, returnTypeCallback), 'Any'); - const moduleType = ModuleType.create('Test', ''); + const moduleType = ModuleType.create('Test', Uri.empty()); assert.strictEqual(printType(moduleType, PrintTypeFlags.None, returnTypeCallback), 'Module("Test")'); assert.strictEqual(printType(moduleType, PrintTypeFlags.PythonSyntax, returnTypeCallback), 'Any'); }); @@ -68,7 +69,7 @@ test('ClassTypes', () => { 'A', '', '', - '', + Uri.empty(), ClassTypeFlags.None, 0, /* declaredMetaclass*/ undefined, @@ -89,7 +90,7 @@ test('ClassTypes', () => { 'int', '', '', - '', + Uri.empty(), ClassTypeFlags.None, 0, /* declaredMetaclass*/ undefined, diff --git a/packages/pyright-internal/src/tests/uri.test.ts b/packages/pyright-internal/src/tests/uri.test.ts new file mode 100644 index 000000000..342383417 --- /dev/null +++ b/packages/pyright-internal/src/tests/uri.test.ts @@ -0,0 +1,789 @@ +/* + * pathUtils.test.ts + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT license. + * Author: Eric Traut + * + * Unit tests for pathUtils module. + */ + +import assert from 'assert'; +import * as nodefs from 'fs-extra'; +import * as os from 'os'; +import * as path from 'path'; + +import { expandPathVariables } from '../common/envVarUtils'; +import { isRootedDiskPath, normalizeSlashes } from '../common/pathUtils'; +import { createFromRealFileSystem } from '../common/realFileSystem'; +import { Uri } from '../common/uri/uri'; +import { + deduplicateFolders, + getWildcardRegexPattern, + getWildcardRoot, + isFileSystemCaseSensitiveInternal, +} from '../common/uri/uriUtils'; +import * as vfs from './harness/vfs/filesystem'; + +test('parse', () => { + assert.throws(() => Uri.parse('\\c:\\foo : bar', true)); + assert.throws(() => Uri.parse('foo:////server/b/c', true)); // No authority component + assert.ok(Uri.parse('foo:///a/b/c', true)); + assert.ok(Uri.parse('foo:a/b/c', true)); + assert.ok(Uri.parse('foo:/a/b/c', true)); + assert.ok(Uri.parse('foo://server/share/dir/file.py', true)); + assert.ok(Uri.parse('foo://server/share/dir/file.py?query#fragment', true)); + assert.ok(Uri.parse('foo:///c:/users/me', true)); + assert.ok(Uri.parse('foo:///c%3A%52users%52me', true)); + assert.ok(Uri.parse('', true)); + assert.ok(Uri.parse(undefined, true)); +}); + +test('file', () => { + const cwd = process.cwd(); + const uri1 = Uri.file('a/b/c', true, true); + assert.ok(uri1.getFilePath().length > 6); + assert.ok( + uri1.getFilePath().toLowerCase().startsWith(cwd.toLowerCase()), + `${uri1.getFilePath()} does not start with ${cwd}` + ); + const uri2 = Uri.file('a/b/c', true, false); + assert.equal(uri2.getFilePath().length, 6); +}); + +test('key', () => { + const key = Uri.parse('foo:///a/b/c', true).key; + const key2 = Uri.parse('foo:///a/b/c', true).key; + assert.equal(key, key2); + const key3 = Uri.parse('foo:///a/b/d', true).key; + assert.notEqual(key, key3); + const key4 = Uri.file('/a/b/c').key; + assert.notEqual(key, key4); + const key5 = Uri.parse('file:///a/b/c', true).key; + assert.equal(key4, key5); + const key6 = Uri.file(normalizeSlashes('c:\\foo\\bar\\d.txt')).key; + const key7 = Uri.parse('file:///c%3A/foo/bar/d.txt', true).key; + const key8 = Uri.parse('file:///c:/foo/bar/d.txt', true).key; + assert.equal(key6, key7); + assert.equal(key6, key8); + const key9 = Uri.parse('file:///c%3A/foo/bar/D.txt', true).key; + const key10 = Uri.parse('file:///c:/foo/bar/d.txt', true).key; + assert.notEqual(key9, key10); + const key11 = Uri.parse('file:///c%3A/foo/bar/D.txt', false).key; + const key12 = Uri.parse('file:///c%3A/foo/bar/d.txt', false).key; + assert.equal(key11, key12); +}); + +test('filename', () => { + const filename = Uri.parse('foo:///a/b/c', true).fileName; + assert.equal(filename, 'c'); + const filename2 = Uri.parse('foo:///a/b/c/', true).fileName; + assert.equal(filename2, 'c'); + const filename3 = Uri.parse('foo:///a/b/c.py', true).fileName; + assert.equal(filename3, 'c.py'); + const filename4 = Uri.parse('foo:///a/b/c.py?query#fragment', true).fileName; + assert.equal(filename4, 'c.py'); + const filename5 = Uri.file('/a/b/c').fileName; + assert.equal(filename5, 'c'); + const filename6 = Uri.parse('file:///a/b/c', true).fileName; + assert.equal(filename6, 'c'); +}); + +test('extname', () => { + const extname = Uri.parse('foo:///a/b/c', true).lastExtension; + assert.equal(extname, ''); + const extname2 = Uri.parse('foo:///a/b/c/', true).lastExtension; + assert.equal(extname2, ''); + const extname3 = Uri.parse('foo:///a/b/c.py', true).lastExtension; + assert.equal(extname3, '.py'); + const extname4 = Uri.parse('foo:///a/b/c.py?query#fragment', true).lastExtension; + assert.equal(extname4, '.py'); + const extname5 = Uri.file('/a/b/c.py.foo').lastExtension; + assert.equal(extname5, '.foo'); + const extname6 = Uri.parse('file:///a/b/c.py.foo', true).lastExtension; + assert.equal(extname6, '.foo'); +}); + +test('root', () => { + const root1 = Uri.parse('foo://authority/a/b/c', true).root; + assert.equal(root1.toString(), 'foo://authority/'); + const root = Uri.parse('file://server/b/c', true).root; + assert.equal(root.toString(), 'file://server/'); + assert.equal(root.getRootPathLength(), 9); + const root2 = Uri.parse('foo:///', true).root; + assert.equal(root2.toString(), 'foo:///'); + const root3 = Uri.parse('foo:///a/b/c/', true).root; + assert.equal(root3.toString(), 'foo:///'); + assert.ok(root3.isRoot()); + const root4 = Uri.parse('foo:///a/b/c.py', true).root; + assert.equal(root4.toString(), 'foo:///'); + const root5 = Uri.parse('foo:///a/b/c.py?query#fragment', true).root; + assert.equal(root5.toString(), 'foo:///'); + const root6 = Uri.file('/a/b/c.py.foo').root; + assert.equal(root6.toString(), 'file:///'); + const root7 = Uri.parse('file:///a/b/c.py.foo', true).root; + assert.equal(root7.toString(), 'file:///'); + assert.equal(root7.getRootPathLength(), 1); + const root8 = Uri.parse('untitled:Untitled-1', true).root; + assert.equal(root8.toString(), 'untitled://'); + assert.equal(root8.getRootPathLength(), 0); + assert.equal(root8.isRoot(), false); + const root9 = Uri.parse('file://a/b/c/d.py', true).root; + assert.equal(root9.toString(), 'file://a/'); + assert.equal(root9.getRootPathLength(), 4); + assert.ok(root9.isRoot()); + const root10 = Uri.parse('file://c%3A/b/c/d.py', true).root; + assert.equal(root10.toString(), 'file://c:/'); + assert.equal(root10.getRootPathLength(), 5); + assert.ok(root10.isRoot()); +}); + +test('empty', () => { + const empty = Uri.parse('', true); + assert.equal(empty.isEmpty(), true); + const empty2 = Uri.parse('foo:///', true).isEmpty(); + assert.equal(empty2, false); + const empty3 = Uri.empty(); + assert.equal(empty3.isEmpty(), true); + const empty4 = Uri.parse(undefined, true); + assert.equal(empty4.isEmpty(), true); + assert.ok(empty4.equals(empty3)); + assert.ok(empty3.equals(empty)); +}); + +test('file', () => { + const file1 = Uri.file(normalizeSlashes('/a/b/c')).getFilePath(); + assert.equal(file1, normalizeSlashes('/a/b/c')); + const file2 = Uri.file('file:///a/b/c').getFilePath(); + assert.equal(file2, normalizeSlashes('/a/b/c')); +}); + +test('isUri', () => { + const isUri = Uri.isUri('foo:///a/b/c'); + assert.equal(isUri, false); + const isUri2 = Uri.isUri('/a/b/c'); + assert.equal(isUri2, false); + const isUri3 = Uri.isUri(undefined); + assert.equal(isUri3, false); + const isUri4 = Uri.isUri(Uri.parse('foo:///a/b/c', true)); + assert.equal(isUri4, true); + const isUri5 = Uri.isUri(Uri.empty()); + assert.equal(isUri5, true); +}); + +test('matchesRegex', () => { + const includeFiles = /\.pyi?$/; + const uri = Uri.parse('file:///a/b/c.pyi', true); + assert.ok(uri.matchesRegex(includeFiles)); + const uri2 = Uri.parse('file:///a/b/c.px', true); + assert.equal(uri2.matchesRegex(includeFiles), false); + const uri3 = Uri.parse('vscode-vfs:///a/b/c.pyi', true); + assert.ok(uri3.matchesRegex(includeFiles)); + const fileRegex = /^(c:\/foo\/bar)($|\/)/i; + const uri4 = Uri.parse('file:///C%3A/foo/bar', true); + assert.ok(uri4.matchesRegex(fileRegex)); + const uri5 = Uri.parse('file:///c%3A/foo/bar', true); + assert.ok(uri5.matchesRegex(fileRegex)); + const uri6 = Uri.parse('file:///c:/foo/bar', true); + assert.ok(uri6.matchesRegex(fileRegex)); + const uri7 = Uri.parse('file:///c:/foo/bar/', true); + assert.ok(uri7.matchesRegex(fileRegex)); + const uri8 = Uri.parse('file:///c:/foo/baz/', true); + assert.equal(uri8.matchesRegex(fileRegex), false); +}); + +test('replaceExtension', () => { + const uri = Uri.parse('file:///a/b/c.pyi', true); + const uri2 = uri.replaceExtension('.py'); + assert.equal(uri2.toString(), 'file:///a/b/c.py'); + const uri3 = Uri.parse('file:///a/b/c', true); + const uri4 = uri3.replaceExtension('.py'); + assert.equal(uri4.toString(), 'file:///a/b/c.py'); + const uri5 = Uri.parse('file:///a/b/c.foo.py', true); + const uri6 = uri5.replaceExtension('.pyi'); + assert.equal(uri6.toString(), 'file:///a/b/c.foo.pyi'); +}); + +test('addExtension', () => { + const uri = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + const uri2 = uri.addExtension('.py'); + assert.equal(uri2.toString(), 'file:///a/b/c.pyi.py'); + const uri3 = Uri.parse('file:///a/b/c', true); + const uri4 = uri3.addExtension('.py'); + assert.equal(uri4.toString(), 'file:///a/b/c.py'); +}); + +test('addPath', () => { + const uri = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + const uri2 = uri.addPath('d'); + assert.equal(uri2.toString(), 'file:///a/b/c.pyid'); +}); + +test('directory', () => { + const uri = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + const uri2 = uri.getDirectory(); + assert.equal(uri2.toString(), 'file:///a/b'); + const uri3 = uri2.getDirectory(); + assert.equal(uri3.toString(), 'file:///a'); + const uri4 = Uri.parse('file:///a/b/', true); + const uri5 = uri4.getDirectory(); + assert.equal(uri5.toString(), 'file:///a'); + const uri6 = uri4.getDirectory(); + assert.ok(uri6.equals(uri5)); +}); + +test('init and pytyped', () => { + const uri = Uri.parse('file:///a/b/c?query#fragment', true); + const uri2 = uri.pytypedUri; + assert.equal(uri2.toString(), 'file:///a/b/c/py.typed'); + const uri3 = uri.initPyUri; + assert.equal(uri3.toString(), 'file:///a/b/c/__init__.py'); + const uri4 = uri.initPyiUri; + assert.equal(uri4.toString(), 'file:///a/b/c/__init__.pyi'); + const uri5 = uri.packageUri; + assert.equal(uri5.toString(), 'file:///a/b/c.py'); + const uri6 = uri.packageStubUri; + assert.equal(uri6.toString(), 'file:///a/b/c.pyi'); + const uri7 = Uri.parse('foo://microsoft.com/a/b/c.py', true); + const uri8 = uri7.pytypedUri; + assert.equal(uri8.toString(), 'foo://microsoft.com/a/b/c.py/py.typed'); + const uri9 = uri7.initPyUri; + assert.equal(uri9.toString(), 'foo://microsoft.com/a/b/c.py/__init__.py'); + const uri10 = uri7.initPyiUri; + assert.equal(uri10.toString(), 'foo://microsoft.com/a/b/c.py/__init__.pyi'); + const uri11 = uri7.packageUri; + assert.equal(uri11.toString(), 'foo://microsoft.com/a/b/c.py.py'); + const uri12 = uri7.packageStubUri; + assert.equal(uri12.toString(), 'foo://microsoft.com/a/b/c.py.pyi'); +}); + +test('isChild', () => { + const parent = Uri.parse('file:///a/b/?query#fragment', true); + const child = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + assert.ok(child.isChild(parent)); + const parent2 = Uri.parse('file:///a/b', true); + const child2 = Uri.parse('file:///a/b/c.pyi', true); + const child2DifferentCase = Uri.parse('file:///a/B/C.pyi', false); + assert.ok(child2.isChild(parent2)); + assert.ok(child2DifferentCase.isChild(parent2)); + const parent3 = Uri.parse('file:///a/b/', true); + const child3 = Uri.parse('file:///a/b/c.pyi', true); + assert.ok(child3.isChild(parent3)); + const parent4 = Uri.parse('file:///a/b/', true); + const notChild4 = Uri.parse('file:///a/bb/c.pyi', true); + assert.ok(!notChild4.isChild(parent4)); + assert.ok(!notChild4.isChild(parent2)); + const notChild5 = Uri.parse('file:///a/b/', true); + assert.ok(!notChild5.isChild(parent4)); +}); + +test('equals', () => { + const uri1 = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + const uri2 = Uri.file('/a/b/c.pyi'); + assert.ok(!uri1.equals(uri2)); + const uri3 = uri1.stripExtension().addExtension('.pyi'); + assert.ok(uri2.equals(uri3)); + const uri4 = Uri.parse('foo:///a/b/c', true); + const uri5 = Uri.parse('foo:///a/b/c', true); + const uri6 = Uri.parse('foo:///a/b/c/', true); + assert.ok(uri4.equals(uri5)); + assert.ok(uri4.equals(uri6)); + const uri7 = Uri.parse('file://c%3A/b/c/d.py', true).root; + const uri8 = Uri.parse('file://c:/', true); + assert.ok(uri7.equals(uri8)); + const uri9 = Uri.parse('foo:///a/b/c?query', true); + assert.ok(!uri9.equals(uri4)); + // Web uris are always case sensitive + const uri10 = Uri.parse('foo:///a/b/c', false); + const uri11 = Uri.parse('foo:///a/B/c', false); + assert.ok(!uri10.equals(uri11)); + // Filre uris pay attention to the parameter. + const uri12 = Uri.parse('file:///a/b/c', false); + const uri13 = Uri.parse('file:///a/B/c', false); + assert.ok(uri12.equals(uri13)); + const uri14 = Uri.parse('file:///a/b/c', true); + const uri15 = Uri.parse('file:///a/B/c', true); + assert.ok(!uri14.equals(uri15)); +}); + +test('startsWith', () => { + const parent = Uri.parse('file:///a/b/?query#fragment', true); + const child = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + assert.ok(child.startsWith(parent)); + const parent2 = Uri.parse('file:///a/b', true); + const child2 = Uri.parse('file:///a/b/c.pyi', true); + assert.ok(child2.startsWith(parent2)); + const parent3 = Uri.parse('file:///a/b/', true); + const child3 = Uri.parse('file:///a/b/c.pyi', true); + assert.ok(child3.startsWith(parent3)); + const parent4 = Uri.parse('file:///a/b/', true); + const notChild4 = Uri.parse('file:///a/bb/c.pyi', true); + assert.ok(!notChild4.startsWith(parent4)); + assert.ok(!notChild4.startsWith(parent2)); +}); + +test('path comparisons', () => { + const uri = Uri.parse('foo:///a/b/c.pyi?query#fragment', true); + assert.ok(uri.pathEndsWith('c.pyi')); + assert.ok(uri.pathEndsWith('b/c.pyi')); + assert.ok(uri.pathEndsWith('a/b/c.pyi')); + assert.ok(!uri.pathEndsWith('a/b/c.py')); + assert.ok(!uri.pathEndsWith('b/c.py')); + assert.ok(uri.pathIncludes('c.pyi')); + assert.ok(uri.pathIncludes('b/c')); + assert.ok(uri.pathIncludes('a/b/c')); + const uri2 = Uri.parse('file:///C%3A/a/b/c.pyi?query#fragment', true); + assert.ok(uri2.pathEndsWith('c.pyi')); + assert.ok(uri2.pathEndsWith('b/c.pyi')); + assert.ok(!uri2.pathStartsWith('C:/a')); + assert.ok(!uri2.pathStartsWith('C:/a/b')); + assert.ok(uri2.pathStartsWith('c:/a')); + assert.ok(uri2.pathStartsWith('c:/a/b')); +}); + +test('combinePaths', () => { + const uri1 = Uri.parse('file:///a/b/c.pyi?query#fragment', true); + const uri2 = uri1.combinePaths('d', 'e'); + assert.equal(uri2.toString(), 'file:///a/b/c.pyi/d/e'); + const uri3 = uri1.combinePaths('d', 'e/'); + assert.equal(uri3.toString(), 'file:///a/b/c.pyi/d/e'); + const uri4 = uri1.combinePaths('d', 'e', 'f/'); + assert.equal(uri4.toString(), 'file:///a/b/c.pyi/d/e/f'); + const uri5 = uri1.combinePaths('d', '..', 'e'); + assert.equal(uri5.toString(), 'file:///a/b/c.pyi/e'); + const rootedPath = process.platform === 'win32' ? 'D:' : '/D'; + const rootedResult = process.platform === 'win32' ? 'file:///d%3A/e/f' : 'file:///D/e/f'; + const uri6 = uri1.combinePaths(rootedPath, 'e', 'f'); + assert.equal(uri6.toString(), rootedResult); + const uri7 = Uri.parse('foo:', true); + const uri8 = uri7.combinePaths('d', 'e'); + assert.equal(uri8.toString(), 'foo://d/e'); + const uri9 = Uri.parse('foo:/', true); + const uri10 = uri9.combinePaths('d', 'e'); + assert.equal(uri10.toString(), 'foo:///d/e'); + const uri11 = Uri.empty().combinePaths('d', 'e'); + assert.equal(uri11.toString(), Uri.empty().toString()); +}); + +test('combinePaths non file', () => { + const uri1 = Uri.parse('baz://authority/a/b/c.pyi?query#fragment', true); + const uri2 = uri1.combinePaths('d', 'e'); + assert.equal(uri2.toString(), 'baz://authority/a/b/c.pyi/d/e'); + const uri3 = uri1.combinePaths('d', 'e/'); + assert.equal(uri3.toString(), 'baz://authority/a/b/c.pyi/d/e'); + const uri4 = uri1.combinePaths('d', 'e', 'f/'); + assert.equal(uri4.toString(), 'baz://authority/a/b/c.pyi/d/e/f'); + const uri5 = uri1.combinePaths('d', '..', 'e'); + assert.equal(uri5.toString(), 'baz://authority/a/b/c.pyi/e'); +}); + +test('getPathComponents1', () => { + const components = Uri.parse('', true).getPathComponents(); + assert.equal(components.length, 0); +}); + +test('getPathComponents2', () => { + const components = Uri.parse('/users/', true).getPathComponents(); + assert.equal(components.length, 2); + assert.equal(components[0], '/'); + assert.equal(components[1], 'users'); +}); + +test('getPathComponents3', () => { + const components = Uri.parse('/users/hello.py', true).getPathComponents(); + assert.equal(components.length, 3); + assert.equal(components[0], '/'); + assert.equal(components[1], 'users'); + assert.equal(components[2], 'hello.py'); +}); + +test('getPathComponents4', () => { + const components = Uri.parse('/users/hello/../', true).getPathComponents(); + assert.equal(components.length, 2); + assert.equal(components[0], '/'); + assert.equal(components[1], 'users'); +}); + +test('getPathComponents5', () => { + const components = Uri.parse('./hello.py', true).getPathComponents(); + assert.equal(components.length, 2); + assert.equal(components[0], '/'); + assert.equal(components[1], 'hello.py'); +}); + +test('getPathComponents6', () => { + const components = Uri.parse('file://server/share/dir/file.py', true).getPathComponents(); + assert.equal(components.length, 4); + assert.ok(components[0].slice(2).includes('server')); + assert.equal(components[1], 'share'); + assert.equal(components[2], 'dir'); + assert.equal(components[3], 'file.py'); +}); + +test('getRelativePathComponents1', () => { + const components = Uri.parse('foo:///users/', true).getRelativePathComponents(Uri.parse('foo:///users/', true)); + assert.equal(components.length, 0); +}); + +test('getRelativePathComponents2', () => { + const components = Uri.parse('foo:///users/', true).getRelativePathComponents(Uri.parse('foo:///users/bar', true)); + assert.equal(components.length, 1); + assert.equal(components[0], 'bar'); +}); + +test('getRelativePathComponents3', () => { + const components = Uri.parse('bar:///users/', true).getRelativePathComponents(Uri.parse('foo:///users/bar', true)); + assert.equal(components.length, 1); + assert.equal(components[0], 'bar'); +}); + +test('getRelativePathComponents4', () => { + const components = Uri.parse('foo:///users', true).getRelativePathComponents(Uri.parse('foo:///users/', true)); + assert.equal(components.length, 0); +}); + +test('getRelativePathComponents5', () => { + const components = Uri.parse('foo:///users/', true).getRelativePathComponents( + Uri.parse('foo:///users/bar/baz/../foo', true) + ); + assert.equal(components.length, 2); + assert.equal(components[0], 'bar'); + assert.equal(components[1], 'foo'); +}); + +test('getRelativePathComponents6', () => { + const components = Uri.parse('foo:///users/bar', true).getRelativePathComponents( + Uri.parse('foo:///users/foo', true) + ); + assert.equal(components.length, 2); + assert.equal(components[0], '..'); + assert.equal(components[1], 'foo'); +}); + +test('getFileExtension1', () => { + const ext = Uri.parse('foo:///blah.blah/hello.JsOn', true).lastExtension; + assert.equal(ext, '.JsOn'); +}); + +test('getFileName1', () => { + const fileName = Uri.parse('foo:///blah.blah/HeLLo.JsOn', true).fileName; + assert.equal(fileName, 'HeLLo.JsOn'); +}); + +test('getFileName2', () => { + const fileName1 = Uri.parse('foo:///blah.blah/hello.cpython-32m.so', true).fileName; + assert.equal(fileName1, 'hello.cpython-32m.so'); +}); + +test('stripFileExtension1', () => { + const path = Uri.parse('foo:///blah.blah/HeLLo.JsOn', true).stripExtension().getPath(); + assert.equal(path, '/blah.blah/HeLLo'); +}); + +test('stripFileExtension2', () => { + const path1 = Uri.parse('foo:/blah.blah/hello.cpython-32m.so', true).stripAllExtensions().getPath(); + assert.equal(path1, '/blah.blah/hello'); + const path2 = Uri.parse('foo:/blah.blah/hello.cpython-32m.so', true).stripExtension().getPath(); + assert.equal(path2, '/blah.blah/hello.cpython-32m'); +}); + +test('getWildcardRegexPattern1', () => { + const pattern = getWildcardRegexPattern(Uri.parse('foo:///users/me', true), './blah/'); + const regex = new RegExp(pattern); + assert.ok(regex.test('/users/me/blah/d')); + assert.ok(!regex.test('/users/me/blad/d')); +}); + +test('getWildcardRegexPattern2', () => { + const pattern = getWildcardRegexPattern(Uri.parse('foo:///users/me', true), './**/*.py?'); + const regex = new RegExp(pattern); + assert.ok(regex.test('/users/me/.blah/foo.pyd')); + assert.ok(!regex.test('/users/me/.blah/foo.py')); // No char after +}); + +test('getWildcardRegexPattern3', () => { + const pattern = getWildcardRegexPattern(Uri.parse('foo:///users/me', true), './**/.*.py'); + const regex = new RegExp(pattern); + assert.ok(regex.test('/users/me/.blah/.foo.py')); + assert.ok(!regex.test('/users/me/.blah/foo.py')); +}); + +test('getWildcardRegexPattern4', () => { + const pattern = getWildcardRegexPattern(Uri.parse('//server/share/dir', true), '.'); + const regex = new RegExp(pattern); + assert.ok(regex.test('//server/share/dir/foo.py')); + assert.ok(!regex.test('//server/share/dix/foo.py')); +}); + +test('getWildcardRoot1', () => { + const p = getWildcardRoot(Uri.parse('foo:///users/me', true), './blah/'); + assert.equal(p.toString(), 'foo:///users/me/blah'); +}); + +test('getWildcardRoot2', () => { + const p = getWildcardRoot(Uri.parse('foo:///users/me', true), './**/*.py?/'); + assert.equal(p.toString(), 'foo:///users/me'); +}); + +test('getWildcardRoot with root', () => { + const p = getWildcardRoot(Uri.parse('foo:///', true), '.'); + assert.equal(p.toString(), 'foo:///'); +}); + +test('getWildcardRoot with drive letter', () => { + const p = getWildcardRoot(Uri.parse('file:///c:/', true), '.'); + assert.equal(p.toString(), 'file:///c%3A'); +}); + +function resolvePaths(uri: string, ...paths: string[]) { + return Uri.file(uri) + .combinePaths(...paths) + .toString(); +} + +test('resolvePath1', () => { + assert.equal(resolvePaths('/path', 'to', 'file.ext'), 'file:///path/to/file.ext'); +}); + +test('resolvePath2', () => { + assert.equal(resolvePaths('/path', 'to', '..', 'from', 'file.ext/'), 'file:///path/from/file.ext'); +}); + +function getHomeDirUri() { + return Uri.file(os.homedir()); +} + +test('resolvePath3 ~ escape', () => { + assert.equal( + resolvePaths(expandPathVariables(Uri.empty(), '~/path'), 'to', '..', 'from', 'file.ext/'), + `${getHomeDirUri().toString()}/path/from/file.ext` + ); +}); + +test('resolvePath4 ~ escape in middle', () => { + assert.equal( + resolvePaths('/path', expandPathVariables(Uri.empty(), '~/file.ext/')), + `${getHomeDirUri().toString()}/file.ext` + ); +}); + +function combinePaths(uri: string, ...paths: string[]) { + return resolvePaths(uri, ...paths); +} + +test('invalid ~ without root', () => { + const path = combinePaths('Library', 'Mobile Documents', 'com~apple~CloudDocs', 'Development', 'mysuperproject'); + assert.equal(resolvePaths(expandPathVariables(Uri.parse('foo:///src', true), path)), path); +}); + +test('invalid ~ with root', () => { + const path = combinePaths('/', 'Library', 'com~apple~CloudDocs', 'Development', 'mysuperproject'); + assert.equal(resolvePaths(expandPathVariables(Uri.parse('foo:///src', true), path)), path); +}); + +function containsPath(uri: string, child: string) { + return Uri.parse(child, true).isChild(Uri.parse(uri, true)); +} + +test('containsPath1', () => { + assert.equal(containsPath('/a/b/c/', '/a/d/../b/c/./d'), true); +}); + +test('containsPath2', () => { + assert.equal(containsPath('/', '\\a'), true); +}); + +test('containsPath3', () => { + assert.equal(containsPath('/a', '/a/B'), true); +}); + +function getAnyExtensionFromPath(uri: string): string { + return Uri.parse(uri, true).lastExtension; +} +test('getAnyExtension1', () => { + assert.equal(getAnyExtensionFromPath('/path/to/file.ext'), '.ext'); +}); + +function getBaseFileName(uri: string): string { + return Uri.parse(uri, true).fileName; +} + +test('getBaseFileName1', () => { + assert.equal(getBaseFileName('/path/to/file.ext'), 'file.ext'); +}); + +test('getBaseFileName2', () => { + assert.equal(getBaseFileName('/path/to/'), 'to'); +}); + +test('getBaseFileName3', () => { + assert.equal(getBaseFileName('c:/'), ''); +}); + +function getUriRootLength(uri: string): number { + return Uri.file(uri).getRootPathLength(); +} + +test('getRootLength1', () => { + assert.equal(getUriRootLength('a'), 1); +}); + +test('getRootLength2', () => { + assert.equal(getUriRootLength('/'), 1); +}); + +test('getRootLength3', () => { + assert.equal(getUriRootLength('c:'), 2); +}); + +test('getRootLength4', () => { + assert.equal(getUriRootLength('c:d'), 0); +}); + +test('getRootLength5', () => { + assert.equal(getUriRootLength('c:/'), 2); +}); + +test('getRootLength6', () => { + assert.equal(getUriRootLength('//server'), 9); +}); + +test('getRootLength7', () => { + assert.equal(getUriRootLength('//server/share'), 9); +}); + +test('getRootLength8', () => { + assert.equal(getUriRootLength('scheme:/no/authority'), 1); +}); + +test('getRootLength9', () => { + assert.equal(getUriRootLength('scheme://with/authority'), 1); +}); + +function isRootedDiskUri(uri: string) { + return isRootedDiskPath(Uri.file(uri).getFilePath()); +} + +test('isRootedDiskPath1', () => { + assert(isRootedDiskUri('C:/a/b')); +}); + +test('isRootedDiskPath2', () => { + assert(isRootedDiskUri('/')); +}); + +test('isRootedDiskPath3', () => { + assert(isRootedDiskUri('a/b')); +}); + +test('isDiskPathRoot1', () => { + assert(isRootedDiskUri('/')); +}); + +test('isDiskPathRoot2', () => { + assert(isRootedDiskUri('c:/')); +}); + +test('isDiskPathRoot3', () => { + assert(isRootedDiskUri('c:')); +}); + +test('isDiskPathRoot4', () => { + assert(!isRootedDiskUri('c:d')); +}); + +function getRelativePath(parent: string, child: string) { + return Uri.parse(parent, true).getRelativePath(Uri.parse(child, true)); +} + +test('getRelativePath', () => { + assert.equal(getRelativePath('/a/b/c', '/a/b/c/d/e/f'), './d/e/f'); + assert.equal(getRelativePath('/a/b/c/d/e/f', '/a/b/c/'), undefined); + assert.equal(getRelativePath('/a/b/c', '/d/e/f'), undefined); +}); + +test('CaseSensitivity', () => { + const cwd = '/'; + + const fsCaseInsensitive = new vfs.TestFileSystem(/*ignoreCase*/ true, { cwd }); + assert.equal(isFileSystemCaseSensitiveInternal(fsCaseInsensitive, fsCaseInsensitive), false); + + const fsCaseSensitive = new vfs.TestFileSystem(/*ignoreCase*/ false, { cwd }); + assert.equal(isFileSystemCaseSensitiveInternal(fsCaseSensitive, fsCaseSensitive), true); +}); + +test('deduplicateFolders', () => { + const listOfFolders = [ + ['/user', '/user/temp', '/xuser/app', '/lib/python', '/home/p/.venv/lib/site-packages'].map((p) => Uri.file(p)), + ['/user', '/user/temp', '/xuser/app', '/lib/python/Python310.zip', '/home/z/.venv/lib/site-packages'].map((p) => + Uri.file(p) + ), + ['/main/python/lib/site-packages', '/home/p'].map((p) => Uri.file(p)), + ]; + + const folders = deduplicateFolders(listOfFolders).map((f) => f.getPath()); + + const expected = [ + '/user', + '/xuser/app', + '/lib/python', + '/home/z/.venv/lib/site-packages', + '/main/python/lib/site-packages', + '/home/p', + ]; + + assert.deepStrictEqual(folders.sort(), expected.sort()); +}); + +test('convert UNC path', () => { + const path = Uri.file('file:///server/c$/folder/file.py'); + + // When converting UNC path, server part shouldn't be removed. + assert(path.getPath().indexOf('server') > 0); +}); + +function lowerCaseDrive(entries: string[]) { + return entries.map((p) => (process.platform === 'win32' ? p[0].toLowerCase() + p.slice(1) : p)); +} + +test('Realcase', () => { + const fs = createFromRealFileSystem(); + const cwd = process.cwd(); + const dir = Uri.file(path.join(cwd, 'src', 'tests', '..', 'tests')); + const dirFilePath = dir.getFilePath()!; + const entries = nodefs + .readdirSync(dirFilePath) + .map((entry) => path.basename(nodefs.realpathSync(path.join(dirFilePath, entry)))); + const normalizedEntries = lowerCaseDrive(entries); + const fsentries = fs.readdirSync(dir); + assert.deepStrictEqual(normalizedEntries, fsentries); + + const paths = entries.map((entry) => nodefs.realpathSync(path.join(dirFilePath, entry))); + const fspaths = fsentries.map((entry) => fs.realCasePath(dir.combinePaths(entry)).getFilePath()!); + assert.deepStrictEqual(lowerCaseDrive(paths), fspaths); + + // Check that the '..' has been removed. + assert.ok(!fspaths.some((p) => p.toString().indexOf('..') >= 0)); + + // If windows, check that the case is correct. + if (process.platform === 'win32') { + for (const p of fspaths) { + const upper = Uri.file(p.toString().toUpperCase()); + const real = fs.realCasePath(upper); + assert.strictEqual(p, real.getFilePath()); + } + } +}); + +test('Realcase use cwd implicitly', () => { + const fs = createFromRealFileSystem(); + const cwd = process.cwd(); + const dir = path.join(cwd, 'src', 'tests'); + const uri = Uri.file(dir); + + const entries = nodefs.readdirSync(dir).map((entry) => path.basename(nodefs.realpathSync(path.join(dir, entry)))); + const fsentries = fs.readdirSync(uri); + const paths = entries.map((entry) => nodefs.realpathSync(path.join(dir, entry))); + + const fspaths = fsentries.map((entry) => fs.realCasePath(uri.combinePaths(entry)).getFilePath()); + assert.deepStrictEqual(lowerCaseDrive(paths), fspaths); +}); diff --git a/packages/pyright-internal/src/tests/workspaceEditUtils.test.ts b/packages/pyright-internal/src/tests/workspaceEditUtils.test.ts index 12b324e02..c5337c870 100644 --- a/packages/pyright-internal/src/tests/workspaceEditUtils.test.ts +++ b/packages/pyright-internal/src/tests/workspaceEditUtils.test.ts @@ -10,14 +10,15 @@ import * as assert from 'assert'; import { TextDocumentEdit, WorkspaceEdit } from 'vscode-languageserver-types'; import { CancellationToken } from 'vscode-languageserver'; +import { AnalyzerService } from '../analyzer/service'; import { IPythonMode } from '../analyzer/sourceFile'; -import { combinePaths, convertPathToUri, getDirectoryPath } from '../common/pathUtils'; +import { combinePaths, getDirectoryPath } from '../common/pathUtils'; +import { Uri } from '../common/uri/uri'; import { applyWorkspaceEdit, generateWorkspaceEdit } from '../common/workspaceEditUtils'; import { AnalyzerServiceExecutor } from '../languageService/analyzerServiceExecutor'; import { TestLanguageService } from './harness/fourslash/testLanguageService'; import { TestState, parseAndGetTestState } from './harness/fourslash/testState'; import { verifyWorkspaceEdit } from './harness/fourslash/workspaceEditTestUtils'; -import { AnalyzerService } from '../analyzer/service'; test('test applyWorkspaceEdits changes', async () => { const code = ` @@ -29,12 +30,12 @@ test('test applyWorkspaceEdits changes', async () => { const cloned = await getClonedService(state); const range = state.getRangeByMarkerName('marker')!; - const fileChanged = new Set(); + const fileChanged = new Map(); applyWorkspaceEditToService( cloned, { changes: { - [convertPathToUri(cloned.fs, range.fileName)]: [ + [Uri.file(range.fileName).toString()]: [ { range: state.convertPositionRange(range), newText: 'Text Changed', @@ -46,7 +47,7 @@ test('test applyWorkspaceEdits changes', async () => { ); assert.strictEqual(fileChanged.size, 1); - assert.strictEqual(cloned.test_program.getSourceFile(range.fileName)?.getFileContent(), 'Text Changed'); + assert.strictEqual(cloned.test_program.getSourceFile(Uri.file(range.fileName))?.getFileContent(), 'Text Changed'); }); test('test edit mode for workspace', async () => { @@ -57,16 +58,16 @@ test('test edit mode for workspace', async () => { const state = parseAndGetTestState(code).state; const range = state.getRangeByMarkerName('marker')!; - const addedFilePath = combinePaths(getDirectoryPath(range.fileName), 'test2.py'); + const addedFileUri = Uri.file(combinePaths(getDirectoryPath(range.fileName), 'test2.py')); const edits = state.workspace.service.runEditMode((program) => { - const fileChanged = new Set(); + const fileChanged = new Map(); applyWorkspaceEdit( program, { documentChanges: [ TextDocumentEdit.create( { - uri: convertPathToUri(program.fileSystem, range.fileName), + uri: Uri.file(range.fileName).toString(), version: null, }, [ @@ -82,19 +83,18 @@ test('test edit mode for workspace', async () => { ); assert.strictEqual(fileChanged.size, 1); - const info = program.getSourceFileInfo(range.fileName)!; + const info = program.getSourceFileInfo(Uri.file(range.fileName))!; const sourceFile = info.sourceFile; - program.analyzeFile(sourceFile.getFilePath(), CancellationToken.None); + program.analyzeFile(sourceFile.getUri(), CancellationToken.None); assert.strictEqual(sourceFile.getFileContent(), 'import sys'); assert.strictEqual(info.imports.length, 2); // Add a new file. - program.setFileOpened(addedFilePath, 0, '', { + program.setFileOpened(addedFileUri, 0, '', { isTracked: true, ipythonMode: IPythonMode.None, - chainedFilePath: undefined, - realFilePath: addedFilePath, + chainedFileUri: undefined, }); applyWorkspaceEdit( @@ -103,7 +103,7 @@ test('test edit mode for workspace', async () => { documentChanges: [ TextDocumentEdit.create( { - uri: convertPathToUri(program.fileSystem, addedFilePath), + uri: addedFileUri.toString(), version: null, }, [ @@ -127,7 +127,7 @@ test('test edit mode for workspace', async () => { documentChanges: [ TextDocumentEdit.create( { - uri: convertPathToUri(program.fileSystem, addedFilePath), + uri: addedFileUri.toString(), version: null, }, [ @@ -145,17 +145,17 @@ test('test edit mode for workspace', async () => { fileChanged ); - const addedInfo = program.getSourceFileInfo(addedFilePath)!; + const addedInfo = program.getSourceFileInfo(addedFileUri)!; const addedSourceFile = addedInfo.sourceFile; - program.analyzeFile(addedSourceFile.getFilePath(), CancellationToken.None); + program.analyzeFile(addedSourceFile.getUri(), CancellationToken.None); assert.strictEqual(addedSourceFile.getFileContent(), 'import os'); assert.strictEqual(addedInfo.imports.length, 2); }, CancellationToken.None); // After leaving edit mode, we should be back to where we were. - const oldSourceFile = state.workspace.service.test_program.getSourceFile(range.fileName); - state.workspace.service.backgroundAnalysisProgram.analyzeFile(oldSourceFile!.getFilePath(), CancellationToken.None); + const oldSourceFile = state.workspace.service.test_program.getSourceFile(Uri.file(range.fileName)); + state.workspace.service.backgroundAnalysisProgram.analyzeFile(oldSourceFile!.getUri(), CancellationToken.None); assert.strictEqual(oldSourceFile?.getFileContent(), ''); assert.strictEqual(oldSourceFile.getImports().length, 1); @@ -164,7 +164,7 @@ test('test edit mode for workspace', async () => { assert.deepStrictEqual(edits[0].replacementText, 'import sys'); assert.deepStrictEqual(edits[1].replacementText, 'import os'); - const addedSourceFile = state.workspace.service.test_program.getSourceFile(addedFilePath); + const addedSourceFile = state.workspace.service.test_program.getSourceFile(addedFileUri); // The added file should not be there. assert.ok(!addedSourceFile); @@ -180,14 +180,14 @@ test('test applyWorkspaceEdits documentChanges', async () => { const cloned = await getClonedService(state); const range = state.getRangeByMarkerName('marker')!; - const fileChanged = new Set(); + const fileChanged = new Map(); applyWorkspaceEditToService( cloned, { documentChanges: [ TextDocumentEdit.create( { - uri: convertPathToUri(cloned.fs, range.fileName), + uri: Uri.file(range.fileName).toString(), version: null, }, [ @@ -203,7 +203,7 @@ test('test applyWorkspaceEdits documentChanges', async () => { ); assert.strictEqual(fileChanged.size, 1); - assert.strictEqual(cloned.test_program.getSourceFile(range.fileName)?.getFileContent(), 'Text Changed'); + assert.strictEqual(cloned.test_program.getSourceFile(Uri.file(range.fileName))?.getFileContent(), 'Text Changed'); }); test('test generateWorkspaceEdits', async () => { @@ -219,12 +219,12 @@ test('test generateWorkspaceEdits', async () => { const cloned = await getClonedService(state); const range1 = state.getRangeByMarkerName('marker1')!; - const fileChanged = new Set(); + const fileChanged = new Map(); applyWorkspaceEditToService( cloned, { changes: { - [convertPathToUri(cloned.fs, range1.fileName)]: [ + [Uri.file(range1.fileName).toString()]: [ { range: state.convertPositionRange(range1), newText: 'Test1 Changed', @@ -241,7 +241,7 @@ test('test generateWorkspaceEdits', async () => { documentChanges: [ TextDocumentEdit.create( { - uri: convertPathToUri(cloned.fs, range1.fileName), + uri: Uri.file(range1.fileName).toString(), version: null, }, [ @@ -263,7 +263,7 @@ test('test generateWorkspaceEdits', async () => { documentChanges: [ TextDocumentEdit.create( { - uri: convertPathToUri(cloned.fs, range2.fileName), + uri: Uri.file(range2.fileName).toString(), version: null, }, [ @@ -282,7 +282,7 @@ test('test generateWorkspaceEdits', async () => { cloned, { changes: { - [convertPathToUri(cloned.fs, range2.fileName)]: [ + [Uri.file(range2.fileName).toString()]: [ { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 5 } }, newText: 'NewTest2', @@ -299,13 +299,13 @@ test('test generateWorkspaceEdits', async () => { verifyWorkspaceEdit( { changes: { - [convertPathToUri(cloned.fs, range1.fileName)]: [ + [Uri.file(range1.fileName).toString()]: [ { range: state.convertPositionRange(range1), newText: 'NewTest1 Changed', }, ], - [convertPathToUri(cloned.fs, range2.fileName)]: [ + [Uri.file(range2.fileName).toString()]: [ { range: state.convertPositionRange(range1), newText: 'NewTest2 Changed', @@ -317,7 +317,7 @@ test('test generateWorkspaceEdits', async () => { ); }); -function applyWorkspaceEditToService(service: AnalyzerService, edits: WorkspaceEdit, filesChanged: Set) { +function applyWorkspaceEditToService(service: AnalyzerService, edits: WorkspaceEdit, filesChanged: Map) { const program = service.backgroundAnalysisProgram.program; applyWorkspaceEdit(program, edits, filesChanged); } diff --git a/packages/pyright-internal/src/tests/zipfs.test.ts b/packages/pyright-internal/src/tests/zipfs.test.ts index 439f0bcee..326ef03fd 100644 --- a/packages/pyright-internal/src/tests/zipfs.test.ts +++ b/packages/pyright-internal/src/tests/zipfs.test.ts @@ -6,13 +6,12 @@ import * as assert from 'assert'; import * as path from 'path'; - -import { combinePaths } from '../common/pathUtils'; import { createFromRealFileSystem } from '../common/realFileSystem'; import { compareStringsCaseSensitive } from '../common/stringUtils'; +import { Uri } from '../common/uri/uri'; function runTests(p: string): void { - const zipRoot = path.resolve(path.dirname(module.filename), p); + const zipRoot = Uri.file(path.resolve(path.dirname(module.filename), p)); const fs = createFromRealFileSystem(); test('stat root', () => { @@ -39,13 +38,13 @@ function runTests(p: string): void { }); test('stat EGG-INFO', () => { - const stats = fs.statSync(combinePaths(zipRoot, 'EGG-INFO')); + const stats = fs.statSync(zipRoot.combinePaths('EGG-INFO')); assert.strictEqual(stats.isDirectory(), true); assert.strictEqual(stats.isFile(), false); }); test('readdirEntriesSync root', () => { - const entries = fs.readdirEntriesSync(combinePaths(zipRoot, 'EGG-INFO')); + const entries = fs.readdirEntriesSync(zipRoot.combinePaths('EGG-INFO')); assert.strictEqual(entries.length, 5); entries.sort((a, b) => compareStringsCaseSensitive(a.name, b.name)); @@ -72,24 +71,24 @@ function runTests(p: string): void { }); test('read file', () => { - const contents = fs.readFileSync(combinePaths(zipRoot, 'EGG-INFO', 'top_level.txt'), 'utf-8'); + const contents = fs.readFileSync(zipRoot.combinePaths('EGG-INFO', 'top_level.txt'), 'utf-8'); assert.strictEqual(contents.trim(), 'test'); }); test('read file async', async () => { - const contents = await fs.readFileText(combinePaths(zipRoot, 'EGG-INFO', 'top_level.txt'), 'utf-8'); + const contents = await fs.readFileText(zipRoot.combinePaths('EGG-INFO', 'top_level.txt'), 'utf-8'); assert.strictEqual(contents.trim(), 'test'); }); test('unlink fails', async () => { expect(() => { - fs.unlinkSync(combinePaths(zipRoot, 'EGG-INFO', 'top_level.txt')); + fs.unlinkSync(zipRoot.combinePaths('EGG-INFO', 'top_level.txt')); }).toThrow(/read-only filesystem/); }); test('isInZip', () => { - assert.strictEqual(fs.isInZip(combinePaths(zipRoot, 'EGG-INFO', 'top_level.txt')), true); - assert.strictEqual(fs.isInZip(module.filename), false); + assert.strictEqual(fs.isInZip(zipRoot.combinePaths('EGG-INFO', 'top_level.txt')), true); + assert.strictEqual(fs.isInZip(Uri.file(module.filename)), false); }); } @@ -98,7 +97,7 @@ describe('egg', () => runTests('./samples/zipfs/basic.egg')); describe('jar', () => runTests('./samples/zipfs/basic.jar')); function runBadTests(p: string): void { - const zipRoot = path.resolve(path.dirname(module.filename), p); + const zipRoot = Uri.file(path.resolve(path.dirname(module.filename), p)); const fs = createFromRealFileSystem(); test('stat root', () => { @@ -108,7 +107,7 @@ function runBadTests(p: string): void { }); test('isInZip', () => { - assert.strictEqual(fs.isInZip(combinePaths(zipRoot, 'EGG-INFO', 'top_level.txt')), false); + assert.strictEqual(fs.isInZip(zipRoot.combinePaths('EGG-INFO', 'top_level.txt')), false); }); } diff --git a/packages/pyright-internal/src/workspaceFactory.ts b/packages/pyright-internal/src/workspaceFactory.ts index 7d5666508..f78ac2a54 100644 --- a/packages/pyright-internal/src/workspaceFactory.ts +++ b/packages/pyright-internal/src/workspaceFactory.ts @@ -9,7 +9,7 @@ import { InitializeParams, WorkspaceFoldersChangeEvent } from 'vscode-languagese import { AnalyzerService } from './analyzer/service'; import { ConsoleInterface } from './common/console'; import { createDeferred } from './common/deferred'; -import { UriParser } from './common/uriParser'; +import { Uri } from './common/uri/uri'; let WorkspaceFactoryIdCounter = 0; @@ -75,16 +75,15 @@ export function createInitStatus(): InitStatus { // rootPath will always point to the folder that contains the workspace. export interface Workspace { workspaceName: string; - rootPath: string; - uri: string; + rootUri: Uri; kinds: string[]; service: AnalyzerService; disableLanguageServices: boolean; disableOrganizeImports: boolean; disableWorkspaceSymbol: boolean; isInitialized: InitStatus; - searchPathsToWatch: string[]; - pythonPath: string | undefined; + searchPathsToWatch: Uri[]; + pythonPath: Uri | undefined; pythonPathKind: WorkspacePythonPathKind; pythonEnvironmentName: string | undefined; } @@ -93,20 +92,14 @@ export class WorkspaceFactory { private _defaultWorkspacePath = ''; private _map = new Map(); private _id = WorkspaceFactoryIdCounter++; - constructor( private readonly _console: ConsoleInterface, - private readonly _uriParser: UriParser, private readonly _isWeb: boolean, - private readonly _createService: ( - name: string, - rootPath: string, - uri: string, - kinds: string[] - ) => AnalyzerService, - private readonly _isPythonPathImmutable: (path: string) => boolean, + private readonly _createService: (name: string, rootPath: Uri, kinds: string[]) => AnalyzerService, + private readonly _isPythonPathImmutable: (uri: Uri) => boolean, private readonly _onWorkspaceCreated: (workspace: Workspace) => void, - private readonly _onWorkspaceRemoved: (workspace: Workspace) => void + private readonly _onWorkspaceRemoved: (workspace: Workspace) => void, + private readonly _isCaseSensitive: boolean ) { this._console.log(`WorkspaceFactory ${this._id} created`); } @@ -115,48 +108,49 @@ export class WorkspaceFactory { // Create a service instance for each of the workspace folders. if (params.workspaceFolders) { params.workspaceFolders.forEach((folder) => { - const path = this._uriParser.decodeTextDocumentUri(folder.uri); - this._add(folder.uri, path, folder.name, undefined, WorkspacePythonPathKind.Mutable, [ - WellKnownWorkspaceKinds.Regular, - ]); + this._add( + Uri.parse(folder.uri, this._isCaseSensitive), + folder.name, + undefined, + WorkspacePythonPathKind.Mutable, + [WellKnownWorkspaceKinds.Regular] + ); }); } else if (params.rootPath) { - this._add(params.rootUri || '', params.rootPath, '', undefined, WorkspacePythonPathKind.Mutable, [ - WellKnownWorkspaceKinds.Regular, - ]); + this._add( + Uri.file(params.rootPath, this._isCaseSensitive), + '', + undefined, + WorkspacePythonPathKind.Mutable, + [WellKnownWorkspaceKinds.Regular] + ); } } handleWorkspaceFoldersChanged(params: WorkspaceFoldersChangeEvent) { params.removed.forEach((workspaceInfo) => { - const rootPath = this._uriParser.decodeTextDocumentUri(workspaceInfo.uri); + const uri = Uri.parse(workspaceInfo.uri, this._isCaseSensitive); // Delete all workspaces for this folder. Even the ones generated for notebook kernels. - const workspaces = this.getNonDefaultWorkspaces().filter((w) => w.rootPath === rootPath); + const workspaces = this.getNonDefaultWorkspaces().filter((w) => w.rootUri.equals(uri)); workspaces.forEach((w) => { this._remove(w); }); }); params.added.forEach((workspaceInfo) => { - const rootPath = this._uriParser.decodeTextDocumentUri(workspaceInfo.uri); - + const uri = Uri.parse(workspaceInfo.uri, this._isCaseSensitive); // If there's a workspace that contains this folder, we need to mimic files from this workspace to // to the new one. Otherwise the subfolder won't have the changes for the files in it. - const containing = this.items().filter((w) => rootPath.startsWith(w.rootPath))[0]; + const containing = this.items().filter((w) => uri.startsWith(w.rootUri))[0]; // Add the new workspace. - const newWorkspace = this._add( - workspaceInfo.uri, - rootPath, - workspaceInfo.name, - undefined, - WorkspacePythonPathKind.Mutable, - [WellKnownWorkspaceKinds.Regular] - ); + const newWorkspace = this._add(uri, workspaceInfo.name, undefined, WorkspacePythonPathKind.Mutable, [ + WellKnownWorkspaceKinds.Regular, + ]); // Move files from the containing workspace to the new one that are in the new folder. if (containing) { - this._mimicOpenFiles(containing, newWorkspace, (f) => f.startsWith(rootPath)); + this._mimicOpenFiles(containing, newWorkspace, (f) => f.startsWith(uri)); } }); } @@ -165,7 +159,7 @@ export class WorkspaceFactory { return Array.from(this._map.values()); } - applyPythonPath(workspace: Workspace, newPythonPath: string | undefined): string | undefined { + applyPythonPath(workspace: Workspace, newPythonPath: Uri | undefined): Uri | undefined { // See if were allowed to apply the new python path if (workspace.pythonPathKind === WorkspacePythonPathKind.Mutable && newPythonPath) { const originalPythonPath = workspace.pythonPath; @@ -181,7 +175,7 @@ export class WorkspaceFactory { } // If the python path has changed, we may need to move the immutable files to the correct workspace. - if (originalPythonPath && newPythonPath !== originalPythonPath && workspaceInMap) { + if (originalPythonPath && !newPythonPath.equals(originalPythonPath) && workspaceInMap) { // Potentially move immutable files from one workspace to another. this._moveImmutableFilesToCorrectWorkspace(originalPythonPath, workspaceInMap); } @@ -219,16 +213,14 @@ export class WorkspaceFactory { return false; } - getContainingWorkspace(filePath: string, pythonPath?: string) { + getContainingWorkspace(filePath: Uri, pythonPath?: Uri) { return this._getBestRegularWorkspace( - this.getNonDefaultWorkspaces(WellKnownWorkspaceKinds.Regular).filter((w) => - filePath.startsWith(w.rootPath) - ), + this.getNonDefaultWorkspaces(WellKnownWorkspaceKinds.Regular).filter((w) => filePath.startsWith(w.rootUri)), pythonPath ); } - moveFiles(filePaths: string[], fromWorkspace: Workspace, toWorkspace: Workspace) { + moveFiles(filePaths: Uri[], fromWorkspace: Workspace, toWorkspace: Workspace) { if (fromWorkspace === toWorkspace) { return; } @@ -241,21 +233,13 @@ export class WorkspaceFactory { const version = fileInfo.sourceFile.getClientVersion() ?? null; const content = fileInfo.sourceFile.getFileContent() || ''; const ipythonMode = fileInfo.sourceFile.getIPythonMode(); - const chainedSourceFile = fileInfo.chainedSourceFile?.sourceFile.getFilePath(); - const realFilePath = fileInfo.sourceFile.getRealFilePath(); + const chainedSourceFile = fileInfo.chainedSourceFile?.sourceFile.getUri(); // Remove the file from the old workspace first (closing will propagate to the toWorkspace automatically). fromWorkspace.service.setFileClosed(f, /* isTracked */ false); // Then open it in the toWorkspace so that it is marked tracked there. - toWorkspace.service.setFileOpened( - f, - version, - content, - ipythonMode, - chainedSourceFile, - realFilePath - ); + toWorkspace.service.setFileOpened(f, version, content, ipythonMode, chainedSourceFile); } }); @@ -269,7 +253,7 @@ export class WorkspaceFactory { getNonDefaultWorkspaces(kind?: string): Workspace[] { const workspaces: Workspace[] = []; this._map.forEach((workspace) => { - if (!workspace.rootPath) { + if (!workspace.rootUri) { return; } @@ -285,13 +269,13 @@ export class WorkspaceFactory { // Returns the best workspace for a file. Waits for the workspace to be finished handling other events before // returning the appropriate workspace. - async getWorkspaceForFile(filePath: string, pythonPath: string | undefined): Promise { + async getWorkspaceForFile(uri: Uri, pythonPath: Uri | undefined): Promise { // Wait for all workspaces to be initialized before attempting to find the best workspace. Otherwise // the list of files won't be complete and the `contains` check might fail. await Promise.all(this.items().map((w) => w.isInitialized.promise)); // Find or create best match. - const workspace = await this._getOrCreateBestWorkspaceForFile(filePath, pythonPath); + const workspace = await this._getOrCreateBestWorkspaceForFile(uri, pythonPath); // The workspace may have just been created. Wait for it to be initialized before returning it. await workspace.isInitialized.promise; @@ -299,12 +283,12 @@ export class WorkspaceFactory { return workspace; } - getWorkspaceForFileSync(filePath: string, pythonPath: string | undefined): Workspace { + getWorkspaceForFileSync(filePath: Uri, pythonPath: Uri | undefined): Workspace { // Find or create best match. return this._getOrCreateBestWorkspaceFileSync(filePath, pythonPath); } - async getContainingWorkspacesForFile(filePath: string): Promise { + async getContainingWorkspacesForFile(filePath: Uri): Promise { // Wait for all workspaces to be initialized before attempting to find the best workspace. Otherwise // the list of files won't be complete and the `contains` check might fail. await Promise.all(this.items().map((w) => w.isInitialized.promise)); @@ -318,17 +302,17 @@ export class WorkspaceFactory { return workspaces; } - getContainingWorkspacesForFileSync(filePath: string): Workspace[] { + getContainingWorkspacesForFileSync(fileUri: Uri): Workspace[] { // All workspaces that track the file should be considered. - let workspaces = this.items().filter((w) => w.service.isTracked(filePath)); + let workspaces = this.items().filter((w) => w.service.isTracked(fileUri)); // If that list is empty, get the best workspace if (workspaces.length === 0) { - workspaces.push(this._getOrCreateBestWorkspaceFileSync(filePath, undefined)); + workspaces.push(this._getOrCreateBestWorkspaceFileSync(fileUri, undefined)); } // If the file is immutable, then only return that workspace. - if (this._isPythonPathImmutable(filePath)) { + if (this._isPythonPathImmutable(fileUri)) { workspaces = workspaces.filter((w) => w.pythonPathKind === WorkspacePythonPathKind.Immutable); } @@ -346,21 +330,18 @@ export class WorkspaceFactory { } } - private async _moveImmutableFilesToCorrectWorkspace(oldPythonPath: string, mutableWorkspace: Workspace) { + private async _moveImmutableFilesToCorrectWorkspace(oldPythonPath: Uri, mutableWorkspace: Workspace) { // If the python path changes we may need to move some immutable files around. // For example, if a notebook had the old python path, we need to create a new workspace // for the notebook. // If a notebook has the new python path but is currently in a workspace with the path hardcoded, we need to move it to // this workspace. - const oldPathFiles = Array.from( - new Set(mutableWorkspace.service.getOpenFiles().filter((f) => this._isPythonPathImmutable(f))) - ); + const oldPathFiles = mutableWorkspace.service.getOpenFiles().filter((f) => this._isPythonPathImmutable(f)); const exitingWorkspaceWithSamePath = this.items().find( (w) => w.pythonPath === mutableWorkspace.pythonPath && w !== mutableWorkspace ); - const newPathFiles = new Set( - exitingWorkspaceWithSamePath?.service.getOpenFiles().filter((f) => this._isPythonPathImmutable(f)) - ); + const newPathFiles = + exitingWorkspaceWithSamePath?.service.getOpenFiles().filter((f) => this._isPythonPathImmutable(f)) ?? []; // Immutable files that were in this mutableWorkspace have to be moved // to a (potentially) new workspace (with the old path). @@ -376,39 +357,37 @@ export class WorkspaceFactory { // Immutable files from a different workspace (with the same path as the new path) // have to be moved to the mutable workspace (which now has the new path) if (exitingWorkspaceWithSamePath) { - this.moveFiles(Array.from(newPathFiles), exitingWorkspaceWithSamePath!, mutableWorkspace); + this.moveFiles(newPathFiles, exitingWorkspaceWithSamePath!, mutableWorkspace); this.removeUnused(exitingWorkspaceWithSamePath); } } private _add( - rootUri: string, - rootPath: string, + rootUri: Uri, name: string, - pythonPath: string | undefined, + pythonPath: Uri | undefined, pythonPathKind: WorkspacePythonPathKind, kinds: string[] ) { // Update the kind based if the uri is local or not - if (!kinds.includes(WellKnownWorkspaceKinds.Default) && (!this._uriParser.isLocal(rootUri) || this._isWeb)) { + if (!kinds.includes(WellKnownWorkspaceKinds.Default) && (!rootUri.isLocal() || this._isWeb)) { // Web based workspace should be limited. kinds = [...kinds, WellKnownWorkspaceKinds.Limited]; } const result: Workspace = { workspaceName: name, - rootPath, - uri: rootUri, + rootUri, kinds, pythonPath, pythonPathKind, - service: this._createService(name, rootPath, rootUri, kinds), + service: this._createService(name, rootUri, kinds), disableLanguageServices: false, disableOrganizeImports: false, disableWorkspaceSymbol: false, isInitialized: createInitStatus(), searchPathsToWatch: [], - pythonEnvironmentName: pythonPath, + pythonEnvironmentName: pythonPath?.toString(), }; // Stick in our map @@ -441,7 +420,7 @@ export class WorkspaceFactory { } } - private _getDefaultWorkspaceKey(pythonPath: string | undefined) { + private _getDefaultWorkspaceKey(pythonPath: Uri | undefined) { return `${this._defaultWorkspacePath}:${ pythonPath !== undefined ? pythonPath : WorkspacePythonPathKind.Mutable }`; @@ -452,7 +431,7 @@ export class WorkspaceFactory { // without a root path const rootPath = value.kinds.includes(WellKnownWorkspaceKinds.Default) ? this._defaultWorkspacePath - : value.rootPath; + : value.rootUri; // Key is defined by the rootPath and the pythonPath. We might include platform in this, but for now // platform is only used by the import resolver. @@ -461,37 +440,34 @@ export class WorkspaceFactory { }`; } - private async _getOrCreateBestWorkspaceForFile( - filePath: string, - pythonPath: string | undefined - ): Promise { + private async _getOrCreateBestWorkspaceForFile(uri: Uri, pythonPath: Uri | undefined): Promise { // Find the current best workspace (without creating a new one) - let bestInstance = this._getBestWorkspaceForFile(filePath, pythonPath); + let bestInstance = this._getBestWorkspaceForFile(uri, pythonPath); // Make sure the best instance is initialized so that it has its pythonPath. await bestInstance.isInitialized.promise; // If this best instance doesn't match the pythonPath, then we need to create a new one. - if (pythonPath !== undefined && bestInstance.pythonPath !== pythonPath) { + if (pythonPath !== undefined && !bestInstance.pythonPath?.equals(pythonPath)) { bestInstance = this._createImmutableCopy(bestInstance, pythonPath); } return bestInstance; } - private _getOrCreateBestWorkspaceFileSync(filePath: string, pythonPath: string | undefined) { + private _getOrCreateBestWorkspaceFileSync(uri: Uri, pythonPath: Uri | undefined) { // Find the current best workspace (without creating a new one) - let bestInstance = this._getBestWorkspaceForFile(filePath, pythonPath); + let bestInstance = this._getBestWorkspaceForFile(uri, pythonPath); // If this best instance doesn't match the pythonPath, then we need to create a new one. - if (pythonPath !== undefined && bestInstance.pythonPath !== pythonPath) { + if (pythonPath !== undefined && !bestInstance.pythonPath?.equals(pythonPath)) { bestInstance = this._createImmutableCopy(bestInstance, pythonPath); } return bestInstance; } - private _mimicOpenFiles(source: Workspace, dest: Workspace, predicate: (f: string) => boolean) { + private _mimicOpenFiles(source: Workspace, dest: Workspace, predicate: (f: Uri) => boolean) { // All mutable open files in the first workspace should be opened in the new workspace. // Immutable files should stay where they are since they're tied to a specific workspace. const files = source.service.getOpenFiles().filter((f) => !this._isPythonPathImmutable(f)); @@ -505,17 +481,15 @@ export class WorkspaceFactory { sourceFile.getClientVersion() || null, fileContents || '', sourceFile.getIPythonMode(), - sourceFileInfo.chainedSourceFile?.sourceFile.getFilePath(), - sourceFile.getRealFilePath() + sourceFileInfo.chainedSourceFile?.sourceFile.getUri() ); } } } - private _createImmutableCopy(workspace: Workspace, pythonPath: string): Workspace { + private _createImmutableCopy(workspace: Workspace, pythonPath: Uri): Workspace { const result = this._add( - workspace.uri, - workspace.rootPath, + workspace.rootUri, workspace.workspaceName, pythonPath, WorkspacePythonPathKind.Immutable, @@ -528,7 +502,7 @@ export class WorkspaceFactory { return result; } - private _getBestWorkspaceForFile(filePath: string, pythonPath: string | undefined): Workspace { + private _getBestWorkspaceForFile(uri: Uri, pythonPath: Uri | undefined): Workspace { let bestInstance: Workspace | undefined; // The order of how we find the best matching workspace for the given file is @@ -543,7 +517,7 @@ export class WorkspaceFactory { // First find the workspaces that are tracking the file const regularWorkspaces = this.getNonDefaultWorkspaces(WellKnownWorkspaceKinds.Regular); - const trackingWorkspaces = this.items().filter((w) => w.service.isTracked(filePath)); + const trackingWorkspaces = this.items().filter((w) => w.service.isTracked(uri)); // Then find the best in all of those that actually matches the pythonPath. bestInstance = this._getBestRegularWorkspace(trackingWorkspaces, pythonPath); @@ -552,7 +526,9 @@ export class WorkspaceFactory { // length root path if ( bestInstance === undefined && - regularWorkspaces.every((w) => w.rootPath.length === regularWorkspaces[0].rootPath.length) + regularWorkspaces.every( + (w) => w.rootUri.getRootPathLength() === regularWorkspaces[0].rootUri.getRootPathLength() + ) ) { bestInstance = this._getBestRegularWorkspace(regularWorkspaces, pythonPath); } @@ -562,7 +538,7 @@ export class WorkspaceFactory { if (bestInstance === undefined || bestInstance.pythonPath !== pythonPath) { bestInstance = this._getBestRegularWorkspace( - regularWorkspaces.filter((w) => w.service.hasSourceFile(filePath)), + regularWorkspaces.filter((w) => w.service.hasSourceFile(uri)), pythonPath ) || bestInstance; } @@ -575,15 +551,14 @@ export class WorkspaceFactory { return bestInstance; } - private _getOrCreateDefaultWorkspace(pythonPath: string | undefined): Workspace { + private _getOrCreateDefaultWorkspace(pythonPath: Uri | undefined): Workspace { // Default key depends upon the pythonPath let defaultWorkspace = this._map.get(this._getDefaultWorkspaceKey(pythonPath)); if (!defaultWorkspace) { // Create a default workspace for files that are outside // of all workspaces. defaultWorkspace = this._add( - '', - '', + Uri.empty(), this._defaultWorkspacePath, pythonPath, pythonPath !== undefined ? WorkspacePythonPathKind.Immutable : WorkspacePythonPathKind.Mutable, @@ -597,18 +572,18 @@ export class WorkspaceFactory { private _getLongestPathWorkspace(workspaces: Workspace[]): Workspace { const longestPath = workspaces.reduce((previousPath, currentWorkspace) => { if (!previousPath) { - return currentWorkspace.rootPath; + return currentWorkspace.rootUri; } - if (currentWorkspace.rootPath.length > previousPath.length) { - return currentWorkspace.rootPath; + if (currentWorkspace.rootUri.getPathLength() > previousPath.getPathLength()) { + return currentWorkspace.rootUri; } return previousPath; - }, ''); - return workspaces.find((w) => w.rootPath === longestPath)!; + }, Uri.empty()); + return workspaces.find((w) => w.rootUri === longestPath)!; } - private _getBestRegularWorkspace(workspaces: Workspace[], pythonPath?: string): Workspace | undefined { + private _getBestRegularWorkspace(workspaces: Workspace[], pythonPath?: Uri): Workspace | undefined { if (workspaces.length === 0) { return undefined; } diff --git a/tsconfig.json b/tsconfig.json index a612a1d41..171104dc5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "noImplicitReturns": true, "noImplicitOverride": true, "checkJs": true, + "experimentalDecorators": true }, "exclude": [ "node_modules",