Skip to content

Commit 8bce69e

Browse files
author
Andy
authored
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
1 parent 9aa99b9 commit 8bce69e

File tree

41 files changed

+1170
-71
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1170
-71
lines changed

src/compiler/binder.ts

+30-24
Original file line numberDiff line numberDiff line change
@@ -2289,30 +2289,13 @@ namespace ts {
22892289
declareSymbol(file.symbol.exports, file.symbol, <PropertyAccessExpression>node.left, SymbolFlags.Property | SymbolFlags.ExportValue, SymbolFlags.None);
22902290
}
22912291

2292-
function isExportsOrModuleExportsOrAlias(node: Node): boolean {
2293-
return isExportsIdentifier(node) ||
2294-
isModuleExportsPropertyAccessExpression(node) ||
2295-
isIdentifier(node) && isNameOfExportsOrModuleExportsAliasDeclaration(node);
2296-
}
2297-
2298-
function isNameOfExportsOrModuleExportsAliasDeclaration(node: Identifier): boolean {
2299-
const symbol = lookupSymbolForName(node.escapedText);
2300-
return symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) &&
2301-
symbol.valueDeclaration.initializer && isExportsOrModuleExportsOrAliasOrAssignment(symbol.valueDeclaration.initializer);
2302-
}
2303-
2304-
function isExportsOrModuleExportsOrAliasOrAssignment(node: Node): boolean {
2305-
return isExportsOrModuleExportsOrAlias(node) ||
2306-
(isAssignmentExpression(node, /*excludeCompoundAssignements*/ true) && (isExportsOrModuleExportsOrAliasOrAssignment(node.left) || isExportsOrModuleExportsOrAliasOrAssignment(node.right)));
2307-
}
2308-
23092292
function bindModuleExportsAssignment(node: BinaryExpression) {
23102293
// A common practice in node modules is to set 'export = module.exports = {}', this ensures that 'exports'
23112294
// is still pointing to 'module.exports'.
23122295
// We do not want to consider this as 'export=' since a module can have only one of these.
23132296
// Similarly we do not want to treat 'module.exports = exports' as an 'export='.
23142297
const assignedExpression = getRightMostAssignedExpression(node.right);
2315-
if (isEmptyObjectLiteral(assignedExpression) || isExportsOrModuleExportsOrAlias(assignedExpression)) {
2298+
if (isEmptyObjectLiteral(assignedExpression) || container === file && isExportsOrModuleExportsOrAlias(file, assignedExpression)) {
23162299
// Mark it as a module in case there are no other exports in the file
23172300
setCommonJsModuleIndicator(node);
23182301
return;
@@ -2393,7 +2376,7 @@ namespace ts {
23932376
if (node.kind === SyntaxKind.BinaryExpression) {
23942377
leftSideOfAssignment.parent = node;
23952378
}
2396-
if (isNameOfExportsOrModuleExportsAliasDeclaration(target)) {
2379+
if (container === file && isNameOfExportsOrModuleExportsAliasDeclaration(file, target)) {
23972380
// This can be an alias for the 'exports' or 'module.exports' names, e.g.
23982381
// var util = module.exports;
23992382
// util.property = function ...
@@ -2406,11 +2389,7 @@ namespace ts {
24062389
}
24072390

24082391
function lookupSymbolForName(name: __String) {
2409-
const local = container.locals && container.locals.get(name);
2410-
if (local) {
2411-
return local.exportSymbol || local;
2412-
}
2413-
return container.symbol && container.symbol.exports && container.symbol.exports.get(name);
2392+
return lookupSymbolForNameWorker(container, name);
24142393
}
24152394

24162395
function bindPropertyAssignment(functionName: __String, propertyAccess: PropertyAccessExpression, isPrototypeProperty: boolean) {
@@ -2649,6 +2628,33 @@ namespace ts {
26492628
}
26502629
}
26512630

2631+
/* @internal */
2632+
export function isExportsOrModuleExportsOrAlias(sourceFile: SourceFile, node: Expression): boolean {
2633+
return isExportsIdentifier(node) ||
2634+
isModuleExportsPropertyAccessExpression(node) ||
2635+
isIdentifier(node) && isNameOfExportsOrModuleExportsAliasDeclaration(sourceFile, node);
2636+
}
2637+
2638+
function isNameOfExportsOrModuleExportsAliasDeclaration(sourceFile: SourceFile, node: Identifier): boolean {
2639+
const symbol = lookupSymbolForNameWorker(sourceFile, node.escapedText);
2640+
return symbol && symbol.valueDeclaration && isVariableDeclaration(symbol.valueDeclaration) &&
2641+
symbol.valueDeclaration.initializer && isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, symbol.valueDeclaration.initializer);
2642+
}
2643+
2644+
function isExportsOrModuleExportsOrAliasOrAssignment(sourceFile: SourceFile, node: Expression): boolean {
2645+
return isExportsOrModuleExportsOrAlias(sourceFile, node) ||
2646+
(isAssignmentExpression(node, /*excludeCompoundAssignements*/ true) && (
2647+
isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, node.left) || isExportsOrModuleExportsOrAliasOrAssignment(sourceFile, node.right)));
2648+
}
2649+
2650+
function lookupSymbolForNameWorker(container: Node, name: __String): Symbol | undefined {
2651+
const local = container.locals && container.locals.get(name);
2652+
if (local) {
2653+
return local.exportSymbol || local;
2654+
}
2655+
return container.symbol && container.symbol.exports && container.symbol.exports.get(name);
2656+
}
2657+
26522658
/**
26532659
* Computes the transform flags for a node, given the transform flags of its subtree
26542660
*

src/compiler/checker.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -268,8 +268,8 @@ namespace ts {
268268
getSuggestionForNonexistentSymbol: (location, name, meaning) => getSuggestionForNonexistentSymbol(location, escapeLeadingUnderscores(name), meaning),
269269
getBaseConstraintOfType,
270270
getDefaultFromTypeParameter: type => type && type.flags & TypeFlags.TypeParameter ? getDefaultFromTypeParameter(type as TypeParameter) : undefined,
271-
resolveName(name, location, meaning) {
272-
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false);
271+
resolveName(name, location, meaning, excludeGlobals) {
272+
return resolveName(location, escapeLeadingUnderscores(name), meaning, /*nameNotFoundMessage*/ undefined, /*nameArg*/ undefined, /*isUse*/ false, excludeGlobals);
273273
},
274274
getJsxNamespace: () => unescapeLeadingUnderscores(getJsxNamespace()),
275275
getAccessibleSymbolChain,
@@ -952,8 +952,9 @@ namespace ts {
952952
nameNotFoundMessage: DiagnosticMessage | undefined,
953953
nameArg: __String | Identifier,
954954
isUse: boolean,
955+
excludeGlobals = false,
955956
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol {
956-
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, getSymbol, suggestedNameNotFoundMessage);
957+
return resolveNameHelper(location, name, meaning, nameNotFoundMessage, nameArg, isUse, excludeGlobals, getSymbol, suggestedNameNotFoundMessage);
957958
}
958959

959960
function resolveNameHelper(
@@ -963,6 +964,7 @@ namespace ts {
963964
nameNotFoundMessage: DiagnosticMessage,
964965
nameArg: __String | Identifier,
965966
isUse: boolean,
967+
excludeGlobals: boolean,
966968
lookup: typeof getSymbol,
967969
suggestedNameNotFoundMessage?: DiagnosticMessage): Symbol {
968970
const originalLocation = location; // needed for did-you-mean error reporting, which gathers candidates starting from the original location
@@ -1209,7 +1211,9 @@ namespace ts {
12091211
}
12101212
}
12111213

1212-
result = lookup(globals, name, meaning);
1214+
if (!excludeGlobals) {
1215+
result = lookup(globals, name, meaning);
1216+
}
12131217
}
12141218

12151219
if (!result) {
@@ -11889,6 +11893,7 @@ namespace ts {
1188911893
Diagnostics.Cannot_find_name_0,
1189011894
node,
1189111895
!isWriteOnlyAccess(node),
11896+
/*excludeGlobals*/ false,
1189211897
Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol;
1189311898
}
1189411899
return links.resolvedSymbol;
@@ -16068,7 +16073,7 @@ namespace ts {
1606816073

1606916074
function getSuggestionForNonexistentSymbol(location: Node, outerName: __String, meaning: SymbolFlags): string {
1607016075
Debug.assert(outerName !== undefined, "outername should always be defined");
16071-
const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, (symbols, name, meaning) => {
16076+
const result = resolveNameHelper(location, outerName, meaning, /*nameNotFoundMessage*/ undefined, outerName, /*isUse*/ false, /*excludeGlobals*/ false, (symbols, name, meaning) => {
1607216077
Debug.assertEqual(outerName, name, "name should equal outerName");
1607316078
const symbol = getSymbol(symbols, name, meaning);
1607416079
// 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

src/compiler/core.ts

+20-7
Original file line numberDiff line numberDiff line change
@@ -412,12 +412,14 @@ namespace ts {
412412
return result;
413413
}
414414

415+
415416
export function mapIterator<T, U>(iter: Iterator<T>, mapFn: (x: T) => U): Iterator<U> {
416-
return { next };
417-
function next(): { value: U, done: false } | { value: never, done: true } {
418-
const iterRes = iter.next();
419-
return iterRes.done ? iterRes : { value: mapFn(iterRes.value), done: false };
420-
}
417+
return {
418+
next() {
419+
const iterRes = iter.next();
420+
return iterRes.done ? iterRes : { value: mapFn(iterRes.value), done: false };
421+
}
422+
};
421423
}
422424

423425
// Maps from T to T and avoids allocation if all elements map to themselves
@@ -551,12 +553,23 @@ namespace ts {
551553
return result || array;
552554
}
553555

556+
export function mapAllOrFail<T, U>(array: ReadonlyArray<T>, mapFn: (x: T, i: number) => U | undefined): U[] | undefined {
557+
const result: U[] = [];
558+
for (let i = 0; i < array.length; i++) {
559+
const mapped = mapFn(array[i], i);
560+
if (mapped === undefined) {
561+
return undefined;
562+
}
563+
result.push(mapped);
564+
}
565+
return result;
566+
}
567+
554568
export function mapDefined<T, U>(array: ReadonlyArray<T> | undefined, mapFn: (x: T, i: number) => U | undefined): U[] {
555569
const result: U[] = [];
556570
if (array) {
557571
for (let i = 0; i < array.length; i++) {
558-
const item = array[i];
559-
const mapped = mapFn(item, i);
572+
const mapped = mapFn(array[i], i);
560573
if (mapped !== undefined) {
561574
result.push(mapped);
562575
}

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -3937,5 +3937,9 @@
39373937
"Use synthetic 'default' member.": {
39383938
"category": "Message",
39393939
"code": 95016
3940+
},
3941+
"Convert to ES6 module": {
3942+
"category": "Message",
3943+
"code": 95017
39403944
}
39413945
}

src/compiler/emitter.ts

+8-20
Original file line numberDiff line numberDiff line change
@@ -1194,27 +1194,15 @@ namespace ts {
11941194
//
11951195

11961196
function emitObjectBindingPattern(node: ObjectBindingPattern) {
1197-
const elements = node.elements;
1198-
if (elements.length === 0) {
1199-
write("{}");
1200-
}
1201-
else {
1202-
write("{");
1203-
emitList(node, elements, ListFormat.ObjectBindingPatternElements);
1204-
write("}");
1205-
}
1197+
write("{");
1198+
emitList(node, node.elements, ListFormat.ObjectBindingPatternElements);
1199+
write("}");
12061200
}
12071201

12081202
function emitArrayBindingPattern(node: ArrayBindingPattern) {
1209-
const elements = node.elements;
1210-
if (elements.length === 0) {
1211-
write("[]");
1212-
}
1213-
else {
1214-
write("[");
1215-
emitList(node, node.elements, ListFormat.ArrayBindingPatternElements);
1216-
write("]");
1217-
}
1203+
write("[");
1204+
emitList(node, node.elements, ListFormat.ArrayBindingPatternElements);
1205+
write("]");
12181206
}
12191207

12201208
function emitBindingElement(node: BindingElement) {
@@ -3167,8 +3155,8 @@ namespace ts {
31673155
TupleTypeElements = CommaDelimited | SpaceBetweenSiblings | SingleLine | Indented,
31683156
UnionTypeConstituents = BarDelimited | SpaceBetweenSiblings | SingleLine,
31693157
IntersectionTypeConstituents = AmpersandDelimited | SpaceBetweenSiblings | SingleLine,
3170-
ObjectBindingPatternElements = SingleLine | AllowTrailingComma | SpaceBetweenBraces | CommaDelimited | SpaceBetweenSiblings,
3171-
ArrayBindingPatternElements = SingleLine | AllowTrailingComma | CommaDelimited | SpaceBetweenSiblings,
3158+
ObjectBindingPatternElements = SingleLine | AllowTrailingComma | SpaceBetweenBraces | CommaDelimited | SpaceBetweenSiblings | NoSpaceIfEmpty,
3159+
ArrayBindingPatternElements = SingleLine | AllowTrailingComma | CommaDelimited | SpaceBetweenSiblings | NoSpaceIfEmpty,
31723160
ObjectLiteralExpressionProperties = PreserveLines | CommaDelimited | SpaceBetweenSiblings | SpaceBetweenBraces | Indented | Braces | NoSpaceIfEmpty,
31733161
ArrayLiteralExpressionElements = PreserveLines | CommaDelimited | SpaceBetweenSiblings | AllowTrailingComma | Indented | SquareBrackets,
31743162
CommaListElements = CommaDelimited | SpaceBetweenSiblings | SingleLine,

src/compiler/factory.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,11 @@ namespace ts {
7171
// Literals
7272

7373
/** If a node is passed, creates a string literal whose source text is read from a source node during emit. */
74-
export function createLiteral(value: string | StringLiteral | NumericLiteral | Identifier): StringLiteral;
74+
export function createLiteral(value: string | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): StringLiteral;
7575
export function createLiteral(value: number): NumericLiteral;
7676
export function createLiteral(value: boolean): BooleanLiteral;
7777
export function createLiteral(value: string | number | boolean): PrimaryExpression;
78-
export function createLiteral(value: string | number | boolean | StringLiteral | NumericLiteral | Identifier): PrimaryExpression {
78+
export function createLiteral(value: string | number | boolean | StringLiteral | NoSubstitutionTemplateLiteral | NumericLiteral | Identifier): PrimaryExpression {
7979
if (typeof value === "number") {
8080
return createNumericLiteral(value + "");
8181
}
@@ -101,7 +101,7 @@ namespace ts {
101101
return node;
102102
}
103103

104-
function createLiteralFromNode(sourceNode: StringLiteral | NumericLiteral | Identifier): StringLiteral {
104+
function createLiteralFromNode(sourceNode: StringLiteralLike | NumericLiteral | Identifier): StringLiteral {
105105
const node = createStringLiteral(getTextOfIdentifierOrLiteral(sourceNode));
106106
node.textSourceNode = sourceNode;
107107
return node;
@@ -3626,7 +3626,7 @@ namespace ts {
36263626
return qualifiedName;
36273627
}
36283628

3629-
export function convertToFunctionBody(node: ConciseBody, multiLine?: boolean) {
3629+
export function convertToFunctionBody(node: ConciseBody, multiLine?: boolean): Block {
36303630
return isBlock(node) ? node : setTextRange(createBlock([setTextRange(createReturn(node), node)], multiLine), node);
36313631
}
36323632

src/compiler/types.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1149,11 +1149,13 @@ namespace ts {
11491149

11501150
export interface StringLiteral extends LiteralExpression {
11511151
kind: SyntaxKind.StringLiteral;
1152-
/* @internal */ textSourceNode?: Identifier | StringLiteral | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms).
1152+
/* @internal */ textSourceNode?: Identifier | StringLiteralLike | NumericLiteral; // Allows a StringLiteral to get its text from another node (used by transforms).
11531153
/** Note: this is only set when synthesizing a node, not during parsing. */
11541154
/* @internal */ singleQuote?: boolean;
11551155
}
11561156

1157+
/* @internal */ export type StringLiteralLike = StringLiteral | NoSubstitutionTemplateLiteral;
1158+
11571159
// Note: 'brands' in our syntax nodes serve to give us a small amount of nominal typing.
11581160
// Consider 'Expression'. Without the brand, 'Expression' is actually no different
11591161
// (structurally) than 'Node'. Because of this you can pass any Node to a function that
@@ -1499,6 +1501,7 @@ namespace ts {
14991501
kind: SyntaxKind.ArrowFunction;
15001502
equalsGreaterThanToken: EqualsGreaterThanToken;
15011503
body: ConciseBody;
1504+
name: never;
15021505
}
15031506

15041507
// 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 {
21562159
export interface ExportDeclaration extends DeclarationStatement {
21572160
kind: SyntaxKind.ExportDeclaration;
21582161
parent?: SourceFile | ModuleBlock;
2162+
/** Will not be assigned in the case of `export * from "foo";` */
21592163
exportClause?: NamedExports;
21602164
/** If this is not a StringLiteral it will be a grammar error. */
21612165
moduleSpecifier?: Expression;
@@ -2878,7 +2882,7 @@ namespace ts {
28782882
*/
28792883
/* @internal */ isArrayLikeType(type: Type): boolean;
28802884
/* @internal */ getAllPossiblePropertiesOfTypes(type: ReadonlyArray<Type>): Symbol[];
2881-
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags): Symbol | undefined;
2885+
/* @internal */ resolveName(name: string, location: Node, meaning: SymbolFlags, excludeGlobals: boolean): Symbol | undefined;
28822886
/* @internal */ getJsxNamespace(): string;
28832887

28842888
/**

src/compiler/utilities.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace ts {
55
export const emptyArray: never[] = [] as never[];
66
export const resolvingEmptyArray: never[] = [] as never[];
77
export const emptyMap: ReadonlyMap<never> = createMap<never>();
8+
export const emptyUnderscoreEscapedMap: ReadonlyUnderscoreEscapedMap<never> = emptyMap as ReadonlyUnderscoreEscapedMap<never>;
89

910
export const externalHelpersModuleNameText = "tslib";
1011

@@ -1419,6 +1420,8 @@ namespace ts {
14191420
* exactly one argument (of the form 'require("name")').
14201421
* This function does not test if the node is in a JavaScript file or not.
14211422
*/
1423+
export function isRequireCall(callExpression: Node, checkArgumentIsStringLiteral: true): callExpression is CallExpression & { expression: Identifier, arguments: [StringLiteralLike] };
1424+
export function isRequireCall(callExpression: Node, checkArgumentIsStringLiteral: boolean): callExpression is CallExpression;
14221425
export function isRequireCall(callExpression: Node, checkArgumentIsStringLiteral: boolean): callExpression is CallExpression {
14231426
if (callExpression.kind !== SyntaxKind.CallExpression) {
14241427
return false;
@@ -1456,7 +1459,7 @@ namespace ts {
14561459
return false;
14571460
}
14581461

1459-
export function getRightMostAssignedExpression(node: Node) {
1462+
export function getRightMostAssignedExpression(node: Expression): Expression {
14601463
while (isAssignmentExpression(node, /*excludeCompoundAssignements*/ true)) {
14611464
node = node.right;
14621465
}

src/harness/fourslash.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,7 @@ namespace FourSlash {
454454
const ranges = this.getRanges();
455455
assert(ranges.length);
456456
for (const range of ranges) {
457-
this.goToRangeStart(range);
457+
this.selectRange(range);
458458
action();
459459
}
460460
}
@@ -482,6 +482,11 @@ namespace FourSlash {
482482
this.selectionEnd = end.position;
483483
}
484484

485+
public selectRange(range: Range): void {
486+
this.goToRangeStart(range);
487+
this.selectionEnd = range.end;
488+
}
489+
485490
public moveCaretRight(count = 1) {
486491
this.currentCaretPosition += count;
487492
this.currentCaretPosition = Math.min(this.currentCaretPosition, this.getFileContent(this.activeFile.fileName).length);
@@ -3835,6 +3840,10 @@ namespace FourSlashInterface {
38353840
public select(startMarker: string, endMarker: string) {
38363841
this.state.select(startMarker, endMarker);
38373842
}
3843+
3844+
public selectRange(range: FourSlash.Range): void {
3845+
this.state.selectRange(range);
3846+
}
38383847
}
38393848

38403849
export class VerifyNegatable {

0 commit comments

Comments
 (0)