@@ -20,7 +20,18 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): voi
2020 $ classLike = $ event ->getStmt ();
2121 $ statementsSource = $ event ->getStatementsSource ();
2222
23- self ::checkClassComment ($ classLike , $ statementsSource );
23+ if (!str_contains ($ statementsSource ->getFilePath (), '/lib/public/ ' )) {
24+ return ;
25+ }
26+
27+ $ isTesting = str_contains ($ statementsSource ->getFilePath (), '/lib/public/Notification/ ' )
28+ || str_contains ($ statementsSource ->getFilePath (), 'CalendarEventStatus ' );
29+
30+ if ($ isTesting ) {
31+ self ::checkStatementAttributes ($ classLike , $ statementsSource );
32+ } else {
33+ self ::checkClassComment ($ classLike , $ statementsSource );
34+ }
2435
2536 foreach ($ classLike ->stmts as $ stmt ) {
2637 if ($ stmt instanceof ClassConst) {
@@ -32,11 +43,58 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event): voi
3243 }
3344
3445 if ($ stmt instanceof EnumCase) {
35- self ::checkStatementComment ($ stmt , $ statementsSource , 'enum ' );
46+ if ($ isTesting ) {
47+ self ::checkStatementAttributes ($ classLike , $ statementsSource );
48+ } else {
49+ self ::checkStatementComment ($ stmt , $ statementsSource , 'enum ' );
50+ }
3651 }
3752 }
3853 }
3954
55+ private static function checkStatementAttributes (ClassLike $ stmt , FileSource $ statementsSource ): void {
56+ $ hasAppFrameworkAttribute = false ;
57+ $ mustBeConsumable = false ;
58+ $ isConsumable = false ;
59+ foreach ($ stmt ->attrGroups as $ attrGroup ) {
60+ foreach ($ attrGroup ->attrs as $ attr ) {
61+ if (in_array ($ attr ->name ->getLast (), [
62+ 'Consumable ' ,
63+ 'Dispatchable ' ,
64+ 'Implementable ' ,
65+ 'Throwable ' ,
66+ ], true )) {
67+ $ hasAppFrameworkAttribute = true ;
68+ self ::checkAttributeHasValidSinceVersion ($ attr , $ statementsSource );
69+ }
70+ if ($ attr ->name ->getLast () === 'Consumable ' ) {
71+ $ isConsumable = true ;
72+ }
73+ if ($ attr ->name ->getLast () === 'ExceptionalImplementable ' ) {
74+ $ mustBeConsumable = true ;
75+ }
76+ }
77+ }
78+
79+ if ($ mustBeConsumable && !$ isConsumable ) {
80+ IssueBuffer::maybeAdd (
81+ new InvalidDocblock (
82+ 'Attribute OCP \\AppFramework \\Attribute \\ExceptionalImplementable is only valid on classes that also have OCP \\AppFramework \\Attribute \\Consumable ' ,
83+ new CodeLocation ($ statementsSource , $ stmt )
84+ )
85+ );
86+ }
87+
88+ if (!$ hasAppFrameworkAttribute ) {
89+ IssueBuffer::maybeAdd (
90+ new InvalidDocblock (
91+ 'At least one of the OCP \\AppFramework \\Attribute attributes is required ' ,
92+ new CodeLocation ($ statementsSource , $ stmt )
93+ )
94+ );
95+ }
96+ }
97+
4098 private static function checkClassComment (ClassLike $ stmt , FileSource $ statementsSource ): void {
4199 $ docblock = $ stmt ->getDocComment ();
42100
@@ -124,4 +182,28 @@ private static function checkStatementComment(Stmt $stmt, FileSource $statements
124182 );
125183 }
126184 }
185+
186+ private static function checkAttributeHasValidSinceVersion (\PhpParser \Node \Attribute $ stmt , FileSource $ statementsSource ): void {
187+ foreach ($ stmt ->args as $ arg ) {
188+ if ($ arg ->name ?->name === 'since ' ) {
189+ if (!$ arg ->value instanceof \PhpParser \Node \Scalar \String_) {
190+ IssueBuffer::maybeAdd (
191+ new InvalidDocblock (
192+ 'Attribute since argument is not a valid version string ' ,
193+ new CodeLocation ($ statementsSource , $ stmt )
194+ )
195+ );
196+ } else {
197+ if (!preg_match ('/^[1-9][0-9]*(\.[0-9]+){0,3}$/ ' , $ arg ->value ->value )) {
198+ IssueBuffer::maybeAdd (
199+ new InvalidDocblock (
200+ 'Attribute since argument is not a valid version string ' ,
201+ new CodeLocation ($ statementsSource , $ stmt )
202+ )
203+ );
204+ }
205+ }
206+ }
207+ }
208+ }
127209}
0 commit comments