@@ -47,23 +47,39 @@ public function process(File $file, $position)
4747 }
4848
4949 list ($ functionStart , $ functionEnd ) = PhpcsHelpers::functionBoundaries ($ file , $ position );
50- if (!$ functionStart < 0 || $ functionEnd <= 0 ) {
50+
51+ if (($ functionStart < 0 ) || ($ functionEnd <= 0 )) {
5152 return ;
5253 }
5354
5455 list (
5556 $ hasNonVoidReturnType ,
5657 $ hasVoidReturnType ,
5758 $ hasNoReturnType ,
58- $ hasNullable
59+ $ hasNullableReturn ,
60+ $ returnsGenerator
5961 ) = $ this ->returnTypeInfo ($ file , $ position );
6062
6163 list ($ nonVoidReturnCount , $ voidReturnCount , $ nullReturnCount ) = PhpcsHelpers::countReturns (
6264 $ file ,
6365 $ position
6466 );
6567
66- if ($ hasNullable ) {
68+ $ yieldCount = $ this ->countYield ($ functionStart , $ functionEnd , $ file );
69+
70+ if ($ yieldCount || $ returnsGenerator ) {
71+ $ this ->maybeGeneratorErrors (
72+ $ yieldCount ,
73+ $ returnsGenerator ,
74+ $ nonVoidReturnCount ,
75+ $ file ,
76+ $ position
77+ );
78+
79+ return ;
80+ }
81+
82+ if ($ hasNullableReturn ) {
6783 $ voidReturnCount -= $ nullReturnCount ;
6884 }
6985
@@ -96,6 +112,7 @@ private function maybeErrors(
96112 File $ file ,
97113 int $ position
98114 ) {
115+
99116 if ($ hasNonVoidReturnType && ($ nonVoidReturnCount === 0 || $ voidReturnCount > 0 )) {
100117 $ msg = 'Return type with ' ;
101118 $ file ->addError (
@@ -134,25 +151,100 @@ private function maybeErrors(
134151 }
135152 }
136153
154+ /**
155+ * @param int $yieldCount
156+ * @param bool $returnsGenerator
157+ * @param int $nonVoidReturnCount
158+ * @param File $file
159+ * @param int $position
160+ */
161+ private function maybeGeneratorErrors (
162+ int $ yieldCount ,
163+ bool $ returnsGenerator ,
164+ int $ nonVoidReturnCount ,
165+ File $ file ,
166+ int $ position
167+ ) {
168+
169+ if ($ nonVoidReturnCount > 1 ) {
170+ $ file ->addWarning (
171+ 'A generator should only contain a single return point. ' ,
172+ $ position ,
173+ 'InvalidGeneratorManyReturns '
174+ );
175+ }
176+
177+ if ($ yieldCount && $ returnsGenerator ) {
178+ return ;
179+ }
180+
181+ if (!$ yieldCount ) {
182+ $ file ->addError (
183+ 'Found a generator return type in non-yielding function. ' ,
184+ $ position ,
185+ 'GeneratorReturnTypeWithoutYield '
186+ );
187+
188+ return ;
189+ }
190+
191+ if (!$ nonVoidReturnCount ) {
192+ $ file ->addWarning (
193+ 'Found a function that yield values but missing Generator return type. ' ,
194+ $ position ,
195+ 'NoGeneratorReturnType '
196+ );
197+
198+ return ;
199+ }
200+
201+ $ returnType = $ this ->returnTypeContent ($ file , $ position );
202+ if ($ returnType === 'Traversable ' || $ returnType === 'Iterator ' ) {
203+ return ;
204+ }
205+
206+ $ file ->addError (
207+ 'Found a function that yield values but declare a return type different than Generator. ' ,
208+ $ position ,
209+ 'IncorrectReturnTypeForGenerator '
210+ );
211+ }
212+
137213 /**
138214 * @param File $file
139215 * @param int $functionPosition
140- * @return array
216+ * @return string
141217 */
142- private function returnTypeInfo (File $ file , int $ functionPosition ): array
218+ private function returnTypeContent (File $ file , int $ functionPosition ): string
143219 {
144220 $ tokens = $ file ->getTokens ();
145- $ functionToken = $ tokens [$ functionPosition ];
146-
147221 $ returnTypeToken = $ file ->findNext (
148222 [T_RETURN_TYPE ],
149223 $ functionPosition + 3 , // 3: open parenthesis, close parenthesis, colon
150- ($ functionToken ['scope_opener ' ] ?? 0 ) - 1
224+ ($ tokens [ $ functionPosition ] ['scope_opener ' ] ?? 0 ) - 1
151225 );
152226
153227 $ returnType = $ tokens [$ returnTypeToken ] ?? null ;
154- if ($ returnType && $ returnType ['code ' ] !== T_RETURN_TYPE ) {
155- return [false , false , true , false ];
228+ if (!$ returnType || $ returnType ['code ' ] !== T_RETURN_TYPE ) {
229+ return '' ;
230+ }
231+
232+ return ltrim ($ returnType ['content ' ] ?? '' , '\\' );
233+ }
234+
235+ /**
236+ * @param File $file
237+ * @param int $functionPosition
238+ * @return array
239+ */
240+ private function returnTypeInfo (File $ file , int $ functionPosition ): array
241+ {
242+ $ tokens = $ file ->getTokens ();
243+
244+ $ returnTypeContent = $ this ->returnTypeContent ($ file , $ functionPosition );
245+
246+ if (!$ returnTypeContent ) {
247+ return [false , false , true , false , false ];
156248 }
157249
158250 $ start = $ tokens [$ functionPosition ]['parenthesis_closer ' ] + 1 ;
@@ -168,10 +260,11 @@ private function returnTypeInfo(File $file, int $functionPosition): array
168260 }
169261 }
170262
171- $ hasNonVoidReturnType = $ returnType ['content ' ] !== 'void ' ;
172- $ hasVoidReturnType = $ returnType ['content ' ] === 'void ' ;
263+ $ hasNonVoidReturnType = $ returnTypeContent !== 'void ' ;
264+ $ hasVoidReturnType = $ returnTypeContent === 'void ' ;
265+ $ returnsGenerator = $ returnTypeContent === 'Generator ' ;
173266
174- return [$ hasNonVoidReturnType , $ hasVoidReturnType , false , $ hasNullable ];
267+ return [$ hasNonVoidReturnType , $ hasVoidReturnType , false , $ hasNullable, $ returnsGenerator ];
175268 }
176269
177270 /**
@@ -214,4 +307,26 @@ private function areNullableReturnTypesSupported(): bool
214307
215308 return $ min && version_compare ($ min , '7.1 ' , '>= ' );
216309 }
310+
311+ /**
312+ * @param int $functionStart
313+ * @param int $functionEnd
314+ * @return int
315+ */
316+ private function countYield (int $ functionStart , int $ functionEnd , File $ file ): int
317+ {
318+ $ count = 0 ;
319+ $ tokens = $ file ->getTokens ();
320+ for ($ i = $ functionStart + 1 ; $ i < $ functionEnd ; $ i ++) {
321+ if ($ tokens [$ i ]['code ' ] === T_CLOSURE ) {
322+ $ i = $ tokens [$ i ]['scope_closer ' ];
323+ continue ;
324+ }
325+ if ($ tokens [$ i ]['code ' ] === T_YIELD || $ tokens [$ i ]['code ' ] === T_YIELD_FROM ) {
326+ $ count ++;
327+ }
328+ }
329+
330+ return $ count ;
331+ }
217332}
0 commit comments