From a4b588d780e4fe6a9d9e046f75fe4f3c4fcb277e Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Fri, 3 Nov 2017 15:04:50 -0700 Subject: [PATCH 1/5] Add refactoring to convert CommonJS module to ES6 module --- src/compiler/binder.ts | 46 +- src/compiler/checker.ts | 15 +- src/compiler/core.ts | 25 +- src/compiler/diagnosticMessages.json | 4 + src/compiler/emitter.ts | 28 +- src/compiler/factory.ts | 4 +- src/compiler/types.ts | 8 +- src/compiler/utilities.ts | 4 +- src/harness/fourslash.ts | 11 +- src/services/codefixes/importFixes.ts | 4 +- src/services/refactors/convertToEs6Module.ts | 567 ++++++++++++++++++ src/services/refactors/extractSymbol.ts | 2 +- src/services/refactors/refactors.ts | 1 + src/services/refactors/useDefaultImport.ts | 4 +- src/services/utilities.ts | 4 + .../reference/api/tsserverlibrary.d.ts | 2 + tests/baselines/reference/api/typescript.d.ts | 2 + tests/cases/fourslash/fourslash.ts | 1 + ...refactorConvertToEs6Module_export_alias.ts | 20 + ...torConvertToEs6Module_export_dotDefault.ts | 19 + ...orConvertToEs6Module_export_invalidName.ts | 19 + ...vertToEs6Module_export_moduleDotExports.ts | 29 + ..._export_moduleDotExports_changesImports.ts | 26 + ...refactorConvertToEs6Module_export_named.ts | 19 + ...efactorConvertToEs6Module_export_object.ts | 25 + ...vertToEs6Module_export_object_shorthand.ts | 18 + ...torConvertToEs6Module_export_referenced.ts | 36 ++ ...vertToEs6Module_expressionToDeclaration.ts | 18 + ...tToEs6Module_import_arrayBindingPattern.ts | 15 + ...rtToEs6Module_import_includeDefaultUses.ts | 18 + ...Module_import_multipleUniqueIdentifiers.ts | 20 + ...ule_import_multipleVariableDeclarations.ts | 18 + ...s6Module_import_nameFromModuleSpecifier.ts | 21 + ...ule_import_objectBindingPattern_complex.ts | 15 + ...odule_import_objectBindingPattern_plain.ts | 14 + ...vertToEs6Module_import_onlyNamedImports.ts | 16 + ...onvertToEs6Module_import_propertyAccess.ts | 24 + ...ctorConvertToEs6Module_import_shadowing.ts | 18 + ...torConvertToEs6Module_import_sideEffect.ts | 16 + .../refactorConvertToEs6Module_triggers.ts | 13 + 40 files changed, 1111 insertions(+), 58 deletions(-) create mode 100644 src/services/refactors/convertToEs6Module.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_dotDefault.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_invalidName.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports_changesImports.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_named.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_object_shorthand.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_referenced.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_arrayBindingPattern.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_includeDefaultUses.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_multipleUniqueIdentifiers.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_multipleVariableDeclarations.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_nameFromModuleSpecifier.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_complex.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_plain.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_onlyNamedImports.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_shadowing.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_import_sideEffect.ts create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_triggers.ts diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index a0146740678df..b38a1e166a792 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2267,30 +2267,13 @@ namespace ts { declareSymbol(file.symbol.exports, file.symbol, node.left, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None); } - function isExportsOrModuleExportsOrAlias(node: Node): boolean { - return isExportsIdentifier(node) || - isModuleExportsPropertyAccessExpression(node) || - isIdentifier(node) && isNameOfExportsOrModuleExportsAliasDeclaration(node); - } - - function isNameOfExportsOrModuleExportsAliasDeclaration(node: Identifier): boolean { - const symbol = lookupSymbolForName(node.escapedText); - return symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && - symbol.valueDeclaration.initializer && isExportsOrModuleExportsOrAliasOrAssignment(symbol.valueDeclaration.initializer); - } - - function isExportsOrModuleExportsOrAliasOrAssignment(node: Node): boolean { - return isExportsOrModuleExportsOrAlias(node) || - (isAssignmentExpression(node, /*excludeCompoundAssignements*/ true) && (isExportsOrModuleExportsOrAliasOrAssignment(node.left) || isExportsOrModuleExportsOrAliasOrAssignment(node.right))); - } - function bindModuleExportsAssignment(node: BinaryExpression) { // A common practice in node modules is to set 'export = module.exports = {}', this ensures that 'exports' // is still pointing to 'module.exports'. // We do not want to consider this as 'export=' since a module can have only one of these. // Similarly we do not want to treat 'module.exports = exports' as an 'export='. const assignedExpression = getRightMostAssignedExpression(node.right); - if (isEmptyObjectLiteral(assignedExpression) || isExportsOrModuleExportsOrAlias(assignedExpression)) { + if (isEmptyObjectLiteral(assignedExpression) || container === file && isExportsOrModuleExportsOrAlias(file, assignedExpression)) { // Mark it as a module in case there are no other exports in the file setCommonJsModuleIndicator(node); return; @@ -2357,7 +2340,7 @@ namespace ts { leftSideOfAssignment.parent = node; target.parent = leftSideOfAssignment; - if (isNameOfExportsOrModuleExportsAliasDeclaration(target)) { + if (container === file && isNameOfExportsOrModuleExportsAliasDeclaration(file, target)) { // This can be an alias for the 'exports' or 'module.exports' names, e.g. // var util = module.exports; // util.property = function ... @@ -2370,7 +2353,7 @@ namespace ts { } function lookupSymbolForName(name: __String) { - return (container.symbol && container.symbol.exports && container.symbol.exports.get(name)) || (container.locals && container.locals.get(name)); + return lookupSymbolForNameWorker(container, name); } function bindPropertyAssignment(functionName: __String, propertyAccessExpression: PropertyAccessExpression, isPrototypeProperty: boolean) { @@ -2589,6 +2572,29 @@ namespace ts { } } + /* @internal */ + export function isExportsOrModuleExportsOrAlias(sourceFile: SourceFile, node: Expression): boolean { + return isExportsIdentifier(node) || + isModuleExportsPropertyAccessExpression(node) || + isIdentifier(node) && isNameOfExportsOrModuleExportsAliasDeclaration(sourceFile, node); + } + + function isNameOfExportsOrModuleExportsAliasDeclaration(sourceFile: SourceFile, node: Identifier): boolean { + const symbol = lookupSymbolForNameWorker(sourceFile, node.escapedText); + return symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) && + symbol.valueDeclaration.initializer && isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, symbol.valueDeclaration.initializer); + } + + function isExportsOrModuleExportsOrAliasOrAssignment(sourceFile: SourceFile, node: Expression): boolean { + return isExportsOrModuleExportsOrAlias(sourceFile, node) || + (isAssignmentExpression(node, /*excludeCompoundAssignements*/ true) && ( + isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, node.left) || isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, node.right))); + } + + function lookupSymbolForNameWorker(container: Node, name: __String): Symbol | undefined { + return (container.symbol && container.symbol.exports && container.symbol.exports.get(name)) || (container.locals && container.locals.get(name)); + } + /** * Computes the transform flags for a node, given the transform flags of its subtree * diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a9aaa77fa74c6..c034cebbac857 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -250,8 +250,8 @@ namespace ts { getSuggestionForNonexistentProperty: (node, type) => getSuggestionForNonexistentProperty(node, type), getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), getBaseConstraintOfType, - resolveName(name, location, meaning) { - return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false); + resolveName(name, location, meaning, includeGlobals) { + return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, includeGlobals); }, getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()), }; @@ -909,8 +909,9 @@ namespace ts { nameNotFoundMessage: DiagnosticMessage | undefined, nameArg: __String | Identifier, isUse: boolean, + includeGlobals = true, suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol { - return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, getSymbol, suggestedNameNotFoundMessage); + return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, includeGlobals, getSymbol, suggestedNameNotFoundMessage); } function resolveNameHelper( @@ -920,6 +921,7 @@ namespace ts { nameNotFoundMessage: DiagnosticMessage, nameArg: __String | Identifier, isUse: boolean, + includeGlobals: boolean, lookup: typeof getSymbol, suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol { const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location @@ -1166,7 +1168,9 @@ namespace ts { } } - result = lookup(globals, name, meaning); + if (includeGlobals) { + result = lookup(globals, name, meaning); + } } if (!result) { @@ -11250,6 +11254,7 @@ namespace ts { Diagnostics.Cannot_find_name_0, node, !isWriteOnlyAccess(node), + /*includeGlobals*/ true, Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol; } return links.resolvedSymbol; @@ -15352,7 +15357,7 @@ namespace ts { function getSuggestionForNonexistentSymbol(location: Node, outerName: __String, meaning: SymbolFlags): string { Debug.assert(outerName !== undefined, "outername should always be defined"); - const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, (symbols, name, meaning) => { + const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*includeGlobals*/ true, (symbols, name, meaning) => { Debug.assertEqual(outerName, name, "name should equal outerName"); const symbol = getSymbol(symbols, name, meaning); // Sometimes the symbol is found when location is a return type of a function: `typeof x` and `x` is declared in the body of the function diff --git a/src/compiler/core.ts b/src/compiler/core.ts index f1322f109ccfe..73beff3a4ef6d 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -394,6 +394,16 @@ namespace ts { return result; } + export function mapIter(iter: Iterator, mapFn: (x: T) => U): U[] { + const result: U[] = []; + while (true) { + const { value, done } = iter.next(); + if (done) break; + result.push(mapFn(value)); + } + return result; + } + // Maps from T to T and avoids allocation if all elements map to themselves export function sameMap(array: T[], f: (x: T, i: number) => T): T[]; export function sameMap(array: ReadonlyArray, f: (x: T, i: number) => T): ReadonlyArray; @@ -515,12 +525,23 @@ namespace ts { return result || array; } + export function mapAllOrFail(array: ReadonlyArray, mapFn: (x: T, i: number) => U | undefined): U[] | undefined { + const result: U[] = []; + for (let i = 0; i < array.length; i++) { + const mapped = mapFn(array[i], i); + if (!mapped) { + return undefined; + } + result.push(mapped); + } + return result; + } + export function mapDefined(array: ReadonlyArray | undefined, mapFn: (x: T, i: number) => U | undefined): U[] { const result: U[] = []; if (array) { for (let i = 0; i < array.length; i++) { - const item = array[i]; - const mapped = mapFn(item, i); + const mapped = mapFn(array[i], i); if (mapped !== undefined) { result.push(mapped); } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 964264ff6e8e4..cb9213da047db 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3809,5 +3809,9 @@ "Import * as '{0}' from \"{1}\".": { "category": "Message", "code": 95016 + }, + "Convert to ES6 module": { + "category": "Message", + "code": 95017 } } diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 7a1d88d3bfd71..909dd0feedeb4 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1194,27 +1194,15 @@ namespace ts { // function emitObjectBindingPattern(node: ObjectBindingPattern) { - const elements = node.elements; - if (elements.length === 0) { - write("{}"); - } - else { - write("{"); - emitList(node, elements, ListFormat.ObjectBindingPatternElements); - write("}"); - } + write("{"); + emitList(node, node.elements, ListFormat.ObjectBindingPatternElements); + write("}"); } function emitArrayBindingPattern(node: ArrayBindingPattern) { - const elements = node.elements; - if (elements.length === 0) { - write("[]"); - } - else { - write("["); - emitList(node, node.elements, ListFormat.ArrayBindingPatternElements); - write("]"); - } + write("["); + emitList(node, node.elements, ListFormat.ArrayBindingPatternElements); + write("]"); } function emitBindingElement(node: BindingElement) { @@ -3167,8 +3155,8 @@ namespace ts { TupleTypeElements = CommaDelimited | SpaceBetweenSiblings | SingleLine | Indented, UnionTypeConstituents = BarDelimited | SpaceBetweenSiblings | SingleLine, IntersectionTypeConstituents = AmpersandDelimited | SpaceBetweenSiblings | SingleLine, - ObjectBindingPatternElements = SingleLine | AllowTrailingComma | SpaceBetweenBraces | CommaDelimited | SpaceBetweenSiblings, - ArrayBindingPatternElements = SingleLine | AllowTrailingComma | CommaDelimited | SpaceBetweenSiblings, + ObjectBindingPatternElements = SingleLine | AllowTrailingComma | SpaceBetweenBraces | CommaDelimited | SpaceBetweenSiblings | NoSpaceIfEmpty, + ArrayBindingPatternElements = SingleLine | AllowTrailingComma | CommaDelimited | SpaceBetweenSiblings | NoSpaceIfEmpty, ObjectLiteralExpressionProperties = PreserveLines | CommaDelimited | SpaceBetweenSiblings | SpaceBetweenBraces | Indented | Braces | NoSpaceIfEmpty, ArrayLiteralExpressionElements = PreserveLines | CommaDelimited | SpaceBetweenSiblings | AllowTrailingComma | Indented | SquareBrackets, CommaListElements = CommaDelimited | SpaceBetweenSiblings | SingleLine, diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index e8dfcbc1e94a8..e71b965c526fc 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -101,7 +101,7 @@ namespace ts { return node; } - function createLiteralFromNode(sourceNode: StringLiteral | NumericLiteral | Identifier): StringLiteral { + function createLiteralFromNode(sourceNode: StringLiteralLike | NumericLiteral | Identifier): StringLiteral { const node = createStringLiteral(getTextOfIdentifierOrLiteral(sourceNode)); node.textSourceNode = sourceNode; return node; @@ -3624,7 +3624,7 @@ namespace ts { return qualifiedName; } - export function convertToFunctionBody(node: ConciseBody, multiLine?: boolean) { + export function convertToFunctionBody(node: ConciseBody, multiLine?: boolean): Block { return isBlock(node) ? node : setTextRange(createBlock([setTextRange(createReturn(node), node)], multiLine), node); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index bb62bead00b80..39a8253ef5a3d 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1074,11 +1074,13 @@ namespace ts { export interface StringLiteral extends LiteralExpression { kind: SyntaxKind.StringLiteral; - /* @internal */ textSourceNode?: Identifier | StringLiteral | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms). + /* @internal */ textSourceNode?: Identifier | StringLiteralLike | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms). /** Note: this is only set when synthesizing a node, not during parsing. */ /* @internal */ singleQuote?: boolean; } + /* @internal */ export type StringLiteralLike = StringLiteral | NoSubstitutionTemplateLiteral; + // Note: 'brands' in our syntax nodes serve to give us a small amount of nominal typing. // Consider 'Expression'. Without the brand, 'Expression' is actually no different // (structurally) than 'Node'. Because of this you can pass any Node to a function that @@ -1424,6 +1426,7 @@ namespace ts { kind: SyntaxKind.ArrowFunction; equalsGreaterThanToken: EqualsGreaterThanToken; body: ConciseBody; + name: never; } // The text property of a LiteralExpression stores the interpreted value of the literal in text form. For a StringLiteral, @@ -2075,6 +2078,7 @@ namespace ts { export interface ExportDeclaration extends DeclarationStatement { kind: SyntaxKind.ExportDeclaration; parent?: SourceFile | ModuleBlock; + /** Will not be assigned in the case of `export * from "foo";` */ exportClause?: NamedExports; /** If this is not a StringLiteral it will be a grammar error. */ moduleSpecifier?: Expression; @@ -2779,7 +2783,7 @@ namespace ts { */ /* @internal */ isArrayLikeType(type: Type): boolean; /* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray): Symbol[]; - /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined; + /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, includeGlobals: boolean): Symbol | undefined; /* @internal */ getJsxNamespace(): string; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e67635cba0411..20b94976a0bd2 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1385,6 +1385,8 @@ namespace ts { * exactly one argument (of the form 'require("name")'). * This function does not test if the node is in a JavaScript file or not. */ + export function isRequireCall(callExpression: Node, checkArgumentIsStringLiteral: true): callExpression is CallExpression & { expression: Identifier, arguments: [StringLiteralLike] }; + export function isRequireCall(callExpression: Node, checkArgumentIsStringLiteral: boolean): callExpression is CallExpression; export function isRequireCall(callExpression: Node, checkArgumentIsStringLiteral: boolean): callExpression is CallExpression { if (callExpression.kind !== SyntaxKind.CallExpression) { return false; @@ -1422,7 +1424,7 @@ namespace ts { return false; } - export function getRightMostAssignedExpression(node: Node) { + export function getRightMostAssignedExpression(node: Expression): Expression { while (isAssignmentExpression(node, /*excludeCompoundAssignements*/ true)) { node = node.right; } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 2687a660ca8e4..6fd99336ad35c 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -448,7 +448,7 @@ namespace FourSlash { const ranges = this.getRanges(); assert(ranges.length); for (const range of ranges) { - this.goToRangeStart(range); + this.selectRange(range); action(); } } @@ -476,6 +476,11 @@ namespace FourSlash { this.selectionEnd = end.position; } + public selectRange(range: Range): void { + this.goToRangeStart(range); + this.selectionEnd = range.end; + } + public moveCaretRight(count = 1) { this.currentCaretPosition += count; this.currentCaretPosition = Math.min(this.currentCaretPosition, this.getFileContent(this.activeFile.fileName).length); @@ -3776,6 +3781,10 @@ namespace FourSlashInterface { public select(startMarker: string, endMarker: string) { this.state.select(startMarker, endMarker); } + + public selectRange(range: FourSlash.Range): void { + this.state.selectRange(range); + } } export class VerifyNegatable { diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 838e19fa35de1..e85aaf7037810 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -658,7 +658,7 @@ namespace ts.codefix { } else if (isJsxOpeningLikeElement(symbolToken.parent) && symbolToken.parent.tagName === symbolToken) { // The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`. - symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), symbolToken.parent.tagName, SymbolFlags.Value)); + symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), symbolToken.parent.tagName, SymbolFlags.Value, /*includeGlobals*/ true)); symbolName = symbol.name; } else { @@ -737,7 +737,7 @@ namespace ts.codefix { return moduleSpecifierToValidIdentifier(removeFileExtension(getBaseFileName(moduleSymbol.name)), target); } - function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget): string { + export function moduleSpecifierToValidIdentifier(moduleSpecifier: string, target: ScriptTarget): string { let res = ""; let lastCharWasValid = true; const firstCharCode = moduleSpecifier.charCodeAt(0); diff --git a/src/services/refactors/convertToEs6Module.ts b/src/services/refactors/convertToEs6Module.ts new file mode 100644 index 0000000000000..c728f8f9910e8 --- /dev/null +++ b/src/services/refactors/convertToEs6Module.ts @@ -0,0 +1,567 @@ +/* @internal */ +namespace ts.refactor { + const actionName = "Convert to ES6 module"; + + const convertToEs6Module: Refactor = { + name: actionName, + description: getLocaleSpecificMessage(Diagnostics.Convert_to_ES6_module), + getEditsForAction, + getAvailableActions, + }; + + registerRefactor(convertToEs6Module); + + function getAvailableActions(context: RefactorContext): ApplicableRefactorInfo[] | undefined { + const { file, startPosition } = context; + if (!isSourceFileJavaScript(file) || !file.commonJsModuleIndicator) { + return undefined; + } + + const node = getTokenAtPosition(file, startPosition, /*includeJsDocComment*/ false); + return !isAtTriggerLocation(file, node) ? undefined : [ + { + name: convertToEs6Module.name, + description: convertToEs6Module.description, + actions: [ + { + description: convertToEs6Module.description, + name: actionName, + }, + ], + }, + ]; + } + + function isAtTriggerLocation(sourceFile: SourceFile, node: Node, onSecondTry = false): boolean { + switch (node.kind) { + case SyntaxKind.CallExpression: + return isAtTopLevelRequire(node as CallExpression); + case SyntaxKind.PropertyAccessExpression: + return isExportsOrModuleExportsOrAlias(sourceFile, node as PropertyAccessExpression) + || isExportsOrModuleExportsOrAlias(sourceFile, (node as PropertyAccessExpression).expression); + case SyntaxKind.VariableDeclarationList: + const decl = (node as VariableDeclarationList).declarations[0]; + return isExportsOrModuleExportsOrAlias(sourceFile, decl.initializer); + case SyntaxKind.VariableDeclaration: + return isExportsOrModuleExportsOrAlias(sourceFile, (node as VariableDeclaration).initializer); + default: + return isExpression(node) && isExportsOrModuleExportsOrAlias(sourceFile, node) + || !onSecondTry && isAtTriggerLocation(sourceFile, node.parent, /*onSecondTry*/ true); + } + } + + function isAtTopLevelRequire(call: CallExpression): boolean { + if (!isRequireCall(call, /*checkArgumentIsStringLiteral*/ true)) { + return false; + } + const { parent: propAccess } = call; + const varDecl = isPropertyAccessExpression(propAccess) ? propAccess.parent : propAccess; + if (isExpressionStatement(varDecl) && isSourceFile(varDecl.parent)) { // `require("x");` as a statement + return true; + } + if (!isVariableDeclaration(varDecl)) { + return false; + } + const { parent: varDeclList } = varDecl; + if (varDeclList.kind !== SyntaxKind.VariableDeclarationList) { + return false; + } + const { parent: varStatement } = varDeclList; + return varStatement.kind === SyntaxKind.VariableStatement && varStatement.parent.kind === SyntaxKind.SourceFile; + } + + function getEditsForAction(context: RefactorContext, _actionName: string): RefactorEditInfo | undefined { + Debug.assertEqual(actionName, _actionName); + const { file, program } = context; + Debug.assert(isSourceFileJavaScript(file)); + const edits = textChanges.ChangeTracker.with(context, changes => { + const moduleExportsChangedToDefault = convertFileToEs6Module(file, program.getTypeChecker(), changes, program.getCompilerOptions().target); + if (moduleExportsChangedToDefault) { + for (const importingFile of program.getSourceFiles()) { + fixImportOfModuleExports(importingFile, file, changes); + } + } + }); + return { edits, renameFilename: undefined, renameLocation: undefined }; + } + + function fixImportOfModuleExports(importingFile: ts.SourceFile, exportingFile: ts.SourceFile, changes: textChanges.ChangeTracker) { + for (const moduleSpecifier of importingFile.imports) { + const imported = getResolvedModule(importingFile, moduleSpecifier.text); + if (!imported || imported.resolvedFileName !== exportingFile.fileName) { + continue; + } + + const { parent } = moduleSpecifier; + switch (parent.kind) { + case SyntaxKind.ExternalModuleReference: { + const importEq = (parent as ExternalModuleReference).parent; + changes.replaceNode(importingFile, importEq, makeImport(importEq.name, /*namedImports*/ undefined, moduleSpecifier.text)); + break; + } + case SyntaxKind.CallExpression: { + const call = parent as CallExpression; + if (isRequireCall(call, /*checkArgumentIsStringLiteral*/ false)) { + changes.replaceNode(importingFile, parent, createPropertyAccess(getSynthesizedDeepClone(call), "default")); + } + break; + } + } + } + } + + /** @returns Whether we converted a `module.exports =` to a default export. */ + function convertFileToEs6Module(sourceFile: SourceFile, checker: TypeChecker, changes: textChanges.ChangeTracker, target: ScriptTarget): 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); + moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged; + } + return moduleExportsChangedToDefault; + } + + /** + * Contains an entry for each renamed export. + * This is necessary because `exports.x = 0;` does not declare a local variable. + * Converting this to `export const x = 0;` would declare a local, so we must be careful to avoid shadowing. + * If there would be shadowing at either the declaration or at any reference to `exports.x` (now just `x`), we must conver to: + * const _x = 0; + * export { _x as x }; + * This conversion also must place if the exported name is not a valid identifier, e.g. `exports.class = 0;`. + */ + type ExportRenames = ReadonlyMap; + + function collectExportRenames(sourceFile: SourceFile, checker: TypeChecker, identifiers: Identifiers): ExportRenames { + const res = createMap(); + forEachExportReference(sourceFile, node => { + const { text, originalKeywordKind } = node.name; + if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind) + || checker.resolveName(node.name.text, node, SymbolFlags.Value, /*includeGlobals*/ false))) { + res.set(text, makeUniqueName(`_${text}`, identifiers)); + } + }); + return res; + } + + function convertExportsAccesses(sourceFile: SourceFile, exports: ExportRenames, changes: textChanges.ChangeTracker): void { + forEachExportReference(sourceFile, (node, isAssignment) => { + if (isAssignment) { + return; + } + const { text } = node.name; + changes.replaceNode(sourceFile, node, createIdentifier(exports.get(text) || text)); + }); + } + + function forEachExportReference(sourceFile: SourceFile, cb: (node: PropertyAccessExpression, isAssignment: boolean) => void): void { + sourceFile.forEachChild(function recur(node) { + if (isPropertyAccessExpression(node) && isExportsOrModuleExportsOrAlias(sourceFile, node.expression)) { + const { parent } = node; + cb(node, isBinaryExpression(parent) && parent.left === node && parent.operatorToken.kind === SyntaxKind.EqualsToken); + } + node.forEachChild(recur); + }); + } + + /** 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 { + switch (statement.kind) { + case SyntaxKind.VariableStatement: + convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target); + return false; + case SyntaxKind.ExpressionStatement: { + const { expression } = statement as ExpressionStatement; + switch (expression.kind) { + case SyntaxKind.CallExpression: { + if (isRequireCall(expression, /*checkArgumentIsStringLiteral*/ true)) { + // For side-effecting require() call, just make a side-effecting import. + changes.replaceNode(sourceFile, statement, makeImport(/*name*/ undefined, /*namedImports*/ undefined, expression.arguments[0].text)); + } + return false; + } + case SyntaxKind.BinaryExpression: { + const { left, operatorToken, right } = expression as BinaryExpression; + return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, statement as ExpressionStatement, left, right, changes, exports); + } + } + } + // falls through + default: + return false; + } + } + + function convertVariableStatement(sourceFile: SourceFile, statement: VariableStatement, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget): void { + const { declarationList } = statement as VariableStatement; + let foundImport = false; + const newNodes = flatMap(declarationList.declarations, decl => { + const { name, initializer } = decl; + if (isExportsOrModuleExportsOrAlias(sourceFile, initializer)) { + // `const alias = module.exports;` can be removed. + foundImport = true; + return []; + } + if (isRequireCall(initializer, /*checkArgumentIsStringLiteral*/ true)) { + foundImport = true; + return convertSingleImport(sourceFile, name, initializer.arguments[0].text, changes, checker, identifiers, target); + } + else if (isPropertyAccessExpression(initializer) && isRequireCall(initializer.expression, /*checkArgumentIsStringLiteral*/ true)) { + foundImport = true; + return convertPropertyAccessImport(name, initializer.name.text, initializer.expression.arguments[0].text, identifiers); + } + else { + // Move it out to its own variable statement. + return createVariableStatement(/*modifiers*/ undefined, createVariableDeclarationList([decl], declarationList.flags)); + } + }); + if (foundImport) { + // useNonAdjustedEndPosition to ensure we don't eat the newline after the statement. + changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true }); + } + } + + /** Converts `const name = require("moduleSpecifier").propertyName` */ + function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: string, identifiers: Identifiers): 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), + 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)]; + default: + Debug.assertNever(name); + } + } + + function convertAssignment( + sourceFile: SourceFile, + statement: ExpressionStatement, + left: Expression, + right: Expression, + changes: textChanges.ChangeTracker, + exports: ExportRenames, + ): ModuleExportsChanged { + if (!isPropertyAccessExpression(left)) { + return false; + } + + if (isExportsOrModuleExportsOrAlias(sourceFile, left)) { + if (isExportsOrModuleExportsOrAlias(sourceFile, right)) { + // `const alias = module.exports;` or `module.exports = alias;` can be removed. + changes.deleteNode(sourceFile, statement); + } + else { + let newNodes = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right) : undefined; + let changedToDefaultExport = false; + if (!newNodes) { + ([newNodes, changedToDefaultExport] = convertModuleExportsToExportDefault(right)); + } + changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true }); + return changedToDefaultExport; + } + } + else if (isExportsOrModuleExportsOrAlias(sourceFile, left.expression)) { + convertNamedExport(sourceFile, statement, left.name, right, changes, exports); + } + + return false; + } + + /** + * 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): ReadonlyArray | undefined { + return mapAllOrFail(object.properties, prop => { + switch (prop.kind) { + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + // TODO: Maybe we should handle this? See fourslash test `refactorConvertToEs6Module_export_object_shorthand.ts`. + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.SpreadAssignment: + return undefined; + case SyntaxKind.PropertyAssignment: { + const { name, initializer } = prop as PropertyAssignment; + return !isIdentifier(name) ? undefined : convertExportsDotXEquals(name.text, initializer); + } + case SyntaxKind.MethodDeclaration: { + const m = prop as MethodDeclaration; + return !isIdentifier(m.name) ? undefined : functionExpressionToDeclaration(m.name.text, [createToken(SyntaxKind.ExportKeyword)], m); + } + default: + Debug.assertNever(prop); + } + }); + } + + function convertNamedExport( + sourceFile: SourceFile, + statement: Statement, + propertyName: Identifier, + right: Expression, + changes: textChanges.ChangeTracker, + exports: ExportRenames, + ): void { + // If "originalKeywordKind" was set, this is e.g. `exports. + const { text } = propertyName; + const rename = exports.get(text); + if (rename !== undefined) { + /* + const _class = 0; + export { _class as class }; + */ + const newNodes = [ + makeConst(/*modifiers*/ undefined, rename, right), + makeExportDeclaration([createExportSpecifier(rename, text)]), + ]; + changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true }); + } + else { + changes.replaceNode(sourceFile, statement, convertExportsDotXEquals(text, right), { useNonAdjustedEndPosition: true }); + } + } + + function convertModuleExportsToExportDefault(exported: Expression): [ReadonlyArray, ModuleExportsChanged] { + const modifiers = [createToken(SyntaxKind.ExportKeyword), createToken(SyntaxKind.DefaultKeyword)]; + switch (exported.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: { + // `module.exports = function f() {}` --> `export default function f() {}` + const fn = exported as FunctionExpression | ArrowFunction; + return [[functionExpressionToDeclaration(fn.name && fn.name.text, modifiers, fn)], true]; + } + case SyntaxKind.ClassExpression: { + // `module.exports = class C {}` --> `export default class C {}` + const cls = exported as ClassExpression; + return [[classExpressionToDeclaration(cls.name && cls.name.text, modifiers, cls)], true]; + } + case SyntaxKind.CallExpression: + if (isRequireCall(exported, /*checkArguementIsStringLiteral*/ true)) { + // `module.exports = require("x");` ==> `export * from "x"; export { default } from "x";` + const moduleSpecifier = exported.arguments[0].text; + const newNodes = [ + makeExportDeclaration(/*exportClause*/ undefined, moduleSpecifier), + makeExportDeclaration([createExportSpecifier(/*propertyName*/ undefined, "default")], moduleSpecifier), + ]; + return [newNodes, false]; + } + // falls through + default: + // `module.exports = 0;` --> `export default 0;` + return [[createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportEquals*/ false, exported)], true]; + } + } + + function convertExportsDotXEquals(name: string | undefined, exported: Expression): Statement { + const modifiers = [createToken(SyntaxKind.ExportKeyword)]; + switch (exported.kind) { + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + // `exports.f = function() {}` --> `export function f() {}` + return functionExpressionToDeclaration(name, modifiers, exported as FunctionExpression | ArrowFunction); + case SyntaxKind.ClassExpression: + // `exports.C = class {}` --> `export class C {}` + return classExpressionToDeclaration(name, modifiers, exported as ClassExpression); + default: + // `exports.x = 0;` --> `export const x = 0;` + return makeConst(modifiers, createIdentifier(name), exported); + } + } + + /** + * Converts `const <> = require("x");`. + * Returns nodes that will replace the variable declaration for the commonjs import. + * 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: string, + changes: textChanges.ChangeTracker, + checker: TypeChecker, + identifiers: Identifiers, + target: ScriptTarget, + ): ReadonlyArray { + switch (name.kind) { + case SyntaxKind.ObjectBindingPattern: { + const importSpecifiers = mapAllOrFail(name.elements, e => + e.dotDotDotToken || e.initializer || e.propertyName && !isIdentifier(e.propertyName) || !isIdentifier(e.name) + ? undefined + : makeImportSpecifier(e.propertyName && (e.propertyName as Identifier).text, e.name.text)); + if (importSpecifiers) { + return [makeImport(/*name*/ undefined, importSpecifiers, moduleSpecifier)]; + } + } + // falls through -- object destructuring has an interesting pattern and must be a variable declaration + case SyntaxKind.ArrayBindingPattern: { + /* + import x from "x"; + const [a, b, c] = x; + */ + const tmp = makeUniqueName(codefix.moduleSpecifierToValidIdentifier(moduleSpecifier, target), identifiers); + return [ + makeImport(createIdentifier(tmp), /*namedImports*/ undefined, moduleSpecifier), + makeConst(/*modifiers*/ undefined, getSynthesizedDeepClone(name), createIdentifier(tmp)), + ]; + } + case SyntaxKind.Identifier: + return convertSingleIdentifierImport(file, name, moduleSpecifier, changes, checker, identifiers); + default: + Debug.assertNever(name); + } + } + + /** + * 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: string, changes: textChanges.ChangeTracker, checker: TypeChecker, identifiers: Identifiers): 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(); + // True if there is some non-property use like `x()` or `f(x)`. + let needDefaultImport = false; + + for (const use of identifiers.original.get(name.text)) { + if (checker.getSymbolAtLocation(use) !== nameSymbol || use === name) { + // This was a use of a different symbol with the same name, due to shadowing. Ignore. + continue; + } + + const { parent } = use; + if (isPropertyAccessExpression(parent)) { + const { expression, name: { text: propertyName } } = parent; + Debug.assert(expression === use); // Else shouldn't have been in `collectIdentifiers` + let idName = namedBindingsNames.get(propertyName); + if (idName === undefined) { + idName = makeUniqueName(propertyName, identifiers); + namedBindingsNames.set(propertyName, idName); + } + changes.replaceNode(file, parent, createIdentifier(idName)); + } + else { + needDefaultImport = true; + } + } + + const namedBindings = namedBindingsNames.size === 0 ? undefined : mapIter(namedBindingsNames.entries(), ([propertyName, idName]) => + createImportSpecifier(propertyName === idName ? undefined : createIdentifier(propertyName), createIdentifier(idName))); + if (!namedBindings) { + // If it was unused, ensure that we at least import *something*. + needDefaultImport = true; + } + return [makeImport(needDefaultImport ? getSynthesizedDeepClone(name) : undefined, namedBindings, moduleSpecifier)]; + } + + // Identifiers helpers + + function makeUniqueName(name: string, identifiers: Identifiers): string { + while (identifiers.original.has(name) || identifiers.additional.has(name)) { + name = `_${name}`; + } + identifiers.additional.set(name, true); + return name; + } + + /** + * Helps us create unique identifiers. + * `original` refers to the local variable names in the original source file. + * `additional` is any new unique identifiers we've generated. (e.g., we'll generate `_x`, then `__x`.) + */ + interface Identifiers { + readonly original: FreeIdentifiers; + // Additional identifiers we've added. Mutable! + readonly additional: Map; + } + + type FreeIdentifiers = ReadonlyMap>; + function collectFreeIdentifiers(file: SourceFile): FreeIdentifiers { + const map = createMultiMap(); + file.forEachChild(function recur(node) { + if (isIdentifier(node) && isFreeIdentifier(node)) { + map.add(node.text, node); + } + node.forEachChild(recur); + }); + return map; + } + + function isFreeIdentifier(node: Identifier): boolean { + const { parent } = node; + switch (parent.kind) { + case SyntaxKind.PropertyAccessExpression: + return (parent as PropertyAccessExpression).name !== node; + case SyntaxKind.BindingElement: + return (parent as BindingElement).propertyName !== node; + default: + return true; + } + } + + // Node helpers + + function functionExpressionToDeclaration(name: string | undefined, additionalModifiers: ReadonlyArray, fn: FunctionExpression | ArrowFunction | MethodDeclaration): FunctionDeclaration { + return createFunctionDeclaration( + getSynthesizedDeepClones(fn.decorators), // TODO: GH#19915 Don't think this is even legal. + concatenate(additionalModifiers, getSynthesizedDeepClones(fn.modifiers)), + getSynthesizedDeepClone(fn.asteriskToken), + name, + getSynthesizedDeepClones(fn.typeParameters), + getSynthesizedDeepClones(fn.parameters), + getSynthesizedDeepClone(fn.type), + convertToFunctionBody(getSynthesizedDeepClone(fn.body))); + } + + function classExpressionToDeclaration(name: string | undefined, additionalModifiers: ReadonlyArray, cls: ClassExpression): ClassDeclaration { + return 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)); + } + + function makeSingleImport(localName: string, propertyName: string, moduleSpecifier: string): ImportDeclaration { + return propertyName === "default" + ? makeImport(createIdentifier(localName), /*namedImports*/ undefined, moduleSpecifier) + : makeImport(/*name*/ undefined, [makeImportSpecifier(propertyName, localName)], moduleSpecifier); + } + + function makeImport(name: Identifier | undefined, namedImports: ReadonlyArray, moduleSpecifier: string): ImportDeclaration { + const importClause = (name || namedImports) && createImportClause(name, namedImports && createNamedImports(namedImports)); + return createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifier)); + } + + function makeImportSpecifier(propertyName: string | undefined, name: string): ImportSpecifier { + return createImportSpecifier(propertyName !== undefined && propertyName !== name ? createIdentifier(propertyName) : undefined, createIdentifier(name)); + } + + function makeConst(modifiers: ReadonlyArray | undefined, name: string | BindingName, init: Expression): VariableStatement { + return createVariableStatement( + modifiers, + createVariableDeclarationList( + [createVariableDeclaration(name, /*type*/ undefined, init)], + NodeFlags.Const)); + } + + function makeExportDeclaration(exportSpecifiers: ExportSpecifier[] | undefined, moduleSpecifier?: string): ExportDeclaration { + return createExportDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + exportSpecifiers && createNamedExports(exportSpecifiers), + moduleSpecifier === undefined ? undefined : createLiteral(moduleSpecifier)); + } +} diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index f1a461b0088fc..881184d23ceec 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -1701,7 +1701,7 @@ namespace ts.refactor.extractSymbol { } for (let i = 0; i < scopes.length; i++) { const scope = scopes[i]; - const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags); + const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags, /*includeGlobals*/ true); if (resolvedSymbol === symbol) { continue; } diff --git a/src/services/refactors/refactors.ts b/src/services/refactors/refactors.ts index 3858b1987434e..8b4561700d554 100644 --- a/src/services/refactors/refactors.ts +++ b/src/services/refactors/refactors.ts @@ -1,5 +1,6 @@ /// /// +/// /// /// /// diff --git a/src/services/refactors/useDefaultImport.ts b/src/services/refactors/useDefaultImport.ts index 56faf082a4952..a103168f67b35 100644 --- a/src/services/refactors/useDefaultImport.ts +++ b/src/services/refactors/useDefaultImport.ts @@ -23,7 +23,7 @@ namespace ts.refactor.installTypesForPackage { return undefined; } - const module = ts.getResolvedModule(file, importInfo.moduleSpecifier.text); + const module = getResolvedModule(file, importInfo.moduleSpecifier.text); const resolvedFile = program.getSourceFile(module.resolvedFileName); if (!(resolvedFile.externalModuleIndicator && isExportAssignment(resolvedFile.externalModuleIndicator) && resolvedFile.externalModuleIndicator.isExportEquals)) { return undefined; @@ -52,7 +52,7 @@ namespace ts.refactor.installTypesForPackage { } const { importStatement, name, moduleSpecifier } = importInfo; const newImportClause = createImportClause(name, /*namedBindings*/ undefined); - const newImportStatement = ts.createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newImportClause, moduleSpecifier); + const newImportStatement = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, newImportClause, moduleSpecifier); return { edits: textChanges.ChangeTracker.with(context, t => t.replaceNode(file, importStatement, newImportStatement)), renameFilename: undefined, diff --git a/src/services/utilities.ts b/src/services/utilities.ts index cf71118292a90..5b10ca1788b79 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1400,6 +1400,10 @@ namespace ts { return visited; } + export function getSynthesizedDeepClones(nodes: NodeArray | undefined): NodeArray | undefined { + return nodes && createNodeArray(nodes.map(getSynthesizedDeepClone), nodes.hasTrailingComma); + } + /** * Sets EmitFlags to suppress leading and trailing trivia on the node. */ diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 78e8a377ec02f..a48f0152b088a 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -900,6 +900,7 @@ declare namespace ts { kind: SyntaxKind.ArrowFunction; equalsGreaterThanToken: EqualsGreaterThanToken; body: ConciseBody; + name: never; } interface LiteralLikeNode extends Node { text: string; @@ -1361,6 +1362,7 @@ declare namespace ts { interface ExportDeclaration extends DeclarationStatement { kind: SyntaxKind.ExportDeclaration; parent?: SourceFile | ModuleBlock; + /** Will not be assigned in the case of `export * from "foo";` */ exportClause?: NamedExports; /** If this is not a StringLiteral it will be a grammar error. */ moduleSpecifier?: Expression; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 3344193d0cee7..f8a1965b14a39 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -900,6 +900,7 @@ declare namespace ts { kind: SyntaxKind.ArrowFunction; equalsGreaterThanToken: EqualsGreaterThanToken; body: ConciseBody; + name: never; } interface LiteralLikeNode extends Node { text: string; @@ -1361,6 +1362,7 @@ declare namespace ts { interface ExportDeclaration extends DeclarationStatement { kind: SyntaxKind.ExportDeclaration; parent?: SourceFile | ModuleBlock; + /** Will not be assigned in the case of `export * from "foo";` */ exportClause?: NamedExports; /** If this is not a StringLiteral it will be a grammar error. */ moduleSpecifier?: Expression; diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index a0ee392a82eba..7aca68c7a95ef 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -133,6 +133,7 @@ declare namespace FourSlashInterface { file(index: number, content?: string, scriptKindName?: string): any; file(name: string, content?: string, scriptKindName?: string): any; select(startMarker: string, endMarker: string): void; + selectRange(range: Range): void; } class verifyNegatable { private negative; diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts new file mode 100644 index 0000000000000..9adf33be1d3d7 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts @@ -0,0 +1,20 @@ +/// + +// Test that we leave it alone if the name is a keyword. + +// @allowJs: true + +// @Filename: /a.js +////const exportsAlias = exports; +////exportsAlias.f = function() {}; +/////*a*/module/*b*/.exports = exportsAlias; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: ` +export function f() { } +`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_dotDefault.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_dotDefault.ts new file mode 100644 index 0000000000000..1c8633eb4eb2b --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_dotDefault.ts @@ -0,0 +1,19 @@ +/// + +// Test that we leave it alone if the name is a keyword. + +// @allowJs: true + +// @Filename: /a.js +/////*a*/exports/*b*/.default = 0; +////exports.default; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `const _default = 0; +export { _default as default }; +_default;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_invalidName.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_invalidName.ts new file mode 100644 index 0000000000000..16b48fcd20467 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_invalidName.ts @@ -0,0 +1,19 @@ +/// + +// Test that we leave it alone if the name is a keyword. + +// @allowJs: true + +// @Filename: /a.js +/////*a*/exports/*b*/.class = 0; +////exports.async = 1; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `const _class = 0; +export { _class as class }; +export const async = 1;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts new file mode 100644 index 0000000000000..45026f86ccbc0 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts @@ -0,0 +1,29 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +/////*a*/module/*b*/.exports = function() {} +////module.exports = function f() {} +////module.exports = class {} +////module.exports = class C {} +////module.exports = 0; +//// +////module.exports = require("./b"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `export default function() { } +export default function f() { } +export default class { +} +export default class C { +} +export default 0; + +export * from "./b"; +export { default } from "./b";`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports_changesImports.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports_changesImports.ts new file mode 100644 index 0000000000000..7b48bd5f91e23 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports_changesImports.ts @@ -0,0 +1,26 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +/////*a*/module/*b*/.exports = 0; + +// @Filename: /b.ts +////import a = require("./a"); + +// @Filename: /c.js +////const a = require("./a"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `export default 0;`, +}); + +goTo.file("/b.ts"); +verify.currentFileContentIs('import a from "./a";'); + +goTo.file("/c.js"); +verify.currentFileContentIs('const a = require("./a").default;'); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_named.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_named.ts new file mode 100644 index 0000000000000..8251f90fc0dee --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_named.ts @@ -0,0 +1,19 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +/////*a*/exports/*b*/.f = function() {} +////exports.C = class {} +////exports.x = 0; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `export function f() { } +export class C { +} +export const x = 0;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts new file mode 100644 index 0000000000000..b18bc94579da6 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts @@ -0,0 +1,25 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +/////*a*/module/*b*/.exports = { +//// x: 0, +//// f: function() {}, +//// g: () => {}, +//// h() {}, +//// C: class {}, +////}; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `export const x = 0; +export function f() { } +export function g() { } +export function h() { } +export class C { +}`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_object_shorthand.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_object_shorthand.ts new file mode 100644 index 0000000000000..05b4903eda385 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_object_shorthand.ts @@ -0,0 +1,18 @@ +/// + +// TODO: Maybe we could transform this to `export function f() {}`. + +// @allowJs: true + +// @Filename: /a.js +////function f() {} +/////*a*/module/*b*/.exports = { f }; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `function f() {} +export default { f };`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_referenced.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_referenced.ts new file mode 100644 index 0000000000000..da9976fa7d06d --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_referenced.ts @@ -0,0 +1,36 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////exports.x = 0; +////exports.x; +//// +////const y = 1; +/////*a*/exports/*b*/.y = y; +////exports.y; +//// +////exports.z = 2; +////function f(z) { +//// exports.z; +////} + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `export const x = 0; +x; + +const y = 1; +const _y = y; +export { _y as y }; +_y; + +const _z = 2; +export { _z as z }; +function f(z) { + _z; +}`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts b/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts new file mode 100644 index 0000000000000..b3b7fbf94c6c0 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts @@ -0,0 +1,18 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +/////*a*/exports/*b*/.f = async function* f(p) {} +////exports.C = class C extends D { m() {} } + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `export async function* f(p) { } +export class C extends D { + m() { } +}`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_arrayBindingPattern.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_arrayBindingPattern.ts new file mode 100644 index 0000000000000..b33b0a1a1609d --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_arrayBindingPattern.ts @@ -0,0 +1,15 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const [x, y] = /*a*/require/*b*/("x"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import _x from "x"; +const [x, y] = _x;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_includeDefaultUses.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_includeDefaultUses.ts new file mode 100644 index 0000000000000..7c5415c145196 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_includeDefaultUses.ts @@ -0,0 +1,18 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const x = /*a*/require/*b*/("x"); +////x(); +////x.y; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import x, { y } from "x"; +x(); +y;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_multipleUniqueIdentifiers.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_multipleUniqueIdentifiers.ts new file mode 100644 index 0000000000000..321a9cccf8b1b --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_multipleUniqueIdentifiers.ts @@ -0,0 +1,20 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const x = require("x"); +////const [a, b] = /*a*/require/*b*/("x"); +////const {c, ...d} = require("x"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import x from "x"; +import _x from "x"; +const [a, b] = _x; +import __x from "x"; +const { c, ...d } = __x;` +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_multipleVariableDeclarations.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_multipleVariableDeclarations.ts new file mode 100644 index 0000000000000..37d65ddc62261 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_multipleVariableDeclarations.ts @@ -0,0 +1,18 @@ +/// + +// Test that we leave it alone if the name is a keyword. + +// @allowJs: true + +// @Filename: /a.js +////const x = /*a*/require/*b*/("x"), y = 0, { z } = require("z"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import x from "x"; +const y = 0; +import { z } from "z";`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_nameFromModuleSpecifier.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_nameFromModuleSpecifier.ts new file mode 100644 index 0000000000000..0c953b2e7e660 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_nameFromModuleSpecifier.ts @@ -0,0 +1,21 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const [] = /*a0*/require/*b0*/("a-b"); +////const [] = /*a1*/require/*b1*/("0a"); +////const [] = /*a2*/require/*b2*/("1a"); + +goTo.select("a0", "b0"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import aB from "a-b"; +const [] = aB; +import A from "0a"; +const [] = A; +import _A from "1a"; +const [] = _A;` +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_complex.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_complex.ts new file mode 100644 index 0000000000000..f757db2164a89 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_complex.ts @@ -0,0 +1,15 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const { x: { a, b } } = /*a*/require/*b*/("x"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import x from "x"; +const { x: { a, b } } = x;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_plain.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_plain.ts new file mode 100644 index 0000000000000..474fd4b0f0fb4 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_objectBindingPattern_plain.ts @@ -0,0 +1,14 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const { x, y: z } = /*a*/require/*b*/("x"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: 'import { x, y as z } from "x";', +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_onlyNamedImports.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_onlyNamedImports.ts new file mode 100644 index 0000000000000..bf7e207550e1e --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_onlyNamedImports.ts @@ -0,0 +1,16 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const x = /*a*/require/*b*/("x"); +////x.y; + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import { y } from "x"; +y;`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts new file mode 100644 index 0000000000000..02394c20cb7d8 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts @@ -0,0 +1,24 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const x = /*a*/require/*b*/("x").default; +////const a = require("b").c; +////const a = require("a").a; +////const [a, b] = require("c").d; +////const [a, b] = require("c").a; // Test that we avoid shadowing 'a' + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import x from "x"; +import { c as a } from "b"; +import { a } from "a"; +import { d } from "c"; +const [a, b] = d; +import { a as _a } from "c"; +const [a, b] = _a; // Test that we avoid shadowing 'a'`, +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_shadowing.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_shadowing.ts new file mode 100644 index 0000000000000..c389280d75c7a --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_shadowing.ts @@ -0,0 +1,18 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////const mod = /*a*/require/*b*/("mod"); +////const x = 0; +////mod.x(x); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: `import { x as _x } from "mod"; +const x = 0; +_x(x);` +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_sideEffect.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_sideEffect.ts new file mode 100644 index 0000000000000..2b81c816e20e4 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_sideEffect.ts @@ -0,0 +1,16 @@ +/// + +// Test that we leave it alone if the name is a keyword. + +// @allowJs: true + +// @Filename: /a.js +/////*a*/require/*b*/("foo"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: 'import "foo";', +}); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_triggers.ts b/tests/cases/fourslash/refactorConvertToEs6Module_triggers.ts new file mode 100644 index 0000000000000..8d441f47dc382 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_triggers.ts @@ -0,0 +1,13 @@ +/// + +// @allowJs: true + +// @Filename: /a.js +////c[|o|]nst [|a|]lias [|=|] [|m|]odule[|.|]export[|s|]; +////[|a|]lias[|.|][|x|] = 0; +////[|module.exports|]; +////[|require("x")|]; +////[|require("x").y;|]; + +goTo.eachRange(() => verify.refactorAvailable("Convert to ES6 module")); + From 22c1f760a4a5612e310a78af947821ad24d2c031 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 14 Nov 2017 07:50:32 -0800 Subject: [PATCH 2/5] Code review --- src/compiler/core.ts | 14 +++---- src/services/refactors/convertToEs6Module.ts | 37 ++++++++++--------- ...refactorConvertToEs6Module_export_alias.ts | 2 - ...onvertToEs6Module_import_propertyAccess.ts | 4 +- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 73beff3a4ef6d..89abe90a6a9c3 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -394,14 +394,12 @@ namespace ts { return result; } - export function mapIter(iter: Iterator, mapFn: (x: T) => U): U[] { - const result: U[] = []; - while (true) { - const { value, done } = iter.next(); - if (done) break; - result.push(mapFn(value)); + export function mapIter(iter: Iterator, mapFn: (x: T) => U): Iterator { + return { next }; + function next(): { value: U, done: false } | { value: never, done: true } { + const iterRes = iter.next(); + return iterRes.done ? iterRes : { value: mapFn(iterRes.value), done: false }; } - return result; } // Maps from T to T and avoids allocation if all elements map to themselves @@ -529,7 +527,7 @@ namespace ts { const result: U[] = []; for (let i = 0; i < array.length; i++) { const mapped = mapFn(array[i], i); - if (!mapped) { + if (mapped === undefined) { return undefined; } result.push(mapped); diff --git a/src/services/refactors/convertToEs6Module.ts b/src/services/refactors/convertToEs6Module.ts index c728f8f9910e8..0f10e8b3e58be 100644 --- a/src/services/refactors/convertToEs6Module.ts +++ b/src/services/refactors/convertToEs6Module.ts @@ -75,7 +75,7 @@ namespace ts.refactor { const { file, program } = context; Debug.assert(isSourceFileJavaScript(file)); const edits = textChanges.ChangeTracker.with(context, changes => { - const moduleExportsChangedToDefault = convertFileToEs6Module(file, program.getTypeChecker(), changes, program.getCompilerOptions().target); + const moduleExportsChangedToDefault = convertFileToEs6Module(file, program.getTypeChecker(), changes, context.newLineCharacter, program.getCompilerOptions().target); if (moduleExportsChangedToDefault) { for (const importingFile of program.getSourceFiles()) { fixImportOfModuleExports(importingFile, file, changes); @@ -111,13 +111,13 @@ namespace ts.refactor { } /** @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, newLine: string, target: ScriptTarget): 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, newLine, identifiers, target, exports); moduleExportsChangedToDefault = moduleExportsChangedToDefault || moduleExportsChanged; } return moduleExportsChangedToDefault; @@ -127,7 +127,7 @@ namespace ts.refactor { * Contains an entry for each renamed export. * This is necessary because `exports.x = 0;` does not declare a local variable. * Converting this to `export const x = 0;` would declare a local, so we must be careful to avoid shadowing. - * If there would be shadowing at either the declaration or at any reference to `exports.x` (now just `x`), we must conver to: + * If there would be shadowing at either the declaration or at any reference to `exports.x` (now just `x`), we must convert to: * const _x = 0; * export { _x as x }; * This conversion also must place if the exported name is not a valid identifier, e.g. `exports.class = 0;`. @@ -140,6 +140,7 @@ namespace ts.refactor { const { text, originalKeywordKind } = node.name; if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind) || checker.resolveName(node.name.text, node, SymbolFlags.Value, /*includeGlobals*/ false))) { + // Unconditionally add an underscore in case `text` is a keyword. res.set(text, makeUniqueName(`_${text}`, identifiers)); } }); @@ -147,8 +148,8 @@ namespace ts.refactor { } function convertExportsAccesses(sourceFile: SourceFile, exports: ExportRenames, changes: textChanges.ChangeTracker): void { - forEachExportReference(sourceFile, (node, isAssignment) => { - if (isAssignment) { + forEachExportReference(sourceFile, (node, isAssignmentLhs) => { + if (isAssignmentLhs) { return; } const { text } = node.name; @@ -156,7 +157,7 @@ namespace ts.refactor { }); } - function forEachExportReference(sourceFile: SourceFile, cb: (node: PropertyAccessExpression, isAssignment: boolean) => void): void { + function forEachExportReference(sourceFile: SourceFile, cb: (node: PropertyAccessExpression, isAssignmentLhs: boolean) => void): void { sourceFile.forEachChild(function recur(node) { if (isPropertyAccessExpression(node) && isExportsOrModuleExportsOrAlias(sourceFile, node.expression)) { const { parent } = node; @@ -169,10 +170,10 @@ namespace ts.refactor { /** 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, newLine: string, identifiers: Identifiers, target: ScriptTarget, exports: ExportRenames): ModuleExportsChanged { switch (statement.kind) { case SyntaxKind.VariableStatement: - convertVariableStatement(sourceFile, statement as VariableStatement, changes, checker, identifiers, target); + convertVariableStatement(sourceFile, statement as VariableStatement, changes, newLine, checker, identifiers, target); return false; case SyntaxKind.ExpressionStatement: { const { expression } = statement as ExpressionStatement; @@ -186,7 +187,7 @@ namespace ts.refactor { } case SyntaxKind.BinaryExpression: { const { left, operatorToken, right } = expression as BinaryExpression; - return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, statement as ExpressionStatement, left, right, changes, exports); + return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, statement as ExpressionStatement, left, right, changes, newLine, exports); } } } @@ -196,7 +197,7 @@ namespace ts.refactor { } } - 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, newLine: string, checker: TypeChecker, identifiers: Identifiers, target: ScriptTarget): void { const { declarationList } = statement as VariableStatement; let foundImport = false; const newNodes = flatMap(declarationList.declarations, decl => { @@ -221,7 +222,7 @@ namespace ts.refactor { }); if (foundImport) { // useNonAdjustedEndPosition to ensure we don't eat the newline after the statement. - changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true }); + changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: newLine, useNonAdjustedEndPosition: true }); } } @@ -251,6 +252,7 @@ namespace ts.refactor { left: Expression, right: Expression, changes: textChanges.ChangeTracker, + newLine: string, exports: ExportRenames, ): ModuleExportsChanged { if (!isPropertyAccessExpression(left)) { @@ -268,12 +270,12 @@ namespace ts.refactor { if (!newNodes) { ([newNodes, changedToDefaultExport] = convertModuleExportsToExportDefault(right)); } - changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true }); + changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: newLine, useNonAdjustedEndPosition: true }); return changedToDefaultExport; } } else if (isExportsOrModuleExportsOrAlias(sourceFile, left.expression)) { - convertNamedExport(sourceFile, statement, left.name, right, changes, exports); + convertNamedExport(sourceFile, statement, left.name, right, changes, newLine, exports); } return false; @@ -312,6 +314,7 @@ namespace ts.refactor { propertyName: Identifier, right: Expression, changes: textChanges.ChangeTracker, + newLine: string, exports: ExportRenames, ): void { // If "originalKeywordKind" was set, this is e.g. `exports. @@ -326,7 +329,7 @@ namespace ts.refactor { makeConst(/*modifiers*/ undefined, rename, right), makeExportDeclaration([createExportSpecifier(rename, text)]), ]; - changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: "\n", useNonAdjustedEndPosition: true }); + changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: newLine, useNonAdjustedEndPosition: true }); } else { changes.replaceNode(sourceFile, statement, convertExportsDotXEquals(text, right), { useNonAdjustedEndPosition: true }); @@ -456,8 +459,8 @@ namespace ts.refactor { } } - const namedBindings = namedBindingsNames.size === 0 ? undefined : mapIter(namedBindingsNames.entries(), ([propertyName, idName]) => - createImportSpecifier(propertyName === idName ? undefined : createIdentifier(propertyName), createIdentifier(idName))); + const namedBindings = namedBindingsNames.size === 0 ? undefined : arrayFrom(mapIter(namedBindingsNames.entries(), ([propertyName, idName]) => + createImportSpecifier(propertyName === idName ? undefined : createIdentifier(propertyName), createIdentifier(idName)))); if (!namedBindings) { // If it was unused, ensure that we at least import *something*. needDefaultImport = true; diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts index 9adf33be1d3d7..6529bb64af045 100644 --- a/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_alias.ts @@ -1,7 +1,5 @@ /// -// Test that we leave it alone if the name is a keyword. - // @allowJs: true // @Filename: /a.js diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts b/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts index 02394c20cb7d8..57efd5370f28e 100644 --- a/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts +++ b/tests/cases/fourslash/refactorConvertToEs6Module_import_propertyAccess.ts @@ -7,7 +7,7 @@ ////const a = require("b").c; ////const a = require("a").a; ////const [a, b] = require("c").d; -////const [a, b] = require("c").a; // Test that we avoid shadowing 'a' +////const [a, b] = require("c").a; // Test that we avoid shadowing the earlier local variable 'a' from 'const [a,b] = d;'. goTo.select("a", "b"); edit.applyRefactor({ @@ -20,5 +20,5 @@ import { a } from "a"; import { d } from "c"; const [a, b] = d; import { a as _a } from "c"; -const [a, b] = _a; // Test that we avoid shadowing 'a'`, +const [a, b] = _a; // Test that we avoid shadowing the earlier local variable 'a' from 'const [a,b] = d;'.`, }); From fe19adf19f226f3ef4fca426a8039c90a12c0fd9 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 30 Nov 2017 14:47:57 -0800 Subject: [PATCH 3/5] includeGlobals -> excludeGlobals --- src/compiler/checker.ts | 16 ++++++++-------- src/compiler/types.ts | 2 +- src/services/codefixes/importFixes.ts | 2 +- src/services/refactors/convertToEs6Module.ts | 2 +- src/services/refactors/extractSymbol.ts | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c034cebbac857..d00a4a3d45f9f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -250,8 +250,8 @@ namespace ts { getSuggestionForNonexistentProperty: (node, type) => getSuggestionForNonexistentProperty(node, type), getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning), getBaseConstraintOfType, - resolveName(name, location, meaning, includeGlobals) { - return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, includeGlobals); + resolveName(name, location, meaning, excludeGlobals) { + return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals); }, getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()), }; @@ -909,9 +909,9 @@ namespace ts { nameNotFoundMessage: DiagnosticMessage | undefined, nameArg: __String | Identifier, isUse: boolean, - includeGlobals = true, + excludeGlobals = false, suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol { - return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, includeGlobals, getSymbol, suggestedNameNotFoundMessage); + return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, suggestedNameNotFoundMessage); } function resolveNameHelper( @@ -921,7 +921,7 @@ namespace ts { nameNotFoundMessage: DiagnosticMessage, nameArg: __String | Identifier, isUse: boolean, - includeGlobals: boolean, + excludeGlobals: boolean, lookup: typeof getSymbol, suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol { const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location @@ -1168,7 +1168,7 @@ namespace ts { } } - if (includeGlobals) { + if (!excludeGlobals) { result = lookup(globals, name, meaning); } } @@ -11254,7 +11254,7 @@ namespace ts { Diagnostics.Cannot_find_name_0, node, !isWriteOnlyAccess(node), - /*includeGlobals*/ true, + /*excludeGlobals*/ false, Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol; } return links.resolvedSymbol; @@ -15357,7 +15357,7 @@ namespace ts { function getSuggestionForNonexistentSymbol(location: Node, outerName: __String, meaning: SymbolFlags): string { Debug.assert(outerName !== undefined, "outername should always be defined"); - const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*includeGlobals*/ true, (symbols, name, meaning) => { + const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*excludeGlobals*/ false, (symbols, name, meaning) => { Debug.assertEqual(outerName, name, "name should equal outerName"); const symbol = getSymbol(symbols, name, meaning); // Sometimes the symbol is found when location is a return type of a function: `typeof x` and `x` is declared in the body of the function diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 39a8253ef5a3d..78313f6a8f7ce 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2783,7 +2783,7 @@ namespace ts { */ /* @internal */ isArrayLikeType(type: Type): boolean; /* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray): Symbol[]; - /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, includeGlobals: boolean): Symbol | undefined; + /* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, excludeGlobals: boolean): Symbol | undefined; /* @internal */ getJsxNamespace(): string; } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index e85aaf7037810..e92b0a839e634 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -658,7 +658,7 @@ namespace ts.codefix { } else if (isJsxOpeningLikeElement(symbolToken.parent) && symbolToken.parent.tagName === symbolToken) { // The error wasn't for the symbolAtLocation, it was for the JSX tag itself, which needs access to e.g. `React`. - symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), symbolToken.parent.tagName, SymbolFlags.Value, /*includeGlobals*/ true)); + symbol = checker.getAliasedSymbol(checker.resolveName(checker.getJsxNamespace(), symbolToken.parent.tagName, SymbolFlags.Value, /*excludeGlobals*/ false)); symbolName = symbol.name; } else { diff --git a/src/services/refactors/convertToEs6Module.ts b/src/services/refactors/convertToEs6Module.ts index 0f10e8b3e58be..d872366e1b0a1 100644 --- a/src/services/refactors/convertToEs6Module.ts +++ b/src/services/refactors/convertToEs6Module.ts @@ -139,7 +139,7 @@ namespace ts.refactor { forEachExportReference(sourceFile, node => { const { text, originalKeywordKind } = node.name; if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind) - || checker.resolveName(node.name.text, node, SymbolFlags.Value, /*includeGlobals*/ false))) { + || checker.resolveName(node.name.text, node, SymbolFlags.Value, /*excludeGlobals*/ true))) { // Unconditionally add an underscore in case `text` is a keyword. res.set(text, makeUniqueName(`_${text}`, identifiers)); } diff --git a/src/services/refactors/extractSymbol.ts b/src/services/refactors/extractSymbol.ts index 881184d23ceec..55b732e24baca 100644 --- a/src/services/refactors/extractSymbol.ts +++ b/src/services/refactors/extractSymbol.ts @@ -1701,7 +1701,7 @@ namespace ts.refactor.extractSymbol { } for (let i = 0; i < scopes.length; i++) { const scope = scopes[i]; - const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags, /*includeGlobals*/ true); + const resolvedSymbol = checker.resolveName(symbol.name, scope, symbol.flags, /*excludeGlobals*/ false); if (resolvedSymbol === symbol) { continue; } From c02ddfcb12d336b1608c3c253e0a401182971ae7 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 30 Nov 2017 15:18:37 -0800 Subject: [PATCH 4/5] Improve handling of `module.exports = require("...")` --- src/compiler/utilities.ts | 1 + src/services/refactors/convertToEs6Module.ts | 36 +++++++++++----- ...vertToEs6Module_export_moduleDotExports.ts | 7 +-- ...le_export_moduleDotExportsEqualsRequire.ts | 43 +++++++++++++++++++ 4 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExportsEqualsRequire.ts diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 20b94976a0bd2..b22e5f983ea2c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4,6 +4,7 @@ namespace ts { export const emptyArray: never[] = [] as never[]; export const emptyMap: ReadonlyMap = createMap(); + export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap = emptyMap as ReadonlyUnderscoreEscapedMap; export const externalHelpersModuleNameText = "tslib"; diff --git a/src/services/refactors/convertToEs6Module.ts b/src/services/refactors/convertToEs6Module.ts index d872366e1b0a1..3f28c3692b499 100644 --- a/src/services/refactors/convertToEs6Module.ts +++ b/src/services/refactors/convertToEs6Module.ts @@ -187,7 +187,7 @@ namespace ts.refactor { } case SyntaxKind.BinaryExpression: { const { left, operatorToken, right } = expression as BinaryExpression; - return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, statement as ExpressionStatement, left, right, changes, newLine, exports); + return operatorToken.kind === SyntaxKind.EqualsToken && convertAssignment(sourceFile, checker, statement as ExpressionStatement, left, right, changes, newLine, exports); } } } @@ -248,6 +248,7 @@ namespace ts.refactor { function convertAssignment( sourceFile: SourceFile, + checker: TypeChecker, statement: ExpressionStatement, left: Expression, right: Expression, @@ -268,7 +269,7 @@ namespace ts.refactor { let newNodes = isObjectLiteralExpression(right) ? tryChangeModuleExportsObject(right) : undefined; let changedToDefaultExport = false; if (!newNodes) { - ([newNodes, changedToDefaultExport] = convertModuleExportsToExportDefault(right)); + ([newNodes, changedToDefaultExport] = convertModuleExportsToExportDefault(right, checker)); } changes.replaceNodeWithNodes(sourceFile, statement, newNodes, { nodeSeparator: newLine, useNonAdjustedEndPosition: true }); return changedToDefaultExport; @@ -336,7 +337,7 @@ namespace ts.refactor { } } - function convertModuleExportsToExportDefault(exported: Expression): [ReadonlyArray, ModuleExportsChanged] { + function convertModuleExportsToExportDefault(exported: Expression, checker: TypeChecker): [ReadonlyArray, ModuleExportsChanged] { const modifiers = [createToken(SyntaxKind.ExportKeyword), createToken(SyntaxKind.DefaultKeyword)]; switch (exported.kind) { case SyntaxKind.FunctionExpression: @@ -351,14 +352,8 @@ namespace ts.refactor { return [[classExpressionToDeclaration(cls.name && cls.name.text, modifiers, cls)], true]; } case SyntaxKind.CallExpression: - if (isRequireCall(exported, /*checkArguementIsStringLiteral*/ true)) { - // `module.exports = require("x");` ==> `export * from "x"; export { default } from "x";` - const moduleSpecifier = exported.arguments[0].text; - const newNodes = [ - makeExportDeclaration(/*exportClause*/ undefined, moduleSpecifier), - makeExportDeclaration([createExportSpecifier(/*propertyName*/ undefined, "default")], moduleSpecifier), - ]; - return [newNodes, false]; + if (isRequireCall(exported, /*checkArgumentIsStringLiteral*/ true)) { + return convertReExportAll(exported.arguments[0], checker); } // falls through default: @@ -367,6 +362,25 @@ namespace ts.refactor { } } + function convertReExportAll(reExported: StringLiteralLike, checker: TypeChecker): [ReadonlyArray, ModuleExportsChanged] { + // `module.exports = require("x");` ==> `export * from "x"; export { default } from "x";` + const moduleSpecifier = reExported.text; + const moduleSymbol = checker.getSymbolAtLocation(reExported); + const exports = moduleSymbol ? moduleSymbol.exports : emptyUnderscoreEscapedMap; + return exports.has("export=" as __String) + ? [[reExportDefault(moduleSpecifier)], true] + : !exports.has("default" as __String) + ? [[reExportStar(moduleSpecifier)], false] + // If there's some non-default export, must include both `export *` and `export default`. + : exports.size > 1 ? [[reExportStar(moduleSpecifier), reExportDefault(moduleSpecifier)], true] : [[reExportDefault(moduleSpecifier)], true]; + } + function reExportStar(moduleSpecifier: string): ExportDeclaration { + return makeExportDeclaration(/*exportClause*/ undefined, moduleSpecifier); + } + function reExportDefault(moduleSpecifier: string): ExportDeclaration { + return makeExportDeclaration([createExportSpecifier(/*propertyName*/ undefined, "default")], moduleSpecifier); + } + function convertExportsDotXEquals(name: string | undefined, exported: Expression): Statement { const modifiers = [createToken(SyntaxKind.ExportKeyword)]; switch (exported.kind) { diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts index 45026f86ccbc0..7a7632cb0fd88 100644 --- a/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExports.ts @@ -8,8 +8,8 @@ ////module.exports = class {} ////module.exports = class C {} ////module.exports = 0; -//// -////module.exports = require("./b"); + +// See also `refactorConvertToEs6Module_export_moduleDotExportsEqualsRequire.ts` goTo.select("a", "b"); edit.applyRefactor({ @@ -23,7 +23,4 @@ export default class { export default class C { } export default 0; - -export * from "./b"; -export { default } from "./b";`, }); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExportsEqualsRequire.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExportsEqualsRequire.ts new file mode 100644 index 0000000000000..8d22ea77c2b37 --- /dev/null +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_moduleDotExportsEqualsRequire.ts @@ -0,0 +1,43 @@ +/// + +// @allowJs: true + +// @Filename: /a.d.ts +////export const x: number; + +// @Filename: /b.d.ts +////export default function f() {} + +// @Filename: /c.d.ts +////export default function f(): void; +////export function g(): void; + +// @Filename: /d.ts +////declare const x: number; +////export = x; + +// @Filename: /z.js +// Normally -- just `export *` +/////*a*/module/*b*/.exports = require("./a"); +// If just a default is exported, just `export { default }` +////module.exports = require("./b"); +// May need both +////module.exports = require("./c"); +// For `export =` re-export the "default" since that's what it will be converted to. +////module.exports = require("./d"); +// In untyped case just go with `export *` +////module.exports = require("./unknown"); + +goTo.select("a", "b"); +edit.applyRefactor({ + refactorName: "Convert to ES6 module", + actionName: "Convert to ES6 module", + actionDescription: "Convert to ES6 module", + newContent: +`export * from "./a"; +export { default } from "./b"; +export * from "./c"; +export { default } from "./c"; +export { default } from "./d"; +export * from "./unknown";`, +}); From 4df53c148f3dc0fd94674b22716bd082c5429a96 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 4 Dec 2017 15:17:56 -0800 Subject: [PATCH 5/5] Allow NoSubstitutionTemplateLiteral as argument to createLiteral --- src/compiler/factory.ts | 4 ++-- tests/baselines/reference/api/tsserverlibrary.d.ts | 2 +- tests/baselines/reference/api/typescript.d.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 275c53822539b..eb8841207caed 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -71,11 +71,11 @@ namespace ts { // Literals /** If a node is passed, creates a string literal whose source text is read from a source node during emit. */ - export function createLiteral(value: string | StringLiteral | NumericLiteral | Identifier): StringLiteral; + export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral; export function createLiteral(value: number): NumericLiteral; export function createLiteral(value: boolean): BooleanLiteral; export function createLiteral(value: string | number | boolean): PrimaryExpression; - export function createLiteral(value: string | number | boolean | StringLiteral | NumericLiteral | Identifier): PrimaryExpression { + export function createLiteral(value: string | number | boolean | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): PrimaryExpression { if (typeof value === "number") { return createNumericLiteral(value + ""); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 975d36da766e0..546e57ffed3cd 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3289,7 +3289,7 @@ declare namespace ts { declare namespace ts { function createNodeArray(elements?: ReadonlyArray, hasTrailingComma?: boolean): NodeArray; /** If a node is passed, creates a string literal whose source text is read from a source node during emit. */ - function createLiteral(value: string | StringLiteral | NumericLiteral | Identifier): StringLiteral; + function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral; function createLiteral(value: number): NumericLiteral; function createLiteral(value: boolean): BooleanLiteral; function createLiteral(value: string | number | boolean): PrimaryExpression; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index d7d797df60c4c..d4036d5a02b2b 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -3236,7 +3236,7 @@ declare namespace ts { declare namespace ts { function createNodeArray(elements?: ReadonlyArray, hasTrailingComma?: boolean): NodeArray; /** If a node is passed, creates a string literal whose source text is read from a source node during emit. */ - function createLiteral(value: string | StringLiteral | NumericLiteral | Identifier): StringLiteral; + function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral; function createLiteral(value: number): NumericLiteral; function createLiteral(value: boolean): BooleanLiteral; function createLiteral(value: string | number | boolean): PrimaryExpression;