wip
This commit is contained in:
parent
474653dfcc
commit
1645750300
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue