Handling more compiler options and minor refactor

This commit is contained in:
Richard Knoll 2016-07-26 13:43:29 -07:00
parent 84a10e439e
commit ed2da32776
6 changed files with 341 additions and 118 deletions

View File

@ -245,14 +245,7 @@ namespace FourSlash {
constructor(private basePath: string, private testType: FourSlashTestType, public testData: FourSlashData) {
// Create a new Services Adapter
this.cancellationToken = new TestCancellationToken();
const compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
if (compilationOptions.typeRoots) {
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
}
const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
this.languageService = languageServiceAdapter.getLanguageService();
let compilationOptions = convertGlobalOptionsToCompilerOptions(this.testData.globalOptions);
// Initialize the language service with all the scripts
let startResolveFileRef: FourSlashFile;
@ -260,6 +253,22 @@ namespace FourSlash {
ts.forEach(testData.files, file => {
// Create map between fileName and its content for easily looking up when resolveReference flag is specified
this.inputFiles[file.fileName] = file.content;
if (ts.getBaseFileName(file.fileName).toLowerCase() === "tsconfig.json") {
const configJson = ts.parseConfigFileTextToJson(file.fileName, file.content);
assert.isTrue(configJson.config !== undefined);
// Extend our existing compiler options so that we can also support tsconfig only options
if (configJson.config.compilerOptions) {
let baseDir = ts.normalizePath(ts.getDirectoryPath(file.fileName));
let tsConfig = ts.convertCompilerOptionsFromJson(configJson.config.compilerOptions, baseDir, file.fileName);
if (!tsConfig.errors || !tsConfig.errors.length) {
compilationOptions = ts.extend(compilationOptions, tsConfig.options);
}
}
}
if (!startResolveFileRef && file.fileOptions[metadataOptionNames.resolveReference] === "true") {
startResolveFileRef = file;
}
@ -269,6 +278,15 @@ namespace FourSlash {
}
});
if (compilationOptions.typeRoots) {
compilationOptions.typeRoots = compilationOptions.typeRoots.map(p => ts.getNormalizedAbsolutePath(p, this.basePath));
}
const languageServiceAdapter = this.getLanguageServiceAdapter(testType, this.cancellationToken, compilationOptions);
this.languageServiceAdapterHost = languageServiceAdapter.getHost();
this.languageService = languageServiceAdapter.getLanguageService();
if (startResolveFileRef) {
// Add the entry-point file itself into the languageServiceShimHost
this.languageServiceAdapterHost.addScript(startResolveFileRef.fileName, startResolveFileRef.content, /*isRootFile*/ true);

View File

@ -1757,6 +1757,12 @@ namespace ts {
owners: string[];
}
interface VisibleModuleInfo {
moduleName: string;
moduleDir: string;
canBeImported: boolean;
}
export interface DisplayPartsSymbolWriter extends SymbolWriter {
displayParts(): SymbolDisplayPart[];
}
@ -4438,18 +4444,100 @@ namespace ts {
/**
* Check all of the declared modules and those in node modules. Possible sources of modules:
* Modules that are found by the type checker
* Modules found relative to "baseUrl" compliler options (including patterns from "paths" compiler option)
* Modules from node_modules (i.e. those listed in package.json)
* This includes all files that are found in node_modules/moduleName/ with acceptable file extensions
*/
function getCompletionEntriesForNonRelativeModules(fragment: string, scriptPath: string): CompletionEntry[] {
return ts.map(enumeratePotentialNonRelativeModules(fragment, scriptPath), (moduleName) => {
return {
name: moduleName,
kind: ScriptElementKind.externalModuleName,
kindModifiers: ScriptElementKindModifier.none,
sortText: moduleName
};
const options = program.getCompilerOptions();
const { baseUrl, paths } = options;
let result: CompletionEntry[];
if (baseUrl) {
const fileExtensions = getSupportedExtensions(options);
const absolute = isRootedDiskPath(baseUrl) ? baseUrl : combinePaths(host.getCurrentDirectory(), baseUrl)
result = getCompletionEntriesForDirectoryFragment(fragment, normalizePath(absolute), fileExtensions, /*includeExtensions*/false);
if (paths) {
for (var path in paths) {
if (paths.hasOwnProperty(path)) {
if (path === "*") {
if (paths[path]) {
forEach(paths[path], pattern => {
forEach(getModulesForPathsPattern(fragment, baseUrl, pattern, fileExtensions), match => {
result.push(createCompletionEntryForModule(match, ScriptElementKind.externalModuleName));
});
});
}
}
else if (startsWith(path, fragment)) {
const entry = paths[path] && paths[path].length === 1 && paths[path][0];
if (entry) {
result.push(createCompletionEntryForModule(path, ScriptElementKind.externalModuleName));
}
}
}
}
}
}
else {
result = [];
}
forEach(enumeratePotentialNonRelativeModules(fragment, scriptPath), moduleName => {
result.push(createCompletionEntryForModule(moduleName, ScriptElementKind.externalModuleName));
});
return result;
}
function getModulesForPathsPattern(fragment: string, baseUrl: string, pattern: string, fileExtensions: string[]): string[] {
const parsed = hasZeroOrOneAsteriskCharacter(pattern) ? tryParsePattern(pattern) : undefined;
if (parsed) {
const hasTrailingSlash = parsed.prefix.charAt(parsed.prefix.length - 1) === "/" || parsed.prefix.charAt(parsed.prefix.length - 1) === "\\";
// The prefix has two effective parts: the directory path and the base component after the filepath that is not a
// full directory component. For example: directory/path/of/prefix/base*
const normalizedPrefix = hasTrailingSlash ? ensureTrailingDirectorySeparator(normalizePath(parsed.prefix)) : normalizePath(parsed.prefix);
const normalizedPrefixDirectory = getDirectoryPath(normalizedPrefix);
const normalizedPrefixBase = getBaseFileName(normalizedPrefix);
const fragmentHasPath = fragment.indexOf(directorySeparator) !== -1;
// Try and expand the prefix to include any path from the fragment so that we can limit the readDirectory call
const expandedPrefixDir = fragmentHasPath ? combinePaths(normalizedPrefixDirectory, normalizedPrefixBase + getDirectoryPath(fragment)) : normalizedPrefixDirectory;
const normalizedSuffix = normalizePath(parsed.suffix);
const baseDirectory = combinePaths(baseUrl, expandedPrefixDir);
const completePrefix = fragmentHasPath ? baseDirectory : ensureTrailingDirectorySeparator(baseDirectory) + normalizedPrefixBase;
// If we have a suffix, then we need to read the directory all the way down. We could create a glob
// that encodes the suffix, but we would have to escape the character "?" which readDirectory
// doesn't support. For now, this is safer but slower
const includeGlob = normalizedSuffix ? "**/*" : "./*"
const matches = host.readDirectory(baseDirectory, fileExtensions, undefined, [includeGlob]);
const result: string[] = [];
// Trim away prefix and suffix
forEach(matches, match => {
const normalizedMatch = normalizePath(match);
if (!endsWith(normalizedMatch, normalizedSuffix) || !startsWith(normalizedMatch, completePrefix)) {
return;
}
const start = completePrefix.length;
const length = normalizedMatch.length - start - normalizedSuffix.length;
result.push(removeFileExtension(normalizedMatch.substr(start, length)));
});
return result;
}
return undefined;
}
function enumeratePotentialNonRelativeModules(fragment: string, scriptPath: string): string[] {
@ -4513,6 +4601,112 @@ namespace ts {
}
}
function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string, modulePrefix?: string) {
const result: VisibleModuleInfo[] = [];
findPackageJsons(scriptPath).forEach((packageJson) => {
const package = tryReadingPackageJson(packageJson);
if (!package) {
return;
}
const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules");
const foundModuleNames: string[] = [];
if (package.dependencies) {
addPotentialPackageNames(package.dependencies, modulePrefix, foundModuleNames);
}
if (package.devDependencies) {
addPotentialPackageNames(package.devDependencies, modulePrefix, foundModuleNames);
}
forEach(foundModuleNames, (moduleName) => {
const moduleDir = combinePaths(nodeModulesDir, moduleName);
result.push({
moduleName,
moduleDir,
canBeImported: moduleCanBeImported(moduleDir)
});
});
});
return result;
function findPackageJsons(currentDir: string): string[] {
const paths: string[] = [];
let currentConfigPath: string;
while (true) {
currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json");
if (currentConfigPath) {
paths.push(currentConfigPath);
currentDir = getDirectoryPath(currentConfigPath);
const parent = getDirectoryPath(currentDir);
if (currentDir === parent) {
break;
}
currentDir = parent;
}
else {
break;
}
}
return paths;
}
function tryReadingPackageJson(filePath: string) {
try {
const fileText = host.readFile(filePath);
return JSON.parse(fileText);
}
catch (e) {
return undefined;
}
}
function addPotentialPackageNames(dependencies: any, prefix: string, result: string[]) {
for (const dep in dependencies) {
if (dependencies.hasOwnProperty(dep) && (!prefix || startsWith(dep, prefix))) {
result.push(dep);
}
}
}
/*
* A module can be imported by name alone if one of the following is true:
* It defines the "typings" property in its package.json
* The module has a "main" export and an index.d.ts file
* The module has an index.ts
*/
function moduleCanBeImported(modulePath: string): boolean {
const packagePath = combinePaths(modulePath, "package.json");
let hasMainExport = false;
if (host.fileExists(packagePath)) {
const package = tryReadingPackageJson(packagePath);
if (package) {
if (package.typings) {
return true;
}
hasMainExport = !!package.main;
}
}
hasMainExport = hasMainExport || host.fileExists(combinePaths(modulePath, "index.js"));
return (hasMainExport && host.fileExists(combinePaths(modulePath, "index.d.ts"))) || host.fileExists(combinePaths(modulePath, "index.ts"));
}
}
function createCompletionEntryForModule(name: string, kind: string): CompletionEntry {
return {
name,
kind,
kindModifiers: ScriptElementKindModifier.none,
sortText: name
}
}
function getCompletionEntryDetails(fileName: string, position: number, entryName: string): CompletionEntryDetails {
synchronizeHostData();

View File

@ -6,12 +6,6 @@ namespace ts {
list: Node;
}
export interface VisibleModuleInfo {
moduleName: string;
moduleDir: string;
canBeImported: boolean;
}
export function getLineStartPositionForPosition(position: number, sourceFile: SourceFile): number {
const lineStarts = sourceFile.getLineStarts();
const line = sourceFile.getLineAndCharacterOfPosition(position).line;
@ -933,101 +927,4 @@ namespace ts {
}
return ensureScriptKind(fileName, scriptKind);
}
export function enumerateNodeModulesVisibleToScript(host: LanguageServiceHost, scriptPath: string, modulePrefix?: string) {
const result: VisibleModuleInfo[] = [];
findPackageJsons(scriptPath).forEach((packageJson) => {
const package = tryReadingPackageJson(packageJson);
if (!package) {
return;
}
const nodeModulesDir = combinePaths(getDirectoryPath(packageJson), "node_modules");
const foundModuleNames: string[] = [];
if (package.dependencies) {
addPotentialPackageNames(package.dependencies, modulePrefix, foundModuleNames);
}
if (package.devDependencies) {
addPotentialPackageNames(package.devDependencies, modulePrefix, foundModuleNames);
}
forEach(foundModuleNames, (moduleName) => {
const moduleDir = combinePaths(nodeModulesDir, moduleName);
result.push({
moduleName,
moduleDir,
canBeImported: moduleCanBeImported(moduleDir)
});
});
});
return result;
function findPackageJsons(currentDir: string): string[] {
const paths: string[] = [];
let currentConfigPath: string;
while (true) {
currentConfigPath = findConfigFile(currentDir, (f) => host.fileExists(f), "package.json");
if (currentConfigPath) {
paths.push(currentConfigPath);
currentDir = getDirectoryPath(currentConfigPath);
const parent = getDirectoryPath(currentDir);
if (currentDir === parent) {
break;
}
currentDir = parent;
}
else {
break;
}
}
return paths;
}
function tryReadingPackageJson(filePath: string) {
try {
const fileText = host.readFile(filePath);
return JSON.parse(fileText);
}
catch (e) {
return undefined;
}
}
function addPotentialPackageNames(dependencies: any, prefix: string, result: string[]) {
for (const dep in dependencies) {
if (dependencies.hasOwnProperty(dep) && (!prefix || startsWith(dep, prefix))) {
result.push(dep);
}
}
}
/*
* A module can be imported by name alone if one of the following is true:
* It defines the "typings" property in its package.json
* The module has a "main" export and an index.d.ts file
* The module has an index.ts
*/
function moduleCanBeImported(modulePath: string): boolean {
const packagePath = combinePaths(modulePath, "package.json");
let hasMainExport = false;
if (host.fileExists(packagePath)) {
const package = tryReadingPackageJson(packagePath);
if (package) {
if (package.typings) {
return true;
}
hasMainExport = !!package.main;
}
}
hasMainExport = hasMainExport || host.fileExists(combinePaths(modulePath, "index.js"));
return (hasMainExport && host.fileExists(combinePaths(modulePath, "index.d.ts"))) || host.fileExists(combinePaths(modulePath, "index.ts"));
}
}
}

View File

@ -0,0 +1,27 @@
/// <reference path='fourslash.ts' />
// @baseUrl: tests/cases/fourslash/modules
// @Filename: tests/test0.ts
//// import * as foo1 from "mod/*import_as0*/
//// import foo2 = require("mod/*import_equals0*/
//// var foo3 = require("mod/*require0*/
// @Filename: modules/module.ts
//// export var x = 5;
// @Filename: package.json
//// { "dependencies": { "module-from-node": "latest" } }
// @Filename: node_modules/module-from-node/index.ts
//// /*module1*/
const kinds = ["import_as", "import_equals", "require"];
for (const kind of kinds) {
goTo.marker(kind + "0");
verify.completionListContains("module");
verify.completionListContains("module-from-node");
verify.not.completionListItemsCountIsGreaterThan(2);
}

View File

@ -0,0 +1,53 @@
/// <reference path='fourslash.ts' />
// @Filename: tsconfig.json
//// {
//// "compilerOptions": {
//// "baseUrl": "./modules",
//// "paths": {
//// "*": [
//// "prefix/0*/suffix.ts",
//// "prefix-only/*",
//// "*/suffix-only.ts"
//// ]
//// }
//// }
//// }
// @Filename: tests/test0.ts
//// import * as foo1 from "0/*import_as0*/
//// import foo2 = require("0/*import_equals0*/
//// var foo3 = require("0/*require0*/
//// import * as foo1 from "1/*import_as1*/
//// import foo2 = require("1/*import_equals1*/
//// var foo3 = require("1/*require1*/
//// import * as foo1 from "2/*import_as2*/
//// import foo2 = require("2/*import_equals2*/
//// var foo3 = require("2/*require2*/
// @Filename: modules/prefix/00test/suffix.ts
//// export var x = 5;
// @Filename: modules/prefix-only/1test.ts
//// export var y = 5;
// @Filename: modules/2test/suffix-only.ts
//// export var z = 5;
const kinds = ["import_as", "import_equals", "require"];
for (const kind of kinds) {
goTo.marker(kind + "0");
verify.completionListContains("0test");
goTo.marker(kind + "1");
verify.completionListContains("1test");
goTo.marker(kind + "2");
verify.completionListContains("2test");
}

View File

@ -0,0 +1,34 @@
/// <reference path='fourslash.ts' />
// @Filename: tsconfig.json
//// {
//// "compilerOptions": {
//// "baseUrl": "./modules",
//// "paths": {
//// "module1": ["some/path/whatever.ts"],
//// "module2": ["some/other/path.ts"]
//// }
//// }
//// }
// @Filename: tests/test0.ts
//// import * as foo1 from "m/*import_as0*/
//// import foo2 = require("m/*import_equals0*/
//// var foo3 = require("m/*require0*/
// @Filename: some/path/whatever.ts
//// export var x = 9;
// @Filename: some/other/path.ts
//// export var y = 10;
const kinds = ["import_as", "import_equals", "require"];
for (const kind of kinds) {
goTo.marker(kind + "0");
verify.completionListContains("module1");
verify.completionListContains("module2");
verify.not.completionListItemsCountIsGreaterThan(2);
}