Add refactoring to convert CommonJS module to ES6 module (#19916)

* Add refactoring to convert CommonJS module to ES6 module

* Code review

* includeGlobals -> excludeGlobals

* Improve handling of `module.exports = require("...")`

* Allow NoSubstitutionTemplateLiteral as argument to createLiteral
This commit is contained in:
Andy 2018-01-09 13:15:47 -08:00 committed by GitHub
parent 9aa99b90c7
commit 8bce69e6bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1170 additions and 71 deletions

View File

@ -2289,30 +2289,13 @@ namespace ts {
declareSymbol(file.symbol.exports, file.symbol, <PropertyAccessExpression>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;
@ -2393,7 +2376,7 @@ namespace ts {
if (node.kind === SyntaxKind.BinaryExpression) {
leftSideOfAssignment.parent = node;
}
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 ...
@ -2406,11 +2389,7 @@ namespace ts {
}
function lookupSymbolForName(name: __String) {
const local = container.locals && container.locals.get(name);
if (local) {
return local.exportSymbol || local;
}
return container.symbol && container.symbol.exports && container.symbol.exports.get(name);
return lookupSymbolForNameWorker(container, name);
}
function bindPropertyAssignment(functionName: __String, propertyAccess: PropertyAccessExpression, isPrototypeProperty: boolean) {
@ -2649,6 +2628,33 @@ 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 {
const local = container.locals && container.locals.get(name);
if (local) {
return local.exportSymbol || local;
}
return container.symbol && container.symbol.exports && container.symbol.exports.get(name);
}
/**
* Computes the transform flags for a node, given the transform flags of its subtree
*

View File

@ -268,8 +268,8 @@ namespace ts {
getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
getBaseConstraintOfType,
getDefaultFromTypeParameter: type => type && type.flags & TypeFlags.TypeParameter ? getDefaultFromTypeParameter(type as TypeParameter) : undefined,
resolveName(name, location, meaning) {
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false);
resolveName(name, location, meaning, excludeGlobals) {
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals);
},
getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()),
getAccessibleSymbolChain,
@ -952,8 +952,9 @@ namespace ts {
nameNotFoundMessage: DiagnosticMessage | undefined,
nameArg: __String | Identifier,
isUse: boolean,
excludeGlobals = false,
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol {
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, getSymbol, suggestedNameNotFoundMessage);
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, suggestedNameNotFoundMessage);
}
function resolveNameHelper(
@ -963,6 +964,7 @@ namespace ts {
nameNotFoundMessage: DiagnosticMessage,
nameArg: __String | Identifier,
isUse: 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
@ -1209,7 +1211,9 @@ namespace ts {
}
}
result = lookup(globals, name, meaning);
if (!excludeGlobals) {
result = lookup(globals, name, meaning);
}
}
if (!result) {
@ -11889,6 +11893,7 @@ namespace ts {
Diagnostics.Cannot_find_name_0,
node,
!isWriteOnlyAccess(node),
/*excludeGlobals*/ false,
Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol;
}
return links.resolvedSymbol;
@ -16068,7 +16073,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, /*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

View File

@ -412,12 +412,14 @@ namespace ts {
return result;
}
export function mapIterator<T, U>(iter: Iterator<T>, mapFn: (x: T) => U): Iterator<U> {
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 {
next() {
const iterRes = iter.next();
return iterRes.done ? iterRes : { value: mapFn(iterRes.value), done: false };
}
};
}
// Maps from T to T and avoids allocation if all elements map to themselves
@ -551,12 +553,23 @@ namespace ts {
return result || array;
}
export function mapAllOrFail<T, U>(array: ReadonlyArray<T>, 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 === undefined) {
return undefined;
}
result.push(mapped);
}
return result;
}
export function mapDefined<T, U>(array: ReadonlyArray<T> | 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);
}

View File

@ -3937,5 +3937,9 @@
"Use synthetic 'default' member.": {
"category": "Message",
"code": 95016
},
"Convert to ES6 module": {
"category": "Message",
"code": 95017
}
}

View File

@ -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,

View File

@ -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 + "");
}
@ -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;
@ -3626,7 +3626,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);
}

View File

@ -1149,11 +1149,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
@ -1499,6 +1501,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,
@ -2156,6 +2159,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;
@ -2878,7 +2882,7 @@ namespace ts {
*/
/* @internal */ isArrayLikeType(type: Type): boolean;
/* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray<Type>): Symbol[];
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined;
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, excludeGlobals: boolean): Symbol | undefined;
/* @internal */ getJsxNamespace(): string;
/**

View File

@ -5,6 +5,7 @@ namespace ts {
export const emptyArray: never[] = [] as never[];
export const resolvingEmptyArray: never[] = [] as never[];
export const emptyMap: ReadonlyMap<never> = createMap<never>();
export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap<never> = emptyMap as ReadonlyUnderscoreEscapedMap<never>;
export const externalHelpersModuleNameText = "tslib";
@ -1419,6 +1420,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;
@ -1456,7 +1459,7 @@ namespace ts {
return false;
}
export function getRightMostAssignedExpression(node: Node) {
export function getRightMostAssignedExpression(node: Expression): Expression {
while (isAssignmentExpression(node, /*excludeCompoundAssignements*/ true)) {
node = node.right;
}

View File

@ -454,7 +454,7 @@ namespace FourSlash {
const ranges = this.getRanges();
assert(ranges.length);
for (const range of ranges) {
this.goToRangeStart(range);
this.selectRange(range);
action();
}
}
@ -482,6 +482,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);
@ -3835,6 +3840,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 {

View File

@ -746,7 +746,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, /*excludeGlobals*/ false));
symbolName = symbol.name;
}
else {
@ -867,7 +867,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);

