@@ -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,64 @@ 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+ 'Catchable ' ,
63+ 'Consumable ' ,
64+ 'Dispatchable ' ,
65+ 'Implementable ' ,
66+ 'Listenable ' ,
67+ 'Throwable ' ,
68+ ], true )) {
69+ $ hasAppFrameworkAttribute = true ;
70+ self ::checkAttributeHasValidSinceVersion ($ attr , $ statementsSource );
71+ }
72+ if (in_array ($ attr ->name ->getLast (), [
73+ 'Catchable ' ,
74+ 'Consumable ' ,
75+ 'Listenable ' ,
76+ ], true )) {
77+ $ isConsumable = true ;
78+ }
79+ if ($ attr ->name ->getLast () === 'ExceptionalImplementable ' ) {
80+ $ mustBeConsumable = true ;
81+ }
82+ }
83+ }
84+
85+ if ($ mustBeConsumable && !$ isConsumable ) {
86+ IssueBuffer::maybeAdd (
87+ new InvalidDocblock (
88+ 'Attribute OCP \\AppFramework \\Attribute \\ExceptionalImplementable is only valid on classes that also have OCP \\AppFramework \\Attribute \\Consumable ' ,
89+ new CodeLocation ($ statementsSource , $ stmt )
90+ )
91+ );
92+ }
93+
94+ if (!$ hasAppFrameworkAttribute ) {
95+ IssueBuffer::maybeAdd (
96+ new InvalidDocblock (
97+ 'At least one of the OCP \\AppFramework \\Attribute attributes is required ' ,
98+ new CodeLocation ($ statementsSource , $ stmt )
99+ )
100+ );
101+ }
102+ }
103+
40104 private static function checkClassComment (ClassLike $ stmt , FileSource $ statementsSource ): void {
41105 $ docblock = $ stmt ->getDocComment ();
42106
@@ -124,4 +188,28 @@ private static function checkStatementComment(Stmt $stmt, FileSource $statements
124188 );
125189 }
126190 }
191+
192+ private static function checkAttributeHasValidSinceVersion (\PhpParser \Node \Attribute $ stmt , FileSource $ statementsSource ): void {
193+ foreach ($ stmt ->args as $ arg ) {
194+ if ($ arg ->name ?->name === 'since ' ) {
195+ if (!$ arg ->value instanceof \PhpParser \Node \Scalar \String_) {
196+ IssueBuffer::maybeAdd (
197+ new InvalidDocblock (
198+ 'Attribute since argument is not a valid version string ' ,
199+ new CodeLocation ($ statementsSource , $ stmt )
200+ )
201+ );
202+ } else {
203+ if (!preg_match ('/^[1-9][0-9]*(\.[0-9]+){0,3}$/ ' , $ arg ->value ->value )) {
204+ IssueBuffer::maybeAdd (
205+ new InvalidDocblock (
206+ 'Attribute since argument is not a valid version string ' ,
207+ new CodeLocation ($ statementsSource , $ stmt )
208+ )
209+ );
210+ }
211+ }
212+ }
213+ }
214+ }
127215}
0 commit comments