Skip to content

Commit f2915e6

Browse files
committed
Finish lodash test case
1 parent 7aeb92f commit f2915e6

File tree

5 files changed

+207
-6
lines changed

5 files changed

+207
-6
lines changed

Diff for: src/compiler/checker.ts

+6
Original file line numberDiff line numberDiff line change
@@ -41488,8 +41488,14 @@ namespace ts {
4148841488
return isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : undefined;
4148941489
case SyntaxKind.MetaProperty:
4149041490
return checkExpression(node as Expression).symbol;
41491+
case SyntaxKind.BinaryExpression:
41492+
// See binary expression handling in `getDeclarationFromName`
41493+
return getSymbolOfNode(node as BinaryExpression) || getSymbolOfNode((node as BinaryExpression).left);
4149141494

4149241495
default:
41496+
if (isDeclaration(node)) {
41497+
return getSymbolOfNode(node);
41498+
}
4149341499
return undefined;
4149441500
}
4149541501
}

Diff for: src/server/session.ts

+98-4
Original file line numberDiff line numberDiff line change
@@ -1285,12 +1285,40 @@ namespace ts.server {
12851285
}
12861286

12871287
let definitions = this.mapDefinitionInfoLocations(unmappedDefinitionAndBoundSpan.definitions, project).slice();
1288-
const needsJsResolution = every(definitions.filter(d => d.isAliasTarget), d => !!d.isAmbient) || some(definitions, d => !!d.failedAliasResolution);
1288+
const needsJsResolution = !some(definitions, d => !!d.isAliasTarget && !d.isAmbient) || some(definitions, d => !!d.failedAliasResolution);
12891289
if (needsJsResolution) {
12901290
project.withAuxiliaryProjectForFiles([file], auxiliaryProject => {
1291-
const jsDefinitions = auxiliaryProject.getLanguageService().getDefinitionAndBoundSpan(file, position);
1292-
for (const jsDefinition of jsDefinitions?.definitions || emptyArray) {
1293-
pushIfUnique(definitions, jsDefinition, (a, b) => a.fileName === b.fileName && a.textSpan.start === b.textSpan.start);
1291+
const ls = auxiliaryProject.getLanguageService();
1292+
const jsDefinitions = ls.getDefinitionAndBoundSpan(file, position, /*aliasesOnly*/ true);
1293+
if (some(jsDefinitions?.definitions)) {
1294+
for (const jsDefinition of jsDefinitions!.definitions) {
1295+
pushIfUnique(definitions, jsDefinition, (a, b) => a.fileName === b.fileName && a.textSpan.start === b.textSpan.start);
1296+
}
1297+
}
1298+
else {
1299+
const ambientCandidates = definitions.filter(d => d.isAliasTarget && d.isAmbient);
1300+
for (const candidate of ambientCandidates) {
1301+
const candidateFileName = getEffectiveFileNameOfDefinition(candidate, project.getLanguageService().getProgram()!);
1302+
if (candidateFileName) {
1303+
const fileNameToSearch = findImplementationFileFromDtsFileName(candidateFileName, file, auxiliaryProject);
1304+
const scriptInfo = fileNameToSearch ? auxiliaryProject.getScriptInfo(fileNameToSearch) : undefined;
1305+
if (!scriptInfo) {
1306+
continue;
1307+
}
1308+
if (!auxiliaryProject.containsScriptInfo(scriptInfo)) {
1309+
auxiliaryProject.addRoot(scriptInfo);
1310+
}
1311+
const auxiliaryProgram = auxiliaryProject.getLanguageService().getProgram()!;
1312+
const fileToSearch = Debug.checkDefined(auxiliaryProgram.getSourceFile(fileNameToSearch!));
1313+
const matches = FindAllReferences.Core.getTopMostDeclarationsInFile(candidate.name, fileToSearch);
1314+
for (const match of matches) {
1315+
const symbol = auxiliaryProgram.getTypeChecker().getSymbolAtLocation(match);
1316+
if (symbol) {
1317+
pushIfUnique(definitions, GoToDefinition.createDefinitionInfo(match, auxiliaryProgram.getTypeChecker(), symbol, match));
1318+
}
1319+
}
1320+
}
1321+
}
12941322
}
12951323
});
12961324
}
@@ -1309,6 +1337,72 @@ namespace ts.server {
13091337
definitions: definitions.map(Session.mapToOriginalLocation),
13101338
textSpan,
13111339
};
1340+
1341+
function getEffectiveFileNameOfDefinition(definition: DefinitionInfo, program: Program) {
1342+
const sourceFile = program.getSourceFile(definition.fileName)!;
1343+
const checker = program.getTypeChecker();
1344+
const symbol = checker.getSymbolAtLocation(getTouchingPropertyName(sourceFile, definition.textSpan.start));
1345+
if (symbol) {
1346+
let parent = symbol.parent;
1347+
while (parent && !isExternalModuleSymbol(parent)) {
1348+
parent = parent.parent;
1349+
}
1350+
if (parent?.declarations && some(parent.declarations, isExternalModuleAugmentation)) {
1351+
// Always CommonJS right now, but who knows in the future
1352+
const mode = getModeForUsageLocation(sourceFile, find(parent.declarations, isExternalModuleAugmentation)!.name as StringLiteral);
1353+
const fileName = sourceFile.resolvedModules?.get(stripQuotes(parent.name), mode)?.resolvedFileName;
1354+
if (fileName) {
1355+
return fileName;
1356+
}
1357+
}
1358+
const fileName = tryCast(parent?.valueDeclaration, isSourceFile)?.fileName;
1359+
if (fileName) {
1360+
return fileName;
1361+
}
1362+
}
1363+
}
1364+
1365+
function findImplementationFileFromDtsFileName(fileName: string, resolveFromFile: string, auxiliaryProject: Project) {
1366+
const nodeModulesPathParts = getNodeModulePathParts(fileName);
1367+
if (nodeModulesPathParts && fileName.lastIndexOf(nodeModulesPathPart) === nodeModulesPathParts.topLevelNodeModulesIndex) {
1368+
// Second check ensures the fileName only contains one `/node_modules/`. If there's more than one I give up.
1369+
const packageDirectory = fileName.substring(0, nodeModulesPathParts.packageRootIndex);
1370+
const packageJsonCache = project.getModuleResolutionCache()?.getPackageJsonInfoCache();
1371+
const compilerOptions = project.getCompilationSettings();
1372+
const packageJson = getPackageScopeForPath(project.toPath(packageDirectory + "/package.json"), packageJsonCache, project, compilerOptions);
1373+
if (!packageJson) return undefined;
1374+
// Use fake options instead of actual compiler options to avoid following export map if the project uses node12 or nodenext -
1375+
// Mapping from an export map entry across packages is out of scope for now. Returned entrypoints will only be what can be
1376+
// resolved from the package root under --moduleResolution node
1377+
const entrypoints = getEntrypointsFromPackageJsonInfo(
1378+
packageJson,
1379+
{ moduleResolution: ModuleResolutionKind.NodeJs },
1380+
project,
1381+
project.getModuleResolutionCache());
1382+
// This substring is correct only because we checked for a single `/node_modules/` at the top.
1383+
const packageNamePathPart = fileName.substring(
1384+
nodeModulesPathParts.topLevelPackageNameIndex + 1,
1385+
nodeModulesPathParts.packageRootIndex);
1386+
const packageName = getPackageNameFromTypesPackageName(unmangleScopedPackageName(packageNamePathPart));
1387+
const path = project.toPath(fileName);
1388+
if (entrypoints && some(entrypoints, e => project.toPath(e) === path)) {
1389+
// This file was the main entrypoint of a package. Try to resolve that same package name with
1390+
// the auxiliary project that only resolves to implementation files.
1391+
const [implementationResolution] = auxiliaryProject.resolveModuleNames([packageName], resolveFromFile);
1392+
return implementationResolution?.resolvedFileName;
1393+
}
1394+
else {
1395+
// It wasn't the main entrypoint but we are in node_modules. Try a subpath into the package.
1396+
const pathToFileInPackage = fileName.substring(nodeModulesPathParts.packageRootIndex + 1);
1397+
const specifier = `${packageName}/${removeFileExtension(pathToFileInPackage)}`;
1398+
const [implementationResolution] = auxiliaryProject.resolveModuleNames([specifier], resolveFromFile);
1399+
return implementationResolution?.resolvedFileName;
1400+
}
1401+
}
1402+
// We're not in node_modules, and we only get to this function if non-dts module resolution failed.
1403+
// I'm not sure what else I can do here that isn't already covered by that module resolution.
1404+
return undefined;
1405+
}
13121406
}
13131407

