Skip to content

Commit

Permalink
feat(7342): class expression decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk committed Jun 4, 2021
1 parent 4b235ec commit 6751fc1
Show file tree
Hide file tree
Showing 189 changed files with 2,895 additions and 386 deletions.
56 changes: 37 additions & 19 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ namespace ts {
/**
* Tests whether we should emit a __decorate call for a class declaration.
*/
function shouldEmitDecorateCallForClass(node: ClassDeclaration) {
function shouldEmitDecorateCallForClass(node: ClassDeclaration | ClassExpression) {
if (node.decorators && node.decorators.length > 0) {
return true;
}
Expand All @@ -589,7 +589,7 @@ namespace ts {
return parameter.decorators !== undefined && parameter.decorators.length > 0;
}

function getClassFacts(node: ClassDeclaration, staticProperties: readonly PropertyDeclaration[]) {
function getClassFacts(node: ClassDeclaration | ClassExpression, staticProperties: readonly PropertyDeclaration[]) {
let facts = ClassFacts.None;
if (some(staticProperties)) facts |= ClassFacts.HasStaticInitializedProperties;
const extendsClauseElement = getEffectiveBaseTypeNode(node);
Expand Down Expand Up @@ -635,8 +635,8 @@ namespace ts {


// Write any decorators of the node.
addClassElementDecorationStatements(statements, node, /*isStatic*/ false);
addClassElementDecorationStatements(statements, node, /*isStatic*/ true);
addClassElementDecorationStatements(statements, createClassPrototype(factory.getDeclarationName(node)), node, /*isStatic*/ false);
addClassElementDecorationStatements(statements, factory.getDeclarationName(node), node, /*isStatic*/ true);
addConstructorDecorationStatement(statements, node);

if (facts & ClassFacts.UseImmediatelyInvokedFunctionExpression) {
Expand Down Expand Up @@ -880,6 +880,9 @@ namespace ts {
return visitEachChild(node, visitor, context);
}

const staticProperties = getProperties(node, /*requireInitializer*/ true, /*isStatic*/ true);
const facts = getClassFacts(node, staticProperties);

const classExpression = factory.createClassExpression(
/*decorators*/ undefined,
/*modifiers*/ undefined,
Expand All @@ -889,6 +892,14 @@ namespace ts {
transformClassMembers(node)
);

if (facts & ClassFacts.HasMemberDecorators) {
const temp = factory.createTempVariable(hoistVariableDeclaration);
const expressions: Expression[] = [factory.createAssignment(temp, classExpression)];
addClassElementDecorationExpressions(expressions, createClassPrototype(temp), node, /*isStatic*/ false);
addClassElementDecorationExpressions(expressions, temp, node, /*isStatic*/ true);
return factory.inlineExpressions([...expressions, temp]);
}

setOriginalNode(classExpression, node);
setTextRange(classExpression, node);

Expand Down Expand Up @@ -1124,31 +1135,44 @@ namespace ts {
return decoratorExpressions;
}

/**
* Generates expressions used to apply decorators to either the static or instance member of a class.
*
* @param target
* @param node The class node.
* @param isStatic A value indicating whether to generate expressions for static or instance members.
*/
function addClassElementDecorationExpressions(expressions: Expression[], target: Identifier | PropertyAccessExpression, node: ClassLikeDeclaration, isStatic: boolean) {
addRange(expressions, map(generateClassElementDecorationExpressions(target, node, isStatic), expression => expression));
}

/**
* Generates statements used to apply decorators to either the static or instance members
* of a class.
*
* @param target
* @param node The class node.
* @param isStatic A value indicating whether to generate statements for static or
* instance members.
*/
function addClassElementDecorationStatements(statements: Statement[], node: ClassDeclaration, isStatic: boolean) {
addRange(statements, map(generateClassElementDecorationExpressions(node, isStatic), expressionToStatement));
function addClassElementDecorationStatements(statements: Statement[], target: Identifier | PropertyAccessExpression, node: ClassLikeDeclaration, isStatic: boolean) {
addRange(statements, map(generateClassElementDecorationExpressions(target, node, isStatic), expressionToStatement));
}

/**
* Generates expressions used to apply decorators to either the static or instance members
* of a class.
*
* @param target
* @param node The class node.
* @param isStatic A value indicating whether to generate expressions for static or
* instance members.
*/
function generateClassElementDecorationExpressions(node: ClassExpression | ClassDeclaration, isStatic: boolean) {
function generateClassElementDecorationExpressions(target: Identifier | PropertyAccessExpression, node: ClassExpression | ClassDeclaration, isStatic: boolean) {
const members = getDecoratedClassElements(node, isStatic);
let expressions: Expression[] | undefined;
for (const member of members) {
const expression = generateClassElementDecorationExpression(node, member);
const expression = generateClassElementDecorationExpression(target, node, member);
if (expression) {
if (!expressions) {
expressions = [expression];
Expand All @@ -1164,10 +1188,11 @@ namespace ts {
/**
* Generates an expression used to evaluate class element decorators at runtime.
*
* @param target
* @param node The class node that contains the member.
* @param member The class member.
*/
function generateClassElementDecorationExpression(node: ClassExpression | ClassDeclaration, member: ClassElement) {
function generateClassElementDecorationExpression(target: Identifier | PropertyAccessExpression, node: ClassExpression | ClassDeclaration, member: ClassElement) {
const allDecorators = getAllDecoratorsOfClassElement(node, member);
const decoratorExpressions = transformAllDecoratorsOfDeclaration(member, node, allDecorators);
if (!decoratorExpressions) {
Expand Down Expand Up @@ -1205,7 +1230,6 @@ namespace ts {
// ], C.prototype, "prop");
//

const prefix = getClassMemberPrefix(node, member);
const memberName = getExpressionForPropertyName(member, /*generateNameForComputedPropertyName*/ true);
const descriptor = languageVersion > ScriptTarget.ES3
? member.kind === SyntaxKind.PropertyDeclaration
Expand All @@ -1220,7 +1244,7 @@ namespace ts {

const helper = emitHelpers().createDecorateHelper(
decoratorExpressions,
prefix,
target,
memberName,
descriptor
);
Expand Down Expand Up @@ -3152,14 +3176,8 @@ namespace ts {
}
}

function getClassPrototype(node: ClassExpression | ClassDeclaration) {
return factory.createPropertyAccessExpression(factory.getDeclarationName(node), "prototype");
}

function getClassMemberPrefix(node: ClassExpression | ClassDeclaration, member: ClassElement) {
return hasSyntacticModifier(member, ModifierFlags.Static)
? factory.getDeclarationName(node)
: getClassPrototype(node);
function createClassPrototype(expression: Expression) {
return factory.createPropertyAccessExpression(expression, "prototype");
}

function enableSubstitutionForNonQualifiedEnumMembers() {
Expand Down
17 changes: 9 additions & 8 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1803,23 +1803,23 @@ namespace ts {
return true;

case SyntaxKind.PropertyDeclaration:
// property declarations are valid if their parent is a class declaration.
return parent!.kind === SyntaxKind.ClassDeclaration;
// property declarations are valid if their parent is a class declaration/expression.
return isClassLike(parent!);

case SyntaxKind.GetAccessor:
case SyntaxKind.SetAccessor:
case SyntaxKind.MethodDeclaration:
// if this method has a body and its parent is a class declaration, this is a valid target.
// if this method has a body and its parent is a class declaration/expression, this is a valid target.
return (node as FunctionLikeDeclaration).body !== undefined
&& parent!.kind === SyntaxKind.ClassDeclaration;
&& isClassLike(parent!);

case SyntaxKind.Parameter:
// if the parameter's parent has a body and its grandparent is a class declaration, this is a valid target;
// if the parameter's parent has a body and its grandparent is a class declaration/expression, this is a valid target;
return (parent as FunctionLikeDeclaration).body !== undefined
&& (parent!.kind === SyntaxKind.Constructor
|| parent!.kind === SyntaxKind.MethodDeclaration
|| parent!.kind === SyntaxKind.SetAccessor)
&& grandparent!.kind === SyntaxKind.ClassDeclaration;
&& isClassLike(grandparent!);
}

return false;
Expand All @@ -1840,12 +1840,13 @@ namespace ts {
return nodeIsDecorated(node, parent!, grandparent!) || childIsDecorated(node, parent!); // TODO: GH#18217
}

export function childIsDecorated(node: ClassDeclaration): boolean;
export function childIsDecorated(node: ClassLikeDeclaration): boolean;
export function childIsDecorated(node: Node, parent: Node): boolean;
export function childIsDecorated(node: Node, parent?: Node): boolean {
switch (node.kind) {
case SyntaxKind.ClassExpression:
case SyntaxKind.ClassDeclaration:
return some((node as ClassDeclaration).members, m => nodeOrChildIsDecorated(m, node, parent!)); // TODO: GH#18217
return some((node as ClassDeclaration | ClassExpression).members, m => nodeOrChildIsDecorated(m, node, parent!)); // TODO: GH#18217
case SyntaxKind.MethodDeclaration:
case SyntaxKind.SetAccessor:
return some((node as FunctionLikeDeclaration).parameters, p => nodeIsDecorated(p, node, parent!)); // TODO: GH#18217
Expand Down
19 changes: 17 additions & 2 deletions tests/baselines/reference/decoratorChecksFunctionBodies.errors.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
tests/cases/conformance/decorators/class/decoratorChecksFunctionBodies.ts(8,14): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
tests/cases/conformance/decorators/class/decoratorChecksFunctionBodies.ts(19,14): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.


==== tests/cases/conformance/decorators/class/decoratorChecksFunctionBodies.ts (1 errors) ====
==== tests/cases/conformance/decorators/class/decoratorChecksFunctionBodies.ts (2 errors) ====
// from #2971
function func(s: string): void {
}
Expand All @@ -17,4 +18,18 @@ tests/cases/conformance/decorators/class/decoratorChecksFunctionBodies.ts(8,14):
m() {

}
}
}

const A1 = class {
@((x, p) => {
var a = 3;
func(a);
~
!!! error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
return x;
})
m() {

}
}

28 changes: 27 additions & 1 deletion tests/baselines/reference/decoratorChecksFunctionBodies.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,19 @@ class A {
m() {

}
}
}

const A1 = class {
@((x, p) => {
var a = 3;
func(a);
return x;
})
m() {

}
}


//// [decoratorChecksFunctionBodies.js]
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
Expand All @@ -21,6 +33,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var _a;
// from #2971
function func(s) {
}
Expand All @@ -38,3 +51,16 @@ var A = /** @class */ (function () {
], A.prototype, "m", null);
return A;
}());
var A1 = (_a = /** @class */ (function () {
function class_1() {
}
class_1.prototype.m = function () {
};
return class_1;
}()), __decorate([
(function (x, p) {
var a = 3;
func(a);
return x;
})
], _a.prototype, "m", null), _a);
25 changes: 25 additions & 0 deletions tests/baselines/reference/decoratorChecksFunctionBodies.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,28 @@ class A {

}
}

const A1 = class {
>A1 : Symbol(A1, Decl(decoratorChecksFunctionBodies.ts, 15, 5))

@((x, p) => {
>x : Symbol(x, Decl(decoratorChecksFunctionBodies.ts, 16, 7))
>p : Symbol(p, Decl(decoratorChecksFunctionBodies.ts, 16, 9))

var a = 3;
>a : Symbol(a, Decl(decoratorChecksFunctionBodies.ts, 17, 11))

func(a);
>func : Symbol(func, Decl(decoratorChecksFunctionBodies.ts, 0, 0))
>a : Symbol(a, Decl(decoratorChecksFunctionBodies.ts, 17, 11))

return x;
>x : Symbol(x, Decl(decoratorChecksFunctionBodies.ts, 16, 7))

})
m() {
>m : Symbol(A1.m, Decl(decoratorChecksFunctionBodies.ts, 15, 18))

}
}

30 changes: 30 additions & 0 deletions tests/baselines/reference/decoratorChecksFunctionBodies.types
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,33 @@ class A {

}
}

const A1 = class {
>A1 : typeof A1
>class { @((x, p) => { var a = 3; func(a); return x; }) m() { }} : typeof A1

@((x, p) => {
>((x, p) => { var a = 3; func(a); return x; }) : (x: any, p: any) => any
>(x, p) => { var a = 3; func(a); return x; } : (x: any, p: any) => any
>x : any
>p : any

var a = 3;
>a : number
>3 : 3

func(a);
>func(a) : void
>func : (s: string) => void
>a : number

return x;
>x : any

})
m() {
>m : () => void

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@ function filter(handler: any) {
};
}

class Wat {
class A {
@filter(() => test == 'abc')
static whatever() {
// ...
}
}
}

const A1 = class {
@filter(() => test == 'abc')
static whatever() {
// ...
}
}


//// [a.js]
"use strict";
Expand All @@ -34,21 +42,32 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
var a_1 = require("./a");
function filter(handler) {
return function (target, propertyKey) {
// ...
};
}
var Wat = /** @class */ (function () {
function Wat() {
var A = /** @class */ (function () {
function A() {
}
Wat.whatever = function () {
A.whatever = function () {
// ...
};
__decorate([
filter(function () { return a_1.test == 'abc'; })
], Wat, "whatever", null);
return Wat;
], A, "whatever", null);
return A;
}());
var A1 = (_a = /** @class */ (function () {
function class_1() {
}
class_1.whatever = function () {
// ...
};
return class_1;
}()), __decorate([
filter(function () { return a_1.test == 'abc'; })
], _a, "whatever", null), _a);
Loading

0 comments on commit 6751fc1

Please sign in to comment.