View File

@ -0,0 +1,582 @@
/* @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<true>() };
const exports = collectExportRenames(sourceFile, checker, identifiers);
convertExportsAccesses(sourceFile, exports, changes);
let moduleExportsChangedToDefault = false;
for (const statement of sourceFile.statements) {
const moduleExportsChanged = convertStatement(sourceFile, statement, checker, changes, identifiers, target, exports);
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 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;`.
*/
type ExportRenames = ReadonlyMap<string>;
function collectExportRenames(sourceFile: SourceFile, checker: TypeChecker, identifiers: Identifiers): ExportRenames {
const res = createMap<string>();
forEachExportReference(sourceFile, node => {
const { text, originalKeywordKind } = node.name;
if (!res.has(text) && (originalKeywordKind !== undefined && isNonContextualKeyword(originalKeywordKind)
|| 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));
}
});
return res;
}
function convertExportsAccesses(sourceFile: SourceFile, exports: ExportRenames, changes: textChanges.ChangeTracker): void {
forEachExportReference(sourceFile, (node, isAssignmentLhs) => {
if (isAssignmentLhs) {
return;
}
const { text } = node.name;
changes.replaceNode(sourceFile, node, createIdentifier(exports.get(text) || text));
});
}
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;
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, checker, 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);
}
}
/** Converts `const name = require("moduleSpecifier").propertyName` */
function convertPropertyAccessImport(name: BindingName, propertyName: string, moduleSpecifier: string, identifiers: Identifiers): ReadonlyArray<Node> {
switch (name.kind) {
case SyntaxKind.ObjectBindingPattern:
case SyntaxKind.ArrayBindingPattern: {
// `const [a, b] = require("c").d` --> `import { d } from "c"; const [a, b] = d;`
const tmp = makeUniqueName(propertyName, identifiers);
return [
makeSingleImport(tmp, propertyName, moduleSpecifier),
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,
checker: TypeChecker,
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, checker));
}
changes.replaceNodeWithNodes(sourceFile, statement, newNodes);
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<Statement> | 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);
}
else {
changes.replaceNode(sourceFile, statement, convertExportsDotXEquals(text, right), { useNonAdjustedEndPosition: true });
}
}
function convertModuleExportsToExportDefault(exported: Expression, checker: TypeChecker): [ReadonlyArray<Statement>, 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, /*checkArgumentIsStringLiteral*/ true)) {
return convertReExportAll(exported.arguments[0], checker);
}
// falls through
default:
// `module.exports = 0;` --> `export default 0;`
return [[createExportAssignment(/*decorators*/ undefined, /*modifiers*/ undefined, /*isExportEquals*/ false, exported)], true];
}
}
function convertReExportAll(reExported: StringLiteralLike, checker: TypeChecker): [ReadonlyArray<Statement>, 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) {
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 <<name>> = 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<Node> {
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<Node> {
const nameSymbol = checker.getSymbolAtLocation(name);
// Maps from module property name to name actually used. (The same if there isn't shadowing.)
const namedBindingsNames = createMap<string>();
// 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 : arrayFrom(mapIterator(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<true>;
}
type FreeIdentifiers = ReadonlyMap<ReadonlyArray<Identifier>>;
function collectFreeIdentifiers(file: SourceFile): FreeIdentifiers {
const map = createMultiMap<Identifier>();
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<Modifier>, 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<Modifier>, 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<ImportSpecifier>, 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<Modifier> | 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));
}
}

