diff --git a/src/services/codefixes/convertToEs6Module.ts b/src/services/codefixes/convertToEs6Module.ts index c763d4da0c8..bd1e02845cd 100644 --- a/src/services/codefixes/convertToEs6Module.ts +++ b/src/services/codefixes/convertToEs6Module.ts @@ -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() }; 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 { + function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): ReadonlyArray { 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 { 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 { + function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, quotePreference: QuotePreference): ReadonlyArray { const nameSymbol = checker.getSymbolAtLocation(name); // Maps from module property name to name actually used. (The same if there isn't shadowing.) const namedBindingsNames = createMap(); @@ -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 { diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index 728a8b5663c..cfdc19e257d 100644 --- a/src/services/codefixes/fixInvalidImportSyntax.ts +++ b/src/services/codefixes/fixInvalidImportSyntax.ts @@ -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"); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index ecd2393418b..66402068fa4 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -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) { diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 34e3d40e514..36aa0bb1697 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -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))); } } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index c1f1ec1fd5e..0c6aaa61b65 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -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, path: string, useEs6Imports: boolean, preferences: UserPreferences): Statement | undefined { + function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray, 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 { 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; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 786de97f20e..efc1488d6a4 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1257,18 +1257,34 @@ namespace ts { return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); } - export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | 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 | 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 | undefined, moduleSpecifier: string | Expression, preferences: UserPreferences): ImportDeclaration { + export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | 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 { diff --git a/tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts b/tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts new file mode 100644 index 00000000000..1e7b4347a87 --- /dev/null +++ b/tests/cases/fourslash/moveToNewFile_inferQuoteStyle.ts @@ -0,0 +1,21 @@ +/// + +// @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;`, + }, +});