Merge pull request #6684 from Microsoft/fixDecoratedClassName

Fix ES6 decorated class double binding.
This commit is contained in:
Ron Buckton
2016-01-29 17:37:24 -08:00
42 changed files with 776 additions and 86 deletions

View File

@@ -7192,11 +7192,32 @@ namespace ts {
markAliasSymbolAsReferenced(symbol);
}
const localOrExportSymbol = getExportSymbolOfValueSymbolIfExported(symbol);
// Due to the emit for class decorators, any reference to the class from inside of the class body
// must instead be rewritten to point to a temporary variable to avoid issues with the double-bind
// behavior of class names in ES6.
if (languageVersion === ScriptTarget.ES6
&& localOrExportSymbol.flags & SymbolFlags.Class
&& localOrExportSymbol.valueDeclaration.kind === SyntaxKind.ClassDeclaration
&& nodeIsDecorated(localOrExportSymbol.valueDeclaration)) {
let container = getContainingClass(node);
while (container !== undefined) {
if (container === localOrExportSymbol.valueDeclaration && container.name !== node) {
getNodeLinks(container).flags |= NodeCheckFlags.ClassWithBodyScopedClassBinding;
getNodeLinks(node).flags |= NodeCheckFlags.BodyScopedClassBinding;
break;
}
container = getContainingClass(container);
}
}
checkCollisionWithCapturedSuperVariable(node, node);
checkCollisionWithCapturedThisVariable(node, node);
checkNestedBlockScopedBinding(node, symbol);
return getNarrowedTypeOfSymbol(getExportSymbolOfValueSymbolIfExported(symbol), node);
return getNarrowedTypeOfSymbol(localOrExportSymbol, node);
}
function isInsideFunction(node: Node, threshold: Node): boolean {
@@ -7238,7 +7259,7 @@ namespace ts {
if (containedInIterationStatement) {
if (usedInFunction) {
// mark iteration statement as containing block-scoped binding captured in some function
// mark iteration statement as containing block-scoped binding captured in some function
getNodeLinks(current).flags |= NodeCheckFlags.LoopWithCapturedBlockScopedBinding;
}
// set 'declared inside loop' bit on the block-scoped binding
@@ -15758,7 +15779,7 @@ namespace ts {
// - binding is not top level - top level bindings never collide with anything
// AND
// - binding is not declared in loop, should be renamed to avoid name reuse across siblings
// let a, b
// let a, b
// { let x = 1; a = () => x; }
// { let x = 100; b = () => x; }
// console.log(a()); // should print '1'

View File

@@ -482,6 +482,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
let generatedNameSet: Map<string>;
let nodeToGeneratedName: string[];
let computedPropertyNamesToGeneratedNames: string[];
let decoratedClassAliases: string[];
let convertedLoopState: ConvertedLoopState;
@@ -532,6 +533,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
sourceMap.initialize(jsFilePath, sourceMapFilePath, sourceFiles, isBundledEmit);
generatedNameSet = {};
nodeToGeneratedName = [];
decoratedClassAliases = [];
isOwnFileEmit = !isBundledEmit;
// Emit helpers from all the files
@@ -561,6 +563,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
contextObjectForFile = undefined;
generatedNameSet = undefined;
nodeToGeneratedName = undefined;
decoratedClassAliases = undefined;
computedPropertyNamesToGeneratedNames = undefined;
convertedLoopState = undefined;
extendsEmitted = false;
@@ -1532,13 +1535,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
}
}
if (languageVersion !== ScriptTarget.ES6) {
if (languageVersion < ScriptTarget.ES6) {
const declaration = resolver.getReferencedDeclarationWithCollidingName(node);
if (declaration) {
write(getGeneratedNameForNode(declaration.name));
return;
}
}
else if (resolver.getNodeCheckFlags(node) & NodeCheckFlags.BodyScopedClassBinding) {
// Due to the emit for class decorators, any reference to the class from inside of the class body
// must instead be rewritten to point to a temporary variable to avoid issues with the double-bind
// behavior of class names in ES6.
const declaration = resolver.getReferencedValueDeclaration(node);
if (declaration) {
const classAlias = decoratedClassAliases[getNodeId(declaration)];
if (classAlias !== undefined) {
write(classAlias);
return;
}
}
}
}
if (nodeIsSynthesized(node)) {
@@ -4070,7 +4086,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
let initializer = node.initializer;
if (!initializer &&
languageVersion < ScriptTarget.ES6 &&
// for names - binding patterns that lack initializer there is no point to emit explicit initializer
// for names - binding patterns that lack initializer there is no point to emit explicit initializer
// since downlevel codegen for destructuring will fail in the absence of initializer so all binding elements will say uninitialized
node.name.kind === SyntaxKind.Identifier) {
@@ -4091,11 +4107,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
// explicitly initialized. One particular case: non-captured binding declared inside loop body (but not in loop initializer)
// let x;
// for (;;) {
// let x;
// let x;
// }
// in downlevel codegen inner 'x' will be renamed so it won't collide with outer 'x' however it will should be reset on every iteration
// as if it was declared anew.
// * Why non-captured binding - because if loop contains block scoped binding captured in some function then loop body will be rewritten
// as if it was declared anew.
// * Why non-captured binding - because if loop contains block scoped binding captured in some function then loop body will be rewritten
// to have a fresh scope on every iteration so everything will just work.
// * Why loop initializer is excluded - since we've introduced a fresh name it already will be undefined.
const isCapturedInFunction = flags & NodeCheckFlags.CapturedBlockScopedBinding;
@@ -5109,64 +5125,107 @@ const _super = (function (geti, seti) {
}
function emitClassLikeDeclarationForES6AndHigher(node: ClassLikeDeclaration) {
let decoratedClassAlias: string;
const thisNodeIsDecorated = nodeIsDecorated(node);
if (node.kind === SyntaxKind.ClassDeclaration) {
if (thisNodeIsDecorated) {
// To preserve the correct runtime semantics when decorators are applied to the class,
// the emit needs to follow one of the following rules:
// When we emit an ES6 class that has a class decorator, we must tailor the
// emit to certain specific cases.
//
// * For a local class declaration:
// In the simplest case, we emit the class declaration as a let declaration, and
// evaluate decorators after the close of the class body:
//
// @dec class C {
// }
// TypeScript | Javascript
// --------------------------------|------------------------------------
// @dec | let C = class C {
// class C { | }
// } | C = __decorate([dec], C);
// --------------------------------|------------------------------------
// @dec | export let C = class C {
// export class C { | }
// } | C = __decorate([dec], C);
// ---------------------------------------------------------------------
// [Example 1]
//
// The emit should be:
// If a class declaration contains a reference to itself *inside* of the class body,
// this introduces two bindings to the class: One outside of the class body, and one
// inside of the class body. If we apply decorators as in [Example 1] above, there
// is the possibility that the decorator `dec` will return a new value for the
// constructor, which would result in the binding inside of the class no longer
// pointing to the same reference as the binding outside of the class.
//
// let C = class {
// };
// C = __decorate([dec], C);
// As a result, we must instead rewrite all references to the class *inside* of the
// class body to instead point to a local temporary alias for the class:
//
// * For an exported class declaration:
// TypeScript | Javascript
// --------------------------------|------------------------------------
// @dec | let C_1;
// class C { | let C = C_1 = class C {
// static x() { return C.y; } | static x() { return C_1.y; }
// static y = 1; | }
// } | C.y = 1;
// | C = C_1 = __decorate([dec], C);
// --------------------------------|------------------------------------
// @dec | let C_1;
// export class C { | export let C = C_1 = class C {
// static x() { return C.y; } | static x() { return C_1.y; }
// static y = 1; | }
// } | C.y = 1;
// | C = C_1 = __decorate([dec], C);
// ---------------------------------------------------------------------
// [Example 2]
//
// @dec export class C {
// }
// If a class declaration is the default export of a module, we instead emit
// the export after the decorated declaration:
//
// The emit should be:
// TypeScript | Javascript
// --------------------------------|------------------------------------
// @dec | let default_1 = class {
// export default class { | }
// } | default_1 = __decorate([dec], default_1);
// | export default default_1;
// --------------------------------|------------------------------------
// @dec | let C = class C {
// export default class { | }
// } | C = __decorate([dec], C);
// | export default C;
// ---------------------------------------------------------------------
// [Example 3]
//
// export let C = class {
// };
// C = __decorate([dec], C);
// If the class declaration is the default export and a reference to itself
// inside of the class body, we must emit both an alias for the class *and*
// move the export after the declaration:
//
// * For a default export of a class declaration with a name:
//
// @dec default export class C {
// }
//
// The emit should be:
//
// let C = class {
// }
// C = __decorate([dec], C);
// export default C;
//
// * For a default export of a class declaration without a name:
//
// @dec default export class {
// }
//
// The emit should be:
//
// let _default = class {
// }
// _default = __decorate([dec], _default);
// export default _default;
// TypeScript | Javascript
// --------------------------------|------------------------------------
// @dec | let C_1;
// export default class C { | let C = C_1 = class C {
// static x() { return C.y; } | static x() { return C_1.y; }
// static y = 1; | }
// } | C.y = 1;
// | C = C_1 = __decorate([dec], C);
// | export default C;
// ---------------------------------------------------------------------
// [Example 4]
//
if (resolver.getNodeCheckFlags(node) & NodeCheckFlags.ClassWithBodyScopedClassBinding) {
decoratedClassAlias = unescapeIdentifier(makeUniqueName(node.name ? node.name.text : "default"));
decoratedClassAliases[getNodeId(node)] = decoratedClassAlias;
write(`let ${decoratedClassAlias};`);
writeLine();
}
if (isES6ExportedDeclaration(node) && !(node.flags & NodeFlags.Default)) {
write("export ");
}
write("let ");
emitDeclarationName(node);
if (decoratedClassAlias !== undefined) {
write(` = ${decoratedClassAlias}`);
}
write(" = ");
}
else if (isES6ExportedDeclaration(node)) {
@@ -5205,7 +5264,7 @@ const _super = (function (geti, seti) {
// emit name if
// - node has a name
// - this is default export with static initializers
if ((node.name || (node.flags & NodeFlags.Default && (staticProperties.length > 0 || modulekind !== ModuleKind.ES6))) && !thisNodeIsDecorated) {
if (node.name || (node.flags & NodeFlags.Default && (staticProperties.length > 0 || modulekind !== ModuleKind.ES6) && !thisNodeIsDecorated)) {
write(" ");
emitDeclarationName(node);
}
@@ -5225,16 +5284,8 @@ const _super = (function (geti, seti) {
writeLine();
emitToken(SyntaxKind.CloseBraceToken, node.members.end);
// TODO(rbuckton): Need to go back to `let _a = class C {}` approach, removing the defineProperty call for now.
// For a decorated class, we need to assign its name (if it has one). This is because we emit
// the class as a class expression to avoid the double-binding of the identifier:
//
// let C = class {
// }
// Object.defineProperty(C, "name", { value: "C", configurable: true });
//
if (thisNodeIsDecorated) {
decoratedClassAliases[getNodeId(node)] = undefined;
write(";");
}
@@ -5259,7 +5310,7 @@ const _super = (function (geti, seti) {
else {
writeLine();
emitPropertyDeclarations(node, staticProperties);
emitDecoratorsOfClass(node);
emitDecoratorsOfClass(node, decoratedClassAlias);
}
if (!(node.flags & NodeFlags.Export)) {
@@ -5333,7 +5384,7 @@ const _super = (function (geti, seti) {
emitMemberFunctionsForES5AndLower(node);
emitPropertyDeclarations(node, getInitializedProperties(node, /*isStatic*/ true));
writeLine();
emitDecoratorsOfClass(node);
emitDecoratorsOfClass(node, /*decoratedClassAlias*/ undefined);
writeLine();
emitToken(SyntaxKind.CloseBraceToken, node.members.end, () => {
write("return ");
@@ -5375,13 +5426,13 @@ const _super = (function (geti, seti) {
}
}
function emitDecoratorsOfClass(node: ClassLikeDeclaration) {
function emitDecoratorsOfClass(node: ClassLikeDeclaration, decoratedClassAlias: string) {
emitDecoratorsOfMembers(node, /*staticFlag*/ 0);
emitDecoratorsOfMembers(node, NodeFlags.Static);
emitDecoratorsOfConstructor(node);
emitDecoratorsOfConstructor(node, decoratedClassAlias);
}
function emitDecoratorsOfConstructor(node: ClassLikeDeclaration) {
function emitDecoratorsOfConstructor(node: ClassLikeDeclaration, decoratedClassAlias: string) {
const decorators = node.decorators;
const constructor = getFirstConstructorWithBody(node);
const firstParameterDecorator = constructor && forEach(constructor.parameters, parameter => parameter.decorators);
@@ -5405,6 +5456,10 @@ const _super = (function (geti, seti) {
writeLine();
emitStart(node.decorators || firstParameterDecorator);
emitDeclarationName(node);
if (decoratedClassAlias !== undefined) {
write(` = ${decoratedClassAlias}`);
}
write(" = __decorate([");
increaseIndent();
writeLine();

View File

@@ -2054,23 +2054,23 @@ namespace ts {
/* @internal */
export const enum NodeCheckFlags {
TypeChecked = 0x00000001, // Node has been type checked
LexicalThis = 0x00000002, // Lexical 'this' reference
CaptureThis = 0x00000004, // Lexical 'this' used in body
SuperInstance = 0x00000100, // Instance 'super' reference
SuperStatic = 0x00000200, // Static 'super' reference
ContextChecked = 0x00000400, // Contextual types have been assigned
AsyncMethodWithSuper = 0x00000800, // An async method that reads a value from a member of 'super'.
AsyncMethodWithSuperBinding = 0x00001000, // An async method that assigns a value to a member of 'super'.
CaptureArguments = 0x00002000, // Lexical 'arguments' used in body (for async functions)
// Values for enum members have been computed, and any errors have been reported for them.
EnumValuesComputed = 0x00004000,
LexicalModuleMergesWithClass = 0x00008000, // Instantiated lexical module declaration is merged with a previous class declaration.
LoopWithCapturedBlockScopedBinding = 0x00010000, // Loop that contains block scoped variable captured in closure
CapturedBlockScopedBinding = 0x00020000, // Block-scoped binding that is captured in some function
BlockScopedBindingInLoop = 0x00040000, // Block-scoped binding with declaration nested inside iteration statement
HasSeenSuperCall = 0x00080000, // Set during the binding when encounter 'super'
TypeChecked = 0x00000001, // Node has been type checked
LexicalThis = 0x00000002, // Lexical 'this' reference
CaptureThis = 0x00000004, // Lexical 'this' used in body
SuperInstance = 0x00000100, // Instance 'super' reference
SuperStatic = 0x00000200, // Static 'super' reference
ContextChecked = 0x00000400, // Contextual types have been assigned
AsyncMethodWithSuper = 0x00000800, // An async method that reads a value from a member of 'super'.
AsyncMethodWithSuperBinding = 0x00001000, // An async method that assigns a value to a member of 'super'.
CaptureArguments = 0x00002000, // Lexical 'arguments' used in body (for async functions)
EnumValuesComputed = 0x00004000, // Values for enum members have been computed, and any errors have been reported for them.
LexicalModuleMergesWithClass = 0x00008000, // Instantiated lexical module declaration is merged with a previous class declaration.
LoopWithCapturedBlockScopedBinding = 0x00010000, // Loop that contains block scoped variable captured in closure
CapturedBlockScopedBinding = 0x00020000, // Block-scoped binding that is captured in some function
BlockScopedBindingInLoop = 0x00040000, // Block-scoped binding with declaration nested inside iteration statement
HasSeenSuperCall = 0x00080000, // Set during the binding when encounter 'super'
ClassWithBodyScopedClassBinding = 0x00100000, // Decorated class that contains a binding to itself inside of the class body.
BodyScopedClassBinding = 0x00200000, // Binding to a decorated class inside of the class's body.
}
/* @internal */