diff --git a/src/services/codefixes/convertToAsyncFunction.ts b/src/services/codefixes/convertToAsyncFunction.ts index 50e00740c68..2b582c03035 100644 --- a/src/services/codefixes/convertToAsyncFunction.ts +++ b/src/services/codefixes/convertToAsyncFunction.ts @@ -199,7 +199,26 @@ namespace ts.codefix { } }); - return getSynthesizedDeepCloneWithRenames(nodeToRename, /*includeTrivia*/ true, identsToRenameMap, checker); + return getSynthesizedDeepCloneWithReplacements(nodeToRename, /*includeTrivia*/ true, original => { + if (isBindingElement(original) && isIdentifier(original.name) && isObjectBindingPattern(original.parent)) { + const symbol = checker.getSymbolAtLocation(original.name); + const renameInfo = symbol && identsToRenameMap.get(String(getSymbolId(symbol))); + if (renameInfo && renameInfo.text !== (original.name || original.propertyName).getText()) { + return factory.createBindingElement( + original.dotDotDotToken, + original.propertyName || original.name, + renameInfo, + original.initializer); + } + } + else if (isIdentifier(original)) { + const symbol = checker.getSymbolAtLocation(original); + const renameInfo = symbol && identsToRenameMap.get(String(getSymbolId(symbol))); + if (renameInfo) { + return factory.createIdentifier(renameInfo.text); + } + } + }); } function getNewNameIfConflict(name: Identifier, originalNames: ReadonlyESMap): SynthIdentifier { @@ -289,7 +308,7 @@ namespace ts.codefix { const tryStatement = factory.createTryStatement(tryBlock, catchClause, /*finallyBlock*/ undefined); const destructuredResult = prevArgName && varDeclIdentifier && isSynthBindingPattern(prevArgName) - && factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(getSynthesizedDeepCloneWithRenames(prevArgName.bindingPattern), /*exclamationToken*/ undefined, /*type*/ undefined, varDeclIdentifier)], NodeFlags.Const)); + && factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(getSynthesizedDeepClone(prevArgName.bindingPattern), /*exclamationToken*/ undefined, /*type*/ undefined, varDeclIdentifier)], NodeFlags.Const)); return compact([varDeclList, tryStatement, destructuredResult]); } diff --git a/src/services/codefixes/convertToEs6Module.ts b/src/services/codefixes/convertToEs6Module.ts index 34522f127d2..963f8923ee6 100644 --- a/src/services/codefixes/convertToEs6Module.ts +++ b/src/services/codefixes/convertToEs6Module.ts @@ -44,10 +44,24 @@ namespace ts.codefix { 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, quotePreference); + let useSitesToUnqualify: ESMap | undefined; + // Process variable statements first to collect use sites that need to be updated inside other transformations + for (const statement of filter(sourceFile.statements, isVariableStatement)) { + const newUseSites = convertVariableStatement(sourceFile, statement, changes, checker, identifiers, target, quotePreference); + if (newUseSites) { + copyEntries(newUseSites, useSitesToUnqualify ??= new Map()); + } + } + // `convertStatement` will delete entries from `useSitesToUnqualify` when containing statements are replaced + for (const statement of filter(sourceFile.statements, s => !isVariableStatement(s))) { + const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports, useSitesToUnqualify, quotePreference); moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged; } + // Remaining use sites can be changed directly + useSitesToUnqualify?.forEach((replacement, original) => { + changes.replaceNode(sourceFile, original, replacement); + }); + return moduleExportsChangedToDefault; } @@ -98,7 +112,17 @@ 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, quotePreference: QuotePreference): ModuleExportsChanged { + function convertStatement( + sourceFile: SourceFile, + statement: Statement, + checker: TypeChecker, + changes: textChanges.ChangeTracker, + identifiers: Identifiers, + target: ScriptTarget, + exports: ExportRenames, + useSitesToUnqualify: ESMap | undefined, + quotePreference: QuotePreference + ): ModuleExportsChanged { switch (statement.kind) { case SyntaxKind.VariableStatement: convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target, quotePreference); @@ -115,7 +139,7 @@ namespace ts.codefix { } case SyntaxKind.BinaryExpression: { const { operatorToken } = expression as BinaryExpression; - return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, expression as BinaryExpression, changes, exports); + return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, expression as BinaryExpression, changes, exports, useSitesToUnqualify); } } } @@ -133,20 +157,20 @@ namespace ts.codefix { identifiers: Identifiers, target: ScriptTarget, quotePreference: QuotePreference, - ): void { + ): ESMap | undefined { const { declarationList } = statement; let foundImport = false; - const newNodes = flatMap(declarationList.declarations, decl => { + const converted = map(declarationList.declarations, decl => { const { name, initializer } = decl; if (initializer) { if (isExportsOrModuleExportsOrAlias(sourceFile, initializer)) { // `const alias = module.exports;` can be removed. foundImport = true; - return []; + return convertedImports([]); } else if (isRequireCall(initializer, /*checkArgumentIsStringLiteralLike*/ true)) { foundImport = true; - return convertSingleImport(sourceFile, name, initializer.arguments[0], changes, checker, identifiers, target, quotePreference); + return convertSingleImport(name, initializer.arguments[0], checker, identifiers, target, quotePreference); } else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteralLike*/ true)) { foundImport = true; @@ -154,29 +178,37 @@ namespace ts.codefix { } } // Move it out to its own variable statement. (This will not be used if `!foundImport`) - return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([decl], declarationList.flags)); + return convertedImports([factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([decl], declarationList.flags))]); }); if (foundImport) { // useNonAdjustedEndPosition to ensure we don't eat the newline after the statement. - changes.replaceNodeWithNodes(sourceFile, statement, newNodes); + changes.replaceNodeWithNodes(sourceFile, statement, flatMap(converted, c => c.newImports)); + let combinedUseSites: ESMap | undefined; + forEach(converted, c => { + if (c.useSitesToUnqualify) { + copyEntries(c.useSitesToUnqualify, combinedUseSites ??= new Map()); + } + }); + + return combinedUseSites; } } /** Converts `const name = require("moduleSpecifier").propertyName` */ - function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): readonly Node[] { + function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: StringLiteralLike, identifiers: Identifiers, quotePreference: QuotePreference): ConvertedImports { 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 [ + return convertedImports([ makeSingleImport(tmp, propertyName, moduleSpecifier, quotePreference), makeConst(/*modifiers*/ undefined, name, factory.createIdentifier(tmp)), - ]; + ]); } case SyntaxKind.Identifier: // `const a = require("b").c` --> `import { c as a } from "./b"; - return [makeSingleImport(name.text, propertyName, moduleSpecifier, quotePreference)]; + return convertedImports([makeSingleImport(name.text, propertyName, moduleSpecifier, quotePreference)]); default: return Debug.assertNever(name, `Convert to ES6 module got invalid syntax form ${(name as BindingName).kind}`); } @@ -188,6 +220,7 @@ namespace ts.codefix { assignment: BinaryExpression, changes: textChanges.ChangeTracker, exports: ExportRenames, + useSitesToUnqualify: ESMap | undefined, ): ModuleExportsChanged { const { left, right } = assignment; if (!isPropertyAccessExpression(left)) { @@ -200,7 +233,7 @@ namespace ts.codefix { changes.delete(sourceFile, assignment.parent); } else { - const replacement = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right) + const replacement = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right, useSitesToUnqualify) : isRequireCall(right, /*checkArgumentIsStringLiteralLike*/ true) ? convertReExportAll(right.arguments[0], checker) : undefined; if (replacement) { @@ -224,7 +257,7 @@ namespace ts.codefix { * Convert `module.exports = { ... }` to individual exports.. * We can't always do this if the module has interesting members -- then it will be a default export instead. */ - function tryChangeModuleExportsObject(object: ObjectLiteralExpression): [readonly Statement[], ModuleExportsChanged] | undefined { + function tryChangeModuleExportsObject(object: ObjectLiteralExpression, useSitesToUnqualify: ESMap | undefined): [readonly Statement[], ModuleExportsChanged] | undefined { const statements = mapAllOrFail(object.properties, prop => { switch (prop.kind) { case SyntaxKind.GetAccessor: @@ -235,9 +268,9 @@ namespace ts.codefix { case SyntaxKind.SpreadAssignment: return undefined; case SyntaxKind.PropertyAssignment: - return !isIdentifier(prop.name) ? undefined : convertExportsDotXEquals_replaceNode(prop.name.text, prop.initializer); + return !isIdentifier(prop.name) ? undefined : convertExportsDotXEquals_replaceNode(prop.name.text, prop.initializer, useSitesToUnqualify); case SyntaxKind.MethodDeclaration: - return !isIdentifier(prop.name) ? undefined : functionExpressionToDeclaration(prop.name.text, [factory.createToken(SyntaxKind.ExportKeyword)], prop); + return !isIdentifier(prop.name) ? undefined : functionExpressionToDeclaration(prop.name.text, [factory.createToken(SyntaxKind.ExportKeyword)], prop, useSitesToUnqualify); default: Debug.assertNever(prop, `Convert to ES6 got invalid prop kind ${(prop as ObjectLiteralElementLike).kind}`); } @@ -307,7 +340,7 @@ namespace ts.codefix { } // TODO: GH#22492 this will cause an error if a change has been made inside the body of the node. - function convertExportsDotXEquals_replaceNode(name: string | undefined, exported: Expression): Statement { + function convertExportsDotXEquals_replaceNode(name: string | undefined, exported: Expression, useSitesToUnqualify: ESMap | undefined): Statement { const modifiers = [factory.createToken(SyntaxKind.ExportKeyword)]; switch (exported.kind) { case SyntaxKind.FunctionExpression: { @@ -321,17 +354,39 @@ namespace ts.codefix { // falls through case SyntaxKind.ArrowFunction: // `exports.f = function() {}` --> `export function f() {}` - return functionExpressionToDeclaration(name, modifiers, exported as FunctionExpression | ArrowFunction); + return functionExpressionToDeclaration(name, modifiers, exported as FunctionExpression | ArrowFunction, useSitesToUnqualify); case SyntaxKind.ClassExpression: // `exports.C = class {}` --> `export class C {}` - return classExpressionToDeclaration(name, modifiers, exported as ClassExpression); + return classExpressionToDeclaration(name, modifiers, exported as ClassExpression, useSitesToUnqualify); default: return exportConst(); } function exportConst() { // `exports.x = 0;` --> `export const x = 0;` - return makeConst(modifiers, factory.createIdentifier(name!), exported); // TODO: GH#18217 + return makeConst(modifiers, factory.createIdentifier(name!), replaceImportUseSites(exported, useSitesToUnqualify)); // TODO: GH#18217 + } + } + + function replaceImportUseSites(node: T, useSitesToUnqualify: ESMap | undefined): T; + function replaceImportUseSites(nodes: NodeArray, useSitesToUnqualify: ESMap | undefined): NodeArray; + function replaceImportUseSites(nodeOrNodes: T | NodeArray, useSitesToUnqualify: ESMap | undefined) { + if (!useSitesToUnqualify || !some(arrayFrom(useSitesToUnqualify.keys()), original => rangeContainsRange(nodeOrNodes, original))) { + return nodeOrNodes; + } + + return isArray(nodeOrNodes) + ? getSynthesizedDeepClonesWithReplacements(nodeOrNodes, /*includeTrivia*/ true, replaceNode) + : getSynthesizedDeepCloneWithReplacements(nodeOrNodes, /*includeTrivia*/ true, replaceNode); + + function replaceNode(original: Node) { + // We are replacing `mod.SomeExport` wih `SomeExport`, so we only need to look at PropertyAccessExpressions + if (original.kind === SyntaxKind.PropertyAccessExpression) { + const replacement = useSitesToUnqualify!.get(original); + // Remove entry from `useSitesToUnqualify` so the refactor knows it's taken care of by the parent statement we're replacing + useSitesToUnqualify!.delete(original); + return replacement; + } } } @@ -341,15 +396,13 @@ namespace ts.codefix { * May also make use `changes` to remove qualifiers at the use sites of imports, to change `mod.x` to `x`. */ function convertSingleImport( - file: SourceFile, name: BindingName, moduleSpecifier: StringLiteralLike, - changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget, quotePreference: QuotePreference, - ): readonly Node[] { + ): ConvertedImports { switch (name.kind) { case SyntaxKind.ObjectBindingPattern: { const importSpecifiers = mapAllOrFail(name.elements, e => @@ -359,7 +412,7 @@ namespace ts.codefix { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion : makeImportSpecifier(e.propertyName && (e.propertyName as Identifier).text, e.name.text)); if (importSpecifiers) { - return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, quotePreference)]; + return convertedImports([makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier, quotePreference)]); } } // falls through -- object destructuring has an interesting pattern and must be a variable declaration @@ -369,13 +422,13 @@ namespace ts.codefix { const [a, b, c] = x; */ const tmp = makeUniqueName(moduleSpecifierToValidIdentifier(moduleSpecifier.text, target), identifiers); - return [ + return convertedImports([ makeImport(factory.createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier, quotePreference), makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), factory.createIdentifier(tmp)), - ]; + ]); } case SyntaxKind.Identifier: - return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers, quotePreference); + return convertSingleIdentifierImport(name, moduleSpecifier, checker, identifiers, quotePreference); default: return Debug.assertNever(name, `Convert to ES6 module got invalid name kind ${(name as BindingName).kind}`); } @@ -385,12 +438,13 @@ 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, quotePreference: QuotePreference): readonly Node[] { + function convertSingleIdentifierImport(name: Identifier, moduleSpecifier: StringLiteralLike, checker: TypeChecker, identifiers: Identifiers, quotePreference: QuotePreference): ConvertedImports { const nameSymbol = checker.getSymbolAtLocation(name); // Maps from module property name to name actually used. (The same if there isn't shadowing.) const namedBindingsNames = new Map(); // True if there is some non-property use like `x()` or `f(x)`. let needDefaultImport = false; + let useSitesToUnqualify: ESMap | undefined; for (const use of identifiers.original.get(name.text)!) { if (checker.getSymbolAtLocation(use) !== nameSymbol || use === name) { @@ -407,7 +461,8 @@ namespace ts.codefix { idName = makeUniqueName(propertyName, identifiers); namedBindingsNames.set(propertyName, idName); } - changes.replaceNode(file, parent, factory.createIdentifier(idName)); + + (useSitesToUnqualify ??= new Map()).set(parent, factory.createIdentifier(idName)); } else { needDefaultImport = true; @@ -420,7 +475,10 @@ namespace ts.codefix { // If it was unused, ensure that we at least import *something*. needDefaultImport = true; } - return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, quotePreference)]; + return convertedImports( + [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier, quotePreference)], + useSitesToUnqualify + ); } // Identifiers helpers @@ -476,7 +534,7 @@ namespace ts.codefix { // Node helpers - function functionExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], fn: FunctionExpression | ArrowFunction | MethodDeclaration): FunctionDeclaration { + function functionExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], fn: FunctionExpression | ArrowFunction | MethodDeclaration, useSitesToUnqualify: ESMap | undefined): FunctionDeclaration { return factory.createFunctionDeclaration( getSynthesizedDeepClones(fn.decorators), // TODO: GH#19915 Don't think this is even legal. concatenate(additionalModifiers, getSynthesizedDeepClones(fn.modifiers)), @@ -485,17 +543,17 @@ namespace ts.codefix { getSynthesizedDeepClones(fn.typeParameters), getSynthesizedDeepClones(fn.parameters), getSynthesizedDeepClone(fn.type), - factory.converters.convertToFunctionBlock(getSynthesizedDeepClone(fn.body!))); + factory.converters.convertToFunctionBlock(replaceImportUseSites(fn.body!, useSitesToUnqualify))); } - function classExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], cls: ClassExpression): ClassDeclaration { + function classExpressionToDeclaration(name: string | undefined, additionalModifiers: readonly Modifier[], cls: ClassExpression, useSitesToUnqualify: ESMap | undefined): ClassDeclaration { return factory.createClassDeclaration( getSynthesizedDeepClones(cls.decorators), // TODO: GH#19915 Don't think this is even legal. concatenate(additionalModifiers, getSynthesizedDeepClones(cls.modifiers)), name, getSynthesizedDeepClones(cls.typeParameters), getSynthesizedDeepClones(cls.heritageClauses), - getSynthesizedDeepClones(cls.members)); + replaceImportUseSites(cls.members, useSitesToUnqualify)); } function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: StringLiteralLike, quotePreference: QuotePreference): ImportDeclaration { @@ -524,4 +582,16 @@ namespace ts.codefix { exportSpecifiers && factory.createNamedExports(exportSpecifiers), moduleSpecifier === undefined ? undefined : factory.createStringLiteral(moduleSpecifier)); } + + interface ConvertedImports { + newImports: readonly Node[]; + useSitesToUnqualify?: ESMap; + } + + function convertedImports(newImports: readonly Node[], useSitesToUnqualify?: ESMap): ConvertedImports { + return { + newImports, + useSitesToUnqualify + }; + } } diff --git a/src/services/utilities.ts b/src/services/utilities.ts index ee06e960550..294b8326ec0 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -2261,45 +2261,26 @@ namespace ts { return clone; } - export function getSynthesizedDeepCloneWithRenames(node: T, includeTrivia = true, renameMap?: ESMap, checker?: TypeChecker, callback?: (originalNode: Node, clone: Node) => any): T { - let clone; - if (renameMap && checker && isBindingElement(node) && isIdentifier(node.name) && isObjectBindingPattern(node.parent)) { - const symbol = checker.getSymbolAtLocation(node.name); - const renameInfo = symbol && renameMap.get(String(getSymbolId(symbol))); - - if (renameInfo && renameInfo.text !== (node.name || node.propertyName).getText()) { - clone = setOriginalNode( - factory.createBindingElement( - node.dotDotDotToken, - node.propertyName || node.name, - renameInfo, - node.initializer), - node); - } + export function getSynthesizedDeepCloneWithReplacements( + node: T, + includeTrivia: boolean, + replaceNode: (node: Node) => Node | undefined + ): T { + let clone = replaceNode(node); + if (clone) { + setOriginalNode(clone, node); } - else if (renameMap && checker && isIdentifier(node)) { - const symbol = checker.getSymbolAtLocation(node); - const renameInfo = symbol && renameMap.get(String(getSymbolId(symbol))); - - if (renameInfo) { - clone = setOriginalNode(factory.createIdentifier(renameInfo.text), node); - } - } - - if (!clone) { - clone = getSynthesizedDeepCloneWorker(node as NonNullable, renameMap, checker, callback); + else { + clone = getSynthesizedDeepCloneWorker(node as NonNullable, replaceNode); } if (clone && !includeTrivia) suppressLeadingAndTrailingTrivia(clone); - if (callback && clone) callback(node, clone); - return clone as T; } - - function getSynthesizedDeepCloneWorker(node: T, renameMap?: ESMap, checker?: TypeChecker, callback?: (originalNode: Node, clone: Node) => any): T { - const visited = (renameMap || checker || callback) ? - visitEachChild(node, wrapper, nullTransformationContext) : + function getSynthesizedDeepCloneWorker(node: T, replaceNode?: (node: Node) => Node | undefined): T { + const visited = replaceNode ? + visitEachChild(node, n => getSynthesizedDeepCloneWithReplacements(n, /*includeTrivia*/ true, replaceNode), nullTransformationContext) : visitEachChild(node, getSynthesizedDeepClone, nullTransformationContext); if (visited === node) { @@ -2316,10 +2297,6 @@ namespace ts { // would have made. (visited as Mutable).parent = undefined!; return visited; - - function wrapper(node: T) { - return getSynthesizedDeepCloneWithRenames(node, /*includeTrivia*/ true, renameMap, checker, callback); - } } export function getSynthesizedDeepClones(nodes: NodeArray, includeTrivia?: boolean): NodeArray; @@ -2328,6 +2305,14 @@ namespace ts { return nodes && factory.createNodeArray(nodes.map(n => getSynthesizedDeepClone(n, includeTrivia)), nodes.hasTrailingComma); } + export function getSynthesizedDeepClonesWithReplacements( + nodes: NodeArray, + includeTrivia: boolean, + replaceNode: (node: Node) => Node | undefined + ): NodeArray { + return factory.createNodeArray(nodes.map(n => getSynthesizedDeepCloneWithReplacements(n, includeTrivia, replaceNode)), nodes.hasTrailingComma); + } + /** * Sets EmitFlags to suppress leading and trailing trivia on the node. */ diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_renameWithinTransformedExports.ts b/tests/cases/fourslash/refactorConvertToEs6Module_renameWithinTransformedExports.ts new file mode 100644 index 00000000000..5908aedd002 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_renameWithinTransformedExports.ts @@ -0,0 +1,43 @@ +/// + +// @allowJs: true +// @checkJs: true +// @target: esnext + +// @Filename: test.js +//// var Q = require("q"); +//// +//// module.exports = { +//// fn: function () { +//// var deferred = Q.defer(); +//// }, +//// method() { +//// Q.defer(); +//// }, +//// Klass: class { +//// prop = Q.defer(); +//// method() { +//// Q.defer(); +//// } +//// } +//// } + +verify.codeFix({ + index: 2, + description: "Convert to ES6 module", + newFileContent: +`import { defer } from "q"; + +export function fn() { + var deferred = defer(); +} +export function method() { + defer(); +} +export class Klass { + prop = defer(); + method() { + defer(); + } +}`, +});