Skip to content

Commit c588201

Browse files
authored
forbidReturnValueInYieldingMethod (#152)
1 parent 62712f6 commit c588201

File tree

7 files changed

+332
-0
lines changed

7 files changed

+332
-0
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ parameters:
9494
enabled: true
9595
forbidProtectedEnumMethod:
9696
enabled: true
97+
forbidReturnValueInYieldingMethod:
98+
enabled: true
99+
reportRegardlessOfReturnType: false
97100
forbidVariableTypeOverwriting:
98101
enabled: true
99102
forbidUnsetClassField:
@@ -580,6 +583,28 @@ enum MyEnum {
580583
}
581584
```
582585

586+
### forbidReturnValueInYieldingMethod
587+
- Disallows returning values in yielding methods unless marked to return Generator as the value is accessible only via [Generator::getReturn](https://www.php.net/manual/en/generator.getreturn.php)
588+
- To prevent misuse, this rule can be configured to even stricter mode where it reports such returns regardless of return type declared
589+
590+
```php
591+
class Get {
592+
public static function oneTwoThree(): iterable { // marked as iterable, caller cannot access the return value by Generator::getReturn
593+
yield 1;
594+
yield 2;
595+
return 3;
596+
}
597+
}
598+
599+
iterator_to_array(Get::oneTwoThree()); // [1, 2] - see https://3v4l.org/Leu9j
600+
```
601+
```neon
602+
parameters:
603+
shipmonkRules:
604+
forbidReturnValueInYieldingMethod:
605+
reportRegardlessOfReturnType: true # optional stricter mode, defaults to false
606+
```
607+
583608
### forbidVariableTypeOverwriting
584609
- Restricts variable assignment to those that does not change its type
585610
- Array append `$array[] = 1;` not yet supported

phpcs.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@
407407
Doctrine\Common\Collections\Collection
408408
"/>
409409
</properties>
410+
<exclude name="SlevomatCodingStandard.TypeHints.ReturnTypeHint.MissingTraversableTypeHintSpecification"/><!-- this has problems with vendor libs, PHPStan checks this much more reliably -->
410411
</rule>
411412
<rule ref="SlevomatCodingStandard.TypeHints.LongTypeHints"/>
412413
<rule ref="SlevomatCodingStandard.TypeHints.ReturnTypeHintSpacing"/>

rules.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ parameters:
7373
enabled: true
7474
forbidProtectedEnumMethod:
7575
enabled: true
76+
forbidReturnValueInYieldingMethod:
77+
enabled: true
78+
reportRegardlessOfReturnType: false
7679
forbidVariableTypeOverwriting:
7780
enabled: true
7881
forbidUnsetClassField:
@@ -171,6 +174,10 @@ parametersSchema:
171174
forbidProtectedEnumMethod: structure([
172175
enabled: bool()
173176
])
177+
forbidReturnValueInYieldingMethod: structure([
178+
enabled: bool()
179+
reportRegardlessOfReturnType: bool()
180+
])
174181
forbidVariableTypeOverwriting: structure([
175182
enabled: bool()
176183
])
@@ -245,6 +252,8 @@ conditionalTags:
245252
phpstan.rules.rule: %shipmonkRules.forbidPhpDocNullabilityMismatchWithNativeTypehint.enabled%
246253
ShipMonk\PHPStan\Rule\ForbidProtectedEnumMethodRule:
247254
phpstan.rules.rule: %shipmonkRules.forbidProtectedEnumMethod.enabled%
255+
ShipMonk\PHPStan\Rule\ForbidReturnValueInYieldingMethodRule:
256+
phpstan.rules.rule: %shipmonkRules.forbidReturnValueInYieldingMethod.enabled%
248257
ShipMonk\PHPStan\Rule\ForbidVariableTypeOverwritingRule:
249258
phpstan.rules.rule: %shipmonkRules.forbidVariableTypeOverwriting.enabled%
250259
ShipMonk\PHPStan\Rule\ForbidUnsetClassFieldRule:
@@ -350,6 +359,10 @@ services:
350359
class: ShipMonk\PHPStan\Rule\ForbidPhpDocNullabilityMismatchWithNativeTypehintRule
351360
-
352361
class: ShipMonk\PHPStan\Rule\ForbidProtectedEnumMethodRule
362+
-
363+
class: ShipMonk\PHPStan\Rule\ForbidReturnValueInYieldingMethodRule
364+
arguments:
365+
reportRegardlessOfReturnType: %shipmonkRules.forbidReturnValueInYieldingMethod.reportRegardlessOfReturnType%
353366
-
354367
class: ShipMonk\PHPStan\Rule\ForbidVariableTypeOverwritingRule
355368
-
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\PHPStan\Rule;
4+
5+
use Generator;
6+
use PhpParser\Node;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\ClosureReturnStatementsNode;
9+
use PHPStan\Node\MethodReturnStatementsNode;
10+
use PHPStan\Node\ReturnStatementsNode;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Rules\Rule;
13+
use PHPStan\Rules\RuleError;
14+
use PHPStan\Rules\RuleErrorBuilder;
15+
use PHPStan\Type\MixedType;
16+
use PHPStan\Type\Type;
17+
use function in_array;
18+
19+
/**
20+
* @implements Rule<ReturnStatementsNode>
21+
*/
22+
class ForbidReturnValueInYieldingMethodRule implements Rule
23+
{
24+
25+
private bool $reportRegardlessOfReturnType;
26+
27+
public function __construct(bool $reportRegardlessOfReturnType)
28+
{
29+
$this->reportRegardlessOfReturnType = $reportRegardlessOfReturnType;
30+
}
31+
32+
public function getNodeType(): string
33+
{
34+
return ReturnStatementsNode::class;
35+
}
36+
37+
/**
38+
* @param ReturnStatementsNode $node
39+
* @return list<RuleError>
40+
*/
41+
public function processNode(
42+
Node $node,
43+
Scope $scope
44+
): array
45+
{
46+
if (!$node->getStatementResult()->hasYield()) {
47+
return [];
48+
}
49+
50+
if (!$this->reportRegardlessOfReturnType) {
51+
$methodReturnType = $this->getReturnType($node, $scope);
52+
53+
if (in_array(Generator::class, $methodReturnType->getObjectClassNames(), true)) {
54+
return [];
55+
}
56+
}
57+
58+
$errors = [];
59+
60+
foreach ($node->getReturnStatements() as $returnStatement) {
61+
$returnNode = $returnStatement->getReturnNode();
62+
63+
if ($returnNode->expr === null) {
64+
continue;
65+
}
66+
67+
$suffix = $this->reportRegardlessOfReturnType
68+
? 'this approach is denied'
69+
: 'but this method is not marked to return Generator';
70+
71+
$callType = $node instanceof MethodReturnStatementsNode // @phpstan-ignore-line ignore bc promise
72+
? 'method'
73+
: 'function';
74+
75+
$errors[] = RuleErrorBuilder::message("Returned value from yielding $callType can be accessed only via Generator::getReturn, $suffix.")
76+
->line($returnNode->getLine())
77+
->build();
78+
}
79+
80+
return $errors;
81+
}
82+
83+
private function getReturnType(ReturnStatementsNode $node, Scope $scope): Type
84+
{
85+
$methodReflection = $scope->getFunction();
86+
87+
if ($node instanceof ClosureReturnStatementsNode) { // @phpstan-ignore-line ignore bc promise
88+
return $scope->getFunctionType($node->getClosureExpr()->getReturnType(), false, false);
89+
}
90+
91+
if ($methodReflection !== null) {
92+
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
93+
}
94+
95+
return new MixedType();
96+
}
97+
98+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Rule;
4+
5+
use LogicException;
6+
use PHPStan\Rules\Rule;
7+
use ShipMonk\PHPStan\Rule\ForbidReturnValueInYieldingMethodRule;
8+
use ShipMonk\PHPStan\RuleTestCase;
9+
10+
/**
11+
* @extends RuleTestCase<ForbidReturnValueInYieldingMethodRule>
12+
*/
13+
class ForbidReturnValueInYieldingMethodRuleTest extends RuleTestCase
14+
{
15+
16+
private ?bool $strictMode = null;
17+
18+
protected function getRule(): Rule
19+
{
20+
if ($this->strictMode === null) {
21+
throw new LogicException('Strict mode must be set');
22+
}
23+
24+
return new ForbidReturnValueInYieldingMethodRule($this->strictMode);
25+
}
26+
27+
public function testNormalMode(): void
28+
{
29+
$this->strictMode = false;
30+
$this->analyseFile(__DIR__ . '/data/ForbidReturnValueInYieldingMethodRule/normal.php');
31+
}
32+
33+
public function testStrictMode(): void
34+
{
35+
$this->strictMode = true;
36+
$this->analyseFile(__DIR__ . '/data/ForbidReturnValueInYieldingMethodRule/strict.php');
37+
}
38+
39+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ForbidReturnValueInYieldingMethodRule;
4+
5+
class Normal {
6+
7+
public function returnMixed() {
8+
yield 1;
9+
return 2; // error: Returned value from yielding method can be accessed only via Generator::getReturn, but this method is not marked to return Generator.
10+
}
11+
12+
public function returnIterable(): iterable {
13+
yield 1;
14+
return 2; // error: Returned value from yielding method can be accessed only via Generator::getReturn, but this method is not marked to return Generator.
15+
}
16+
17+
public function returnGenerator(): \Generator {
18+
yield 1;
19+
return 2;
20+
}
21+
22+
/**
23+
* @return iterable&\Generator
24+
*/
25+
public function returnGeneratorAndIterable() {
26+
yield 1;
27+
return 2;
28+
}
29+
30+
}
31+
32+
class NoYield {
33+
public function returnGeneratorNoYield(): \Generator {
34+
return $this->returnGeneratorByYield();
35+
}
36+
37+
public function returnGeneratorByYield(): \Generator {
38+
yield 1;
39+
}
40+
}
41+
42+
class NoValue {
43+
public function returnMixed() {
44+
yield 1;
45+
return;
46+
}
47+
48+
public function returnIterable(): iterable {
49+
yield 1;
50+
return;
51+
}
52+
53+
public function returnGenerator(): \Generator {
54+
yield 1;
55+
return;
56+
}
57+
}
58+
59+
function returnIterable(): iterable {
60+
yield 1;
61+
return 2; // error: Returned value from yielding function can be accessed only via Generator::getReturn, but this method is not marked to return Generator.
62+
}
63+
64+
function returnGenerator(): \Generator {
65+
yield 1;
66+
return 2;
67+
}
68+
69+
function (): iterable {
70+
yield 1;
71+
return 2; // error: Returned value from yielding function can be accessed only via Generator::getReturn, but this method is not marked to return Generator.
72+
};
73+
74+
function (): \Generator {
75+
yield 1;
76+
return 2;
77+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Rule\data\ForbidReturnValueInYieldingMethodRule;
4+
5+
class Strict {
6+
7+
public function returnMixed() {
8+
yield 1;
9+
return 2; // error: Returned value from yielding method can be accessed only via Generator::getReturn, this approach is denied.
10+
}
11+
12+
public function returnIterable(): iterable {
13+
yield 1;
14+
return 2; // error: Returned value from yielding method can be accessed only via Generator::getReturn, this approach is denied.
15+
}
16+
17+
public function returnGenerator(): \Generator {
18+
yield 1;
19+
return 2; // error: Returned value from yielding method can be accessed only via Generator::getReturn, this approach is denied.
20+
}
21+
22+
/**
23+
* @return iterable&\Generator
24+
*/
25+
public function returnGeneratorAndIterable() {
26+
yield 1;
27+
return 2; // error: Returned value from yielding method can be accessed only via Generator::getReturn, this approach is denied.
28+
}
29+
30+
}
31+
32+
class NoYield {
33+
public function returnGeneratorNoYield(): \Generator {
34+
return $this->returnGeneratorByYield();
35+
}
36+
37+
public function returnGeneratorByYield(): \Generator {
38+
yield 1;
39+
}
40+
}
41+
42+
class NoValue {
43+
public function returnMixed() {
44+
yield 1;
45+
return;
46+
}
47+
48+
public function returnIterable(): iterable {
49+
yield 1;
50+
return;
51+
}
52+
53+
public function returnGenerator(): \Generator {
54+
yield 1;
55+
return;
56+
}
57+
}
58+
59+
60+
function returnIterable(): iterable {
61+
yield 1;
62+
return 2; // error: Returned value from yielding function can be accessed only via Generator::getReturn, this approach is denied.
63+
}
64+
65+
function returnGenerator(): \Generator {
66+
yield 1;
67+
return 2; // error: Returned value from yielding function can be accessed only via Generator::getReturn, this approach is denied.
68+
}
69+
70+
function (): iterable {
71+
yield 1;
72+
return 2; // error: Returned value from yielding function can be accessed only via Generator::getReturn, this approach is denied.
73+
};
74+
75+
function (): \Generator {
76+
yield 1;
77+
return 2; // error: Returned value from yielding function can be accessed only via Generator::getReturn, this approach is denied.
78+
};
79+

0 commit comments

Comments
 (0)