13141408
private getEmitOutput(args: protocol.EmitOutputRequestArgs): EmitOutput | protocol.EmitOutput {

Diff for: src/services/findAllReferences.ts

+24
Original file line numberDiff line numberDiff line change
@@ -1333,6 +1333,30 @@ namespace ts.FindAllReferences {
13331333
}
13341334
}
13351335

1336+
export function getTopMostDeclarationsInFile(declarationName: string, sourceFile: SourceFile): readonly Declaration[] {
1337+
const candidates = mapDefined(getPossibleSymbolReferenceNodes(sourceFile, declarationName), getDeclarationFromName);
1338+
return candidates.reduce((topMost, decl) => {
1339+
const depth = getDepth(decl);
1340+
if (!some(topMost.declarations) || depth === topMost.depth) {
1341+
topMost.declarations.push(decl);
1342+
}
1343+
else if (depth < topMost.depth) {
1344+
topMost.declarations = [decl];
1345+
}
1346+
topMost.depth = depth;
1347+
return topMost;
1348+
}, { depth: Infinity, declarations: [] as Declaration[] }).declarations;
1349+
1350+
function getDepth(declaration: Declaration | undefined) {
1351+
let depth = 0;
1352+
while (declaration) {
1353+
declaration = getContainerNode(declaration);
1354+
depth++;
1355+
}
1356+
return depth;
1357+
}
1358+
}
1359+
13361360
export function someSignatureUsage(
13371361
signature: SignatureDeclaration,
13381362
sourceFiles: readonly SourceFile[],

Diff for: src/services/goToDefinition.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ namespace ts.GoToDefinition {
4444
const { symbol, isAliasTarget, failedAliasResolution } = getSymbol(node, typeChecker);
4545
if (!symbol && isModuleSpecifierLike(node)) {
4646
// We couldn't resolve the symbol as an external module, but it could
47-
// that module resolution succeeded but the target was not a module.
47+
// be that module resolution succeeded but the target was not a module.
4848
const ref = sourceFile.resolvedModules?.get(node.text, getModeForUsageLocation(sourceFile, node));
4949
if (ref) {
5050
return [{
@@ -379,7 +379,7 @@ namespace ts.GoToDefinition {
379379
}
380380

381381
/** Creates a DefinitionInfo from a Declaration, using the declaration's name if possible. */
382-
function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node, isAliasTarget?: boolean, failedAliasResolution?: boolean): DefinitionInfo {
382+
export function createDefinitionInfo(declaration: Declaration, checker: TypeChecker, symbol: Symbol, node: Node, isAliasTarget?: boolean, failedAliasResolution?: boolean): DefinitionInfo {
383383
const symbolName = checker.symbolToString(symbol); // Do not get scoped name, just the name of the symbol
384384
const symbolKind = SymbolDisplay.getSymbolKind(checker, symbol, node);
385385
const containerName = symbol.parent ? checker.symbolToString(symbol.parent, node) : "";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/// <reference path="../fourslash.ts" />
2+
3+
// @moduleResolution: node
4+
5+
// @Filename: /node_modules/lodash/package.json
6+
//// { "name": "lodash", "version": "4.17.15", "main": "./lodash.js" }
7+
8+
// @Filename: /node_modules/lodash/lodash.js
9+
//// ;(function() {
10+
//// /**
11+
//// * Adds two numbers.
12+
//// *
13+
//// * @static
14+
//// * @memberOf _
15+
//// * @since 3.4.0
16+
//// * @category Math
17+
//// * @param {number} augend The first number in an addition.
18+
//// * @param {number} addend The second number in an addition.
19+
//// * @returns {number} Returns the total.
20+
//// * @example
21+
//// *
22+
//// * _.add(6, 4);
23+
//// * // => 10
24+
//// */
25+
//// var [|/*variable*/add|] = createMathOperation(function(augend, addend) {
26+
//// return augend + addend;
27+
//// }, 0);
28+
////
29+
//// function lodash(value) {}
30+
//// lodash.[|/*property*/add|] = add;
31+
////
32+
//// /** Detect free variable `global` from Node.js. */
33+
//// var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
34+
//// /** Detect free variable `self`. */
35+
//// var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
36+
//// /** Used as a reference to the global object. */
37+
//// var root = freeGlobal || freeSelf || Function('return this')();
38+
//// /** Detect free variable `exports`. */
39+
//// var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;////
40+
//// /** Detect free variable `module`. */
41+
//// var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
42+
//// if (freeModule) {
43+
//// // Export for Node.js.
44+
//// (freeModule.exports = _)._ = _;
45+
//// // Export for CommonJS support.
46+
//// freeExports._ = _;
47+
//// }
48+
//// else {
49+
//// // Export to the global object.
50+
//// root._ = _;
51+
//// }
52+
//// }.call(this));
53+
54+
// @Filename: /node_modules/@types/lodash/package.json
55+
//// { "name": "@types/lodash", "version": "4.14.97", "types": "index.d.ts" }
56+
57+
// @Filename: /node_modules/@types/lodash/index.d.ts
58+
//// /// <reference path="./common/math.d.ts" />
59+
//// export = _;
60+
//// export as namespace _;
61+
//// declare const _: _.LoDashStatic;
62+
//// declare namespace _ {
63+
//// interface LoDashStatic {}
64+
//// }
65+
66+
// @Filename: /node_modules/@types/lodash/common/math.d.ts
67+
//// import _ = require("../index");
68+
//// declare module "../index" {
69+
//// interface LoDashStatic {
70+
//// add(augend: number, addend: number): number;
71+
//// }
72+
//// }
73+
74+
// @Filename: /index.ts
75+
//// import { [|/*start*/add|] } from 'lodash';
76+
77+
verify.goToSourceDefinition("start", ["variable", "property"]);

0 commit comments

Comments
 (0)