moveToNewFile: Respect UserPreferences#quote (#24365)

This commit is contained in:
Andy 2018-05-24 09:36:37 -07:00 committed by GitHub
parent 22252d50c7
commit bc570cd85f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 60 additions and 52 deletions

View File

@ -3130,7 +3130,7 @@ Actual: ${stringify(fullActual)}`);
const action = ts.first(refactor.actions);
assert(action.name === "Move to a new file" && action.description === "Move to a new file");
const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, ts.defaultPreferences)!;
const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.defaultPreferences)!;
for (const edit of editInfo.edits) {
const newContent = options.newFileContents[edit.fileName];
if (newContent === undefined) {
@ -4836,5 +4836,6 @@ namespace FourSlashInterface {
export interface MoveToNewFileOptions {
readonly newFileContents: { readonly [fileName: string]: string };
readonly preferences?: ts.UserPreferences;
}
}

View File

@ -3,12 +3,12 @@ namespace ts.codefix {
registerCodeFix({
errorCodes: [Diagnostics.File_is_a_CommonJS_module_it_may_be_converted_to_an_ES6_module.code],
getCodeActions(context) {
const { sourceFile, program } = context;
const { sourceFile, program, preferences } = context;
const changes = textChanges.ChangeTracker.with(context, changes => {
const moduleExportsChangedToDefault = convertFileToEs6Module(sourceFile, program.getTypeChecker(), changes, program.getCompilerOptions().target!);
const moduleExportsChangedToDefault = convertFileToEs6Module(sourceFile, program.getTypeChecker(), changes, program.getCompilerOptions().target!, preferences);
if (moduleExportsChangedToDefault) {
for (const importingFile of program.getSourceFiles()) {
fixImportOfModuleExports(importingFile, sourceFile, changes);
fixImportOfModuleExports(importingFile, sourceFile, changes, preferences);
}
}
});
@ -17,7 +17,7 @@ namespace ts.codefix {
},
});
function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker) {
function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, preferences: UserPreferences) {
for (const moduleSpecifier of importingFile.imports) {
const imported = getResolvedModule(importingFile, moduleSpecifier.text);
if (!imported || imported.resolvedFileName !== exportingFile.fileName) {
@ -27,7 +27,7 @@ namespace ts.codefix {
const importNode = importFromModuleSpecifier(moduleSpecifier);
switch (importNode.kind) {
case SyntaxKind.ImportEqualsDeclaration:
changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier));
changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier, preferences));
break;
case SyntaxKind.CallExpression:
if (isRequireCall(importNode, /*checkArgumentIsStringLiteralLike*/ false)) {
@ -39,13 +39,13 @@ namespace ts.codefix {
}
/** @returns Whether we converted a `module.exports =` to a default export. */
function convertFileToEs6Module(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget): ModuleExportsChanged {
function convertFileToEs6Module(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget, preferences: UserPreferences): ModuleExportsChanged {
const identifiers: Identifiers = { original: collectFreeIdentifiers(sourceFile), additional: createMap<true>() };
const exports = collectExportRenames(sourceFile, checker, identifiers);
convertExportsAccesses(sourceFile, exports, changes);
let moduleExportsChangedToDefault = false;
for (const statement of sourceFile.statements) {
const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports);
const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, preferences);
moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged;
}
return moduleExportsChangedToDefault;
@ -98,10 +98,10 @@ namespace ts.codefix {
/** Whether `module.exports =` was changed to `export default` */
type ModuleExportsChanged = boolean;
function convertStatement(sourceFile: SourceFile, statement: Statement, checker: TypeChecker, changes: textChanges.ChangeTracker, identifiers: Identifiers, target: ScriptTarget, exports: ExportRenames): ModuleExportsChanged {
function convertStatement(sourceFile: SourceFile, statement: Statement, checker: TypeChecker, changes: textChanges.ChangeTracker, identifiers: Identifiers, target: ScriptTarget, exports: ExportRenames, preferences: UserPreferences): ModuleExportsChanged {
switch (statement.kind) {
case SyntaxKind.VariableStatement:
convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target);
convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, preferences);
return false;
case SyntaxKind.ExpressionStatement: {
const { expression } = statement as ExpressionStatement;
@ -109,7 +109,7 @@ namespace ts.codefix {
case SyntaxKind.CallExpression: {
if (isRequireCall(expression, /*checkArgumentIsStringLiteralLike*/ true)) {
// For side-effecting require() call, just make a side-effecting import.
changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0]));
changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0], preferences));
}
return false;
}
@ -125,7 +125,7 @@ namespace ts.codefix {
}
}
function convertVariableStatement(sourceFile: SourceFile, statement: VariableStatement, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget): void {
function convertVariableStatement(sourceFile: SourceFile, statement: VariableStatement, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget, preferences: UserPreferences): void {
const { declarationList } = statement;
let foundImport = false;
const newNodes = flatMap(declarationList.declarations, decl => {
@ -138,11 +138,11 @@ namespace ts.codefix {
}
else if (isRequireCall(initializer, /*checkArgumentIsStringLiteralLike*/ true)) {
foundImport = true;
return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target);
return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target, preferences);
}
else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteralLike*/ true)) {
foundImport = true;
return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers);
return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, preferences);
}
}
// Move it out to its own variable statement. (This will not be used if `!foundImport`)
@ -155,20 +155,20 @@ namespace ts.codefix {
}
/** Converts `const name = require("moduleSpecifier").propertyName` */
function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers): ReadonlyArray<Node> {
function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, preferences: UserPreferences): ReadonlyArray<Node> {
switch (name.kind) {
case SyntaxKind.ObjectBindingPattern:
case SyntaxKind.ArrayBindingPattern: {
// `const [a, b] = require("c").d` --> `import { d } from "c"; const [a, b] = d;`
const tmp = makeUniqueName(propertyName, identifiers);
return [
makeSingleImport(tmp, propertyName, moduleSpecifier),
makeSingleImport(tmp, propertyName, moduleSpecifier, preferences),
makeConst(/*modifiers*/ undefined, name, createIdentifier(tmp)),
];
}
case SyntaxKind.Identifier:
// `const a = require("b").c` --> `import { c as a } from "./b";
return [makeSingleImport(name.text, propertyName, moduleSpecifier)];
return [makeSingleImport(name.text, propertyName, moduleSpecifier, preferences)];
default:
return Debug.assertNever(name);
}
@ -340,6 +340,7 @@ namespace ts.codefix {
checker: TypeChecker,
identifiers: Identifiers,
target: ScriptTarget,
preferences: UserPreferences,
): ReadonlyArray<Node> {
switch (name.kind) {
case SyntaxKind.ObjectBindingPattern: {
@ -348,7 +349,7 @@ namespace ts.codefix {
? undefined
: makeImportSpecifier(e.propertyName && (e.propertyName as Identifier).text, e.name.text)); // tslint:disable-line no-unnecessary-type-assertion (TODO: GH#18217)
if (importSpecifiers) {
return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier)];
return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, preferences)];
}
}
// falls through -- object destructuring has an interesting pattern and must be a variable declaration
@ -359,12 +360,12 @@ namespace ts.codefix {
*/
const tmp = makeUniqueName(moduleSpecifierToValidIdentifier(moduleSpecifier.text, target), identifiers);
return [
makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier),
makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, preferences),
makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), createIdentifier(tmp)),
];
}
case SyntaxKind.Identifier:
return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers);
return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers, preferences);
default:
return Debug.assertNever(name);
}
@ -374,7 +375,7 @@ namespace ts.codefix {
* Convert `import x = require("x").`
* Also converts uses like `x.y()` to `y()` and uses a named import.
*/
function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers): ReadonlyArray<Node> {
function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, preferences: UserPreferences): ReadonlyArray<Node> {
const nameSymbol = checker.getSymbolAtLocation(name);
// Maps from module property name to name actually used. (The same if there isn't shadowing.)
const namedBindingsNames = createMap<string>();
@ -409,7 +410,7 @@ namespace ts.codefix {
// If it was unused, ensure that we at least import *something*.
needDefaultImport = true;
}
return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier)];
return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, preferences)];
}
// Identifiers helpers
@ -481,10 +482,10 @@ namespace ts.codefix {
getSynthesizedDeepClones(cls.members));
}
function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike): ImportDeclaration {
function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, preferences: UserPreferences): ImportDeclaration {
return propertyName === "default"
? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier)
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier);
? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, preferences)
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, preferences);
}
function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier {

View File

@ -28,7 +28,7 @@ namespace ts.codefix {
const variations: CodeFixAction[] = [];
// import Bluebird from "bluebird";
variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier)));
variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, context.preferences)));
if (getEmitModuleKind(opts) === ModuleKind.CommonJS) {
// import Bluebird = require("bluebird");

View File

@ -8,13 +8,13 @@ namespace ts.codefix {
const { sourceFile, span: { start } } = context;
const info = getInfo(sourceFile, start);
if (!info) return undefined;
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, info));
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, sourceFile, info, context.preferences));
return [createCodeFixAction(fixId, changes, Diagnostics.Convert_to_default_import, fixId, Diagnostics.Convert_all_to_default_imports)];
},
fixIds: [fixId],
getAllCodeActions: context => codeFixAll(context, errorCodes, (changes, diag) => {
const info = getInfo(diag.file, diag.start);
if (info) doChange(changes, diag.file, info);
if (info) doChange(changes, diag.file, info, context.preferences);
}),
});
@ -36,7 +36,7 @@ namespace ts.codefix {
}
}
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info): void {
changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier));
function doChange(changes: textChanges.ChangeTracker, sourceFile: SourceFile, info: Info, preferences: UserPreferences): void {
changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, preferences));
}
}

