Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add refactoring to convert CommonJS module to ES6 module #19916

Merged
7 commits merged into from
Jan 9, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 30 additions & 24 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ...
Expand All @@ -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) {
Expand Down Expand Up @@ -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
*
Expand Down
15 changes: 10 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -1209,7 +1211,9 @@ namespace ts {
}
}

result = lookup(globals, name, meaning);
if (!excludeGlobals) {
result = lookup(globals, name, meaning);
}
}

if (!result) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
27 changes: 20 additions & 7 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can pass a function that only takes x, right?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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);
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3937,5 +3937,9 @@
"Use synthetic 'default' member.": {
"category": "Message",
"code": 95016
},
"Convert to ES6 module": {
"category": "Message",
"code": 95017
}
}
28 changes: 8 additions & 20 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/compiler/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 + "");
}
Expand All @@ -101,7 +101,7 @@ namespace ts {
return node;
}

function createLiteralFromNode(sourceNode: StringLiteral | NumericLiteral | Identifier): StringLiteral {
function createLiteralFromNode(sourceNode: StringLiteralLike | NumericLiteral | Identifier): StringLiteral {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't internal, but it uses StringLiteralLike. You can technically make an internal overload for it, or use NoSubstitutionTemplateLiteral

Copy link
Author

@ghost ghost Dec 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't exported so it won't show up in the public API. API baseline tests would have told us if we tried to use an internal type in a public api.

const node = createStringLiteral(getTextOfIdentifierOrLiteral(sourceNode));
node.textSourceNode = sourceNode;
return node;
Expand Down Expand Up @@ -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);
}

Expand Down
8 changes: 6 additions & 2 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

/**
Expand Down
5 changes: 4 additions & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
11 changes: 10 additions & 1 deletion src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down
Loading