Skip to content

Commit 70682b7

Browse files
author
Andy
authored
Clean up code for nonrelative path completions (#23150)
* Clean up code for nonrelative path completions * Remove unnecessary test and simplify based on that * More code review * Call getCompletionEntriesFromTypings unconditionally
1 parent 724b746 commit 70682b7

7 files changed

+56
-125
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ namespace ts {
117117
}
118118
}
119119

120-
function readJson(path: string, host: ModuleResolutionHost): PackageJson {
120+
/* @internal */
121+
export function readJson(path: string, host: { readFile(fileName: string): string | undefined }): object {
121122
try {
122123
const jsonText = host.readFile(path);
123124
return jsonText ? JSON.parse(jsonText) : {};
@@ -300,7 +301,7 @@ namespace ts {
300301
// `types-publisher` sometimes creates packages with `"typings": null` for packages that don't provide their own types.
301302
// See `createNotNeededPackageJSON` in the types-publisher` repo.
302303
// tslint:disable-next-line:no-null-keyword
303-
const isNotNeededPackage = host.fileExists(packageJsonPath) && readJson(packageJsonPath, host).typings === null;
304+
const isNotNeededPackage = host.fileExists(packageJsonPath) && (readJson(packageJsonPath, host) as PackageJson).typings === null;
304305
if (!isNotNeededPackage) {
305306
// Return just the type directive names
306307
result.push(getBaseFileName(normalized));
@@ -983,7 +984,7 @@ namespace ts {
983984
const directoryExists = !onlyRecordFailures && directoryProbablyExists(nodeModuleDirectory, host);
984985
const packageJsonPath = pathToPackageJson(nodeModuleDirectory);
985986
if (directoryExists && host.fileExists(packageJsonPath)) {
986-
const packageJsonContent = readJson(packageJsonPath, host);
987+
const packageJsonContent = readJson(packageJsonPath, host) as PackageJson;
987988
if (subModuleName === "") { // looking up the root - need to handle types/typings/main redirects for subModuleName
988989
const path = tryReadPackageJsonFields(/*readTypes*/ true, packageJsonContent, nodeModuleDirectory, state);
989990
if (typeof path === "string") {

src/services/pathCompletions.ts

Lines changed: 48 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,9 @@ namespace ts.Completions.PathCompletions {
137137
if (directories) {
138138
for (const directory of directories) {
139139
const directoryName = getBaseFileName(normalizePath(directory));
140-
141-
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
140+
if (directoryName !== "@types") {
141+
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
142+
}
142143
}
143144
}
144145
}
@@ -177,19 +178,33 @@ namespace ts.Completions.PathCompletions {
177178
}
178179
}
179180

180-
if (compilerOptions.moduleResolution === ModuleResolutionKind.NodeJs) {
181-
forEachAncestorDirectory(scriptPath, ancestor => {
182-
const nodeModules = combinePaths(ancestor, "node_modules");
183-
if (host.directoryExists(nodeModules)) {
184-
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result);
185-
}
186-
});
181+
const fragmentDirectory = containsSlash(fragment) ? getDirectoryPath(fragment) : undefined;
182+
for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
183+
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName));
187184
}
188185

189186
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, result);
190187

191-
for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) {
192-
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
188+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) {
189+
// If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
190+
// (But do if we didn't find anything, e.g. 'package.json' missing.)
191+
let foundGlobal = false;
192+
if (fragmentDirectory === undefined) {
193+
for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
194+
if (!result.some(entry => entry.name === moduleName)) {
195+
foundGlobal = true;
196+
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
197+
}
198+
}
199+
}
200+
if (!foundGlobal) {
201+
forEachAncestorDirectory(scriptPath, ancestor => {
202+
const nodeModules = combinePaths(ancestor, "node_modules");
203+
if (tryDirectoryExists(host, nodeModules)) {
204+
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result);
205+
}
206+
});
207+
}
193208
}
194209

195210
return result;
@@ -228,7 +243,7 @@ namespace ts.Completions.PathCompletions {
228243
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
229244
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
230245

231-
const fragmentHasPath = stringContains(fragment, directorySeparator);
246+
const fragmentHasPath = containsSlash(fragment);
232247

233248
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
234249
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
@@ -262,45 +277,19 @@ namespace ts.Completions.PathCompletions {
262277
return path[0] === directorySeparator ? path.slice(1) : path;
263278
}
264279

265-
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] {
266-
// Check If this is a nested module
267-
const isNestedModule = stringContains(fragment, directorySeparator);
268-
const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined;
269-
280+
function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): ReadonlyArray<string> {
270281
// Get modules that the type checker picked up
271-
const ambientModules = map(typeChecker.getAmbientModules(), sym => stripQuotes(sym.name));
272-
let nonRelativeModuleNames = filter(ambientModules, moduleName => startsWith(moduleName, fragment));
282+
const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name));
283+
const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment));
273284