View File

@ -10,7 +10,7 @@ namespace ts.refactor {
getEditsForAction(context, actionName): RefactorEditInfo {
Debug.assert(actionName === refactorName);
const statements = Debug.assertDefined(getStatementsToMove(context));
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host));
const edits = textChanges.ChangeTracker.with(context, t => doChange(context.file, context.program, statements, t, context.host, context.preferences));
return { edits, renameFilename: undefined, renameLocation: undefined };
}
});
@ -37,7 +37,7 @@ namespace ts.refactor {
return statements.slice(startNodeIndex, afterEndNodeIndex === -1 ? statements.length : afterEndNodeIndex);
}
function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost): void {
function doChange(oldFile: SourceFile, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void {
const checker = program.getTypeChecker();
const usage = getUsageInfo(oldFile, toMove.all, checker);
@ -47,7 +47,7 @@ namespace ts.refactor {
const newFileNameWithExtension = newModuleName + extension;
// If previous file was global, this is easy.
changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatements(oldFile, usage, changes, toMove, program, newModuleName));
changes.createNewFile(oldFile, combinePaths(currentDirectory, newFileNameWithExtension), getNewStatements(oldFile, usage, changes, toMove, program, newModuleName, preferences));
addNewFileToTsconfig(program, changes, oldFile.fileName, newFileNameWithExtension, hostGetCanonicalFileName(host));
}
@ -103,7 +103,7 @@ namespace ts.refactor {
}
function getNewStatements(
oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string,
oldFile: SourceFile, usage: UsageInfo, changes: textChanges.ChangeTracker, toMove: ToMove, program: Program, newModuleName: string, preferences: UserPreferences,
): ReadonlyArray<Statement> {
const checker = program.getTypeChecker();
@ -113,7 +113,7 @@ namespace ts.refactor {
}
const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator;
const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax);
const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, preferences);
if (importsFromNewFile) {
changes.insertNodeBefore(oldFile, oldFile.statements[0], importsFromNewFile, /*blankLineBetween*/ true);
}
@ -124,7 +124,7 @@ namespace ts.refactor {
updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName);
return [
...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax),
...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, preferences),
...addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax),
];
}
@ -196,7 +196,7 @@ namespace ts.refactor {
| ImportEqualsDeclaration
| VariableStatement;
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean): Statement | undefined {
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined {
let defaultImport: Identifier | undefined;
const imports: string[] = [];
newFileNeedExport.forEach(symbol => {
@ -207,14 +207,14 @@ namespace ts.refactor {
imports.push(symbol.name);
}
});
return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports);
return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, preferences);
}
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray<string>, path: string, useEs6Imports: boolean): Statement | undefined {
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray<string>, path: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined {
path = ensurePathIsNonModuleName(path);
if (useEs6Imports) {
const specifiers = imports.map(i => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(i)));
return makeImportIfNecessary(defaultImport, specifiers, path);
return makeImportIfNecessary(defaultImport, specifiers, path, preferences);
}
else {
Debug.assert(!defaultImport); // If there's a default export, it should have been an es6 module.
@ -320,6 +320,7 @@ namespace ts.refactor {
changes: textChanges.ChangeTracker,
checker: TypeChecker,
useEs6ModuleSyntax: boolean,
preferences: UserPreferences,
): ReadonlyArray<SupportedImportStatement> {
const copiedOldImports: SupportedImportStatement[] = [];
for (const oldStatement of oldFile.statements) {
@ -351,7 +352,7 @@ namespace ts.refactor {
}
});
append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax));
append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, preferences));
return copiedOldImports;
}

