moveToNewFile: Infer quote preference (#24652)

This commit is contained in:
Andy 2018-06-07 12:10:48 -07:00 committed by GitHub
parent 83c58a4fb5
commit 0fefaf286d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 85 additions and 49 deletions

View File

@ -5,10 +5,10 @@ namespace ts.codefix {
getCodeActions(context) {
const { sourceFile, program, preferences } = context;
const changes = textChanges.ChangeTracker.with(context, changes => {
const moduleExportsChangedToDefault = convertFileToEs6Module(sourceFile, program.getTypeChecker(), changes, program.getCompilerOptions().target!, preferences);
const moduleExportsChangedToDefault = convertFileToEs6Module(sourceFile, program.getTypeChecker(), changes, program.getCompilerOptions().target!, getQuotePreference(sourceFile, preferences));
if (moduleExportsChangedToDefault) {
for (const importingFile of program.getSourceFiles()) {
fixImportOfModuleExports(importingFile, sourceFile, changes, preferences);
fixImportOfModuleExports(importingFile, sourceFile, changes, getQuotePreference(importingFile, preferences));
}
}
});
@ -17,7 +17,7 @@ namespace ts.codefix {
},
});
function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, preferences: UserPreferences) {
function fixImportOfModuleExports(importingFile: SourceFile, exportingFile: SourceFile, changes: textChanges.ChangeTracker, quotePreference: QuotePreference) {
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, preferences));
changes.replaceNode(importingFile, importNode, makeImport(importNode.name, /*namedImports*/ undefined, moduleSpecifier, quotePreference));
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, preferences: UserPreferences): ModuleExportsChanged {
function convertFileToEs6Module(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget, quotePreference: QuotePreference): 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, preferences);
const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, quotePreference);
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, preferences: UserPreferences): ModuleExportsChanged {
function convertStatement(sourceFile: SourceFile, statement: Statement, checker: TypeChecker, changes: textChanges.ChangeTracker, identifiers: Identifiers, target: ScriptTarget, exports: ExportRenames, quotePreference: QuotePreference): ModuleExportsChanged {
switch (statement.kind) {
case SyntaxKind.VariableStatement:
convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, preferences);
convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, quotePreference);
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], preferences));
changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0], quotePreference));
}
return false;
}
@ -125,7 +125,15 @@ namespace ts.codefix {
}
}
function convertVariableStatement(sourceFile: SourceFile, statement: VariableStatement, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget, preferences: UserPreferences): void {
function convertVariableStatement(
sourceFile: SourceFile,
statement: VariableStatement,
changes: textChanges.ChangeTracker,
checker: TypeChecker,
identifiers: Identifiers,
target: ScriptTarget,
quotePreference: QuotePreference,
): void {
const { declarationList } = statement;
let foundImport = false;
const newNodes = flatMap(declarationList.declarations, decl => {
@ -138,11 +146,11 @@ namespace ts.codefix {
}
else if (isRequireCall(initializer, /*checkArgumentIsStringLiteralLike*/ true)) {
foundImport = true;
return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target, preferences);
return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target, quotePreference);
}
else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteralLike*/ true)) {
foundImport = true;
return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, preferences);
return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0], identifiers, quotePreference);
}
}
// Move it out to its own variable statement. (This will not be used if `!foundImport`)
@ -155,20 +163,20 @@ namespace ts.codefix {
}
/** Converts `const name = require("moduleSpecifier").propertyName` */
function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, preferences: UserPreferences): ReadonlyArray<Node> {
function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): 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, preferences),
makeSingleImport(tmp, propertyName, moduleSpecifier, quotePreference),
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, preferences)];
return [makeSingleImport(name.text, propertyName, moduleSpecifier, quotePreference)];
default:
return Debug.assertNever(name);
}
@ -340,7 +348,7 @@ namespace ts.codefix {
checker: TypeChecker,
identifiers: Identifiers,
target: ScriptTarget,
preferences: UserPreferences,
quotePreference: QuotePreference,
): ReadonlyArray<Node> {
switch (name.kind) {
case SyntaxKind.ObjectBindingPattern: {
@ -349,7 +357,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, preferences)];
return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, quotePreference)];
}
}
// falls through -- object destructuring has an interesting pattern and must be a variable declaration
@ -360,12 +368,12 @@ namespace ts.codefix {
*/
const tmp = makeUniqueName(moduleSpecifierToValidIdentifier(moduleSpecifier.text, target), identifiers);
return [
makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, preferences),
makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, quotePreference),
makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), createIdentifier(tmp)),
];
}
case SyntaxKind.Identifier:
return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers, preferences);
return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers, quotePreference);
default:
return Debug.assertNever(name);
}
@ -375,7 +383,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, preferences: UserPreferences): ReadonlyArray<Node> {
function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, quotePreference: QuotePreference): 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>();
@ -410,7 +418,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, preferences)];
return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, quotePreference)];
}
// Identifiers helpers
@ -488,10 +496,10 @@ namespace ts.codefix {
getSynthesizedDeepClones(cls.members));
}
function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, preferences: UserPreferences): ImportDeclaration {
function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, quotePreference: QuotePreference): ImportDeclaration {
return propertyName === "default"
? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, preferences)
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, preferences);
? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier, quotePreference)
: makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier, quotePreference);
}
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, context.preferences)));
variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, getQuotePreference(sourceFile, context.preferences))));
if (getEmitModuleKind(opts) === ModuleKind.CommonJS) {
// import Bluebird = require("bluebird");

View File

@ -197,7 +197,7 @@ namespace ts.codefix {
const lastImportDeclaration = findLast(sourceFile.statements, isAnyImportSyntax);
const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier);
const quotedModuleSpecifier = createLiteral(moduleSpecifierWithoutQuotes, shouldUseSingleQuote(sourceFile, preferences));
const quotedModuleSpecifier = makeStringLiteral(moduleSpecifierWithoutQuotes, getQuotePreference(sourceFile, preferences));
const importDecl = importKind !== ImportKind.Equals
? createImportDeclaration(
/*decorators*/ undefined,
@ -225,16 +225,6 @@ namespace ts.codefix {
return createCodeAction(Diagnostics.Import_0_from_module_1, [symbolName, moduleSpecifierWithoutQuotes], changes);
}
function shouldUseSingleQuote(sourceFile: SourceFile, preferences: UserPreferences): boolean {
if (preferences.quotePreference) {
return preferences.quotePreference === "single";
}
else {
const firstModuleSpecifier = firstOrUndefined(sourceFile.imports);
return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile);
}
}
function createImportClauseOfKind(kind: ImportKind.Default | ImportKind.Named | ImportKind.Namespace, symbolName: string) {
const id = createIdentifier(symbolName);
switch (kind) {

View File

@ -37,6 +37,6 @@ namespace ts.codefix {
}
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));
changes.replaceNode(sourceFile, info.importNode, makeImport(info.name, /*namedImports*/ undefined, info.moduleSpecifier, getQuotePreference(sourceFile, preferences)));
}
}