274285
// Nested modules of the form "module-name/sub" need to be adjusted to only return the string
275286
// after the last '/' that appears in the fragment because that's where the replacement span
276287
// starts
277-
if (isNestedModule) {
278-
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment);
279-
nonRelativeModuleNames = map(nonRelativeModuleNames, nonRelativeModuleName => {
280-
return removePrefix(nonRelativeModuleName, moduleNameWithSeperator);
281-
});
282-
}
283-
284-
285-
if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) {
286-
for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
287-
if (!isNestedModule) {
288-
nonRelativeModuleNames.push(visibleModule.moduleName);
289-
}
290-
else if (startsWith(visibleModule.moduleName, moduleNameFragment)) {
291-
const nestedFiles = tryReadDirectory(host, visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/ undefined, /*include*/ ["./*"]);
292-
if (nestedFiles) {
293-
for (let f of nestedFiles) {
294-
f = normalizePath(f);
295-
const nestedModule = removeFileExtension(getBaseFileName(f));
296-
nonRelativeModuleNames.push(nestedModule);
297-
}
298-
}
299-
}
300-
}
288+
if (fragmentDirectory !== undefined) {
289+
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(fragmentDirectory);
290+
return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeperator));
301291
}
302-
303-
return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive);
292+
return nonRelativeModuleNames;
304293
}
305294

306295
export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): ReadonlyArray<PathCompletion> | undefined {
@@ -390,55 +379,24 @@ namespace ts.Completions.PathCompletions {
390379
return paths;
391380
}
392381

393-
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string) {
394-
const result: VisibleModuleInfo[] = [];
395-
396-
if (host.readFile && host.fileExists) {
397-
for (const packageJson of findPackageJsons(scriptPath, host)) {
398-
const contents = tryReadingPackageJson(packageJson);
399-
if (!contents) {
400-
return;
401-
}
382+
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): ReadonlyArray<string> {
383+
if (!host.readFile || !host.fileExists) return emptyArray;
402384

403-
const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules");
404-
const foundModuleNames: string[] = [];
405-
406-
// Provide completions for all non @types dependencies
407-
for (const key of nodeModulesDependencyKeys) {
408-
addPotentialPackageNames(contents[key], foundModuleNames);
409-
}
410-
411-
for (const moduleName of foundModuleNames) {
412-
const moduleDir = combinePaths(nodeModulesDir, moduleName);
413-
result.push({
414-
moduleName,
415-
moduleDir
416-
});
417-
}
418-
}
419-
}
420-
421-
return result;
422-
423-
function tryReadingPackageJson(filePath: string) {
424-
try {
425-
const fileText = tryReadFile(host, filePath);
426-
return fileText ? JSON.parse(fileText) : undefined;
427-
}
428-
catch (e) {
429-
return undefined;
430-
}
431-
}
432-
433-
function addPotentialPackageNames(dependencies: any, result: string[]) {
434-
if (dependencies) {
385+
const result: string[] = [];
386+
for (const packageJson of findPackageJsons(scriptPath, host)) {
387+
const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined }); // Cast to assert that readFile is defined
388+
// Provide completions for all non @types dependencies
389+
for (const key of nodeModulesDependencyKeys) {
390+
const dependencies: object | undefined = (contents as any)[key];
391+
if (!dependencies) continue;
435392
for (const dep in dependencies) {
436393
if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) {
437394
result.push(dep);
438395
}
439396
}
440397
}
441398
}
399+
return result;
442400
}
443401

