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
This commit is contained in:
Andy 2018-04-06 12:19:08 -07:00 committed by GitHub
parent 724b74615b
commit 70682b7799
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 56 additions and 125 deletions

View file

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

View file

@ -137,8 +137,9 @@ namespace ts.Completions.PathCompletions {
if (directories) {
for (const directory of directories) {
const directoryName = getBaseFileName(normalizePath(directory));
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
if (directoryName !== "@types") {
result.push(nameAndKind(directoryName, ScriptElementKind.directory));
}
}
}
}
@ -177,19 +178,33 @@ namespace ts.Completions.PathCompletions {
}
}
if (compilerOptions.moduleResolution === ModuleResolutionKind.NodeJs) {
forEachAncestorDirectory(scriptPath, ancestor => {
const nodeModules = combinePaths(ancestor, "node_modules");
if (host.directoryExists(nodeModules)) {
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result);
}
});
const fragmentDirectory = containsSlash(fragment) ? getDirectoryPath(fragment) : undefined;
for (const ambientName of getAmbientModuleCompletions(fragment, fragmentDirectory, typeChecker)) {
result.push(nameAndKind(ambientName, ScriptElementKind.externalModuleName));
}
getCompletionEntriesFromTypings(host, compilerOptions, scriptPath, result);
for (const moduleName of enumeratePotentialNonRelativeModules(fragment, scriptPath, compilerOptions, typeChecker, host)) {
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeJs) {
// If looking for a global package name, don't just include everything in `node_modules` because that includes dependencies' own dependencies.
// (But do if we didn't find anything, e.g. 'package.json' missing.)
let foundGlobal = false;
if (fragmentDirectory === undefined) {
for (const moduleName of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
if (!result.some(entry => entry.name === moduleName)) {
foundGlobal = true;
result.push(nameAndKind(moduleName, ScriptElementKind.externalModuleName));
}
}
}
if (!foundGlobal) {
forEachAncestorDirectory(scriptPath, ancestor => {
const nodeModules = combinePaths(ancestor, "node_modules");
if (tryDirectoryExists(host, nodeModules)) {
getCompletionEntriesForDirectoryFragment(fragment, nodeModules, fileExtensions, /*includeExtensions*/ false, host, /*exclude*/ undefined, result);
}
});
}
}
return result;
@ -228,7 +243,7 @@ namespace ts.Completions.PathCompletions {
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
const fragmentHasPath = stringContains(fragment, directorySeparator);
const fragmentHasPath = containsSlash(fragment);
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
const expandedPrefixDirectory = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
@ -262,45 +277,19 @@ namespace ts.Completions.PathCompletions {
return path[0] === directorySeparator ? path.slice(1) : path;
}
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string, options: CompilerOptions, typeChecker: TypeChecker, host: LanguageServiceHost): string[] {
// Check If this is a nested module
const isNestedModule = stringContains(fragment, directorySeparator);
const moduleNameFragment = isNestedModule ? fragment.substr(0, fragment.lastIndexOf(directorySeparator)) : undefined;
function getAmbientModuleCompletions(fragment: string, fragmentDirectory: string | undefined, checker: TypeChecker): ReadonlyArray<string> {
// Get modules that the type checker picked up
const ambientModules = map(typeChecker.getAmbientModules(), sym => stripQuotes(sym.name));
let nonRelativeModuleNames = filter(ambientModules, moduleName => startsWith(moduleName, fragment));
const ambientModules = checker.getAmbientModules().map(sym => stripQuotes(sym.name));
const nonRelativeModuleNames = ambientModules.filter(moduleName => startsWith(moduleName, fragment));
// Nested modules of the form "module-name/sub" need to be adjusted to only return the string
// after the last '/' that appears in the fragment because that's where the replacement span
// starts
if (isNestedModule) {
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(moduleNameFragment);
nonRelativeModuleNames = map(nonRelativeModuleNames, nonRelativeModuleName => {
return removePrefix(nonRelativeModuleName, moduleNameWithSeperator);
});
if (fragmentDirectory !== undefined) {
const moduleNameWithSeperator = ensureTrailingDirectorySeparator(fragmentDirectory);
return nonRelativeModuleNames.map(nonRelativeModuleName => removePrefix(nonRelativeModuleName, moduleNameWithSeperator));
}
if (!options.moduleResolution || options.moduleResolution === ModuleResolutionKind.NodeJs) {
for (const visibleModule of enumerateNodeModulesVisibleToScript(host, scriptPath)) {
if (!isNestedModule) {
nonRelativeModuleNames.push(visibleModule.moduleName);
}
else if (startsWith(visibleModule.moduleName, moduleNameFragment)) {
const nestedFiles = tryReadDirectory(host, visibleModule.moduleDir, supportedTypeScriptExtensions, /*exclude*/ undefined, /*include*/ ["./*"]);
if (nestedFiles) {
for (let f of nestedFiles) {
f = normalizePath(f);
const nestedModule = removeFileExtension(getBaseFileName(f));
nonRelativeModuleNames.push(nestedModule);
}
}
}
}
}
return deduplicate(nonRelativeModuleNames, equateStringsCaseSensitive, compareStringsCaseSensitive);
return nonRelativeModuleNames;
}
export function getTripleSlashReferenceCompletion(sourceFile: SourceFile, position: number, compilerOptions: CompilerOptions, host: LanguageServiceHost): ReadonlyArray<PathCompletion> | undefined {
@ -390,48 +379,16 @@ namespace ts.Completions.PathCompletions {
return paths;
}
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string) {
const result: VisibleModuleInfo[] = [];
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string): ReadonlyArray<string> {
if (!host.readFile || !host.fileExists) return emptyArray;
if (host.readFile && host.fileExists) {
for (const packageJson of findPackageJsons(scriptPath, host)) {
const contents = tryReadingPackageJson(packageJson);
if (!contents) {
return;
}
const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules");
const foundModuleNames: string[] = [];
// Provide completions for all non @types dependencies
for (const key of nodeModulesDependencyKeys) {
addPotentialPackageNames(contents[key], foundModuleNames);
}
for (const moduleName of foundModuleNames) {
const moduleDir = combinePaths(nodeModulesDir, moduleName);
result.push({
moduleName,
moduleDir
});
}
}
}
return result;
function tryReadingPackageJson(filePath: string) {
try {
const fileText = tryReadFile(host, filePath);
return fileText ? JSON.parse(fileText) : undefined;
}
catch (e) {
return undefined;
}
}
function addPotentialPackageNames(dependencies: any, result: string[]) {
if (dependencies) {
const result: string[] = [];
for (const packageJson of findPackageJsons(scriptPath, host)) {
const contents = readJson(packageJson, host as { readFile: (filename: string) => string | undefined }); // Cast to assert that readFile is defined
// Provide completions for all non @types dependencies
for (const key of nodeModulesDependencyKeys) {
const dependencies: object | undefined = (contents as any)[key];
if (!dependencies) continue;
for (const dep in dependencies) {
if (dependencies.hasOwnProperty(dep) && !startsWith(dep, "@types/")) {
result.push(dep);
@ -439,6 +396,7 @@ namespace ts.Completions.PathCompletions {
}
}
}
return result;
}
// Replace everything after the last directory seperator that appears
@ -484,11 +442,6 @@ namespace ts.Completions.PathCompletions {
*/
const tripleSlashDirectiveFragmentRegex = /^(\/\/\/\s*<reference\s+(path|types)\s*=\s*(?:'|"))([^\3"]*)$/;
interface VisibleModuleInfo {
moduleName: string;
moduleDir: string;
}
const nodeModulesDependencyKeys = ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"];
function tryGetDirectories(host: LanguageServiceHost, directoryName: string): string[] {
@ -499,10 +452,6 @@ namespace ts.Completions.PathCompletions {
return tryIOAndConsumeErrors(host, host.readDirectory, path, extensions, exclude, include) || emptyArray;
}
function tryReadFile(host: LanguageServiceHost, path: string): string | undefined {
return tryIOAndConsumeErrors(host, host.readFile, path);
}
function tryFileExists(host: LanguageServiceHost, path: string): boolean {
return tryIOAndConsumeErrors(host, host.fileExists, path);
}
@ -522,4 +471,8 @@ namespace ts.Completions.PathCompletions {
catch { /*ignore*/ }
return undefined;
}
function containsSlash(fragment: string) {
return stringContains(fragment, directorySeparator);
}
}

View file

@ -27,11 +27,4 @@
// @Filename: ambient.ts
//// declare module "fake-module/other"
const kinds = ["import_as", "import_equals", "require"];
for (const kind of kinds) {
goTo.marker(kind + "0");
verify.completionListContains("repeated");
verify.completionListContains("other");
verify.not.completionListItemsCountIsGreaterThan(2);
}
verify.completionsAt(["import_as0", "import_equals0", "require0"], ["other", "repeated"], { isNewIdentifierLocation: true })

View file

@ -27,12 +27,4 @@
// @Filename: node_modules/fake-module/repeated.jsx
//// /*repeatedjsx*/
const kinds = ["import_as", "import_equals", "require"];
for (const kind of kinds) {
goTo.marker(kind + "0");
verify.completionListContains("ts");
verify.completionListContains("tsx");
verify.completionListContains("dts");
verify.not.completionListItemsCountIsGreaterThan(3);
}
verify.completionsAt(["import_as0", "import_equals0", "require0"], ["dts", "js", "jsx", "repeated", "ts", "tsx"], { isNewIdentifierLocation: true });

View file

@ -18,11 +18,4 @@
// @Filename: package.json
//// { "dependencies": { "@types/module-y": "latest" } }
const kinds = ["types_ref", "import_as", "import_equals", "require"];
for (const kind of kinds) {
goTo.marker(kind + "0");
verify.completionListContains("module-x");
verify.completionListContains("module-y");
verify.not.completionListItemsCountIsGreaterThan(2);
}
verify.completionsAt(["types_ref0", "import_as0", "import_equals0", "require0"], ["module-x", "module-y"], { isNewIdentifierLocation: true });

View file

@ -12,4 +12,4 @@
// NOTE: The node_modules folder is in "/", rather than ".", because it requires
// less scaffolding to mock. In particular, "/" is where we look for type roots.
verify.completionsAt("1", ["@a/b", "@c/d", "@e/f"], { isNewIdentifierLocation: true });
verify.completionsAt("1", ["@e/f", "@a/b", "@c/d"], { isNewIdentifierLocation: true });

View file

@ -23,6 +23,5 @@
// @Filename: /src/folder/4.ts
////const foo = require(`x//*4*/`);
const [r0, r1, r2, r3] = test.ranges();
verify.completionsAt("1", ["y", "x"], { isNewIdentifierLocation: true });
verify.completionsAt(["2", "3", "4"], ["bar", "foo"], { isNewIdentifierLocation: true });