This commit is contained in:
Andy Hanson 2017-09-27 12:38:55 -07:00
parent 474653dfcc
commit 1645750300

View file

@ -157,9 +157,11 @@ namespace ts.codefix {
const useCaseSensitiveFileNames = context.host.useCaseSensitiveFileNames ? context.host.useCaseSensitiveFileNames() : false;
const checker = context.program.getTypeChecker();
const token = getTokenAtPosition(context.sourceFile, context.span.start, /*includeJsDocComment*/ false);
//TODO: invalid cast!
return <ImportCodeFixContext>{
...context,
return {
host: context.host,
newLineCharacter: context.newLineCharacter,
rulesProvider: context.rulesProvider,
sourceFile: context.sourceFile,
checker,
compilerOptions: context.program.getCompilerOptions(),
cachedImportDeclarations: [],
@ -234,7 +236,7 @@ namespace ts.codefix {
}
}
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport());
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport(sourceFile, moduleSymbol, compilerOptions, getCanonicalFileName, host));
const changeTracker = createChangeTracker(context);
const importClause = isDefault
? createImportClause(createIdentifier(symbolName), /*namedBindings*/ undefined)
@ -242,7 +244,7 @@ namespace ts.codefix {
? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(symbolName)))
: createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(symbolName))]));
const moduleSpecifierLiteral = createLiteral(moduleSpecifierWithoutQuotes);
moduleSpecifierLiteral.singleQuote = getSingleQuoteStyleFromExistingImports();
moduleSpecifierLiteral.singleQuote = getSingleQuoteStyleFromExistingImports(sourceFile);
const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, moduleSpecifierLiteral);
if (!lastImportDeclaration) {
changeTracker.insertNodeAt(sourceFile, getSourceFileImportLocation(sourceFile), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` });
@ -261,6 +263,7 @@ namespace ts.codefix {
"NewImport",
moduleSpecifierWithoutQuotes
);
}
function getSourceFileImportLocation(node: SourceFile) {
// For a source file, it is possible there are detached comments we should not skip
@ -284,7 +287,7 @@ namespace ts.codefix {
return position;
}
function getSingleQuoteStyleFromExistingImports() {
function getSingleQuoteStyleFromExistingImports(sourceFile: SourceFile) {
const firstModuleSpecifier = forEach(sourceFile.statements, node => {
if (isImportDeclaration(node) || isExportDeclaration(node)) {
if (node.moduleSpecifier && isStringLiteral(node.moduleSpecifier)) {
@ -302,32 +305,31 @@ namespace ts.codefix {
}
}
function getModuleSpecifierForNewImport() {
const fileName = sourceFile.fileName;
function getModuleSpecifierForNewImport(sourceFile: SourceFile, moduleSymbol: Symbol, options: CompilerOptions, getCanonicalFileName: (file: string) => string, host: LanguageServiceHost) {
const moduleFileName = moduleSymbol.valueDeclaration.getSourceFile().fileName;
const sourceDirectory = getDirectoryPath(fileName);
const options = compilerOptions;
const sourceDirectory = getDirectoryPath(sourceFile.fileName);
return tryGetModuleNameFromAmbientModule() ||
tryGetModuleNameFromTypeRoots() ||
tryGetModuleNameAsNodeModule() ||
tryGetModuleNameFromBaseUrl() ||
tryGetModuleNameFromRootDirs() ||
removeFileExtension(getRelativePath(moduleFileName, sourceDirectory));
return tryGetModuleNameFromAmbientModule(moduleSymbol) ||
tryGetModuleNameFromTypeRoots(options, host, getCanonicalFileName, moduleFileName) ||
tryGetModuleNameAsNodeModule(options, moduleFileName, host, getCanonicalFileName, sourceDirectory) ||
tryGetModuleNameFromBaseUrl(options, moduleFileName, getCanonicalFileName) ||
tryGetModuleNameFromRootDirs(options, moduleFileName, sourceDirectory, getCanonicalFileName) ||
removeFileExtension(getRelativePath(moduleFileName, sourceDirectory, getCanonicalFileName));
}
function tryGetModuleNameFromAmbientModule(): string {
function tryGetModuleNameFromAmbientModule(moduleSymbol: Symbol): string | undefined {
const decl = moduleSymbol.valueDeclaration;
if (isModuleDeclaration(decl) && isStringLiteral(decl.name)) {
return decl.name.text;
}
}
function tryGetModuleNameFromBaseUrl() {
function tryGetModuleNameFromBaseUrl(options: CompilerOptions, moduleFileName: string, getCanonicalFileName: (file: string) => string): string | undefined {
if (!options.baseUrl) {
return undefined;
}
let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl);
let relativeName = getRelativePathIfInDirectory(moduleFileName, options.baseUrl, getCanonicalFileName);
if (!relativeName) {
return undefined;
}
@ -362,21 +364,24 @@ namespace ts.codefix {
return relativeName;
}
function tryGetModuleNameFromRootDirs() {
function tryGetModuleNameFromRootDirs(options: CompilerOptions, moduleFileName: string, sourceDirectory: string, getCanonicalFileName: (file: string) => string): string | undefined {
if (options.rootDirs) {
const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs);
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs);
const normalizedTargetPath = getPathRelativeToRootDirs(moduleFileName, options.rootDirs, getCanonicalFileName);
const normalizedSourcePath = getPathRelativeToRootDirs(sourceDirectory, options.rootDirs, getCanonicalFileName);
if (normalizedTargetPath !== undefined) {
const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath) : normalizedTargetPath;
const relativePath = normalizedSourcePath !== undefined ? getRelativePath(normalizedTargetPath, normalizedSourcePath, getCanonicalFileName) : normalizedTargetPath;
return removeFileExtension(relativePath);
}
}
return undefined;
}
function tryGetModuleNameFromTypeRoots() {
function tryGetModuleNameFromTypeRoots(options: CompilerOptions, host: LanguageServiceHost, getCanonicalFileName: (file: string) => string, moduleFileName: string): string | undefined {
const typeRoots = getEffectiveTypeRoots(options, host);
if (typeRoots) {
if (!typeRoots) {
return undefined;
}
const normalizedTypeRoots = map(typeRoots, typeRoot => toPath(typeRoot, /*basePath*/ undefined, getCanonicalFileName));
for (const typeRoot of normalizedTypeRoots) {
if (startsWith(moduleFileName, typeRoot)) {
@ -385,9 +390,14 @@ namespace ts.codefix {
}
}
}
}
function tryGetModuleNameAsNodeModule() {
function tryGetModuleNameAsNodeModule(
options: CompilerOptions,
moduleFileName: string,
host: LanguageServiceHost,
getCanonicalFileName: (file: string) => string,
sourceDirectory: string,
): string | undefined {
if (getEmitModuleResolutionKind(options) !== ModuleResolutionKind.NodeJs) {
// nothing to do here
return undefined;
@ -443,8 +453,7 @@ namespace ts.codefix {
return path.substring(parts.topLevelPackageNameIndex + 1);
}
else {
return getRelativePath(path, sourceDirectory);
}
return getRelativePath(path, sourceDirectory, getCanonicalFileName);
}
}
}
@ -507,9 +516,9 @@ namespace ts.codefix {
return state > States.NodeModules ? { topLevelNodeModulesIndex, topLevelPackageNameIndex, packageRootIndex, fileNameIndex } : undefined;
}
function getPathRelativeToRootDirs(path: string, rootDirs: string[]) {
function getPathRelativeToRootDirs(path: string, rootDirs: string[], getCanonicalFileName: (fileName: string) => string) {
for (const rootDir of rootDirs) {
const relativeName = getRelativePathIfInDirectory(path, rootDir);
const relativeName = getRelativePathIfInDirectory(path, rootDir, getCanonicalFileName);
if (relativeName !== undefined) {
return relativeName;
}
@ -525,22 +534,98 @@ namespace ts.codefix {
return fileName;
}
function getRelativePathIfInDirectory(path: string, directoryPath: string) {
function getRelativePathIfInDirectory(path: string, directoryPath: string, getCanonicalFileName: (fileName: string) => string) {
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return isRootedDiskPath(relativePath) || startsWith(relativePath, "..") ? undefined : relativePath;
}
function getRelativePath(path: string, directoryPath: string) {
function getRelativePath(path: string, directoryPath: string, getCanonicalFileName: (fileName: string) => string) {
const relativePath = getRelativePathToDirectoryOrUrl(directoryPath, path, directoryPath, getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
return !pathIsRelative(relativePath) ? "./" + relativePath : relativePath;
}
}
function getCodeActionsForExistingImport(moduleSymbol: Symbol, context: ImportCodeFixContext, symbolName: string, isDefault: boolean, isNamespaceImport: boolean, declarations: AnyImportSyntax[]): ImportCodeAction[] {
const { symbolName: name, sourceFile, symbolToken } = context;
const { namespaceImportDeclaration, namedImportDeclaration, existingModuleSpecifier } = getDeclarations(declarations);
const actions: ImportCodeAction[] = [];
if (symbolToken && namespaceImportDeclaration) {
actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration));
}
if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause &&
(namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) {
/**
* If the existing import declaration already has a named import list, just
* insert the identifier into that list.
*/
const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause);
const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText());
actions.push(createCodeAction(
Diagnostics.Add_0_to_existing_import_declaration_from_1,
[name, moduleSpecifierWithoutQuotes],
fileTextChanges,
"InsertingIntoExistingImport",
moduleSpecifierWithoutQuotes
));
}
else {
// we need to create a new import statement, but the existing module specifier can be reused.
actions.push(getCodeActionForNewImport(context, moduleSymbol, symbolName, isDefault, existingModuleSpecifier, isNamespaceImport));
}
return actions;
function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] {
const importList = <NamedImports>importClause.namedBindings;
const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name));
// case 1:
// original text: import default from "module"
// change to: import default, { name } from "module"
// case 2:
// original text: import {} from "module"
// change to: import { name } from "module"
if (!importList || importList.elements.length === 0) {
const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier]));
return createChangeTracker(context).replaceNode(sourceFile, importClause, newImportClause).getChanges();
}
/**
* If the import list has one import per line, preserve that. Otherwise, insert on same line as last element
* import {
* foo
* } from "./module";
*/
return createChangeTracker(context).insertNodeInListAfter(
sourceFile,
importList.elements[importList.elements.length - 1],
newImportSpecifier).getChanges();
}
function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction {
const namespacePrefix = stripQuotes(declaration.kind === SyntaxKind.ImportDeclaration
? (<NamespaceImport>declaration.importClause.namedBindings).name.getText()
: declaration.name.getText());
/**
* Cases:
* import * as ns from "mod"
* import default, * as ns from "mod"
* import ns = require("mod")
*
* Because there is no import list, we alter the reference to include the
* namespace instead of altering the import declaration. For example, "foo" would
* become "ns.foo"
*/
return createCodeAction(
Diagnostics.Change_0_to_1,
[name, `${namespacePrefix}.${name}`],
createChangeTracker(context).replaceNode(sourceFile, symbolToken, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(),
"CodeChange"
);
}
}
function getDeclarations(declarations: AnyImportSyntax[]) {
// It is possible that multiple import statements with the same specifier exist in the file.
// e.g.
//
@ -580,92 +665,10 @@ namespace ts.codefix {
existingModuleSpecifier = getModuleSpecifierFromImportEqualsDeclaration(declaration);
}
}
return { namespaceImportDeclaration, namedImportDeclaration, existingModuleSpecifier };
if (symbolToken && namespaceImportDeclaration) {
actions.push(getCodeActionForNamespaceImport(namespaceImportDeclaration));
}
if (!isNamespaceImport && namedImportDeclaration && namedImportDeclaration.importClause &&
(namedImportDeclaration.importClause.name || namedImportDeclaration.importClause.namedBindings)) {
/**
* If the existing import declaration already has a named import list, just
* insert the identifier into that list.
*/
const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause);
const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText());
actions.push(createCodeAction(
Diagnostics.Add_0_to_existing_import_declaration_from_1,
[name, moduleSpecifierWithoutQuotes],
fileTextChanges,
"InsertingIntoExistingImport",
moduleSpecifierWithoutQuotes
));
}
else {
// we need to create a new import statement, but the existing module specifier can be reused.
actions.push(getCodeActionForNewImport(context, moduleSymbol, symbolName, isDefault, existingModuleSpecifier, isNamespaceImport));
}
return actions;
function getModuleSpecifierFromImportEqualsDeclaration(declaration: ImportEqualsDeclaration) {
if (declaration.moduleReference && declaration.moduleReference.kind === SyntaxKind.ExternalModuleReference) {
return declaration.moduleReference.expression.getText();
}
return declaration.moduleReference.getText();
}
function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] {
const importList = <NamedImports>importClause.namedBindings;
const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name));
// case 1:
// original text: import default from "module"
// change to: import default, { name } from "module"
// case 2:
// original text: import {} from "module"
// change to: import { name } from "module"
if (!importList || importList.elements.length === 0) {
const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier]));
return createChangeTracker(context).replaceNode(sourceFile, importClause, newImportClause).getChanges();
}
/**
* If the import list has one import per line, preserve that. Otherwise, insert on same line as last element
* import {
* foo
* } from "./module";
*/
return createChangeTracker(context).insertNodeInListAfter(
sourceFile,
importList.elements[importList.elements.length - 1],
newImportSpecifier).getChanges();
}
function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction {
let namespacePrefix: string;
if (declaration.kind === SyntaxKind.ImportDeclaration) {
namespacePrefix = (<NamespaceImport>declaration.importClause.namedBindings).name.getText();
}
else {
namespacePrefix = declaration.name.getText();
}
namespacePrefix = stripQuotes(namespacePrefix);
/**
* Cases:
* import * as ns from "mod"
* import default, * as ns from "mod"
* import ns = require("mod")
*
* Because there is no import list, we alter the reference to include the
* namespace instead of altering the import declaration. For example, "foo" would
* become "ns.foo"
*/
return createCodeAction(
Diagnostics.Change_0_to_1,
[name, `${namespacePrefix}.${name}`],
createChangeTracker(context).replaceNode(sourceFile, symbolToken, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(),
"CodeChange"
);
function getModuleSpecifierFromImportEqualsDeclaration({ moduleReference }: ImportEqualsDeclaration): string {
return (moduleReference.kind === SyntaxKind.ExternalModuleReference ? moduleReference.expression : moduleReference).getText();
}
}