View File

@ -1252,18 +1252,18 @@ namespace ts {
return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host));
}
export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string): ImportDeclaration | undefined {
return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier) : undefined;
export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string, preferences: UserPreferences): ImportDeclaration | undefined {
return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, preferences) : undefined;
}
export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string | Expression): ImportDeclaration {
export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string | Expression, preferences: UserPreferences): ImportDeclaration {
return createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
defaultImport || namedImports
? createImportClause(defaultImport, namedImports && namedImports.length ? createNamedImports(namedImports) : undefined)
: undefined,
typeof moduleSpecifier === "string" ? createLiteral(moduleSpecifier) : moduleSpecifier);
typeof moduleSpecifier === "string" ? createLiteral(moduleSpecifier, preferences.quotePreference === "single") : moduleSpecifier);
}
export function symbolNameNoDefault(symbol: Symbol): string | undefined {

View File

@ -340,6 +340,7 @@ declare namespace FourSlashInterface {
}): void;
moveToNewFile(options: {
readonly newFileContents: { readonly [fileName: string]: string };
readonly preferences?: UserPreferences;
}): void;
noMoveToNewFile(): void;
}

View File

@ -1,8 +1,8 @@
/// <reference path='fourslash.ts' />
// @Filename: /a.ts
////import "./foo";
////import { a, b, alreadyUnused } from "./other";
////import './foo';
////import { a, b, alreadyUnused } from './other';
////const p = 0;
////[|const y = p + b;|]
////a; y;
@ -10,16 +10,20 @@
verify.moveToNewFile({
newFileContents: {
"/a.ts":
`import { y } from "./y";
`import { y } from './y';
import "./foo";
import { a, alreadyUnused } from "./other";
import './foo';
import { a, alreadyUnused } from './other';
export const p = 0;
a; y;`,
"/y.ts":
`import { b } from "./other";
import { p } from "./a";
`import { b } from './other';
import { p } from './a';
export const y = p + b;`,
},
preferences: {
quotePreference: "single",
}
});