Skip to content

Class emit: cached repeat prototype sets in a variable #33363

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

Closed
  •  
  •  
  •  
89 changes: 84 additions & 5 deletions src/compiler/transformers/es2015.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1543,21 +1543,28 @@ namespace ts {
* @param node The ClassExpression or ClassDeclaration node.
*/
function addClassMembers(statements: Statement[], node: ClassExpression | ClassDeclaration): void {
if (!node.members.length) {
return;
}

const prototypeStoragePlacement = statements.length;
let prototypeStorageName: Identifier | PropertyAccessExpression;

for (const member of node.members) {
switch (member.kind) {
case SyntaxKind.SemicolonClassElement:
statements.push(transformSemicolonClassElementToStatement(<SemicolonClassElement>member));
break;

case SyntaxKind.MethodDeclaration:
statements.push(transformClassMethodDeclarationToStatement(getClassMemberPrefix(node, member), <MethodDeclaration>member, node));
statements.push(transformClassMethodDeclarationToStatement(getClassMemberPrefix(node, member, getPrototypeStorageName), <MethodDeclaration>member, node));
break;

case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
const accessors = getAllAccessorDeclarations(node.members, <AccessorDeclaration>member);
if (member === accessors.firstAccessor) {
statements.push(transformAccessorsToStatement(getClassMemberPrefix(node, member), accessors, node));
statements.push(transformAccessorsToStatement(getClassMemberPrefix(node, member, getPrototypeStorageName), accessors, node));
}

break;
Expand All @@ -1571,6 +1578,78 @@ namespace ts {
break;
}
}

/**
* Lazily creates the way class members will access the class prototype.
*/
function getPrototypeStorageName() {
if (prototypeStorageName) {
return prototypeStorageName;
}

const originalPrototypeAccess = createPropertyAccess(getInternalName(node), "prototype");

// If the class has exactly one non-static member, it'll access prototype members on itself:
// ClassName.prototype.member = ...
if (!membersContainMultipleUniqueNames(node.members.filter(classMemberAssignsToPrototype))) {
prototypeStorageName = originalPrototypeAccess;
return prototypeStorageName;
}

// Since it has multiple non-static members, it'll store that prototype as a variable that can be minified:
// var proto = ClassName.prototype;
// proto.memberOne = ...
// proto.memberTwo = ...
prototypeStorageName = createUniqueName("proto");

statements.splice(
prototypeStoragePlacement,
0,
createVariableStatement(
/*modifiers*/ undefined,
createVariableDeclarationList([
createVariableDeclaration(
prototypeStorageName,
/*type*/ undefined,
originalPrototypeAccess,
)
])
)
);

return prototypeStorageName;
}
}

function classMemberAssignsToPrototype(node: ClassElement) {
return !hasModifier(node, ModifierFlags.Static) && !isConstructorDeclaration(node);
}

function membersContainMultipleUniqueNames(members: ClassElement[]) {
if (members.length <= 1) {
return false;
}

let foundName: string | undefined;

for (const member of members) {
// If a name isn't immediately identifiable, we assume it's unique
if (!member.name || !isPropertyNameLiteral(member.name)) {
return true;
}

const text = getTextOfIdentifierOrLiteral(member.name);
if (foundName === undefined) {
foundName = text;
continue;
}

if (text !== foundName) {
return true;
}
}

return false;
}

/**
Expand Down Expand Up @@ -4302,10 +4381,10 @@ namespace ts {
return node;
}

function getClassMemberPrefix(node: ClassExpression | ClassDeclaration, member: ClassElement) {
return hasModifier(member, ModifierFlags.Static)
function getClassMemberPrefix(node: ClassExpression | ClassDeclaration, member: ClassElement, getPrototypeStorageName: () => LeftHandSideExpression) {
return member && hasModifier(member, ModifierFlags.Static)
? getInternalName(node)
: createPropertyAccess(getInternalName(node), "prototype");
: getPrototypeStorageName();
}

function hasSynthesizedDefaultSuperCall(constructor: ConstructorDeclaration | undefined, hasExtendsClause: boolean) {
Expand Down
5 changes: 3 additions & 2 deletions tests/baselines/reference/ES5For-ofTypeCheck10.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ for (var v of new StringIterator) { }
var StringIterator = /** @class */ (function () {
function StringIterator() {
}
StringIterator.prototype.next = function () {
var proto_1 = StringIterator.prototype;
proto_1.next = function () {
return {
done: true,
value: ""
};
};
StringIterator.prototype[Symbol.iterator] = function () {
proto_1[Symbol.iterator] = function () {
return this;
};
return StringIterator;
Expand Down
5 changes: 3 additions & 2 deletions tests/baselines/reference/abstractProperty.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ var C = /** @class */ (function (_super) {
_this.ro = "readonly please";
return _this;
}
Object.defineProperty(C.prototype, "prop", {
var proto_1 = C.prototype;
Object.defineProperty(proto_1, "prop", {
get: function () { return "foo"; },
set: function (v) { },
enumerable: true,
configurable: true
});
C.prototype.m = function () { };
proto_1.m = function () { };
return C;
}(B));
5 changes: 3 additions & 2 deletions tests/baselines/reference/abstractPropertyNegative.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,14 @@ var WrongTypeAccessorImpl2 = /** @class */ (function (_super) {
var AbstractAccessorMismatch = /** @class */ (function () {
function AbstractAccessorMismatch() {
}
Object.defineProperty(AbstractAccessorMismatch.prototype, "p1", {
var proto_1 = AbstractAccessorMismatch.prototype;
Object.defineProperty(proto_1, "p1", {
set: function (val) { },
enumerable: true,
configurable: true
});
;
Object.defineProperty(AbstractAccessorMismatch.prototype, "p2", {
Object.defineProperty(proto_1, "p2", {
get: function () { return "should work"; },
enumerable: true,
configurable: true
Expand Down
7 changes: 4 additions & 3 deletions tests/baselines/reference/accessibilityModifiers.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,14 @@ var D = /** @class */ (function () {
var E = /** @class */ (function () {
function E() {
}
E.prototype.method = function () { };
Object.defineProperty(E.prototype, "getter", {
var proto_1 = E.prototype;
proto_1.method = function () { };
Object.defineProperty(proto_1, "getter", {
get: function () { return 0; },
enumerable: true,
configurable: true
});
Object.defineProperty(E.prototype, "setter", {
Object.defineProperty(proto_1, "setter", {
set: function (a) { },
enumerable: true,
configurable: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,26 @@ class LanguageSpec_section_4_5_error_cases {
var LanguageSpec_section_4_5_error_cases = /** @class */ (function () {
function LanguageSpec_section_4_5_error_cases() {
}
Object.defineProperty(LanguageSpec_section_4_5_error_cases.prototype, "AnnotatedSetter_SetterFirst", {
var proto_1 = LanguageSpec_section_4_5_error_cases.prototype;
Object.defineProperty(proto_1, "AnnotatedSetter_SetterFirst", {
get: function () { return ""; },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_error_cases.prototype, "AnnotatedSetter_SetterLast", {
Object.defineProperty(proto_1, "AnnotatedSetter_SetterLast", {
get: function () { return ""; },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_error_cases.prototype, "AnnotatedGetter_GetterFirst", {
Object.defineProperty(proto_1, "AnnotatedGetter_GetterFirst", {
get: function () { return ""; },
set: function (aStr) { aStr = 0; },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_error_cases.prototype, "AnnotatedGetter_GetterLast", {
Object.defineProperty(proto_1, "AnnotatedGetter_GetterLast", {
get: function () { return ""; },
set: function (aStr) { aStr = 0; },
enumerable: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,37 +53,38 @@ var B = /** @class */ (function (_super) {
var LanguageSpec_section_4_5_inference = /** @class */ (function () {
function LanguageSpec_section_4_5_inference() {
}
Object.defineProperty(LanguageSpec_section_4_5_inference.prototype, "InferredGetterFromSetterAnnotation", {
var proto_1 = LanguageSpec_section_4_5_inference.prototype;
Object.defineProperty(proto_1, "InferredGetterFromSetterAnnotation", {
get: function () { return new B(); },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_inference.prototype, "InferredGetterFromSetterAnnotation_GetterFirst", {
Object.defineProperty(proto_1, "InferredGetterFromSetterAnnotation_GetterFirst", {
get: function () { return new B(); },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_inference.prototype, "InferredFromGetter", {
Object.defineProperty(proto_1, "InferredFromGetter", {
get: function () { return new B(); },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_inference.prototype, "InferredFromGetter_SetterFirst", {
Object.defineProperty(proto_1, "InferredFromGetter_SetterFirst", {
get: function () { return new B(); },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_inference.prototype, "InferredSetterFromGetterAnnotation", {
Object.defineProperty(proto_1, "InferredSetterFromGetterAnnotation", {
get: function () { return new B(); },
set: function (a) { },
enumerable: true,
configurable: true
});
Object.defineProperty(LanguageSpec_section_4_5_inference.prototype, "InferredSetterFromGetterAnnotation_GetterFirst", {
Object.defineProperty(proto_1, "InferredSetterFromGetterAnnotation_GetterFirst", {
get: function () { return new B(); },
set: function (a) { },
enumerable: true,
Expand Down
10 changes: 6 additions & 4 deletions tests/baselines/reference/ambiguousCallsWhereReturnTypesAgree.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,22 @@ class TestClass2 {
var TestClass = /** @class */ (function () {
function TestClass() {
}
TestClass.prototype.bar = function (x) {
var proto_1 = TestClass.prototype;
proto_1.bar = function (x) {
};
TestClass.prototype.foo = function (x) {
proto_1.foo = function (x) {
this.bar(x); // should not error
};
return TestClass;
}());
var TestClass2 = /** @class */ (function () {
function TestClass2() {
}
TestClass2.prototype.bar = function (x) {
var proto_2 = TestClass2.prototype;
proto_2.bar = function (x) {
return 0;
};
TestClass2.prototype.foo = function (x) {
proto_2.foo = function (x) {
return this.bar(x); // should not error
};
return TestClass2;
Expand Down
5 changes: 3 additions & 2 deletions tests/baselines/reference/arrayAssignmentTest1.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,9 @@ var __extends = (this && this.__extends) || (function () {
var C1 = /** @class */ (function () {
function C1() {
}
C1.prototype.IM1 = function () { return null; };
C1.prototype.C1M1 = function () { return null; };
var proto_1 = C1.prototype;
proto_1.IM1 = function () { return null; };
proto_1.C1M1 = function () { return null; };
return C1;
}());
var C2 = /** @class */ (function (_super) {
Expand Down
5 changes: 3 additions & 2 deletions tests/baselines/reference/arrayAssignmentTest2.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ var __extends = (this && this.__extends) || (function () {
var C1 = /** @class */ (function () {
function C1() {
}
C1.prototype.IM1 = function () { return null; };
C1.prototype.C1M1 = function () { return null; };
var proto_1 = C1.prototype;
proto_1.IM1 = function () { return null; };
proto_1.C1M1 = function () { return null; };
return C1;
}());
var C2 = /** @class */ (function (_super) {
Expand Down
5 changes: 3 additions & 2 deletions tests/baselines/reference/arrayAssignmentTest5.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ var Test;
var Bug = /** @class */ (function () {
function Bug() {
}
Bug.prototype.onEnter = function (line, state, offset) {
var proto_1 = Bug.prototype;
proto_1.onEnter = function (line, state, offset) {
var lineTokens = this.tokenize(line, state, true);
var tokens = lineTokens.tokens;
if (tokens.length === 0) {
return this.onEnter(line, tokens, offset); // <== this should produce an error since onEnter can not be called with (string, IStateToken[], offset)
}
};
Bug.prototype.tokenize = function (line, state, includeStates) {
proto_1.tokenize = function (line, state, includeStates) {
return null;
};
return Bug;
Expand Down
10 changes: 6 additions & 4 deletions tests/baselines/reference/arrayBestCommonTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,12 @@ var EmptyTypes;
var f = /** @class */ (function () {
function f() {
}
f.prototype.voidIfAny = function (x, y) {
var proto_1 = f.prototype;
proto_1.voidIfAny = function (x, y) {
if (y === void 0) { y = false; }
return null;
};
f.prototype.x = function () {
proto_1.x = function () {
(this.voidIfAny([4, 2][0]));
(this.voidIfAny([4, 2, undefined][0]));
(this.voidIfAny([undefined, 2, 4][0]));
Expand Down Expand Up @@ -202,11 +203,12 @@ var NonEmptyTypes;
var f = /** @class */ (function () {
function f() {
}
f.prototype.voidIfAny = function (x, y) {
var proto_2 = f.prototype;
proto_2.voidIfAny = function (x, y) {
if (y === void 0) { y = false; }
return null;
};
f.prototype.x = function () {
proto_2.x = function () {
(this.voidIfAny([4, 2][0]));
(this.voidIfAny([4, 2, undefined][0]));
(this.voidIfAny([undefined, 2, 4][0]));
Expand Down
Loading