2020 * - Checks if the method returns the expected type or object, is nullable or void.
2121 * - Check if the types of the parameters match the expected types.
2222 * - 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).
2324 */
2425class MethodMustReturnTypeRule implements Rule
2526{
@@ -31,14 +32,18 @@ class MethodMustReturnTypeRule implements Rule
3132 private const ERROR_MESSAGE_OBJECT_TYPE_PATTERN = 'Method %s must return an object matching pattern %s, %s given. ' ;
3233 private const ERROR_MESSAGE_OBJECT_TYPE = 'Method %s must return an object type. ' ;
3334 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. ' ;
3437
3538 /**
3639 * @param array<array{
3740 * pattern: string,
38- * type: string,
41+ * type? : string,
3942 * nullable: bool,
4043 * void: bool,
4144 * objectTypePattern: string|null,
45+ * oneOf?: array<string>,
46+ * allOf?: array<string>,
4247 * }> $returnTypePatterns
4348 */
4449 public function __construct (
@@ -81,7 +86,8 @@ public function processNode(Node $node, Scope $scope): array
8186
8287 // Check for missing return type
8388 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 ());
8591 continue ;
8692 }
8793
@@ -92,18 +98,32 @@ public function processNode(Node $node, Scope $scope): array
9298 $ errors [] = $ this ->buildNullabilityError ($ fullName , $ patternConfig ['nullable ' ], $ method ->getLine ());
9399 }
94100
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 ());
101123 }
102- } else {
103- $ errors [] = $ this ->buildObjectTypeError ($ fullName , $ method ->getLine ());
124+ } elseif ( $ this -> shouldErrorOnTypeMismatch ( $ returnType , $ expectedType )) {
125+ $ errors [] = $ this ->buildTypeMismatchError ($ fullName, $ expectedType , $ returnType , $ method ->getLine ());
104126 }
105- } elseif ($ this ->shouldErrorOnTypeMismatch ($ returnType , $ patternConfig ['type ' ])) {
106- $ errors [] = $ this ->buildTypeMismatchError ($ fullName , $ patternConfig ['type ' ], $ returnType , $ method ->getLine ());
107127 }
108128 }
109129 }
@@ -134,6 +154,17 @@ private function shouldErrorOnMissingReturnType(?string $returnType): bool
134154 return $ returnType === null ;
135155 }
136156
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+
137168 private function buildMissingReturnTypeError (string $ fullName , string $ expectedType , int $ line )
138169 {
139170 return RuleErrorBuilder::message (
@@ -167,6 +198,114 @@ private function buildNullabilityError(string $fullName, bool $expectedNullable,
167198 ->build ();
168199 }
169200
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+
170309 private function shouldErrorOnObjectTypePattern (array $ patternConfig , string $ objectType ): bool
171310 {
172311 return $ patternConfig ['objectTypePattern ' ] !== null &&
@@ -203,7 +342,7 @@ private function buildObjectTypeError(string $fullName, int $line)
203342
204343 private function shouldErrorOnTypeMismatch (?string $ returnType , string $ expectedType ): bool
205344 {
206- return $ returnType !== $ expectedType && ! $ this ->isNullableMatch ($ returnType , $ expectedType );
345+ return ! $ this ->isTypeMatch ($ returnType , $ expectedType );
207346 }
208347
209348 private function buildTypeMismatchError (string $ fullName , string $ expectedType , ?string $ actualType , int $ line )
@@ -213,7 +352,7 @@ private function buildTypeMismatchError(string $fullName, string $expectedType,
213352 self ::ERROR_MESSAGE_TYPE_MISMATCH ,
214353 $ fullName ,
215354 $ expectedType ,
216- $ actualType
355+ $ actualType ?? ' no return type '
217356 )
218357 )
219358 ->identifier (self ::IDENTIFIER )
0 commit comments