diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fcdac1191ab..9de7b21a7fb 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -41488,8 +41488,14 @@ namespace ts { return isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : undefined; case SyntaxKind.MetaProperty: return checkExpression(node as Expression).symbol; + case SyntaxKind.BinaryExpression: + // See binary expression handling in `getDeclarationFromName` + return getSymbolOfNode(node as BinaryExpression) || getSymbolOfNode((node as BinaryExpression).left); default: + if (isDeclaration(node)) { + return getSymbolOfNode(node); + } return undefined; } } diff --git a/src/server/session.ts b/src/server/session.ts index 708221835a4..d658f7e5227 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1285,12 +1285,40 @@ namespace ts.server { } let definitions = this.mapDefinitionInfoLocations(unmappedDefinitionAndBoundSpan.definitions, project).slice(); - const needsJsResolution = every(definitions.filter(d => d.isAliasTarget), d => !!d.isAmbient) || some(definitions, d => !!d.failedAliasResolution); + const needsJsResolution = !some(definitions, d => !!d.isAliasTarget && !d.isAmbient) || some(definitions, d => !!d.failedAliasResolution); if (needsJsResolution) { project.withAuxiliaryProjectForFiles([file], auxiliaryProject => { - const jsDefinitions = auxiliaryProject.getLanguageService().getDefinitionAndBoundSpan(file, position); - for (const jsDefinition of jsDefinitions?.definitions || emptyArray) { - pushIfUnique(definitions, jsDefinition, (a, b) => a.fileName === b.fileName && a.textSpan.start === b.textSpan.start); + const ls = auxiliaryProject.getLanguageService(); + const jsDefinitions = ls.getDefinitionAndBoundSpan(file, position, /*aliasesOnly*/ true); + if (some(jsDefinitions?.definitions)) { + for (const jsDefinition of jsDefinitions!.definitions) { + pushIfUnique(definitions, jsDefinition, (a, b) => a.fileName === b.fileName && a.textSpan.start === b.textSpan.start); + } + } + else { + const ambientCandidates = definitions.filter(d => d.isAliasTarget && d.isAmbient); + for (const candidate of ambientCandidates) { + const candidateFileName = getEffectiveFileNameOfDefinition(candidate, project.getLanguageService().getProgram()!); + if (candidateFileName) { + const fileNameToSearch = findImplementationFileFromDtsFileName(candidateFileName, file, auxiliaryProject); + const scriptInfo = fileNameToSearch ? auxiliaryProject.getScriptInfo(fileNameToSearch) : undefined; + if (!scriptInfo) { + continue; + } + if (!auxiliaryProject.containsScriptInfo(scriptInfo)) { + auxiliaryProject.addRoot(scriptInfo); + } + const auxiliaryProgram = auxiliaryProject.getLanguageService().getProgram()!; + const fileToSearch = Debug.checkDefined(auxiliaryProgram.getSourceFile(fileNameToSearch!)); + const matches = FindAllReferences.Core.getTopMostDeclarationsInFile(candidate.name, fileToSearch); + for (const match of matches) { + const symbol = auxiliaryProgram.getTypeChecker().getSymbolAtLocation(match); + if (symbol) { + pushIfUnique(definitions, GoToDefinition.createDefinitionInfo(match, auxiliaryProgram.getTypeChecker(), symbol, match)); + } + } + } + } } }); } @@ -1309,6 +1337,72 @@ namespace ts.server { definitions: definitions.map(Session.mapToOriginalLocation), textSpan, }; + + function getEffectiveFileNameOfDefinition(definition: DefinitionInfo, program: Program) { + const sourceFile = program.getSourceFile(definition.fileName)!; + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(getTouchingPropertyName(sourceFile, definition.textSpan.start)); + if (symbol) { + let parent = symbol.parent; + while (parent && !isExternalModuleSymbol(parent)) { + parent = parent.parent; + } + if (parent?.declarations && some(parent.declarations, isExternalModuleAugmentation)) { + // Always CommonJS right now, but who knows in the future + const mode = getModeForUsageLocation(sourceFile, find(parent.declarations, isExternalModuleAugmentation)!.name as StringLiteral); + const fileName = sourceFile.resolvedModules?.get(stripQuotes(parent.name), mode)?.resolvedFileName; + if (fileName) { + return fileName; + } + } + const fileName = tryCast(parent?.valueDeclaration, isSourceFile)?.fileName; + if (fileName) { + return fileName; + } + } + } + + function findImplementationFileFromDtsFileName(fileName: string, resolveFromFile: string, auxiliaryProject: Project) { + const nodeModulesPathParts = getNodeModulePathParts(fileName); + if (nodeModulesPathParts && fileName.lastIndexOf(nodeModulesPathPart) === nodeModulesPathParts.topLevelNodeModulesIndex) { + // Second check ensures the fileName only contains one `/node_modules/`. If there's more than one I give up. + const packageDirectory = fileName.substring(0, nodeModulesPathParts.packageRootIndex); + const packageJsonCache = project.getModuleResolutionCache()?.getPackageJsonInfoCache(); + const compilerOptions = project.getCompilationSettings(); + const packageJson = getPackageScopeForPath(project.toPath(packageDirectory + "/package.json"), packageJsonCache, project, compilerOptions); + if (!packageJson) return undefined; + // Use fake options instead of actual compiler options to avoid following export map if the project uses node12 or nodenext - + // Mapping from an export map entry across packages is out of scope for now. Returned entrypoints will only be what can be + // resolved from the package root under --moduleResolution node + const entrypoints = getEntrypointsFromPackageJsonInfo( + packageJson, + { moduleResolution: ModuleResolutionKind.NodeJs }, + project, + project.getModuleResolutionCache()); + // This substring is correct only because we checked for a single `/node_modules/` at the top. + const packageNamePathPart = fileName.substring( + nodeModulesPathParts.topLevelPackageNameIndex + 1, + nodeModulesPathParts.packageRootIndex); + const packageName = getPackageNameFromTypesPackageName(unmangleScopedPackageName(packageNamePathPart)); + const path = project.toPath(fileName); + if (entrypoints && some(entrypoints, e => project.toPath(e) === path)) { + // This file was the main entrypoint of a package. Try to resolve that same package name with + // the auxiliary project that only resolves to implementation files. + const [implementationResolution] = auxiliaryProject.resolveModuleNames([packageName], resolveFromFile); + return implementationResolution?.resolvedFileName; + } + else { + // It wasn't the main entrypoint but we are in node_modules. Try a subpath into the package. + const pathToFileInPackage = fileName.substring(nodeModulesPathParts.packageRootIndex + 1); + const specifier = `${packageName}/${removeFileExtension(pathToFileInPackage)}`; + const [implementationResolution] = auxiliaryProject.resolveModuleNames([specifier], resolveFromFile); + return implementationResolution?.resolvedFileName; + } + } + // We're not in node_modules, and we only get to this function if non-dts module resolution failed. + // I'm not sure what else I can do here that isn't already covered by that module resolution. + return undefined; + } } private getEmitOutput(args: protocol.EmitOutputRequestArgs): EmitOutput | protocol.EmitOutput { diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index d4cccf374ec..bd2b73101c7 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1333,6 +1333,30 @@ namespace ts.FindAllReferences { } } + export function getTopMostDeclarationsInFile(declarationName: string, sourceFile: SourceFile): readonly Declaration[] { + const candidates = mapDefined(getPossibleSymbolReferenceNodes(sourceFile, declarationName), getDeclarationFromName); + return candidates.reduce((topMost, decl) => { + const depth = getDepth(decl); + if (!some(topMost.declarations) || depth === topMost.depth) { + topMost.declarations.push(decl); + } + else if (depth < topMost.depth) { + topMost.declarations = [decl]; + } + topMost.depth = depth; + return topMost; + }, { depth: Infinity, declarations: [] as Declaration[] }).declarations; + + function getDepth(declaration: Declaration | undefined) { + let depth = 0; + while (declaration) { + declaration = getContainerNode(declaration); + depth++; + } + return depth; + } + } + export function someSignatureUsage( signature: SignatureDeclaration, sourceFiles: readonly SourceFile[], diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index dc6ed645d63..522e3a574c7 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -44,7 +44,7 @@ namespace ts.GoToDefinition { const { symbol, isAliasTarget, failedAliasResolution } = getSymbol(node, typeChecker); if (!symbol && isModuleSpecifierLike(node)) { // We couldn't resolve the symbol as an external module, but it could - // that module resolution succeeded but the target was not a module. + // be that module resolution succeeded but the target was not a module. const ref = sourceFile.resolvedModules?.get(node.text, getModeForUsageLocation(sourceFile, node)); if (ref) { return [{ @@ -379,7 +379,7 @@ namespace ts.GoToDefinition { } /** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */ - function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node, isAliasTarget?: boolean, failedAliasResolution?: boolean): DefinitionInfo { + export function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node, isAliasTarget?: boolean, failedAliasResolution?: boolean): DefinitionInfo { const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol const symbolKind = SymbolDisplay.getSymbolKind(checker, symbol, node); const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : ""; diff --git a/tests/cases/fourslash/server/goToSource8_mapFromAtTypes.ts b/tests/cases/fourslash/server/goToSource8_mapFromAtTypes.ts new file mode 100644 index 00000000000..e596fd97d9e --- /dev/null +++ b/tests/cases/fourslash/server/goToSource8_mapFromAtTypes.ts @@ -0,0 +1,77 @@ +/// + +// @moduleResolution: node + +// @Filename: /node_modules/lodash/package.json +//// { "name": "lodash", "version": "4.17.15", "main": "./lodash.js" } + +// @Filename: /node_modules/lodash/lodash.js +//// ;(function() { +//// /** +//// * Adds two numbers. +//// * +//// * @static +//// * @memberOf _ +//// * @since 3.4.0 +//// * @category Math +//// * @param {number} augend The first number in an addition. +//// * @param {number} addend The second number in an addition. +//// * @returns {number} Returns the total. +//// * @example +//// * +//// * _.add(6, 4); +//// * // => 10 +//// */ +//// var [|/*variable*/add|] = createMathOperation(function(augend, addend) { +//// return augend + addend; +//// }, 0); +//// +//// function lodash(value) {} +//// lodash.[|/*property*/add|] = add; +//// +//// /** Detect free variable `global` from Node.js. */ +//// var freeGlobal = typeof global == 'object' && global && global.Object === Object && global; +//// /** Detect free variable `self`. */ +//// var freeSelf = typeof self == 'object' && self && self.Object === Object && self; +//// /** Used as a reference to the global object. */ +//// var root = freeGlobal || freeSelf || Function('return this')(); +//// /** Detect free variable `exports`. */ +//// var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;//// +//// /** Detect free variable `module`. */ +//// var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module; +//// if (freeModule) { +//// // Export for Node.js. +//// (freeModule.exports = _)._ = _; +//// // Export for CommonJS support. +//// freeExports._ = _; +//// } +//// else { +//// // Export to the global object. +//// root._ = _; +//// } +//// }.call(this)); + +// @Filename: /node_modules/@types/lodash/package.json +//// { "name": "@types/lodash", "version": "4.14.97", "types": "index.d.ts" } + +// @Filename: /node_modules/@types/lodash/index.d.ts +//// /// +//// export = _; +//// export as namespace _; +//// declare const _: _.LoDashStatic; +//// declare namespace _ { +//// interface LoDashStatic {} +//// } + +// @Filename: /node_modules/@types/lodash/common/math.d.ts +//// import _ = require("../index"); +//// declare module "../index" { +//// interface LoDashStatic { +//// add(augend: number, addend: number): number; +//// } +//// } + +// @Filename: /index.ts +//// import { [|/*start*/add|] } from 'lodash'; + +verify.goToSourceDefinition("start", ["variable", "property"]);