20
20
* - Checks if the method returns the expected type or object, is nullable or void.
21
21
* - Check if the types of the parameters match the expected types.
22
22
* - If an object type is expected, it can match a specific class or a pattern.
23
+ * - Supports union types with "oneOf" (one type must match) and "allOf" (all types must match).
23
24
*/
24
25
class MethodMustReturnTypeRule implements Rule
25
26
{
@@ -31,14 +32,18 @@ class MethodMustReturnTypeRule implements Rule
31
32
private const ERROR_MESSAGE_OBJECT_TYPE_PATTERN = 'Method %s must return an object matching pattern %s, %s given. ' ;
32
33
private const ERROR_MESSAGE_OBJECT_TYPE = 'Method %s must return an object type. ' ;
33
34
private const ERROR_MESSAGE_TYPE_MISMATCH = 'Method %s must have return type %s, %s given. ' ;
35
+ private const ERROR_MESSAGE_ONE_OF_MISMATCH = 'Method %s must have one of the return types: %s, %s given. ' ;
36
+ private const ERROR_MESSAGE_ALL_OF_MISMATCH = 'Method %s must have all of the return types: %s, %s given. ' ;
34
37
35
38
/**
36
39
* @param array<array{
37
40
* pattern: string,
38
- * type: string,
41
+ * type? : string,
39
42
* nullable: bool,
40
43
* void: bool,
41
44
* objectTypePattern: string|null,
45
+ * oneOf?: array<string>,
46
+ * allOf?: array<string>,
42
47
* }> $returnTypePatterns
43
48
*/
44
49
public function __construct (
@@ -81,7 +86,8 @@ public function processNode(Node $node, Scope $scope): array
81
86
82
87
// Check for missing return type
83
88
if ($ this ->shouldErrorOnMissingReturnType ($ returnType )) {
84
- $ errors [] = $ this ->buildMissingReturnTypeError ($ fullName , $ patternConfig ['type ' ], $ method ->getLine ());
89
+ $ expectedType = $ this ->getExpectedTypeDescription ($ patternConfig );
90
+ $ errors [] = $ this ->buildMissingReturnTypeError ($ fullName , $ expectedType , $ method ->getLine ());
85
91
continue ;
86
92
}
87
93
@@ -92,18 +98,32 @@ public function processNode(Node $node, Scope $scope): array
92
98
$ errors [] = $ this ->buildNullabilityError ($ fullName , $ patternConfig ['nullable ' ], $ method ->getLine ());
93
99
}
94
100
95
- // Check for type
96
- if ($ patternConfig ['type ' ] === 'object ' ) {
97
- if ($ returnTypeNode instanceof Name) {
98
- $ objectType = $ returnTypeNode ->toString ();
99
- if ($ this ->shouldErrorOnObjectTypePattern ($ patternConfig , $ objectType )) {
100
- $ errors [] = $ this ->buildObjectTypePatternError ($ fullName , $ patternConfig ['objectTypePattern ' ], $ objectType , $ method ->getLine ());
101
+ // Check for union types (oneOf/allOf)
102
+ if (isset ($ patternConfig ['oneOf ' ])) {
103
+ if ($ this ->shouldErrorOnOneOf ($ patternConfig ['oneOf ' ], $ returnType )) {
104
+ $ errors [] = $ this ->buildOneOfError ($ fullName , $ patternConfig ['oneOf ' ], $ returnType , $ method ->getLine ());
105
+ continue ;
106
+ }
107
+ } elseif (isset ($ patternConfig ['allOf ' ])) {
108
+ if ($ this ->shouldErrorOnAllOf ($ patternConfig ['allOf ' ], $ returnType )) {
109
+ $ errors [] = $ this ->buildAllOfError ($ fullName , $ patternConfig ['allOf ' ], $ returnType , $ method ->getLine ());
110
+ continue ;
111
+ }
112
+ } else {
113
+ // Check for single type
114
+ $ expectedType = $ patternConfig ['type ' ] ?? 'void ' ;
115
+ if ($ expectedType === 'object ' ) {
116
+ if ($ returnTypeNode instanceof Name) {
117
+ $ objectType = $ returnTypeNode ->toString ();
118
+ if ($ this ->shouldErrorOnObjectTypePattern ($ patternConfig , $ objectType )) {
119
+ $ errors [] = $ this ->buildObjectTypePatternError ($ fullName , $ patternConfig ['objectTypePattern ' ], $ objectType , $ method ->getLine ());
120
+ }
121
+ } else {
122
+ $ errors [] = $ this ->buildObjectTypeError ($ fullName , $ method ->getLine ());
101
123
}
102
- } else {
103
- $ errors [] = $ this ->buildObjectTypeError ($ fullName , $ method ->getLine ());
124
+ } elseif ( $ this -> shouldErrorOnTypeMismatch ( $ returnType , $ expectedType )) {
125
+ $ errors [] = $ this ->buildTypeMismatchError ($ fullName, $ expectedType , $ returnType , $ method ->getLine ());
104
126
}
105
- } elseif ($ this ->shouldErrorOnTypeMismatch ($ returnType , $ patternConfig ['type ' ])) {
106
- $ errors [] = $ this ->buildTypeMismatchError ($ fullName , $ patternConfig ['type ' ], $ returnType , $ method ->getLine ());
107
127
}
108
128
}
109
129
}
@@ -134,6 +154,17 @@ private function shouldErrorOnMissingReturnType(?string $returnType): bool
134
154
return $ returnType === null ;
135
155
}
136
156
157
+ private function getExpectedTypeDescription (array $ patternConfig ): string
158
+ {
159
+ if (isset ($ patternConfig ['oneOf ' ])) {
160
+ return 'one of: ' . implode (', ' , $ patternConfig ['oneOf ' ]);
161
+ }
162
+ if (isset ($ patternConfig ['allOf ' ])) {
163
+ return 'all of: ' . implode (', ' , $ patternConfig ['allOf ' ]);
164
+ }
165
+ return $ patternConfig ['type ' ] ?? 'void ' ;
166
+ }
167
+
137
168
private function buildMissingReturnTypeError (string $ fullName , string $ expectedType , int $ line )
138
169
{
139
170
return RuleErrorBuilder::message (
@@ -167,6 +198,114 @@ private function buildNullabilityError(string $fullName, bool $expectedNullable,
167
198
->build ();
168
199
}
169
200
201
+ private function shouldErrorOnOneOf (array $ expectedTypes , ?string $ returnType ): bool
202
+ {
203
+ if ($ returnType === null ) {
204
+ return true ;
205
+ }
206
+
207
+ foreach ($ expectedTypes as $ expectedType ) {
208
+ if ($ this ->isTypeMatch ($ returnType , $ expectedType )) {
209
+ return false ;
210
+ }
211
+ }
212
+
213
+ return true ;
214
+ }
215
+
216
+ private function buildOneOfError (string $ fullName , array $ expectedTypes , ?string $ actualType , int $ line )
217
+ {
218
+ return RuleErrorBuilder::message (
219
+ sprintf (
220
+ self ::ERROR_MESSAGE_ONE_OF_MISMATCH ,
221
+ $ fullName ,
222
+ implode (', ' , $ expectedTypes ),
223
+ $ actualType ?? 'no return type '
224
+ )
225
+ )
226
+ ->identifier (self ::IDENTIFIER )
227
+ ->line ($ line )
228
+ ->build ();
229
+ }
230
+
231
+ private function shouldErrorOnAllOf (array $ expectedTypes , ?string $ returnType ): bool
232
+ {
233
+ if ($ returnType === null ) {
234
+ return true ;
235
+ }
236
+
237
+ // For allOf, we need to check if the return type is a union type that contains all expected types
238
+ // This is a simplified implementation - in practice, you might need more sophisticated union type parsing
239
+ $ actualTypes = $ this ->parseUnionType ($ returnType );
240
+
241
+ foreach ($ expectedTypes as $ expectedType ) {
242
+ $ found = false ;
243
+ foreach ($ actualTypes as $ actualType ) {
244
+ if ($ this ->isTypeMatch ($ actualType , $ expectedType )) {
245
+ $ found = true ;
246
+ break ;
247
+ }
248
+ }
249
+ if (!$ found ) {
250
+ return true ;
251
+ }
252
+ }
253
+
254
+ return false ;
255
+ }
256
+
257
+ private function buildAllOfError (string $ fullName , array $ expectedTypes , ?string $ actualType , int $ line )
258
+ {
259
+ return RuleErrorBuilder::message (
260
+ sprintf (
261
+ self ::ERROR_MESSAGE_ALL_OF_MISMATCH ,
262
+ $ fullName ,
263
+ implode (', ' , $ expectedTypes ),
264
+ $ actualType ?? 'no return type '
265
+ )
266
+ )
267
+ ->identifier (self ::IDENTIFIER )
268
+ ->line ($ line )
269
+ ->build ();
270
+ }
271
+
272
+ private function parseUnionType (?string $ type ): array
273
+ {
274
+ if ($ type === null ) {
275
+ return [];
276
+ }
277
+
278
+ // Simple union type parsing - split by '|' and trim
279
+ return array_map ('trim ' , explode ('| ' , $ type ));
280
+ }
281
+
282
+ private function isTypeMatch (?string $ actual , string $ expected ): bool
283
+ {
284
+ if ($ actual === null ) {
285
+ return false ;
286
+ }
287
+
288
+ // Direct match
289
+ if ($ actual === $ expected ) {
290
+ return true ;
291
+ }
292
+
293
+ // Handle nullable types
294
+ if ($ this ->isNullableMatch ($ actual , $ expected )) {
295
+ return true ;
296
+ }
297
+
298
+ // Handle union types
299
+ $ actualTypes = $ this ->parseUnionType ($ actual );
300
+ foreach ($ actualTypes as $ actualType ) {
301
+ if ($ actualType === $ expected || $ this ->isNullableMatch ($ actualType , $ expected )) {
302
+ return true ;
303
+ }
304
+ }
305
+
306
+ return false ;
307
+ }
308
+
170
309
private function shouldErrorOnObjectTypePattern (array $ patternConfig , string $ objectType ): bool
171
310
{
172
311
return $ patternConfig ['objectTypePattern ' ] !== null &&
@@ -203,7 +342,7 @@ private function buildObjectTypeError(string $fullName, int $line)
203
342
204
343
private function shouldErrorOnTypeMismatch (?string $ returnType , string $ expectedType ): bool
205
344
{
206
- return $ returnType !== $ expectedType && ! $ this ->isNullableMatch ($ returnType , $ expectedType );
345
+ return ! $ this ->isTypeMatch ($ returnType , $ expectedType );
207
346
}
208
347
209
348
private function buildTypeMismatchError (string $ fullName , string $ expectedType , ?string $ actualType , int $ line )
@@ -213,7 +352,7 @@ private function buildTypeMismatchError(string $fullName, string $expectedType,
213
352
self ::ERROR_MESSAGE_TYPE_MISMATCH ,
214
353
$ fullName ,
215
354
$ expectedType ,
216
- $ actualType
355
+ $ actualType ?? ' no return type '
217
356
)
218
357
)
219
358
->identifier (self ::IDENTIFIER )
0 commit comments