444402
// Replace everything after the last directory seperator that appears
@@ -484,11 +442,6 @@ namespace ts.Completions.PathCompletions {
484442
*/
485443
const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3"]*)$/;
486444

487-
interface VisibleModuleInfo {
488-
moduleName: string;
489-
moduleDir: string;
490-
}
491-
492445
const nodeModulesDependencyKeys = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
493446

494447
function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] {
@@ -499,10 +452,6 @@ namespace ts.Completions.PathCompletions {
499452
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
500453
}
501454

502-
function tryReadFile(host: LanguageServiceHost, path: string): string | undefined {
503-
return tryIOAndConsumeErrors(host, host.readFile, path);
504-
}
505-
506455
function tryFileExists(host: LanguageServiceHost, path: string): boolean {
507456
return tryIOAndConsumeErrors(host, host.fileExists, path);
508457
}
@@ -522,4 +471,8 @@ namespace ts.Completions.PathCompletions {
522471
catch { /*ignore*/ }
523472
return undefined;
524473
}
474+
475+
function containsSlash(fragment: string) {
476+
return stringContains(fragment, directorySeparator);
477+
}
525478
}

tests/cases/fourslash/completionForStringLiteralNonrelativeImport2.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,4 @@
2727
// @Filename: ambient.ts
2828
//// declare module "fake-module/other"
2929

30-
const kinds = ["import_as", "import_equals", "require"];
31-
32-
for (const kind of kinds) {
33-
goTo.marker(kind + "0");
34-
verify.completionListContains("repeated");
35-
verify.completionListContains("other");
36-
verify.not.completionListItemsCountIsGreaterThan(2);
37-
}
30+
verify.completionsAt(["import_as0", "import_equals0", "require0"], ["other", "repeated"], { isNewIdentifierLocation: true })

tests/cases/fourslash/completionForStringLiteralNonrelativeImport3.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,4 @@
2727
// @Filename: node_modules/fake-module/repeated.jsx
2828
//// /*repeatedjsx*/
2929

30-
const kinds = ["import_as", "import_equals", "require"];
31-
32-
for (const kind of kinds) {
33-
goTo.marker(kind + "0");
34-
verify.completionListContains("ts");
35-
verify.completionListContains("tsx");
36-
verify.completionListContains("dts");
37-
verify.not.completionListItemsCountIsGreaterThan(3);
38-
}
30+
verify.completionsAt(["import_as0", "import_equals0", "require0"], ["dts", "js", "jsx", "repeated", "ts", "tsx"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionForStringLiteralNonrelativeImportTypings3.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,4 @@
1818
// @Filename: package.json
1919
//// { "dependencies": { "@types/module-y": "latest" } }
2020

21-
const kinds = ["types_ref", "import_as", "import_equals", "require"];
22-
23-
for (const kind of kinds) {
24-
goTo.marker(kind + "0");
25-
verify.completionListContains("module-x");
26-
verify.completionListContains("module-y");
27-
verify.not.completionListItemsCountIsGreaterThan(2);
28-
}
21+
verify.completionsAt(["types_ref0", "import_as0", "import_equals0", "require0"], ["module-x", "module-y"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionListInImportClause05.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
// NOTE: The node_modules folder is in "/", rather than ".", because it requires
1313
// less scaffolding to mock. In particular, "/" is where we look for type roots.
1414

15-
verify.completionsAt("1", ["@a/b", "@c/d", "@e/f"], { isNewIdentifierLocation: true });
15+
verify.completionsAt("1", ["@e/f", "@a/b", "@c/d"], { isNewIdentifierLocation: true });

tests/cases/fourslash/completionsPaths.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,5 @@
2323
// @Filename: /src/folder/4.ts
2424
////const foo = require(`x//*4*/`);
2525

26-
const [r0, r1, r2, r3] = test.ranges();
2726
verify.completionsAt("1", ["y", "x"], { isNewIdentifierLocation: true });
2827
verify.completionsAt(["2", "3", "4"], ["bar", "foo"], { isNewIdentifierLocation: true });

0 commit comments

Comments
 (0)