Skip to content

[Experiment] feat(7342): Decorators not allowed classes expressions #42198

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
wants to merge 1 commit into from
Closed
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
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