diff --git a/client/package-lock.json b/client/package-lock.json index 1b41c810f..ca8ce9b35 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@types/node": { - "version": "12.7.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.8.tgz", - "integrity": "sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A==", + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", + "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", "dev": true }, "agent-base": { @@ -199,9 +199,9 @@ } }, "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, "concat-map": { @@ -1016,9 +1016,9 @@ } }, "typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", - "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", "dev": true }, "uc.micro": { @@ -1082,9 +1082,9 @@ } }, "vsce": { - "version": "1.66.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.66.0.tgz", - "integrity": "sha512-Zf4+WD4PhEcOr7jkU08SI9lwFqDhmhk73YOCGQ/tNLaBy+PnnX4eSdqj9LdzDLuI2dsyomJLXzDSNgxuaInxCQ==", + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.68.0.tgz", + "integrity": "sha512-yFbRYu4x4GbdQzZdEQQeRJBxgPdummgcUOFHUtnclW8XQl3MTuKgXL3TtI09gb5oq7jE6kdyvBmpBcmDGsmhcQ==", "dev": true, "requires": { "azure-devops-node-api": "^7.2.0", diff --git a/client/package.json b/client/package.json index 32223fca8..91248840b 100644 --- a/client/package.json +++ b/client/package.json @@ -104,8 +104,8 @@ "postinstall": "node ./node_modules/vscode/bin/install" }, "devDependencies": { - "typescript": "^3.6.3", - "vsce": "^1.66.0", + "typescript": "^3.6.4", + "vsce": "^1.68.0", "vscode": "^1.1.36" }, "dependencies": { diff --git a/package-lock.json b/package-lock.json index 82d26e34d..b8ae40392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,15 @@ "dev": true }, "@types/node": { - "version": "11.13.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.21.tgz", - "integrity": "sha512-fLwcSjMmDnjfk4FP7/QDiNzXSCEOGNvEe9eA6vaITmC784+Gm70wF7woaFQxUb2CpMjgLBhSPyhH0oIe1JS2uw==", + "version": "11.13.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.22.tgz", + "integrity": "sha512-rOsaPRUGTOXbRBOKToy4cgZXY4Y+QSVhxcLwdEveozbk7yuudhWMpxxcaXqYizLMP3VY7OcWCFtx9lGFh5j5kg==", "dev": true }, "typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", - "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", "dev": true } } diff --git a/package.json b/package.json index 952fb7775..d41f432ef 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,8 @@ }, "devDependencies": { "@types/mocha": "^5.2.7", - "@types/node": "^11.13.21", - "typescript": "^3.6.3" + "@types/node": "^11.13.22", + "typescript": "^3.6.4" }, "main": "index.js", "bin": { diff --git a/server/package-lock.json b/server/package-lock.json index 0bc90dcac..12e55243c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -484,9 +484,9 @@ } }, "@types/jest": { - "version": "24.0.18", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.18.tgz", - "integrity": "sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ==", + "version": "24.0.19", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.19.tgz", + "integrity": "sha512-YYiqfSjocv7lk5H/T+v5MjATYjaTMsUkbDnjGqSMoO88jWdtJXJV4ST/7DKZcoMHMBvB2SeSfyOzZfkxXHR5xg==", "dev": true, "requires": { "@types/jest-diff": "*" @@ -499,9 +499,9 @@ "dev": true }, "@types/node": { - "version": "11.13.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.21.tgz", - "integrity": "sha512-fLwcSjMmDnjfk4FP7/QDiNzXSCEOGNvEe9eA6vaITmC784+Gm70wF7woaFQxUb2CpMjgLBhSPyhH0oIe1JS2uw==", + "version": "11.13.22", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.22.tgz", + "integrity": "sha512-rOsaPRUGTOXbRBOKToy4cgZXY4Y+QSVhxcLwdEveozbk7yuudhWMpxxcaXqYizLMP3VY7OcWCFtx9lGFh5j5kg==", "dev": true }, "@types/stack-utils": { @@ -1070,9 +1070,9 @@ "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==" }, "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz", + "integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg==", "dev": true }, "bn.js": { @@ -6131,9 +6131,9 @@ "dev": true }, "terser": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.4.tgz", - "integrity": "sha512-Kcrn3RiW8NtHBP0ssOAzwa2MsIRQ8lJWiBG/K7JgqPlomA3mtb2DEmp4/hrUA+Jujx+WZ02zqd7GYD+QRBB/2Q==", + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.9.tgz", + "integrity": "sha512-NFGMpHjlzmyOtPL+fDw3G7+6Ueh/sz4mkaUYa4lJCxOPTNzd0Uj0aZJOmsDYoSQyfuVoWDMSWTPU3huyOm2zdA==", "dev": true, "requires": { "commander": "^2.20.0", @@ -6415,9 +6415,9 @@ "dev": true }, "typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", - "integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", "dev": true }, "typescript-char": { @@ -6780,9 +6780,9 @@ "dev": true }, "webpack": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz", - "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==", + "version": "4.41.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.2.tgz", + "integrity": "sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", @@ -6989,9 +6989,9 @@ "dev": true }, "yallist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.0.tgz", - "integrity": "sha512-6gpP93MR+VOOehKbCPchro3wFZNSNmek8A2kbkOAZLIZAYx1KP/zAqwO0sOHi3xJEb+UBz8NaYt/17UNit1Q9w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, "yargs": { diff --git a/server/package.json b/server/package.json index 3be8704cf..bf5fadef6 100644 --- a/server/package.json +++ b/server/package.json @@ -32,8 +32,8 @@ "@types/chokidar": "^2.1.3", "@types/command-line-args": "^5.0.0", "@types/fs-extra": "^5.1.0", - "@types/jest": "^24.0.18", - "@types/node": "^11.13.21", + "@types/jest": "^24.0.19", + "@types/node": "^11.13.22", "fs-extra": "^7.0.1", "jest": "^24.9.0", "node-loader": "^0.6.0", @@ -41,8 +41,8 @@ "ts-loader": "^5.4.5", "tslint": "^5.20.0", "tslint-microsoft-contrib": "^6.2.0", - "typescript": "^3.6.3", - "webpack": "^4.41.0", + "typescript": "^3.6.4", + "webpack": "^4.41.2", "webpack-cli": "^3.3.9" }, "types": "out/main.d.ts", diff --git a/server/src/analyzer/analyzerFileInfo.ts b/server/src/analyzer/analyzerFileInfo.ts index fa77c8f70..ecad9cb1e 100644 --- a/server/src/analyzer/analyzerFileInfo.ts +++ b/server/src/analyzer/analyzerFileInfo.ts @@ -13,16 +13,22 @@ import StringMap from '../common/stringMap'; import { TextRange } from '../common/textRange'; import { TextRangeCollection } from '../common/textRangeCollection'; import { Scope } from './scope'; -import { ModuleType } from './types'; +import { SymbolTable } from './symbol'; -// Maps import paths to the parse tree for the imported module. -export type ImportMap = Map; +// Maps import paths to the symbol table for the imported module. +export type ImportLookup = (filePath: string) => ImportLookupResult | undefined; + +export interface ImportLookupResult { + symbolTable: SymbolTable; + docString?: string; +} export interface AnalyzerFileInfo { - importMap: ImportMap; + importLookup: ImportLookup; futureImports: StringMap; builtinsScope?: Scope; typingModulePath?: string; + collectionsModulePath?: string; diagnosticSink: TextRangeDiagnosticSink; executionEnvironment: ExecutionEnvironment; diagnosticSettings: DiagnosticSettings; diff --git a/server/src/analyzer/binder.ts b/server/src/analyzer/binder.ts index 91b921331..fbd1be431 100644 --- a/server/src/analyzer/binder.ts +++ b/server/src/analyzer/binder.ts @@ -27,18 +27,18 @@ import StringMap from '../common/stringMap'; import { TextRange } from '../common/textRange'; import { AssignmentExpressionNode, AssignmentNode, AugmentedAssignmentExpressionNode, AwaitExpressionNode, ClassNode, DelNode, ExceptNode, ExpressionNode, ForNode, - FunctionNode, GlobalNode, IfNode, ImportAsNode, ImportFromAsNode, LambdaNode, - ListComprehensionNode, ModuleNameNode, ModuleNode, NameNode, NonlocalNode, ParseNode, - ParseNodeArray, ParseNodeType, RaiseNode, StatementNode, StringListNode, SuiteNode, - TryNode, TypeAnnotationExpressionNode, WhileNode, WithNode, YieldExpressionNode, - YieldFromExpressionNode } from '../parser/parseNodes'; + FunctionNode, GlobalNode, IfNode, ImportAsNode, ImportFromNode, LambdaNode, + ListComprehensionNode, ModuleNameNode, ModuleNode, NameNode, NonlocalNode, + ParseNode, ParseNodeArray, ParseNodeType, RaiseNode, StatementNode, StringListNode, + SuiteNode, TryNode, TypeAnnotationExpressionNode, WhileNode, WithNode, + YieldExpressionNode, YieldFromExpressionNode } from '../parser/parseNodes'; import * as StringTokenUtils from '../parser/stringTokenUtils'; import { StringTokenFlags } from '../parser/tokenizerTypes'; import { AnalyzerFileInfo } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; -import { DeclarationType } from './declaration'; +import { AliasDeclaration, DeclarationType, ModuleLoaderActions } from './declaration'; import * as DocStringUtils from './docStringUtils'; -import { ImportType } from './importResult'; +import { ImplicitImport, ImportResult, ImportType } from './importResult'; import { defaultTypeSourceId, TypeSourceId } from './inferredType'; import * as ParseTreeUtils from './parseTreeUtils'; import { ParseTreeWalker } from './parseTreeWalker'; @@ -593,18 +593,171 @@ export abstract class Binder extends ParseTreeWalker { } visitImportAs(node: ImportAsNode): boolean { - if (node.alias) { - this._bindNameToScope(this._currentScope, node.alias.nameToken.value); - } else if (node.module.nameParts.length > 0) { - this._bindNameToScope(this._currentScope, node.module.nameParts[0].nameToken.value); + if (node.module.nameParts.length > 0) { + const firstNamePartValue = node.module.nameParts[0].nameToken.value; + + let symbolName: string | undefined; + if (node.alias) { + // The symbol name is defined by the alias. + symbolName = node.alias.nameToken.value; + } else { + // There was no alias, so we need to use the first element of + // the name parts as the symbol. + symbolName = firstNamePartValue; + } + + const symbol = this._bindNameToScope(this._currentScope, symbolName); + + const importInfo = AnalyzerNodeInfo.getImportInfo(node.module); + assert(importInfo !== undefined); + + if (importInfo && importInfo.isImportFound && importInfo.resolvedPaths.length > 0 && symbol) { + // See if there's already a matching alias delaration for this import. + // if so, we'll update it rather than creating a new one. This is required + // to handle cases where multiple import statements target the same + // starting symbol such as "import a.b.c" and "import a.d". In this case, + // we'll build a single declaration that describes the combined actions + // of both import statements, thus reflecting the behavior of the + // python module loader. + const existingDecl = symbol.getDeclarations().find( + decl => decl.type === DeclarationType.Alias && + decl.firstNamePart === firstNamePartValue); + + const newDecl: AliasDeclaration = existingDecl as AliasDeclaration || { + type: DeclarationType.Alias, + path: '', + range: getEmptyRange(), + firstNamePart: firstNamePartValue, + implicitImports: new Map() + }; + + // Add the implicit imports for this module if it's the last + // name part we're resolving. + if (node.alias || node.module.nameParts.length === 1) { + newDecl.path = importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1]; + this._addImplicitImportsToLoaderActions(importInfo, newDecl); + } else { + // Fill in the remaining name parts. + let curLoaderActions: ModuleLoaderActions = newDecl; + + for (let i = 1; i < node.module.nameParts.length; i++) { + if (i >= importInfo.resolvedPaths.length) { + break; + } + + const namePartValue = node.module.nameParts[i].nameToken.value; + + // Is there an existing loader action for this name? + let loaderActions = curLoaderActions.implicitImports.get(namePartValue); + if (!loaderActions) { + // Allocate a new loader action. + loaderActions = { + path: '', + implicitImports: new Map() + }; + curLoaderActions.implicitImports.set(namePartValue, loaderActions); + } + + // If this is the last name part we're resolving, add in the + // implicit imports as well. + if (i === node.module.nameParts.length - 1) { + loaderActions.path = importInfo.resolvedPaths[i]; + this._addImplicitImportsToLoaderActions(importInfo, loaderActions); + } + + curLoaderActions = loaderActions; + } + } + + if (!existingDecl) { + symbol.addDeclaration(newDecl); + } + } } return true; } - visitImportFromAs(node: ImportFromAsNode): boolean { - const nameNode = node.alias || node.name; - this._bindNameToScope(this._currentScope, nameNode.nameToken.value); + visitImportFrom(node: ImportFromNode): boolean { + const importInfo = AnalyzerNodeInfo.getImportInfo(node.module); + + let resolvedPath = ''; + if (importInfo && importInfo.isImportFound) { + resolvedPath = importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1]; + } + + if (node.isWildcardImport) { + if (importInfo && importInfo.implicitImports) { + const lookupInfo = this._fileInfo.importLookup(resolvedPath); + if (lookupInfo) { + lookupInfo.symbolTable.forEach((_, name) => { + const symbol = this._bindNameToScope(this._currentScope, name); + if (symbol) { + const aliasDecl: AliasDeclaration = { + type: DeclarationType.Alias, + path: resolvedPath, + range: getEmptyRange(), + symbolName: name, + implicitImports: new Map() + }; + symbol.addDeclaration(aliasDecl); + } + }); + } + + // Also add all of the implicitly-imported modules for + // the import module. + importInfo.implicitImports.forEach(implicitImport => { + const symbol = this._bindNameToScope(this._currentScope, implicitImport.name); + if (symbol) { + const aliasDecl: AliasDeclaration = { + type: DeclarationType.Alias, + path: implicitImport.path, + range: getEmptyRange(), + implicitImports: new Map() + }; + symbol.addDeclaration(aliasDecl); + } + }); + } + } else { + node.imports.forEach(importSymbolNode => { + const importedName = importSymbolNode.name.nameToken.value; + const nameNode = importSymbolNode.alias || importSymbolNode.name; + const symbol = this._bindNameToScope(this._currentScope, nameNode.nameToken.value); + + if (symbol) { + let aliasDecl: AliasDeclaration | undefined; + + // Is the import referring to an implicitly-imported module? + let implicitImport: ImplicitImport | undefined; + if (importInfo && importInfo.implicitImports) { + implicitImport = importInfo.implicitImports.find(imp => imp.name === importedName); + } + + if (implicitImport) { + aliasDecl = { + type: DeclarationType.Alias, + path: implicitImport.path, + range: getEmptyRange(), + implicitImports: new Map() + }; + } else if (resolvedPath) { + aliasDecl = { + type: DeclarationType.Alias, + path: resolvedPath, + range: getEmptyRange(), + symbolName: importedName, + implicitImports: new Map() + }; + } + + if (aliasDecl) { + symbol.addDeclaration(aliasDecl); + } + } + }); + } return true; } @@ -775,6 +928,20 @@ export abstract class Binder extends ParseTreeWalker { }); } + private _addImplicitImportsToLoaderActions(importResult: ImportResult, loaderActions: ModuleLoaderActions) { + importResult.implicitImports.forEach(implicitImport => { + const existingLoaderAction = loaderActions.implicitImports.get(implicitImport.name); + if (existingLoaderAction) { + existingLoaderAction.path = implicitImport.path; + } else { + loaderActions.implicitImports.set(implicitImport.name, { + path: implicitImport.path, + implicitImports: new Map() + }); + } + }); + } + // Handles some special-case assignment statements that are found // within the typings.pyi file. private _handleTypingStubAssignment(node: AssignmentNode) { @@ -943,6 +1110,8 @@ export abstract class Binder extends ParseTreeWalker { } export class ModuleScopeBinder extends Binder { + private _moduleDocString?: string; + constructor(node: ModuleNode, fileInfo: AnalyzerFileInfo) { super(node, fileInfo.builtinsScope ? ScopeType.Module : ScopeType.Builtin, fileInfo.builtinsScope, fileInfo); @@ -963,15 +1132,16 @@ export class ModuleScopeBinder extends Binder { this._addParentLinks(moduleNode, moduleNode.statements); this.walkMultiple(moduleNode.statements); - // Associate the module's scope with the module type. - const moduleType = ModuleType.create(this._currentScope.getSymbolTable(), - this._getDocString((this._scopedNode as ModuleNode).statements)); - AnalyzerNodeInfo.setExpressionType(this._scopedNode, moduleType); + this._moduleDocString = this._getDocString((this._scopedNode as ModuleNode).statements); } bind() { this.bindDeferred(); } + + getModuleDocString() { + return this._moduleDocString; + } } export class ClassScopeBinder extends Binder { diff --git a/server/src/analyzer/declaration.ts b/server/src/analyzer/declaration.ts index d48448c32..a31a003e2 100644 --- a/server/src/analyzer/declaration.ts +++ b/server/src/analyzer/declaration.ts @@ -12,7 +12,7 @@ import { DiagnosticTextRange } from '../common/diagnostic'; import { ClassNode, ExpressionNode, FunctionNode, NameNode, ParameterNode, ParseNode, StringListNode } from '../parser/parseNodes'; -import { ModuleType, Type } from './types'; +import { Type } from './types'; export const enum DeclarationType { BuiltIn, @@ -21,7 +21,6 @@ export const enum DeclarationType { Function, Method, Class, - Module, Alias } @@ -70,26 +69,42 @@ export interface VariableDeclaration extends DeclarationBase { isConstant?: boolean; } -export interface ModuleDeclaration extends DeclarationBase { - type: DeclarationType.Module; - moduleType: ModuleType; -} - // Alias declarations are used for imports. They are resolved // after the binding phase. export interface AliasDeclaration extends DeclarationBase { type: DeclarationType.Alias; - // If a symbol is present, the alias refers to the symbol - // within a module (whose path is defined in the 'path' - // field). If symbolName is missing, the alias refers to - // the module itself. + // If a symbolName is defined, the alias refers to a symbol + // within a resolved module (whose path is defined in the + // 'path' field). If symbolName is missing, the alias refers + // to the module itself. symbolName?: string; - // The resolved declaration. - resolvedDeclarations?: Declaration[]; + // The first part of the multi-part name used in the import + // statement (e.g. for "import a.b.c", firstNamePart would + // be "a"). + firstNamePart?: string; + + // If the alias is targeting a module, multiple other modules + // may also need to be resolved and inserted implicitly into + // the module's namespace to emulate the behavior of the python + // module loader. This can be recursive (e.g. in the case of + // an "import a.b.c.d" statement). + implicitImports: Map; +} + +// 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 + // a directory). + path: string; + + // See comment for "implicitImports" field in AliasDeclaration. + implicitImports: Map; } export type Declaration = BuiltInDeclaration | ClassDeclaration | FunctionDeclaration | ParameterDeclaration | VariableDeclaration | - ModuleDeclaration | AliasDeclaration; + AliasDeclaration; diff --git a/server/src/analyzer/declarationUtils.ts b/server/src/analyzer/declarationUtils.ts index 1bc49150a..48d1397da 100644 --- a/server/src/analyzer/declarationUtils.ts +++ b/server/src/analyzer/declarationUtils.ts @@ -9,12 +9,14 @@ import * as assert from 'assert'; +import { getEmptyRange } from '../common/diagnostic'; import { NameNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; +import { ImportLookup } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; -import { AliasDeclaration, Declaration, DeclarationType } from './declaration'; +import { AliasDeclaration, Declaration, DeclarationType, ModuleLoaderActions } from './declaration'; import * as ParseTreeUtils from './parseTreeUtils'; import { Symbol } from './symbol'; -import { ClassType, ModuleType, ObjectType, Type, TypeCategory, UnknownType } from './types'; +import { ClassType, ModuleType, ObjectType, Type, TypeCategory } from './types'; import * as TypeUtils from './typeUtils'; export function getDeclarationsForNameNode(node: NameNode): Declaration[] | undefined { @@ -51,6 +53,22 @@ export function getDeclarationsForNameNode(node: NameNode): Declaration[] | unde return subtype; }); } + } else if (node.parent && node.parent.nodeType === ParseNodeType.ModuleName) { + const namePartIndex = node.parent.nameParts.findIndex(part => part === node); + const importInfo = AnalyzerNodeInfo.getImportInfo(node.parent); + if (namePartIndex >= 0 && importInfo && namePartIndex < importInfo.resolvedPaths.length) { + if (importInfo.resolvedPaths[namePartIndex]) { + // Synthesize an alias declaration for this name part. The only + // time this case is used is for the hover provider. + const aliasDeclaration: AliasDeclaration = { + type: DeclarationType.Alias, + path: importInfo.resolvedPaths[namePartIndex], + range: getEmptyRange(), + implicitImports: new Map() + }; + declarations = [aliasDeclaration]; + } + } } else { const scopeNode = ParseTreeUtils.getScopeNodeForNode(node); if (scopeNode) { @@ -73,44 +91,65 @@ export function isFunctionOrMethodDeclaration(declaration: Declaration) { return declaration.type === DeclarationType.Method || declaration.type === DeclarationType.Function; } -export function resolveDeclarationAliases(declaration: Declaration) { - let resolvedDeclaration: Declaration | undefined = declaration; - const resolvedDeclarations: AliasDeclaration[] = []; +// If the specified declaration is an alias declaration that points +// to a symbol, it resolves the alias and looks up the symbol, then +// returns the first declaration associated with that symbol. It does +// this recursively if necessary. If a symbol lookup fails, undefined +// is returned. +export function resolveAliasDeclaration(declaration: Declaration, importLookup: ImportLookup): + Declaration | undefined { - while (resolvedDeclaration && resolvedDeclaration.type === DeclarationType.Alias) { - // Detect circular dependencies. - if (resolvedDeclarations.find(decl => decl === resolvedDeclaration)) { + let curDeclaration: Declaration | undefined = declaration; + const alreadyVisited: Declaration[] = []; + + while (true) { + if (curDeclaration.type !== DeclarationType.Alias) { + return curDeclaration; + } + + if (!curDeclaration.symbolName) { + return curDeclaration; + } + + const lookupResult = importLookup(declaration.path); + if (!lookupResult) { return undefined; } - resolvedDeclarations.push(resolvedDeclaration); - resolvedDeclaration = resolvedDeclaration.resolvedDeclarations ? - resolvedDeclaration.resolvedDeclarations[0] : undefined; - } + const symbol = lookupResult.symbolTable.get(curDeclaration.symbolName); + if (!symbol) { + return undefined; + } - return resolvedDeclaration; + const declarations = symbol.getDeclarations(); + if (declarations.length === 0) { + return undefined; + } + + curDeclaration = declarations[0]; + + // Make sure we don't follow a circular list indefinitely. + if (alreadyVisited.find(decl => decl === curDeclaration)) { + return declaration; + } + alreadyVisited.push(curDeclaration); + } } export function getTypeForDeclaration(declaration: Declaration): Type | undefined { - const resolvedDeclaration = resolveDeclarationAliases(declaration); - - if (!resolvedDeclaration) { - return undefined; - } - - switch (resolvedDeclaration.type) { + switch (declaration.type) { case DeclarationType.BuiltIn: - return resolvedDeclaration.declaredType; + return declaration.declaredType; case DeclarationType.Class: - return AnalyzerNodeInfo.getExpressionType(resolvedDeclaration.node.name); + return AnalyzerNodeInfo.getExpressionType(declaration.node.name); case DeclarationType.Function: case DeclarationType.Method: - return AnalyzerNodeInfo.getExpressionType(resolvedDeclaration.node.name); + return AnalyzerNodeInfo.getExpressionType(declaration.node.name); case DeclarationType.Parameter: { - let typeAnnotationNode = resolvedDeclaration.node.typeAnnotation; + let typeAnnotationNode = declaration.node.typeAnnotation; if (typeAnnotationNode && typeAnnotationNode.nodeType === ParseNodeType.StringList) { typeAnnotationNode = typeAnnotationNode.typeAnnotation; } @@ -125,7 +164,7 @@ export function getTypeForDeclaration(declaration: Declaration): Type | undefine } case DeclarationType.Variable: { - let typeAnnotationNode = resolvedDeclaration.typeAnnotationNode; + let typeAnnotationNode = declaration.typeAnnotationNode; if (typeAnnotationNode && typeAnnotationNode.nodeType === ParseNodeType.StringList) { typeAnnotationNode = typeAnnotationNode.typeAnnotation; } @@ -140,20 +179,14 @@ export function getTypeForDeclaration(declaration: Declaration): Type | undefine return undefined; } - case DeclarationType.Module: - return resolvedDeclaration.moduleType; + case DeclarationType.Alias: { + return undefined; + } } } -export function hasTypeForDeclaration(declaration: Declaration, resolveAliases = true): boolean { - const resolvedDeclaration = resolveAliases ? - resolveDeclarationAliases(declaration) : declaration; - - if (!resolvedDeclaration) { - return false; - } - - switch (resolvedDeclaration.type) { +export function hasTypeForDeclaration(declaration: Declaration): boolean { + switch (declaration.type) { case DeclarationType.BuiltIn: case DeclarationType.Class: case DeclarationType.Function: @@ -161,13 +194,10 @@ export function hasTypeForDeclaration(declaration: Declaration, resolveAliases = return true; case DeclarationType.Parameter: - return !!resolvedDeclaration.node.typeAnnotation; + return !!declaration.node.typeAnnotation; case DeclarationType.Variable: - return !!resolvedDeclaration.typeAnnotationNode; - - case DeclarationType.Module: - return true; + return !!declaration.typeAnnotationNode; case DeclarationType.Alias: return false; diff --git a/server/src/analyzer/expressionEvaluator.ts b/server/src/analyzer/expressionEvaluator.ts index e70a04326..6afa88969 100644 --- a/server/src/analyzer/expressionEvaluator.ts +++ b/server/src/analyzer/expressionEvaluator.ts @@ -608,16 +608,12 @@ export class ExpressionEvaluator { return undefined; } - const moduleType = this._fileInfo.importMap.get(typingImportPath); - if (!moduleType) { + const lookupResult = this._fileInfo.importLookup(typingImportPath); + if (!lookupResult) { return undefined; } - if (moduleType.category !== TypeCategory.Module) { - return undefined; - } - - const symbol = ModuleType.getField(moduleType, symbolName); + const symbol = lookupResult.symbolTable.get(symbolName); if (!symbol) { return undefined; } diff --git a/server/src/analyzer/program.ts b/server/src/analyzer/program.ts index 9b4c55649..f358ecd05 100644 --- a/server/src/analyzer/program.ts +++ b/server/src/analyzer/program.ts @@ -23,14 +23,13 @@ import { Duration, timingStats } from '../common/timing'; import { ModuleSymbolMap } from '../languageService/completionProvider'; import { HoverResults } from '../languageService/hoverProvider'; import { SignatureHelpResults } from '../languageService/signatureHelpProvider'; -import { ImportMap } from './analyzerFileInfo'; +import { ImportLookupResult } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; import { CircularDependency } from './circularDependency'; import { ImportResolver } from './importResolver'; import { ImportResult, ImportType } from './importResult'; import { Scope } from './scope'; import { SourceFile } from './sourceFile'; -import { ModuleType } from './types'; import { TypeStubWriter } from './typeStubWriter'; const _maxImportDepth = 256; @@ -308,7 +307,9 @@ export class Program { // Now do binding of the open files. for (const sourceFileInfo of openFiles) { - this._bindFile(sourceFileInfo, options, importResolver); + if (this._bindFile(sourceFileInfo, options, importResolver, isTimeElapsedOpenFiles)) { + return true; + } if (isTimeElapsedOpenFiles()) { return true; @@ -505,11 +506,15 @@ export class Program { } } + // Binds the specified file and all of its dependencies, recursively. If + // it runs out of time, it returns true. If it completes, it returns false. private _bindFile(fileToAnalyze: SourceFileInfo, - options: ConfigOptions, importResolver: ImportResolver) { + options: ConfigOptions, importResolver: ImportResolver, + timeElapsedCallback: () => boolean, + recursionMap: Map = new Map()): boolean { if (!this._isFileNeeded(fileToAnalyze) || !fileToAnalyze.sourceFile.isBindingRequired()) { - return; + return false; } this._parseFile(fileToAnalyze, options, importResolver); @@ -517,7 +522,11 @@ export class Program { // We need to parse and bind the builtins import first. let builtinsScope: Scope | undefined; if (fileToAnalyze.builtinsImport) { - this._bindFile(fileToAnalyze.builtinsImport, options, importResolver); + if (this._bindFile(fileToAnalyze.builtinsImport, options, + importResolver, timeElapsedCallback)) { + + return true; + } // Get the builtins scope to pass to the binding pass. const parseResults = fileToAnalyze.builtinsImport.sourceFile.getParseResults(); @@ -525,22 +534,49 @@ export class Program { builtinsScope = AnalyzerNodeInfo.getScope(parseResults.parseTree); assert(builtinsScope !== undefined); } - } - fileToAnalyze.sourceFile.bind(options, builtinsScope); - } + const filePath = fileToAnalyze.sourceFile.getFilePath(); + if (recursionMap.has(filePath)) { + // Avoid infinite recursion for cyclical dependencies. + return false; + } - private _buildImportMap(sourceFileInfo: SourceFileInfo): ImportMap { - const importMap: ImportMap = new Map(); + // Bind any other files that this file depends upon. + recursionMap.set(filePath, true); + for (const importedFile of fileToAnalyze.imports) { + if (this._bindFile(importedFile, options, importResolver, + timeElapsedCallback, recursionMap)) { - for (const importedFileInfo of sourceFileInfo.imports) { - const moduleType = importedFileInfo.sourceFile.getModuleType(); - if (moduleType) { - importMap.set(importedFileInfo.sourceFile.getFilePath(), moduleType); + return true; + } } } - return importMap; + if (timeElapsedCallback()) { + return true; + } + + fileToAnalyze.sourceFile.bind(options, this._lookUpImport, builtinsScope); + return false; + } + + private _lookUpImport = (filePath: string): ImportLookupResult | undefined => { + const sourceFileInfo = this._sourceFileMap[filePath]; + if (!sourceFileInfo) { + return undefined; + } + + const symbolTable = sourceFileInfo.sourceFile.getModuleSymbolTable(); + if (!symbolTable) { + return undefined; + } + + const docString = sourceFileInfo.sourceFile.getModuleDocString(); + + return { + symbolTable, + docString + }; } // Build a map of all modules within this program and the module- @@ -550,9 +586,9 @@ export class Program { this._sourceFileList.forEach(fileInfo => { if (fileInfo !== sourceFileToExclude) { - const moduleType = fileInfo.sourceFile.getModuleType(); - if (moduleType) { - moduleSymbolMap[fileInfo.sourceFile.getFilePath()] = moduleType.fields; + const symbolTable = fileInfo.sourceFile.getModuleSymbolTable(); + if (symbolTable) { + moduleSymbolMap[fileInfo.sourceFile.getFilePath()] = symbolTable; } } }); @@ -590,15 +626,12 @@ export class Program { closureMap.set(fileToAnalyze.sourceFile.getFilePath(), false); if (fileToAnalyze.sourceFile.isTypeAnalysisRequired()) { - // Build the import map for the file. - const importMap = this._buildImportMap(fileToAnalyze); - // Do a type analysis pass and determine if any internal changes occurred // during the pass. If so, continue to analyze until it stops changing and // mark all of its dependencies as needing to be reanalyzed. let didAnalysisChange = false; while (true) { - fileToAnalyze.sourceFile.doTypeAnalysis(options, importMap); + fileToAnalyze.sourceFile.doTypeAnalysis(options, this._lookUpImport); if (!fileToAnalyze.sourceFile.isTypeAnalysisRequired()) { break; @@ -690,7 +723,10 @@ export class Program { } // Make sure the file is parsed and bound. - this._bindFile(fileToAnalyze, options, importResolver); + if (this._bindFile(fileToAnalyze, options, importResolver, timeElapsedCallback)) { + return true; + } + if (timeElapsedCallback()) { return true; } @@ -840,7 +876,7 @@ export class Program { return undefined; } - return sourceFile.getDefinitionsForPosition(position); + return sourceFile.getDefinitionsForPosition(position, this._lookUpImport); } getReferencesForPosition(filePath: string, position: DiagnosticTextPosition, @@ -860,7 +896,7 @@ export class Program { } const referencesResult = sourceFileInfo.sourceFile.getReferencesForPosition( - position, includeDeclaration); + position, includeDeclaration, this._lookUpImport); if (!referencesResult) { return undefined; @@ -878,7 +914,7 @@ export class Program { } curSourceFileInfo.sourceFile.addReferences(referencesResult, - includeDeclaration); + includeDeclaration, this._lookUpImport); } } } @@ -889,7 +925,8 @@ export class Program { addSymbolsForDocument(filePath: string, symbolList: SymbolInformation[]) { const sourceFileInfo = this._sourceFileMap[filePath]; if (sourceFileInfo) { - sourceFileInfo.sourceFile.addSymbolsForDocument(symbolList); + sourceFileInfo.sourceFile.addSymbolsForDocument( + symbolList, this._lookUpImport); } } @@ -901,7 +938,8 @@ export class Program { } for (const sourceFileInfo of this._sourceFileList) { - sourceFileInfo.sourceFile.addSymbolsForDocument(symbolList, query); + sourceFileInfo.sourceFile.addSymbolsForDocument( + symbolList, this._lookUpImport, query); } } @@ -914,7 +952,7 @@ export class Program { } return sourceFileInfo.sourceFile.getHoverForPosition(position, - this._buildImportMap(sourceFileInfo)); + this._lookUpImport); } getSignatureHelpForPosition(filePath: string, position: DiagnosticTextPosition, @@ -955,7 +993,7 @@ export class Program { return sourceFileInfo.sourceFile.getCompletionsForPosition( position, options, importResolver, - () => this._buildImportMap(sourceFileInfo), + this._lookUpImport, () => this._buildModuleSymbolsMap(sourceFileInfo)); } @@ -989,7 +1027,7 @@ export class Program { } const referencesResult = sourceFileInfo.sourceFile.getReferencesForPosition( - position, true); + position, true, this._lookUpImport); if (!referencesResult) { return undefined; @@ -1006,7 +1044,8 @@ export class Program { }); } - curSourceFileInfo.sourceFile.addReferences(referencesResult, true); + curSourceFileInfo.sourceFile.addReferences(referencesResult, + true, this._lookUpImport); } } } @@ -1178,30 +1217,26 @@ export class Program { const newImportPathMap = new Map(); imports.forEach(importResult => { if (importResult.isImportFound) { - if (!this._isImportAllowed(sourceFileInfo, importResult, importResult.isStubFile)) { - return; - } - - if (importResult.resolvedPaths.length > 0) { - const filePath = importResult.resolvedPaths[ - importResult.resolvedPaths.length - 1]; - if (filePath) { - newImportPathMap.set(filePath, { - isTypeshedFile: !!importResult.isTypeshedFile, - isThirdPartyImport: importResult.importType === ImportType.ThirdParty - }); + if (this._isImportAllowed(sourceFileInfo, importResult, importResult.isStubFile)) { + if (importResult.resolvedPaths.length > 0) { + const filePath = importResult.resolvedPaths[ + importResult.resolvedPaths.length - 1]; + if (filePath) { + newImportPathMap.set(filePath, { + isTypeshedFile: !!importResult.isTypeshedFile, + isThirdPartyImport: importResult.importType === ImportType.ThirdParty + }); + } } } importResult.implicitImports.forEach(implicitImport => { - if (!this._isImportAllowed(sourceFileInfo, importResult, implicitImport.isStubFile)) { - return; + if (this._isImportAllowed(sourceFileInfo, importResult, implicitImport.isStubFile)) { + newImportPathMap.set(implicitImport.path, { + isTypeshedFile: !!importResult.isTypeshedFile, + isThirdPartyImport: importResult.importType === ImportType.ThirdParty + }); } - - newImportPathMap.set(implicitImport.path, { - isTypeshedFile: !!importResult.isTypeshedFile, - isThirdPartyImport: importResult.importType === ImportType.ThirdParty - }); }); } else if (options.verboseOutput) { if (!sourceFileInfo.isTypeshedFile || options.diagnosticSettings.reportTypeshedErrors) { diff --git a/server/src/analyzer/sourceFile.ts b/server/src/analyzer/sourceFile.ts index 38b7050e3..43b551f95 100644 --- a/server/src/analyzer/sourceFile.ts +++ b/server/src/analyzer/sourceFile.ts @@ -34,7 +34,7 @@ import { SignatureHelpProvider, SignatureHelpResults } from '../languageService/ import { ModuleNode } from '../parser/parseNodes'; import { ModuleImport, ParseOptions, Parser, ParseResults } from '../parser/parser'; import { Token } from '../parser/tokenizerTypes'; -import { AnalyzerFileInfo, ImportMap } from './analyzerFileInfo'; +import { AnalyzerFileInfo, ImportLookup } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; import { ModuleScopeBinder } from './binder'; import { CircularDependency } from './circularDependency'; @@ -43,9 +43,9 @@ import { ImportResolver } from './importResolver'; import { ImportResult } from './importResult'; import { ParseTreeCleanerWalker } from './parseTreeCleaner'; import { Scope } from './scope'; +import { SymbolTable } from './symbol'; import { TestWalker } from './testWalker'; import { TypeAnalyzer } from './typeAnalyzer'; -import { ModuleType, TypeCategory } from './types'; const _maxImportCyclesPerFile = 4; @@ -107,7 +107,8 @@ export class SourceFile { private _parseTreeNeedsCleaning = false; private _parseResults?: ParseResults; - private _moduleType?: ModuleType; + private _moduleSymbolTable?: SymbolTable; + private _moduleDocString?: string; // Diagnostics generated during different phases of analysis. private _parseDiagnostics: Diagnostic[] = []; @@ -144,6 +145,7 @@ export class SourceFile { private _imports?: ImportResult[]; private _builtinsImport?: ImportResult; private _typingModulePath?: string; + private _collectionsModulePath?: string; constructor(filePath: string, isTypeshedStubFile: boolean, isThirdPartyImport: boolean, console?: ConsoleInterface) { @@ -280,8 +282,12 @@ export class SourceFile { return this._builtinsImport; } - getModuleType(): ModuleType | undefined { - return this._moduleType; + getModuleSymbolTable(): SymbolTable | undefined { + return this._moduleSymbolTable; + } + + getModuleDocString(): string | undefined { + return this._moduleDocString; } // Indicates whether the contents of the file have changed since @@ -318,7 +324,8 @@ export class SourceFile { this._fileContentsVersion++; this._isTypeAnalysisFinalized = false; this._isTypeAnalysisPassNeeded = true; - this._moduleType = undefined; + this._moduleSymbolTable = undefined; + this._moduleDocString = undefined; } markReanalysisRequired(): void { @@ -327,6 +334,14 @@ export class SourceFile { this._typeAnalysisPassNumber = 1; this._isTypeAnalysisFinalized = false; this._isTypeAnalysisPassNeeded = true; + + // If the file continas a wildcard import, we need to rebind + // also because the dependent import may have changed. + if (this._parseResults && this._parseResults.containsWildcardImport) { + this._isBindingNeeded = true; + this._moduleSymbolTable = undefined; + this._moduleDocString = undefined; + } } setClientVersion(version: number | null, contents: string): void { @@ -450,7 +465,7 @@ export class SourceFile { // Resolve imports. timingStats.resolveImportsTime.timeOperation(() => { - [this._imports, this._builtinsImport, this._typingModulePath] = + [this._imports, this._builtinsImport, this._typingModulePath, this._collectionsModulePath] = this._resolveImports(importResolver, parseResults.importedModules, execEnvironment); this._parseDiagnostics = diagSink.diagnostics; }); @@ -481,7 +496,8 @@ export class SourceFile { typeIgnoreLines: {}, predominantEndOfLineSequence: '\n', predominantTabSequence: ' ' - } + }, + containsWildcardImport: false }; this._imports = undefined; this._builtinsImport = undefined; @@ -502,18 +518,20 @@ export class SourceFile { return true; } - getDefinitionsForPosition(position: DiagnosticTextPosition): DocumentTextRange[] | undefined { + getDefinitionsForPosition(position: DiagnosticTextPosition, importLookup: ImportLookup): + DocumentTextRange[] | undefined { + // If we have no completed analysis job, there's nothing to do. if (!this._parseResults) { return undefined; } return DefinitionProvider.getDefinitionsForPosition( - this._parseResults, position); + this._parseResults, position, importLookup); } - getReferencesForPosition(position: DiagnosticTextPosition, includeDeclaration: boolean): - ReferencesResult | undefined { + getReferencesForPosition(position: DiagnosticTextPosition, includeDeclaration: boolean, + importLookup: ImportLookup): ReferencesResult | undefined { // If we have no completed analysis job, there's nothing to do. if (!this._parseResults) { @@ -521,37 +539,43 @@ export class SourceFile { } return ReferencesProvider.getReferencesForPosition( - this._parseResults, this._filePath, position, includeDeclaration); + this._parseResults, this._filePath, position, includeDeclaration, importLookup); } - addReferences(referencesResult: ReferencesResult, includeDeclaration: boolean): void { + addReferences(referencesResult: ReferencesResult, includeDeclaration: boolean, + importLookup: ImportLookup): void { + // If we have no completed analysis job, there's nothing to do. if (!this._parseResults) { return; } ReferencesProvider.addReferences( - this._parseResults, this._filePath, referencesResult, includeDeclaration); + this._parseResults, this._filePath, referencesResult, includeDeclaration, importLookup); } - addSymbolsForDocument(symbolList: SymbolInformation[], query?: string) { + addSymbolsForDocument(symbolList: SymbolInformation[], importLookup: ImportLookup, + query?: string) { + // If we have no completed analysis job, there's nothing to do. if (!this._parseResults) { return; } DocumentSymbolProvider.addSymbolsForDocument(symbolList, query, - this._filePath, this._parseResults); + this._filePath, this._parseResults, importLookup); } - getHoverForPosition(position: DiagnosticTextPosition, importMap: ImportMap): HoverResults | undefined { + getHoverForPosition(position: DiagnosticTextPosition, + importLookup: ImportLookup): HoverResults | undefined { + // If we have no completed analysis job, there's nothing to do. if (!this._parseResults) { return undefined; } return HoverProvider.getHoverForPosition( - this._parseResults, position, importMap); + this._parseResults, position, importLookup); } getSignatureHelpForPosition(position: DiagnosticTextPosition): SignatureHelpResults | undefined { @@ -572,7 +596,7 @@ export class SourceFile { getCompletionsForPosition(position: DiagnosticTextPosition, configOptions: ConfigOptions, importResolver: ImportResolver, - importMapCallback: () => ImportMap, + importLookup: ImportLookup, moduleSymbolsCallback: () => ModuleSymbolMap): CompletionList | undefined { // If we have no completed analysis job, there's nothing to do. @@ -589,7 +613,7 @@ export class SourceFile { const completionProvider = new CompletionProvider( this._parseResults, this._fileContents, importResolver, position, - this._filePath, configOptions, importMapCallback, + this._filePath, configOptions, importLookup, moduleSymbolsCallback); return completionProvider.getCompletionsForPosition(); @@ -623,7 +647,7 @@ export class SourceFile { this._isTypeAnalysisFinalized = false; } - bind(configOptions: ConfigOptions, builtinsScope?: Scope) { + bind(configOptions: ConfigOptions, importLookup: ImportLookup, builtinsScope?: Scope) { assert(!this.isParseRequired()); assert(this.isBindingRequired()); assert(this._parseResults); @@ -631,7 +655,7 @@ export class SourceFile { try { // Perform name binding. timingStats.bindTime.timeOperation(() => { - const fileInfo = this._buildFileInfo(configOptions, undefined, builtinsScope); + const fileInfo = this._buildFileInfo(configOptions, importLookup, builtinsScope); this._cleanParseTreeIfRequired(); const binder = new ModuleScopeBinder( @@ -648,10 +672,8 @@ export class SourceFile { this._bindDiagnostics = fileInfo.diagnosticSink.diagnostics; const moduleScope = AnalyzerNodeInfo.getScope(this._parseResults!.parseTree); assert(moduleScope !== undefined); - const moduleType = AnalyzerNodeInfo.getExpressionType( - this._parseResults!.parseTree); - assert(moduleType && moduleType.category === TypeCategory.Module); - this._moduleType = moduleType as ModuleType; + this._moduleSymbolTable = moduleScope!.getSymbolTable(); + this._moduleDocString = binder.getModuleDocString(); }); } catch (e) { const message: string = (e.stack ? e.stack.toString() : undefined) || @@ -675,7 +697,7 @@ export class SourceFile { this._isBindingNeeded = false; } - doTypeAnalysis(configOptions: ConfigOptions, importMap: ImportMap) { + doTypeAnalysis(configOptions: ConfigOptions, importLookup: ImportLookup) { assert(!this.isParseRequired()); assert(!this.isBindingRequired()); assert(this.isTypeAnalysisRequired()); @@ -683,7 +705,7 @@ export class SourceFile { try { timingStats.typeAnalyzerTime.timeOperation(() => { - const fileInfo = this._buildFileInfo(configOptions, importMap, undefined); + const fileInfo = this._buildFileInfo(configOptions, importLookup, undefined); // Perform static type analysis. const typeAnalyzer = new TypeAnalyzer(this._parseResults!.parseTree, @@ -741,15 +763,18 @@ export class SourceFile { this._diagnosticVersion++; } - private _buildFileInfo(configOptions: ConfigOptions, importMap?: ImportMap, builtinsScope?: Scope) { + private _buildFileInfo(configOptions: ConfigOptions, importLookup: ImportLookup, + builtinsScope?: Scope) { + assert(this._parseResults !== undefined); const analysisDiagnostics = new TextRangeDiagnosticSink(this._parseResults!.tokenizerOutput.lines); const fileInfo: AnalyzerFileInfo = { - importMap: importMap || new Map(), + importLookup, futureImports: this._parseResults!.futureImports, builtinsScope, typingModulePath: this._typingModulePath, + collectionsModulePath: this._collectionsModulePath, diagnosticSink: analysisDiagnostics, executionEnvironment: configOptions.findExecEnvironment(this._filePath), diagnosticSettings: this._diagnosticSettings, @@ -775,7 +800,7 @@ export class SourceFile { private _resolveImports(importResolver: ImportResolver, moduleImports: ModuleImport[], execEnv: ExecutionEnvironment): - [ImportResult[], ImportResult?, string?] { + [ImportResult[], ImportResult?, string?, string?] { const imports: ImportResult[] = []; @@ -817,6 +842,8 @@ export class SourceFile { typingModulePath = typingImportResult.resolvedPaths[0]; } + let collectionsModulePath: string | undefined; + for (const moduleImport of moduleImports) { const importResult = importResolver.resolveImport( this._filePath, @@ -827,6 +854,16 @@ export class SourceFile { importedSymbols: moduleImport.importedSymbols } ); + + // If the file imports the stdlib 'collections' module, stash + // away its file path. The type analyzer may need this to + // access types defined in the collections module. + if (importResult.isImportFound && importResult.isTypeshedFile) { + if (moduleImport.nameParts.length >= 1 && moduleImport.nameParts[0] === 'collections') { + collectionsModulePath = importResult.resolvedPaths[importResult.resolvedPaths.length - 1]; + } + } + imports.push(importResult); // Associate the import results with the module import @@ -835,6 +872,6 @@ export class SourceFile { AnalyzerNodeInfo.setImportInfo(moduleImport.nameNode, importResult); } - return [imports, builtinsImportResult, typingModulePath]; + return [imports, builtinsImportResult, typingModulePath, collectionsModulePath]; } } diff --git a/server/src/analyzer/symbol.ts b/server/src/analyzer/symbol.ts index 20f5f981e..b75d6dadc 100644 --- a/server/src/analyzer/symbol.ts +++ b/server/src/analyzer/symbol.ts @@ -160,7 +160,7 @@ export class Symbol { getTypedDeclarations() { return this.getDeclarations().filter( - decl => hasTypeForDeclaration(decl, false)); + decl => hasTypeForDeclaration(decl)); } } diff --git a/server/src/analyzer/typeAnalyzer.ts b/server/src/analyzer/typeAnalyzer.ts index 74fd32106..6390f6783 100644 --- a/server/src/analyzer/typeAnalyzer.ts +++ b/server/src/analyzer/typeAnalyzer.ts @@ -12,8 +12,7 @@ import * as assert from 'assert'; import { DiagnosticLevel } from '../common/configOptions'; -import { AddMissingOptionalToParamAction, Diagnostic, - DiagnosticAddendum, getEmptyRange } from '../common/diagnostic'; +import { AddMissingOptionalToParamAction, Diagnostic, DiagnosticAddendum } from '../common/diagnostic'; import { DiagnosticRule } from '../common/diagnosticRules'; import { convertOffsetsToRange } from '../common/positionUtils'; import { PythonVersion } from '../common/pythonVersion'; @@ -22,15 +21,15 @@ import { AssertNode, AssignmentExpressionNode, AssignmentNode, AugmentedAssignme BinaryExpressionNode, BreakNode, CallExpressionNode, ClassNode, ContinueNode, DecoratorNode, DelNode, ErrorExpressionNode, ExceptNode, ExpressionNode, FormatStringNode, ForNode, FunctionNode, IfNode, ImportAsNode, ImportFromNode, IndexExpressionNode, LambdaNode, - ListComprehensionNode, MemberAccessExpressionNode, ModuleNode, NameNode, ParameterCategory, ParameterNode, - ParseNode, ParseNodeType, RaiseNode, ReturnNode, SliceExpressionNode, StringListNode, - SuiteNode, TernaryExpressionNode, TryNode, TupleExpressionNode, + ListComprehensionNode, MemberAccessExpressionNode, ModuleNode, NameNode, ParameterCategory, + ParameterNode, ParseNode, ParseNodeType, RaiseNode, ReturnNode, SliceExpressionNode, + StringListNode, SuiteNode, TernaryExpressionNode, TryNode, TupleExpressionNode, TypeAnnotationExpressionNode, UnaryExpressionNode, UnpackExpressionNode, WhileNode, WithNode, YieldExpressionNode, YieldFromExpressionNode } from '../parser/parseNodes'; import { KeywordType } from '../parser/tokenizerTypes'; import { AnalyzerFileInfo } from './analyzerFileInfo'; import * as AnalyzerNodeInfo from './analyzerNodeInfo'; -import { AliasDeclaration, Declaration, DeclarationType, ModuleDeclaration, +import { AliasDeclaration, Declaration, DeclarationType, ModuleLoaderActions, VariableDeclaration } from './declaration'; import * as DeclarationUtils from './declarationUtils'; import { EvaluatorFlags, ExpressionEvaluator } from './expressionEvaluator'; @@ -1297,95 +1296,62 @@ export class TypeAnalyzer extends ParseTreeWalker { } visitImportAs(node: ImportAsNode): boolean { - const importInfo = AnalyzerNodeInfo.getImportInfo(node.module); - assert(importInfo !== undefined); + if (node.module.nameParts.length === 0) { + return false; + } - if (importInfo && importInfo.isImportFound && importInfo.resolvedPaths.length > 0) { - const resolvedPath = importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1]; + let symbolNameNode: NameNode; + if (node.alias) { + // The symbol name is defined by the alias. + symbolNameNode = node.alias; + } else { + // There was no alias, so we need to use the first element of + // the name parts as the symbol. + symbolNameNode = node.module.nameParts[0]; + } - let moduleType = this._getModuleTypeForImportPath(importInfo, resolvedPath); + // Look up the symbol to find the alias declaration. + let symbolType: Type | undefined; + let symbol: Symbol | undefined; + [symbol, symbolType] = this._getAliasedSymbolTypeForName(symbolNameNode.nameToken.value); + if (!symbolType) { + symbolType = UnknownType.create(); + } - // Is there a cached module type associated with this node? - const cachedModuleType = AnalyzerNodeInfo.getExpressionType(node) as ModuleType; - if (cachedModuleType && cachedModuleType.category === TypeCategory.Module && moduleType) { - // If the fields match, use the cached version instead of the - // newly-created version so we preserve the work done to - // populate the loader fields. - if (moduleType.fields === cachedModuleType.fields) { - moduleType = cachedModuleType; - } + // Is there a cached module type associated with this node? If so, use + // it instead of the type we just created. This will preserve the + // symbol accessed flags. + const cachedModuleType = AnalyzerNodeInfo.getExpressionType(node) as ModuleType; + if (cachedModuleType && cachedModuleType.category === TypeCategory.Module && symbolType) { + if (isTypeSame(symbolType, cachedModuleType)) { + symbolType = cachedModuleType; } + } - if (moduleType) { - // Import the implicit imports in the module's namespace. - importInfo.implicitImports.forEach(implicitImport => { - const implicitModuleType = this._getModuleTypeForImportPath( - importInfo, implicitImport.path); - if (implicitModuleType) { - if (!ModuleType.getField(moduleType!, implicitImport.name)) { - const moduleDeclaration: ModuleDeclaration = { - type: DeclarationType.Module, - moduleType: implicitModuleType, - path: implicitImport.path, - range: getEmptyRange() - }; - const aliasDeclaration: AliasDeclaration = { - type: DeclarationType.Alias, - resolvedDeclarations: [moduleDeclaration], - path: '', - range: getEmptyRange() - }; + // Cache the module type for subsequent passes. + AnalyzerNodeInfo.setExpressionType(node, symbolType); - const newSymbol = Symbol.createWithType( - SymbolFlags.ClassMember, implicitModuleType, defaultTypeSourceId); - newSymbol.addDeclaration(aliasDeclaration); - setSymbolPreservingAccess(moduleType!.loaderFields!, implicitImport.name, - newSymbol); - } - } - }); + this._assignTypeToNameNode(symbolNameNode, symbolType); + this._updateExpressionTypeForNode(symbolNameNode, symbolType); - const moduleDeclaration: ModuleDeclaration = { - type: DeclarationType.Module, - moduleType, - path: resolvedPath, - range: getEmptyRange() - }; + if (node.alias) { + this._conditionallyReportUnusedName(symbolNameNode, false, + this._fileInfo.diagnosticSettings.reportUnusedImport, + DiagnosticRule.reportUnusedImport, + `Import '${ node.alias.nameToken.value }' is not accessed`); + } else { + if (symbol && !symbol.isAccessed()) { + const nameParts = node.module.nameParts; + if (nameParts.length > 0) { + const multipartName = nameParts.map(np => np.nameToken.value).join('.'); + const textRange: TextRange = { start: nameParts[0].start, length: nameParts[0].length }; + TextRange.extend(textRange, nameParts[nameParts.length - 1]); + this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange( + `'${ multipartName }' is not accessed`, textRange); - if (node.alias) { - this._assignTypeToNameNode(node.alias, moduleType, moduleDeclaration); - this._updateExpressionTypeForNode(node.alias, moduleType); - - this._conditionallyReportUnusedName(node.alias, false, - this._fileInfo.diagnosticSettings.reportUnusedImport, + this._addDiagnostic(this._fileInfo.diagnosticSettings.reportUnusedImport, DiagnosticRule.reportUnusedImport, - `Import '${ node.alias.nameToken.value }' is not accessed`); - } else { - this._bindMultiPartModuleNameToType(node.module.nameParts, - moduleType, moduleDeclaration); - } - - // Cache the module type for subsequent passes. - AnalyzerNodeInfo.setExpressionType(node, moduleType); - } else { - // We were unable to resolve the import. Bind the names (or alias) - // to an unknown type. - const symbolType = UnknownType.create(); - const nameNode = node.module.nameParts.length > 0 ? node.module.nameParts[0] : undefined; - const aliasNode = node.alias || nameNode; - - if (node.alias && nameNode) { - this._updateExpressionTypeForNode(nameNode, symbolType); - } - - if (aliasNode) { - this._assignTypeToNameNode(aliasNode, symbolType); - this._updateExpressionTypeForNode(aliasNode, symbolType); - - this._conditionallyReportUnusedName(aliasNode, false, - this._fileInfo.diagnosticSettings.reportUnusedImport, - DiagnosticRule.reportUnusedImport, - `Import '${ aliasNode.nameToken.value }' is not accessed`); + `Import '${ multipartName }' is not accessed`, textRange); } } } @@ -1395,72 +1361,44 @@ export class TypeAnalyzer extends ParseTreeWalker { visitImportFrom(node: ImportFromNode): boolean { const importInfo = AnalyzerNodeInfo.getImportInfo(node.module); + let symbol: Symbol | undefined; + let symbolType: Type | undefined; if (importInfo && importInfo.isImportFound) { const resolvedPath = importInfo.resolvedPaths.length > 0 ? importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1] : ''; - // Empty list implies "import *" if (node.isWildcardImport) { - const moduleType = this._getModuleTypeForImportPath(importInfo, resolvedPath); - if (moduleType) { + if (resolvedPath) { // Import the fields in the current permanent scope. - const moduleFields = moduleType.fields; - moduleFields.forEach((boundValue, fieldName) => { - this._addSymbolToPermanentScope(fieldName); - this._addTypeSourceToName(fieldName, TypeUtils.getEffectiveTypeOfSymbol(boundValue), - node.id, boundValue.hasDeclarations() ? boundValue.getDeclarations()[0] : undefined); - }); + const lookupInfo = this._fileInfo.importLookup(resolvedPath); + if (lookupInfo) { + lookupInfo.symbolTable.forEach((_, name) => { + [symbol, symbolType] = this._getAliasedSymbolTypeForName(name); + if (symbol) { + this._addTypeSourceToName(name, symbolType || UnknownType.create(), + node.id); + } + }); + } - // Import the fields in the current permanent scope. importInfo.implicitImports.forEach(implicitImport => { - const moduleType = this._getModuleTypeForImportPath(importInfo, resolvedPath); - if (moduleType) { - this._addSymbolToPermanentScope(implicitImport.name); - this._addTypeSourceToName(implicitImport.name, moduleType, node.id); + [symbol, symbolType] = this._getAliasedSymbolTypeForName(implicitImport.name); + if (symbol) { + this._addTypeSourceToName(implicitImport.name, + symbolType || UnknownType.create(), node.id); } }); } } else { node.imports.forEach(importAs => { - const name = importAs.name.nameToken.value; const aliasNode = importAs.alias || importAs.name; - let symbolType: Type | undefined; - let declarations: Declaration[] | undefined; - - // Is the name referring to an implicit import? - const implicitImport = importInfo.implicitImports.find(impImport => impImport.name === name); - if (implicitImport) { - const moduleType = this._getModuleTypeForImportPath(importInfo, implicitImport.path); - if (moduleType && this._fileInfo.importMap.has(implicitImport.path)) { - symbolType = moduleType; - declarations = [{ - type: DeclarationType.Module, - moduleType, - path: implicitImport.path, - range: getEmptyRange() - }]; - } - } else { - const moduleType = this._getModuleTypeForImportPath(importInfo, resolvedPath); - if (moduleType) { - const symbol = ModuleType.getField(moduleType, name); - - // For imports of the form "from . import X", the symbol - // will have no declarations. - if (symbol && symbol.hasDeclarations()) { - symbolType = TypeUtils.getEffectiveTypeOfSymbol(symbol); - declarations = symbol.getDeclarations(); - } else { - this._addError( - `'${ importAs.name.nameToken.value }' is unknown import symbol`, - importAs.name - ); - } - } - } - + [symbol, symbolType] = this._getAliasedSymbolTypeForName(aliasNode.nameToken.value); if (!symbolType) { + this._addError( + `'${ importAs.name.nameToken.value }' is unknown import symbol`, + importAs.name + ); symbolType = UnknownType.create(); } @@ -1468,53 +1406,27 @@ export class TypeAnalyzer extends ParseTreeWalker { if (importAs.alias) { this._updateExpressionTypeForNode(importAs.alias, symbolType); } - - let aliasDeclaration: AliasDeclaration | undefined; - if (declarations) { - aliasDeclaration = { - type: DeclarationType.Alias, - resolvedDeclarations: declarations, - symbolName: name, - path: '', - range: getEmptyRange() - }; - } - - this._assignTypeToNameNode(aliasNode, symbolType, aliasDeclaration); - - // Python files generated by protoc ("_pb2.py" files) contain - // unused imports. Don't report these because they're in generated - // files that shouldn't be edited. - if (importInfo.importName !== '__future__' && - !this._fileInfo.filePath.endsWith('_pb2.py')) { - - this._conditionallyReportUnusedName(aliasNode, false, - this._fileInfo.diagnosticSettings.reportUnusedImport, - DiagnosticRule.reportUnusedImport, - `Import '${ aliasNode.nameToken.value }' is not accessed`); - } + this._addTypeSourceToName(aliasNode.nameToken.value, symbolType, node.id); + this._assignTypeToNameNode(aliasNode, symbolType); }); } - } else { - // We were unable to resolve the import. Bind the names (or aliases) - // to an unknown type. - if (!node.isWildcardImport) { - node.imports.forEach(importAs => { - const aliasNode = importAs.alias || importAs.name; - const symbolType = UnknownType.create(); + } - this._updateExpressionTypeForNode(importAs.name, symbolType); - if (importAs.alias) { - this._updateExpressionTypeForNode(importAs.name, symbolType); - } + if (!node.isWildcardImport) { + node.imports.forEach(importAs => { + const aliasNode = importAs.alias || importAs.name; + // Python files generated by protoc ("_pb2.py" files) contain + // unused imports. Don't report these because they're in generated + // files that shouldn't be edited. + if ((!importInfo || importInfo.importName !== '__future__') && + !this._fileInfo.filePath.endsWith('_pb2.py')) { - this._assignTypeToNameNode(aliasNode, symbolType); this._conditionallyReportUnusedName(aliasNode, false, this._fileInfo.diagnosticSettings.reportUnusedImport, DiagnosticRule.reportUnusedImport, `Import '${ aliasNode.nameToken.value }' is not accessed`); - }); - } + } + }); } return false; @@ -1587,6 +1499,58 @@ export class TypeAnalyzer extends ParseTreeWalker { return false; } + private _getAliasedSymbolTypeForName(name: string): [Symbol | undefined, Type | undefined] { + const symbolWithScope = this._currentScope.lookUpSymbolRecursive(name); + if (!symbolWithScope) { + return [undefined, undefined]; + } + + const aliasDecl = symbolWithScope.symbol.getDeclarations().find( + decl => decl.type === DeclarationType.Alias); + + let symbolType: Type | undefined; + if (aliasDecl && aliasDecl.type === DeclarationType.Alias) { + if (aliasDecl.symbolName) { + assert(aliasDecl.path); + const lookupResults = this._fileInfo.importLookup(aliasDecl.path); + if (lookupResults) { + const symbol = lookupResults.symbolTable.get(aliasDecl.symbolName); + if (symbol) { + symbolType = TypeUtils.getEffectiveTypeOfSymbol(symbol); + } + } + } else { + // Build a module type that corresponds to the declaration and + // its associated loader actions. + const moduleType = ModuleType.create(); + this._applyLoaderActionsToModuleType(moduleType, aliasDecl); + symbolType = moduleType; + } + } + + return [symbolWithScope ? symbolWithScope.symbol : undefined, symbolType]; + } + + private _applyLoaderActionsToModuleType(moduleType: ModuleType, loaderActions: ModuleLoaderActions) { + if (loaderActions.path) { + const lookupResults = this._fileInfo.importLookup(loaderActions.path); + if (lookupResults) { + moduleType.fields = lookupResults.symbolTable; + moduleType.docString = lookupResults.docString; + } + } + + loaderActions.implicitImports.forEach((implicitImport, name) => { + const importedModuleType = ModuleType.create(); + const importedModuleSymbol = Symbol.createWithType( + SymbolFlags.None, importedModuleType, defaultTypeSourceId); + moduleType.loaderFields.set(name, importedModuleSymbol); + + // Recursively apply loader actions. + this._applyLoaderActionsToModuleType(importedModuleType, implicitImport); + }); + } + // Validates that a call to isinstance is necessary. This is a // common source of programming errors. private _validateIsInstanceCallNecessary(node: CallExpressionNode) { @@ -2082,7 +2046,8 @@ export class TypeAnalyzer extends ParseTreeWalker { return; } - primaryDeclaration = DeclarationUtils.resolveDeclarationAliases(primaryDeclaration); + primaryDeclaration = DeclarationUtils.resolveAliasDeclaration(primaryDeclaration, + this._fileInfo.importLookup); if (!primaryDeclaration || primaryDeclaration.node === node) { return; } @@ -2683,18 +2648,13 @@ export class TypeAnalyzer extends ParseTreeWalker { } private _findCollectionsImportSymbolTable(): SymbolTable | undefined { - let moduleType: ModuleType | undefined; - for (const key of this._fileInfo.importMap.keys()) { - if (key.endsWith('collections/__init__.pyi')) { - moduleType = this._fileInfo.importMap.get(key); - break; + if (this._fileInfo.collectionsModulePath) { + const lookupResult = this._fileInfo.importLookup(this._fileInfo.collectionsModulePath); + if (lookupResult) { + return lookupResult.symbolTable; } } - if (moduleType) { - return moduleType.fields; - } - return undefined; } @@ -3055,23 +3015,24 @@ export class TypeAnalyzer extends ParseTreeWalker { } } - const moduleType = this._fileInfo.importMap.get(path); - if (moduleType) { - return ModuleType.cloneForLoadedModule(moduleType); + const lookupResults = this._fileInfo.importLookup(path); + if (lookupResults) { + const moduleType = ModuleType.create(lookupResults.symbolTable); + moduleType.docString = lookupResults.docString; + return moduleType; } else if (importResult) { // There was no module even though the import was resolved. This // happens in the case of namespace packages, where an __init__.py // is not necessarily present. We'll synthesize a module type in // this case. - const moduleType = ModuleType.cloneForLoadedModule( - ModuleType.create(new SymbolTable())); + const moduleType = ModuleType.create(); // Add the implicit imports. importResult.implicitImports.forEach(implicitImport => { const implicitModuleType = this._getModuleTypeForImportPath( undefined, implicitImport.path); if (implicitModuleType) { - setSymbolPreservingAccess(moduleType.loaderFields!, implicitImport.name, + setSymbolPreservingAccess(moduleType.loaderFields, implicitImport.name, Symbol.createWithType( SymbolFlags.ClassMember, implicitModuleType, defaultTypeSourceId)); } @@ -3364,83 +3325,6 @@ export class TypeAnalyzer extends ParseTreeWalker { this._evaluateExpressionForAssignment(target, srcType, srcExpr); } - private _bindMultiPartModuleNameToType(nameParts: NameNode[], type: ModuleType, - declaration?: Declaration): void { - - // The target symbol table will change as we progress through - // the multi-part name. Start with the current scope's symbol - // table, which should include the first part of the name. - const permanentScope = ScopeUtils.getPermanentScope(this._currentScope); - let targetSymbolTable = permanentScope.getSymbolTable(); - - for (let i = 0; i < nameParts.length; i++) { - const name = nameParts[i].nameToken.value; - - // Does this symbol already exist within this scope? - let targetSymbol = targetSymbolTable.get(name); - let symbolType = targetSymbol ? - TypeUtils.getEffectiveTypeOfSymbol(targetSymbol) : undefined; - - if (!symbolType || symbolType.category !== TypeCategory.Module) { - symbolType = ModuleType.create(new SymbolTable()); - symbolType = ModuleType.cloneForLoadedModule(symbolType); - } - - // If the symbol didn't exist, create a new one. - if (!targetSymbol) { - targetSymbol = Symbol.createWithType(SymbolFlags.ClassMember, - symbolType, defaultTypeSourceId); - } - - if (i === 0) { - // Assign the first part of the multi-part name to the current scope. - this._assignTypeToNameNode(nameParts[0], symbolType); - } - - if (i === nameParts.length - 1) { - const moduleType = symbolType; - moduleType.fields = type.fields; - moduleType.docString = type.docString; - - if (type.loaderFields) { - assert(moduleType.loaderFields !== undefined); - - // Copy the loader fields, which may include implicit - // imports for the module. - type.loaderFields.forEach((symbol, name) => { - setSymbolPreservingAccess(moduleType.loaderFields!, name, symbol); - }); - } - - if (declaration) { - targetSymbol.addDeclaration(declaration); - } - } - - setSymbolPreservingAccess(targetSymbolTable, name, targetSymbol); - assert(symbolType.loaderFields !== undefined); - targetSymbolTable = symbolType.loaderFields!; - - // If this is the last element, determine if it's accessed. - if (i === nameParts.length - 1) { - // Is this module ever accessed? - if (targetSymbol && !targetSymbol.isAccessed()) { - const multipartName = nameParts.map(np => np.nameToken.value).join('.'); - const textRange = { start: nameParts[0].start, length: nameParts[0].length }; - if (nameParts.length > 1) { - TextRange.extend(textRange, nameParts[nameParts.length - 1]); - } - this._fileInfo.diagnosticSink.addUnusedCodeWithTextRange( - `'${ multipartName }' is not accessed`, textRange); - - this._addDiagnostic(this._fileInfo.diagnosticSettings.reportUnusedImport, - DiagnosticRule.reportUnusedImport, - `Import '${ multipartName }' is not accessed`, textRange); - } - } - } - } - private _assignTypeToNameNode(nameNode: NameNode, srcType: Type, declaration?: Declaration, srcExpressionNode?: ParseNode) { @@ -3508,28 +3392,6 @@ export class TypeAnalyzer extends ParseTreeWalker { this._addAssignmentTypeConstraint(node, type); } - // Finds the nearest permanent scope (as opposed to temporary scope) and - // adds a new symbol with the specified name if it doesn't already exist. - private _addSymbolToPermanentScope(name: string) { - const permanentScope = ScopeUtils.getPermanentScope(this._currentScope); - assert(permanentScope.getType() !== ScopeType.Temporary); - - let symbol = permanentScope.lookUpSymbol(name); - if (!symbol) { - symbol = permanentScope.addSymbol(name, SymbolFlags.ClassMember); - } - - // Variables that are defined within a module or a class - // are considered public by default. Don't flag them - // "not access" unless the name indicates that it's private. - const scopeType = permanentScope.getType(); - if (scopeType === ScopeType.Class || scopeType === ScopeType.Module) { - if (!this._isSymbolPrivate(name, scopeType)) { - this._setSymbolAccessed(symbol); - } - } - } - private _addTypeSourceToName(name: string, type: Type, typeSourceId: TypeSourceId, declaration?: Declaration) { diff --git a/server/src/analyzer/types.ts b/server/src/analyzer/types.ts index 94299942c..b0139b8a5 100644 --- a/server/src/analyzer/types.ts +++ b/server/src/analyzer/types.ts @@ -115,24 +115,14 @@ export interface ModuleType extends TypeBase { // A "loader" module includes symbols that were injected by // the module loader. We keep these separate so we don't // pollute the symbols exported by the module itself. - loaderFields?: SymbolTable; + loaderFields: SymbolTable; } export namespace ModuleType { - export function create(fields: SymbolTable, docString?: string) { + export function create(symbolTable?: SymbolTable) { const newModuleType: ModuleType = { category: TypeCategory.Module, - fields, - docString - }; - return newModuleType; - } - - export function cloneForLoadedModule(moduleType: ModuleType) { - const newModuleType: ModuleType = { - category: TypeCategory.Module, - fields: moduleType.fields, - docString: moduleType.docString, + fields: symbolTable || new SymbolTable(), loaderFields: new SymbolTable() }; return newModuleType; @@ -1217,6 +1207,24 @@ export function isTypeSame(type1: Type, type2: Type, recursionCount = 0): boolea return true; } + + case TypeCategory.Module: { + const type2Module = type2 as ModuleType; + + // Module types are the same if they share the same + // module symbol table. + if (type1.fields === type2Module.fields) { + return true; + } + + // If both symbol tables are empty, we can also assume + // they're equal. + if (type1.fields.isEmpty() && type2Module.fields.isEmpty()) { + return true; + } + + return false; + } } return true; diff --git a/server/src/languageService/completionProvider.ts b/server/src/languageService/completionProvider.ts index e0c732d3f..99894b1af 100644 --- a/server/src/languageService/completionProvider.ts +++ b/server/src/languageService/completionProvider.ts @@ -11,10 +11,10 @@ import { CompletionItem, CompletionItemKind, CompletionList, MarkupKind, TextEdit } from 'vscode-languageserver'; -import { ImportMap } from '../analyzer/analyzerFileInfo'; +import { ImportLookup } from '../analyzer/analyzerFileInfo'; import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { Declaration, DeclarationType } from '../analyzer/declaration'; -import { getTypeForDeclaration } from '../analyzer/declarationUtils'; +import { getTypeForDeclaration, resolveAliasDeclaration } from '../analyzer/declarationUtils'; import { ImportedModuleDescriptor, ImportResolver, ModuleNameAndType } from '../analyzer/importResolver'; import { ImportType } from '../analyzer/importResult'; import * as ImportStatementUtils from '../analyzer/importStatementUtils'; @@ -142,7 +142,7 @@ export class CompletionProvider { private _position: DiagnosticTextPosition, private _filePath: string, private _configOptions: ConfigOptions, - private _importMapCallback: () => ImportMap, + private _importLookup: ImportLookup, private _moduleSymbolsCallback: () => ModuleSymbolMap) { } @@ -566,13 +566,9 @@ export class CompletionProvider { const resolvedPath = importInfo.resolvedPaths.length > 0 ? importInfo.resolvedPaths[importInfo.resolvedPaths.length - 1] : ''; - const importMap = this._importMapCallback(); - - const moduleType = importMap.get(resolvedPath); - if (moduleType) { - const symbolTable = new SymbolTable(); - TypeUtils.getMembersForModule(moduleType, symbolTable); - this._addSymbolsForSymbolTable(symbolTable, + const lookupResults = this._importLookup(resolvedPath); + if (lookupResults) { + this._addSymbolsForSymbolTable(lookupResults.symbolTable, name => { // Don't suggest symbols that have already been imported. return !importFromNode.imports.find( @@ -646,46 +642,60 @@ export class CompletionProvider { let typeDetail: string | undefined; let documentation: string | undefined; - const declaration = declarations[0]; - const type = getTypeForDeclaration(declaration); - itemKind = this._convertDeclarationTypeToItemKind(declaration, type); + const declaration = resolveAliasDeclaration(declarations[0], this._importLookup); + if (declaration) { + const type = getTypeForDeclaration(declaration); + itemKind = this._convertDeclarationTypeToItemKind(declaration, type); - if (type) { - switch (declaration.type) { - case DeclarationType.BuiltIn: - case DeclarationType.Variable: - case DeclarationType.Parameter: - typeDetail = name + ': ' + printType(type); - break; + if (type) { + switch (declaration.type) { + case DeclarationType.BuiltIn: + case DeclarationType.Variable: + case DeclarationType.Parameter: + typeDetail = name + ': ' + printType(type); + break; - case DeclarationType.Function: - case DeclarationType.Method: - if (type.category === TypeCategory.OverloadedFunction) { - typeDetail = type.overloads.map(overload => - name + printType(overload.type)).join('\n'); - } else { - typeDetail = name + printType(type); + case DeclarationType.Function: + case DeclarationType.Method: + if (type.category === TypeCategory.OverloadedFunction) { + typeDetail = type.overloads.map(overload => + name + printType(overload.type)).join('\n'); + } else { + typeDetail = name + printType(type); + } + break; + + case DeclarationType.Class: { + typeDetail = 'class ' + name + '()'; + break; } - break; - case DeclarationType.Class: - typeDetail = 'class ' + name + '()'; - break; + case DeclarationType.Alias: { + typeDetail = name; + if (declaration.path) { + const lookupResults = this._importLookup(declaration.path); + if (lookupResults) { + documentation = lookupResults.docString; + } + } + break; + } - case DeclarationType.Module: - default: - typeDetail = name; - break; + default: { + typeDetail = name; + break; + } + } } - } - if (type) { - if (type.category === TypeCategory.Module) { - documentation = type.docString; - } else if (type.category === TypeCategory.Class) { - documentation = ClassType.getDocString(type); - } else if (type.category === TypeCategory.Function) { - documentation = FunctionType.getDocString(type); + if (type) { + if (type.category === TypeCategory.Module) { + documentation = type.docString; + } else if (type.category === TypeCategory.Class) { + documentation = ClassType.getDocString(type); + } else if (type.category === TypeCategory.Function) { + documentation = FunctionType.getDocString(type); + } } } @@ -834,7 +844,12 @@ export class CompletionProvider { private _convertDeclarationTypeToItemKind(declaration: Declaration, type?: Type): CompletionItemKind { - switch (declaration.type) { + const resolvedDeclaration = resolveAliasDeclaration(declaration, this._importLookup); + if (!resolvedDeclaration) { + return CompletionItemKind.Variable; + } + + switch (resolvedDeclaration.type) { case DeclarationType.BuiltIn: if (type) { if (type.category === TypeCategory.Class) { @@ -849,7 +864,7 @@ export class CompletionProvider { return CompletionItemKind.Variable; case DeclarationType.Variable: - return declaration.isConstant ? + return resolvedDeclaration.isConstant ? CompletionItemKind.Constant : CompletionItemKind.Variable; @@ -865,15 +880,8 @@ export class CompletionProvider { case DeclarationType.Class: return CompletionItemKind.Class; - case DeclarationType.Module: - return CompletionItemKind.Module; - case DeclarationType.Alias: - if (declaration.resolvedDeclarations) { - return this._convertDeclarationTypeToItemKind( - declaration.resolvedDeclarations[0], type); - } - return CompletionItemKind.Variable; + return CompletionItemKind.Module; } } diff --git a/server/src/languageService/definitionProvider.ts b/server/src/languageService/definitionProvider.ts index 010050223..d0f6aa28a 100644 --- a/server/src/languageService/definitionProvider.ts +++ b/server/src/languageService/definitionProvider.ts @@ -10,9 +10,10 @@ * definition is the top of the resolved import file. */ +import { ImportLookup } from '../analyzer/analyzerFileInfo'; import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { Declaration } from '../analyzer/declaration'; -import { resolveDeclarationAliases } from '../analyzer/declarationUtils'; +import { resolveAliasDeclaration } from '../analyzer/declarationUtils'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; import { Symbol } from '../analyzer/symbol'; import { ModuleType, TypeCategory } from '../analyzer/types'; @@ -29,7 +30,8 @@ const _startOfFileRange: DiagnosticTextRange = { start: _startOfFilePosition, en export class DefinitionProvider { static getDefinitionsForPosition(parseResults: ParseResults, - position: DiagnosticTextPosition): DocumentTextRange[] | undefined { + position: DiagnosticTextPosition, importLookup: ImportLookup): + DocumentTextRange[] | undefined { const offset = convertPositionToOffset(position, parseResults.tokenizerOutput.lines); if (offset === undefined) { @@ -43,17 +45,17 @@ export class DefinitionProvider { const definitions: DocumentTextRange[] = []; - if (node.nodeType === ParseNodeType.ModuleName) { - this._addDefinitionsForModuleNameNode(definitions, node, offset); - } else if (node.nodeType === ParseNodeType.Name) { + if (node.nodeType === ParseNodeType.Name) { // Is the user hovering over a member name? If so, we need to search // in the scope of that type rather than the current node's scope. if (node.parent && node.parent.nodeType === ParseNodeType.MemberAccess && node === node.parent.memberName) { - this._addDefinitionsForMemberAccessNode(definitions, node.parent); + this._addDefinitionsForMemberAccessNode(definitions, node.parent, importLookup); + } else if (node.parent && node.parent.nodeType === ParseNodeType.ModuleName) { + this._addDefinitionsForModuleNameNode(definitions, node.parent, offset); } else { - this._addDefinitionsForNameNode(definitions, node); + this._addDefinitionsForNameNode(definitions, node, importLookup); } } @@ -61,7 +63,7 @@ export class DefinitionProvider { } private static _addDefinitionsForMemberAccessNode(definitions: DocumentTextRange[], - node: MemberAccessExpressionNode) { + node: MemberAccessExpressionNode, importLookup: ImportLookup) { const baseType = AnalyzerNodeInfo.getExpressionType(node.leftExpression); if (!baseType) { @@ -88,14 +90,16 @@ export class DefinitionProvider { if (symbol) { const declarations = symbol.getDeclarations(); - this._addResultsForDeclarations(definitions, declarations); + this._addResultsForDeclarations(definitions, declarations, importLookup); } return subtype; }); } - private static _addDefinitionsForNameNode(definitions: DocumentTextRange[], node: NameNode) { + private static _addDefinitionsForNameNode(definitions: DocumentTextRange[], + node: NameNode, importLookup: ImportLookup) { + const scopeNode = ParseTreeUtils.getScopeNodeForNode(node); if (!scopeNode) { return; @@ -113,15 +117,15 @@ export class DefinitionProvider { const declarations = symbolWithScope.symbol.getDeclarations(); if (declarations) { - this._addResultsForDeclarations(definitions, declarations); + this._addResultsForDeclarations(definitions, declarations, importLookup); } } private static _addResultsForDeclarations(definitions: DocumentTextRange[], - declarations: Declaration[]) { + declarations: Declaration[], importLookup: ImportLookup) { declarations.forEach(decl => { - const resolvedDecl = resolveDeclarationAliases(decl); + const resolvedDecl = resolveAliasDeclaration(decl, importLookup); if (resolvedDecl && resolvedDecl.path) { definitions.push({ path: resolvedDecl.path, diff --git a/server/src/languageService/documentSymbolProvider.ts b/server/src/languageService/documentSymbolProvider.ts index bbdfe3085..4222678a0 100644 --- a/server/src/languageService/documentSymbolProvider.ts +++ b/server/src/languageService/documentSymbolProvider.ts @@ -11,9 +11,10 @@ import { Location, Position, Range, SymbolInformation, SymbolKind } from 'vscode-languageserver'; import VSCodeUri from 'vscode-uri'; +import { ImportLookup } from '../analyzer/analyzerFileInfo'; import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { Declaration, DeclarationType } from '../analyzer/declaration'; -import { getTypeForDeclaration } from '../analyzer/declarationUtils'; +import { getTypeForDeclaration, resolveAliasDeclaration } from '../analyzer/declarationUtils'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; import { TypeCategory } from '../analyzer/types'; @@ -31,15 +32,18 @@ class FindSymbolTreeWalker extends ParseTreeWalker { private _parseResults: ParseResults; private _symbolResults: SymbolInformation[]; private _query: string | undefined; + private _importLookup: ImportLookup; constructor(filePath: string, parseResults: ParseResults, - results: SymbolInformation[], query: string | undefined) { + results: SymbolInformation[], query: string | undefined, + importLookup: ImportLookup) { super(); this._filePath = filePath; this._parseResults = parseResults; this._symbolResults = results; this._query = query; + this._importLookup = importLookup; } findSymbols() { @@ -99,6 +103,11 @@ class FindSymbolTreeWalker extends ParseTreeWalker { } } + const resolvedSymbol = resolveAliasDeclaration(declaration, this._importLookup); + if (!resolvedSymbol) { + return; + } + let symbolKind: SymbolKind; switch (declaration.type) { case DeclarationType.Class: @@ -118,7 +127,7 @@ class FindSymbolTreeWalker extends ParseTreeWalker { } break; - case DeclarationType.Module: + case DeclarationType.Alias: symbolKind = SymbolKind.Module; break; @@ -173,10 +182,10 @@ class FindSymbolTreeWalker extends ParseTreeWalker { export class DocumentSymbolProvider { static addSymbolsForDocument(symbolList: SymbolInformation[], query: string | undefined, - filePath: string, parseResults: ParseResults) { + filePath: string, parseResults: ParseResults, importLookup: ImportLookup) { const symbolTreeWalker = new FindSymbolTreeWalker(filePath, parseResults, - symbolList, query); + symbolList, query, importLookup); symbolTreeWalker.findSymbols(); } } diff --git a/server/src/languageService/hoverProvider.ts b/server/src/languageService/hoverProvider.ts index edeb66e11..fd8f2440b 100644 --- a/server/src/languageService/hoverProvider.ts +++ b/server/src/languageService/hoverProvider.ts @@ -9,17 +9,16 @@ * position within a smart editor. */ -import { ImportMap } from '../analyzer/analyzerFileInfo'; +import { ImportLookup } from '../analyzer/analyzerFileInfo'; import * as AnalyzerNodeInfo from '../analyzer/analyzerNodeInfo'; import { Declaration, DeclarationType } from '../analyzer/declaration'; import * as DeclarationUtils from '../analyzer/declarationUtils'; -import { ImportType } from '../analyzer/importResult'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; import { ClassType, FunctionType, printType, Type, TypeCategory, UnknownType } from '../analyzer/types'; import { DiagnosticTextPosition, DiagnosticTextRange } from '../common/diagnostic'; import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils'; import { TextRange } from '../common/textRange'; -import { ModuleNameNode, ParseNode, ParseNodeType } from '../parser/parseNodes'; +import { ParseNode, ParseNodeType } from '../parser/parseNodes'; import { ParseResults } from '../parser/parser'; export interface HoverTextPart { @@ -34,7 +33,7 @@ export interface HoverResults { export class HoverProvider { static getHoverForPosition(parseResults: ParseResults, position: DiagnosticTextPosition, - importMap: ImportMap): HoverResults | undefined { + importLookup: ImportLookup): HoverResults | undefined { const offset = convertPositionToOffset(position, parseResults.tokenizerOutput.lines); if (offset === undefined) { @@ -54,18 +53,19 @@ export class HoverProvider { } }; - if (node.nodeType === ParseNodeType.ModuleName) { - this._addResultsForModuleNameNode(results.parts, node, offset, importMap); - } else if (node.nodeType === ParseNodeType.Name) { + if (node.nodeType === ParseNodeType.Name) { const declarations = DeclarationUtils.getDeclarationsForNameNode(node); if (declarations && declarations.length > 0) { - this._addResultsForDeclaration(results.parts, declarations[0], node); - } - - // If we had no declaration, see if we can provide a minimal tooltip. - if (results.parts.length === 0) { - this._addResultsPart(results.parts, node.nameToken.value + this._getTypeText(node), true); - this._addDocumentationPart(results.parts, node); + this._addResultsForDeclaration(results.parts, declarations[0], node, importLookup); + } else if (!node.parent || node.parent.nodeType !== ParseNodeType.ModuleName) { + // If we had no declaration, see if we can provide a minimal tooltip. We'll skip + // this if it's part of a module name, since a module name part with no declaration + // is a directory (a namespace package), and we don't want to provide any hover + // information in that case. + if (results.parts.length === 0) { + this._addResultsPart(results.parts, node.nameToken.value + this._getTypeText(node), true); + this._addDocumentationPart(results.parts, node); + } } } @@ -73,16 +73,11 @@ export class HoverProvider { } private static _addResultsForDeclaration(parts: HoverTextPart[], - declaration: Declaration, node: ParseNode): void { - - let resolvedDecl: Declaration | undefined = declaration; - while (resolvedDecl && resolvedDecl.type === DeclarationType.Alias) { - resolvedDecl = resolvedDecl.resolvedDeclarations ? - resolvedDecl.resolvedDeclarations[0] : undefined; - } + declaration: Declaration, node: ParseNode, importLookup: ImportLookup): void { + const resolvedDecl = DeclarationUtils.resolveAliasDeclaration(declaration, importLookup); if (!resolvedDecl) { - return undefined; + return; } switch (resolvedDecl.type) { @@ -109,7 +104,7 @@ export class HoverProvider { case DeclarationType.Class: { if (node.nodeType === ParseNodeType.Name) { - this._addResultsPart(parts, '(class) ' + this._getTypeText(node), true); + this._addResultsPart(parts, '(class) ' + node.nameToken.value, true); this._addDocumentationPart(parts, node); return; } @@ -139,7 +134,7 @@ export class HoverProvider { break; } - case DeclarationType.Module: { + case DeclarationType.Alias: { if (node.nodeType === ParseNodeType.Name) { this._addResultsPart(parts, '(module) ' + node.nameToken.value, true); this._addDocumentationPart(parts, node); @@ -150,46 +145,6 @@ export class HoverProvider { } } - private static _addResultsForModuleNameNode(parts: HoverTextPart[], node: ModuleNameNode, - offset: number, importMap: ImportMap) { - - // If this is an imported module name, try to map the position - // to the resolved import path. - const importInfo = AnalyzerNodeInfo.getImportInfo(node); - if (!importInfo) { - return; - } - - let pathOffset = node.nameParts.findIndex(range => { - return offset >= range.start && offset < TextRange.getEnd(range); - }); - - if (pathOffset < 0) { - return; - } - - if (pathOffset >= importInfo.resolvedPaths.length) { - pathOffset = importInfo.resolvedPaths.length - 1; - } - - if (importInfo.resolvedPaths[pathOffset]) { - const resolvedPath = importInfo.resolvedPaths[pathOffset]; - this._addResultsPart(parts, '(module) "' + resolvedPath + '"', true); - - if (importInfo.importType === ImportType.ThirdParty && !importInfo.isStubFile) { - this._addResultsPart(parts, - 'No type stub found for this module. Imported symbol types are unknown.'); - } - - // If the module has been resolved and already analyzed, - // we can add the docString for it as well. - const moduleType = importMap.get(resolvedPath); - if (moduleType) { - this._addDocumentationPartForType(parts, moduleType); - } - } - } - private static _getTypeFromNode(node: ParseNode): Type | undefined { return AnalyzerNodeInfo.getExpressionType(node); } diff --git a/server/src/languageService/referencesProvider.ts b/server/src/languageService/referencesProvider.ts index 37f0f69ad..cc48c5aa4 100644 --- a/server/src/languageService/referencesProvider.ts +++ b/server/src/languageService/referencesProvider.ts @@ -8,11 +8,11 @@ * by a location within a file. */ +import { ImportLookup } from '../analyzer/analyzerFileInfo'; import { Declaration, DeclarationType } from '../analyzer/declaration'; import * as DeclarationUtils from '../analyzer/declarationUtils'; import * as ParseTreeUtils from '../analyzer/parseTreeUtils'; import { ParseTreeWalker } from '../analyzer/parseTreeWalker'; -import { Symbol } from '../analyzer/symbol'; import { DiagnosticTextPosition, DocumentTextRange } from '../common/diagnostic'; import { convertOffsetToPosition, convertPositionToOffset } from '../common/positionUtils'; import { TextRange } from '../common/textRange'; @@ -31,15 +31,18 @@ class FindReferencesTreeWalker extends ParseTreeWalker { private _filePath: string; private _referencesResult: ReferencesResult; private _includeDeclaration: boolean; + private _importLookup: ImportLookup; constructor(parseResults: ParseResults, filePath: string, - referencesResult: ReferencesResult, includeDeclaration: boolean) { + referencesResult: ReferencesResult, includeDeclaration: boolean, + importLookup: ImportLookup) { super(); this._parseResults = parseResults; this._filePath = filePath; this._referencesResult = referencesResult; this._includeDeclaration = includeDeclaration; + this._importLookup = importLookup; } findReferences() { @@ -69,14 +72,22 @@ class FindReferencesTreeWalker extends ParseTreeWalker { } private _resultsContainsDeclaration(declaration: Declaration) { + const resolvedDecl = DeclarationUtils.resolveAliasDeclaration(declaration, this._importLookup); + if (!resolvedDecl) { + return false; + } + + // The reference results declarations are already resolved, so we don't + // need to call resolveAliasDeclaration on them. return this._referencesResult.declarations.some(decl => - DeclarationUtils.areDeclarationsSame(decl, declaration)); + DeclarationUtils.areDeclarationsSame(decl, resolvedDecl)); } } export class ReferencesProvider { static getReferencesForPosition(parseResults: ParseResults, filePath: string, - position: DiagnosticTextPosition, includeDeclaration: boolean): + position: DiagnosticTextPosition, includeDeclaration: boolean, + importLookup: ImportLookup): ReferencesResult | undefined { const offset = convertPositionToOffset(position, parseResults.tokenizerOutput.lines); @@ -94,12 +105,24 @@ export class ReferencesProvider { } const declarations = DeclarationUtils.getDeclarationsForNameNode(node); - if (!declarations || declarations.length === 0) { + if (!declarations) { + return undefined; + } + + const resolvedDeclarations: Declaration[] = []; + declarations.forEach(decl => { + const resovledDecl = DeclarationUtils.resolveAliasDeclaration(decl, importLookup); + if (resovledDecl) { + resolvedDeclarations.push(resovledDecl); + } + }); + + if (resolvedDeclarations.length === 0) { return undefined; } // Is this a type that potentially requires a global search? - const symbolDeclType = declarations[0].type; + const symbolDeclType = resolvedDeclarations[0].type; // Parameters are local to a scope, so they don't require a global search. const requiresGlobalSearch = symbolDeclType !== DeclarationType.Parameter; @@ -107,22 +130,23 @@ export class ReferencesProvider { const results: ReferencesResult = { requiresGlobalSearch, nodeAtOffset: node, - declarations, + declarations: resolvedDeclarations, locations: [] }; const refTreeWalker = new FindReferencesTreeWalker(parseResults, - filePath, results, includeDeclaration); + filePath, results, includeDeclaration, importLookup); refTreeWalker.findReferences(); return results; } static addReferences(parseResults: ParseResults, filePath: string, - referencesResult: ReferencesResult, includeDeclaration: boolean): void { + referencesResult: ReferencesResult, includeDeclaration: boolean, + importLookup: ImportLookup): void { const refTreeWalker = new FindReferencesTreeWalker(parseResults, - filePath, referencesResult, includeDeclaration); + filePath, referencesResult, includeDeclaration, importLookup); refTreeWalker.findReferences(); } } diff --git a/server/src/parser/parser.ts b/server/src/parser/parser.ts index bbc796ff6..176d84945 100644 --- a/server/src/parser/parser.ts +++ b/server/src/parser/parser.ts @@ -62,6 +62,7 @@ export interface ParseResults { importedModules: ModuleImport[]; futureImports: StringMap; tokenizerOutput: TokenizerOutput; + containsWildcardImport: boolean; } export interface ParseExpressionTextResults { @@ -92,6 +93,7 @@ export class Parser { private _isParsingTypeAnnotation = false; private _futureImportMap = new StringMap(); private _importedModules: ModuleImport[] = []; + private _containsWildcardImport = false; parseSourceFile(fileContents: string, parseOptions: ParseOptions, diagSink: DiagnosticSink, cancelToken?: CancelToken): ParseResults { @@ -132,7 +134,8 @@ export class Parser { parseTree: moduleNode, importedModules: this._importedModules, futureImports: this._futureImportMap, - tokenizerOutput: this._tokenizerOutput! + tokenizerOutput: this._tokenizerOutput!, + containsWildcardImport: this._containsWildcardImport }; } @@ -979,6 +982,7 @@ export class Parser { if (this._consumeTokenIfOperator(OperatorType.Multiply)) { extendRange(importFromNode, possibleStarToken); importFromNode.isWildcardImport = true; + this._containsWildcardImport = true; } else { const inParen = this._consumeTokenIfType(TokenType.OpenParenthesis); diff --git a/server/src/tests/testUtils.ts b/server/src/tests/testUtils.ts index 8b922f1f5..2e407635f 100644 --- a/server/src/tests/testUtils.ts +++ b/server/src/tests/testUtils.ts @@ -15,8 +15,8 @@ import { AnalyzerFileInfo } from '../analyzer/analyzerFileInfo'; import { ModuleScopeBinder } from '../analyzer/binder'; import { ImportResolver } from '../analyzer/importResolver'; import { Program } from '../analyzer/program'; +import { SymbolTable } from '../analyzer/symbol'; import { TestWalker } from '../analyzer/testWalker'; -import { ModuleType } from '../analyzer/types'; import { cloneDiagnosticSettings, ConfigOptions, ExecutionEnvironment } from '../common/configOptions'; import { Diagnostic, DiagnosticCategory } from '../common/diagnostic'; import { DiagnosticSink, TextRangeDiagnosticSink } from '../common/diagnosticSink'; @@ -77,7 +77,7 @@ export function buildAnalyzerFileInfo(filePath: string, parseResults: ParseResul const analysisDiagnostics = new TextRangeDiagnosticSink(parseResults.tokenizerOutput.lines); const fileInfo: AnalyzerFileInfo = { - importMap: new Map(), + importLookup: _ => undefined, futureImports: new StringMap(), builtinsScope: undefined, diagnosticSink: analysisDiagnostics,