Skip to content

Commit

Permalink
EnforceListReturnRule (#76)
Browse files Browse the repository at this point in the history
* EnforceListReturnRule

* dont use feature toggle from PHPStan

* bump phpstan 1.9.1, BC promise of AccessoryArrayListType needed

* Fix readme typo
  • Loading branch information
janedbal committed Jan 31, 2023
1 parent 3a3853f commit 7e1755d
Show file tree
Hide file tree
Showing 29 changed files with 248 additions and 28 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ parameters:
enabled: true
backedEnumGenerics:
enabled: true
enforceListReturn:
enabled: true
enforceNativeReturnTypehint:
enabled: true
enforceReadonlyPublicProperty:
Expand Down Expand Up @@ -121,6 +123,20 @@ enum MyEnum: string { // missing @implements tag
}
```

### enforceListReturn
- Enforces usage of `list<T>` when list is always returned from a class method
- When only single return with empty array is present in the method, it is not considered as list
- Does nothing when [list types](https://phpstan.org/blog/phpstan-1-9-0-with-phpdoc-asserts-list-type#list-type) are disabled in PHPStan
```php
/**
* @return array<string>
*/
public function returnList(): array // error, return phpdoc is generic array, but list is always returned
{
return ['item'];
}
```

### enforceNativeReturnTypehint
- Enforces usage of native return typehints if supported by your PHP version
- If PHPDoc is present, it deduces needed typehint from that, if not, deduction is performed based on real types returned
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"require": {
"php": "^7.4 || ^8.0",
"nikic/php-parser": "^4.14.0",
"phpstan/phpstan": "^1.8.7"
"phpstan/phpstan": "^1.9.1"
},
"require-dev": {
"editorconfig-checker/editorconfig-checker": "^10.3.0",
Expand Down
9 changes: 9 additions & 0 deletions rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ parameters:
enabled: true
backedEnumGenerics:
enabled: true
enforceListReturn:
enabled: true
enforceNativeReturnTypehint:
enabled: true
enforceReadonlyPublicProperty:
Expand Down Expand Up @@ -65,6 +67,9 @@ parametersSchema:
backedEnumGenerics: structure([
enabled: bool()
])
enforceListReturn: structure([
enabled: bool()
])
enforceNativeReturnTypehint: structure([
enabled: bool()
])
Expand Down Expand Up @@ -144,6 +149,8 @@ conditionalTags:
phpstan.rules.rule: %shipmonkRules.backedEnumGenerics.enabled%
ShipMonk\PHPStan\Rule\EnforceNativeReturnTypehintRule:
phpstan.rules.rule: %shipmonkRules.enforceNativeReturnTypehint.enabled%
ShipMonk\PHPStan\Rule\EnforceListReturnRule:
phpstan.rules.rule: %shipmonkRules.enforceListReturn.enabled%
ShipMonk\PHPStan\Rule\EnforceReadonlyPublicPropertyRule:
phpstan.rules.rule: %shipmonkRules.enforceReadonlyPublicProperty.enabled%
ShipMonk\PHPStan\Rule\ForbidAssignmentNotMatchingVarDocRule:
Expand Down Expand Up @@ -205,6 +212,8 @@ services:
class: ShipMonk\PHPStan\Rule\AllowNamedArgumentOnlyInAttributesRule
-
class: ShipMonk\PHPStan\Rule\BackedEnumGenericsRule
-
class: ShipMonk\PHPStan\Rule\EnforceListReturnRule
-
class: ShipMonk\PHPStan\Rule\EnforceNativeReturnTypehintRule
arguments:
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/AllowComparingOnlyComparableTypesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function getNodeType(): string

/**
* @param BinaryOp $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/AllowNamedArgumentOnlyInAttributesRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function getNodeType(): string

/**
* @param Arg $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/BackedEnumGenericsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function getNodeType(): string

/**
* @param InClassNode $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
96 changes: 96 additions & 0 deletions src/Rule/EnforceListReturnRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php declare(strict_types = 1);

namespace ShipMonk\PHPStan\Rule;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\MethodReturnStatementsNode;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\Rule;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\NeverType;
use PHPStan\Type\VerbosityLevel;
use function count;

/**
* @implements Rule<MethodReturnStatementsNode>
*/
class EnforceListReturnRule implements Rule
{

public function getNodeType(): string
{
return MethodReturnStatementsNode::class;
}

/**
* @param MethodReturnStatementsNode $node
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
if (AccessoryArrayListType::isListTypeEnabled() === false) {
return [];
}

if ($scope->getClassReflection() === null) {
return [];
}

$method = $scope->getFunction();

if (!$method instanceof MethodReflection) {
return [];
}

if ($this->alwaysReturnList($node) && !$this->isMarkedWithListReturn($method)) {
return ["Method {$method->getName()} always return list, but is marked as {$this->getReturnPhpDoc($method)}"];
}

return [];
}

private function getReturnPhpDoc(MethodReflection $methodReflection): string
{
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
return $returnType->describe(VerbosityLevel::precise());
}

private function alwaysReturnList(MethodReturnStatementsNode $node): bool
{
$returnStatementsCount = count($node->getReturnStatements());

if ($returnStatementsCount === 0) {
return false;
}

foreach ($node->getReturnStatements() as $returnStatement) {
$returnExpr = $returnStatement->getReturnNode()->expr;

if ($returnExpr === null) {
return false;
}

$returnType = $returnStatement->getScope()->getType($returnExpr);

if (!$returnType->isList()->yes()) {
return false;
}

if ($returnStatementsCount === 1 && $returnType instanceof ArrayType && $returnType->getItemType() instanceof NeverType) {
return false; // do not consider empty array as list when it is the only return statement
}
}

return true;
}

private function isMarkedWithListReturn(MethodReflection $methodReflection): bool
{
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
return $returnType->isList()->yes();
}

}
2 changes: 1 addition & 1 deletion src/Rule/EnforceReadonlyPublicPropertyRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public function getNodeType(): string

/**
* @param ClassPropertyNode $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidAssignmentNotMatchingVarDocRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function getNodeType(): string

/**
* @param Assign $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidCastRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function getNodeType(): string

/**
* @param Cast $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidCustomFunctionsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function getNodeType(): string

/**
* @param CallLike $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidEnumInFunctionArgumentsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function getNodeType(): string

/**
* @param FuncCall $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidFetchOnMixedRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function getNodeType(): string

/**
* @param PropertyFetch $node
* @return string[] errors
* @return list<string> errors
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidMatchDefaultArmForEnumsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public function getNodeType(): string

/**
* @param MatchExpressionNode $node
* @return RuleError[]
* @return list<RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidMethodCallOnMixedRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function getNodeType(): string

/**
* @param MethodCall $node
* @return string[] errors
* @return list<string> errors
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidNullInAssignOperationsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function getNodeType(): string

/**
* @param AssignOp $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidNullInBinaryOperationsRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public function getNodeType(): string

/**
* @param BinaryOp $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidNullInInterpolatedStringRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function getNodeType(): string

/**
* @param Encapsed $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidReturnInConstructorRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function getNodeType(): string

/**
* @param Return_ $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidUnsetClassFieldRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function getNodeType(): string

/**
* @param Unset_ $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
6 changes: 3 additions & 3 deletions src/Rule/ForbidUnusedExceptionRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function getNodeType(): string

/**
* @param Expr $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand All @@ -52,7 +52,7 @@ public function processNode(Node $node, Scope $scope): array

/**
* @param MethodCall|StaticCall $node
* @return string[]
* @return list<string>
*/
private function processCall(CallLike $node, Scope $scope): array
{
Expand All @@ -68,7 +68,7 @@ private function processCall(CallLike $node, Scope $scope): array
}

/**
* @return string[]
* @return list<string>
*/
private function processNew(New_ $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidUnusedMatchResultRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function getNodeType(): string

/**
* @param Match_ $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidUselessNullableReturnRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public function getNodeType(): string

/**
* @param MethodReturnStatementsNode $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/ForbidVariableTypeOverwritingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function getNodeType(): string

/**
* @param Assign $node
* @return string[]
* @return list<string>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
4 changes: 2 additions & 2 deletions src/Rule/RequirePreviousExceptionPassRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public function getNodeType(): string

/**
* @param TryCatch $node
* @return RuleError[]
* @return list<RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down Expand Up @@ -95,7 +95,7 @@ public function processNode(Node $node, Scope $scope): array
}

/**
* @return RuleError[]
* @return list<RuleError>
*/
private function processExceptionCreation(bool $strictTypes, ?string $caughtExceptionVariableName, Type $caughtExceptionType, CallLike $node, Scope $scope): array
{
Expand Down
2 changes: 1 addition & 1 deletion src/Rule/UselessPrivatePropertyDefaultValueRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function getNodeType(): string

/**
* @param ClassPropertiesNode $node
* @return RuleError[]
* @return list<RuleError>
*/
public function processNode(Node $node, Scope $scope): array
{
Expand Down
Loading

0 comments on commit 7e1755d

Please sign in to comment.