diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 4bc53455c9e..369709c31e5 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -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; } } diff --git a/src/services/codefixes/convertToEs6Module.ts b/src/services/codefixes/convertToEs6Module.ts index c0a444b42e0..8eadbc3a306 100644 --- a/src/services/codefixes/convertToEs6Module.ts +++ b/src/services/codefixes/convertToEs6Module.ts @@ -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() }; 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 { + function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, preferences: UserPreferences): 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), + 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 { 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 { + function convertSingleIdentifierImport(file: SourceFile, name: Identifier, moduleSpecifier: StringLiteralLike, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, preferences: UserPreferences): 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(); @@ -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 { diff --git a/src/services/codefixes/fixInvalidImportSyntax.ts b/src/services/codefixes/fixInvalidImportSyntax.ts index 192375791ce..728a8b5663c 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))); + variations.push(createAction(context, sourceFile, node, makeImport(namespace.name, /*namedImports*/ undefined, node.moduleSpecifier, context.preferences))); if (getEmitModuleKind(opts) === ModuleKind.CommonJS) { // import Bluebird = require("bluebird"); diff --git a/src/services/codefixes/useDefaultImport.ts b/src/services/codefixes/useDefaultImport.ts index 3fe451c41fb..34e3d40e514 100644 --- a/src/services/codefixes/useDefaultImport.ts +++ b/src/services/codefixes/useDefaultImport.ts @@ -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)); } } diff --git a/src/services/refactors/moveToNewFile.ts b/src/services/refactors/moveToNewFile.ts index b415b9048ef..d04588e01d5 100644 --- a/src/services/refactors/moveToNewFile.ts +++ b/src/services/refactors/moveToNewFile.ts @@ -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 { 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, path: string, useEs6Imports: boolean): Statement | undefined { + function makeImportOrRequire(defaultImport: Identifier | undefined, imports: ReadonlyArray, 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 { 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; } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index e6ebdb78447..cfbfcf261c6 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1252,18 +1252,18 @@ namespace ts { return createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host)); } - export function makeImportIfNecessary(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string): ImportDeclaration | undefined { - return defaultImport || namedImports && namedImports.length ? makeImport(defaultImport, namedImports, moduleSpecifier) : undefined; + 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 makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | undefined, moduleSpecifier: string | Expression): ImportDeclaration { + export function makeImport(defaultImport: Identifier | undefined, namedImports: ReadonlyArray | 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 { diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 98180182ad8..e9ca57c0445 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -340,6 +340,7 @@ declare namespace FourSlashInterface { }): void; moveToNewFile(options: { readonly newFileContents: { readonly [fileName: string]: string }; + readonly preferences?: UserPreferences; }): void; noMoveToNewFile(): void; } diff --git a/tests/cases/fourslash/moveToNewFile.ts b/tests/cases/fourslash/moveToNewFile.ts index a4d243e4a03..8caa8538a13 100644 --- a/tests/cases/fourslash/moveToNewFile.ts +++ b/tests/cases/fourslash/moveToNewFile.ts @@ -1,8 +1,8 @@ /// // @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", + } });