View File

@ -1692,7 +1692,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, /*excludeGlobals*/ false);
if (resolvedSymbol === symbol) {
continue;
}

View File

@ -1,5 +1,6 @@
/// <reference path="annotateWithTypeFromJSDoc.ts" />
/// <reference path="convertFunctionToEs6Class.ts" />
/// <reference path="convertToEs6Module.ts" />
/// <reference path="extractSymbol.ts" />
/// <reference path="installTypesForPackage.ts" />
/// <reference path="useDefaultImport.ts" />

View File

@ -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,

View File

@ -1354,6 +1354,10 @@ namespace ts {
return visited;
}
export function getSynthesizedDeepClones<T extends Node>(nodes: NodeArray<T> | undefined): NodeArray<T> | undefined {
return nodes && createNodeArray(nodes.map(getSynthesizedDeepClone), nodes.hasTrailingComma);
}
/**
* Sets EmitFlags to suppress leading and trailing trivia on the node.
*/

View File

@ -901,6 +901,7 @@ declare namespace ts {
kind: SyntaxKind.ArrowFunction;
equalsGreaterThanToken: EqualsGreaterThanToken;
body: ConciseBody;
name: never;
}
interface LiteralLikeNode extends Node {
text: string;
@ -1362,6 +1363,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;
@ -3288,7 +3290,7 @@ declare namespace ts {
declare namespace ts {
function createNodeArray<T extends Node>(elements?: ReadonlyArray<T>, hasTrailingComma?: boolean): NodeArray<T>;
/** 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;

View File

@ -901,6 +901,7 @@ declare namespace ts {
kind: SyntaxKind.ArrowFunction;
equalsGreaterThanToken: EqualsGreaterThanToken;
body: ConciseBody;
name: never;
}
interface LiteralLikeNode extends Node {
text: string;
@ -1362,6 +1363,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;
@ -3235,7 +3237,7 @@ declare namespace ts {
declare namespace ts {
function createNodeArray<T extends Node>(elements?: ReadonlyArray<T>, hasTrailingComma?: boolean): NodeArray<T>;
/** 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;

View File

@ -135,6 +135,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;

View File

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @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() { }
`,
});

View File

@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />
// 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;`,
});

View File

@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />
// 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;`,
});

View File

@ -0,0 +1,26 @@
/// <reference path='fourslash.ts' />
// @allowJs: true
// @Filename: /a.js
/////*a*/module/*b*/.exports = function() {}
////module.exports = function f() {}
////module.exports = class {}
////module.exports = class C {}
////module.exports = 0;
// See also `refactorConvertToEs6Module_export_moduleDotExportsEqualsRequire.ts`
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;`
});

View File

@ -0,0 +1,43 @@
/// <reference path='fourslash.ts' />
// @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";`,
});

View File

@ -0,0 +1,26 @@
/// <reference path='fourslash.ts' />
// @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;');

View File

@ -0,0 +1,19 @@
/// <reference path='fourslash.ts' />
// @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;`,
});

View File

@ -0,0 +1,25 @@
/// <reference path='fourslash.ts' />
// @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 {
}`,
});

View File

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// 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 };`,
});

View File

@ -0,0 +1,36 @@
/// <reference path='fourslash.ts' />
// @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;
}`,
});

View File

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @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() { }
}`,
});

View File

@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @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;`,
});

View File

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @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;`,
});

View File

@ -0,0 +1,20 @@
/// <reference path='fourslash.ts' />
// @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;`
});

View File

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// 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";`,
});

View File

@ -0,0 +1,21 @@
/// <reference path='fourslash.ts' />
// @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;`
});

View File

@ -0,0 +1,15 @@
/// <reference path='fourslash.ts' />
// @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;`,
});

View File

@ -0,0 +1,14 @@
/// <reference path='fourslash.ts' />
// @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";',
});

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
// @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;`,
});

View File

@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />
// @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 the earlier local variable 'a' from 'const [a,b] = d;'.
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 the earlier local variable 'a' from 'const [a,b] = d;'.`,
});

View File

@ -0,0 +1,18 @@
/// <reference path='fourslash.ts' />
// @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);`
});

View File

@ -0,0 +1,16 @@
/// <reference path='fourslash.ts' />
// 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";',
});

View File

@ -0,0 +1,13 @@
/// <reference path='fourslash.ts' />
// @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"));