View File

@ -118,7 +118,8 @@ namespace ts.refactor {
}
const useEs6ModuleSyntax = !!oldFile.externalModuleIndicator;
const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, preferences);
const quotePreference = getQuotePreference(oldFile, preferences);
const importsFromNewFile = createOldFileImportsFromNewFile(usage.oldFileImportsFromNewFile, newModuleName, useEs6ModuleSyntax, quotePreference);
if (importsFromNewFile) {
changes.insertNodeBefore(oldFile, oldFile.statements[0], importsFromNewFile, /*blankLineBetween*/ true);
}
@ -129,7 +130,7 @@ namespace ts.refactor {
updateImportsInOtherFiles(changes, program, oldFile, usage.movedSymbols, newModuleName);
return [
...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, preferences),
...getNewFileImportsAndAddExportInOldFile(oldFile, usage.oldImportsNeededByNewFile, usage.newFileImportsFromOldFile, changes, checker, useEs6ModuleSyntax, quotePreference),
...addExports(oldFile, toMove.all, usage.oldFileImportsFromNewFile, useEs6ModuleSyntax),
];
}
@ -268,7 +269,7 @@ namespace ts.refactor {
| ImportEqualsDeclaration
| VariableStatement;
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined {
function createOldFileImportsFromNewFile(newFileNeedExport: ReadonlySymbolSet, newFileNameWithExtension: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined {
let defaultImport: Identifier | undefined;
const imports: string[] = [];
newFileNeedExport.forEach(symbol => {
@ -279,14 +280,14 @@ namespace ts.refactor {
imports.push(symbol.name);
}
});
return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, preferences);
return makeImportOrRequire(defaultImport, imports, newFileNameWithExtension, useEs6Imports, quotePreference);
}
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray<string>, path: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined {
function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray<string>, path: string, useEs6Imports: boolean, quotePreference: QuotePreference): Statement | undefined {
path = ensurePathIsNonModuleName(path);
if (useEs6Imports) {
const specifiers = imports.map(i => createImportSpecifier(/*propertyName*/ undefined, createIdentifier(i)));
return makeImportIfNecessary(defaultImport, specifiers, path, preferences);
return makeImportIfNecessary(defaultImport, specifiers, path, quotePreference);
}
else {
Debug.assert(!defaultImport); // If there's a default export, it should have been an es6 module.
@ -392,7 +393,7 @@ namespace ts.refactor {
changes: textChanges.ChangeTracker,
checker: TypeChecker,
useEs6ModuleSyntax: boolean,
preferences: UserPreferences,
quotePreference: QuotePreference,
): ReadonlyArray<SupportedImportStatement> {
const copiedOldImports: SupportedImportStatement[] = [];
for (const oldStatement of oldFile.statements) {
@ -424,7 +425,7 @@ namespace ts.refactor {
}
});
append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, preferences));
append(copiedOldImports, makeImportOrRequire(oldFileDefault, oldFileNamedImports, removeFileExtension(getBaseFileName(oldFile.fileName)), useEs6ModuleSyntax, quotePreference));
return copiedOldImports;
}

View File

@ -1257,18 +1257,34 @@ namespace ts {
return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host));
}
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 makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string, quotePreference: QuotePreference): ImportDeclaration | undefined {
return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier, quotePreference) : undefined;
}
export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string | Expression, preferences: UserPreferences): ImportDeclaration {
export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray<ImportSpecifier> | undefined, moduleSpecifier: string | Expression, quotePreference: QuotePreference): ImportDeclaration {
return createImportDeclaration(
/*decorators*/ undefined,
/*modifiers*/ undefined,
defaultImport || namedImports
? createImportClause(defaultImport, namedImports && namedImports.length ? createNamedImports(namedImports) : undefined)
: undefined,
typeof moduleSpecifier === "string" ? createLiteral(moduleSpecifier, preferences.quotePreference === "single") : moduleSpecifier);
typeof moduleSpecifier === "string" ? makeStringLiteral(moduleSpecifier, quotePreference) : moduleSpecifier);
}
export function makeStringLiteral(text: string, quotePreference: QuotePreference): StringLiteral {
return createLiteral(text, quotePreference === QuotePreference.Single);
}
export const enum QuotePreference { Single, Double }
export function getQuotePreference(sourceFile: SourceFile, preferences: UserPreferences): QuotePreference {
if (preferences.quotePreference) {
return preferences.quotePreference === "single" ? QuotePreference.Single : QuotePreference.Double;
}
else {
const firstModuleSpecifier = firstOrUndefined(sourceFile.imports);
return !!firstModuleSpecifier && !isStringDoubleQuoted(firstModuleSpecifier, sourceFile) ? QuotePreference.Single : QuotePreference.Double;
}
}
export function symbolNameNoDefault(symbol: Symbol): string | undefined {

View File

@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />
// @Filename: /a.ts
////import 'unrelated';
////
////[|const x = 0;|]
////x;
verify.moveToNewFile({
newFileContents: {
"/a.ts":
`import { x } from './x';
import 'unrelated';
x;`,
"/x.ts":
`export const x = 0;`,
},
});