Skip to content

Commit

Permalink
ConditionalReturnTypeRule - level 2
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Apr 25, 2022
1 parent d2bb725 commit a8d9628
Show file tree
Hide file tree
Showing 12 changed files with 592 additions and 1 deletion.
2 changes: 2 additions & 0 deletions conf/config.level2.neon
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ rules:
- PHPStan\Rules\Operators\InvalidBinaryOperationRule
- PHPStan\Rules\Operators\InvalidUnaryOperationRule
- PHPStan\Rules\Operators\InvalidComparisonOperationRule
- PHPStan\Rules\PhpDoc\FunctionConditionalReturnTypeRule
- PHPStan\Rules\PhpDoc\MethodConditionalReturnTypeRule
- PHPStan\Rules\PhpDoc\IncompatibleClassConstantPhpDocTypeRule
- PHPStan\Rules\PhpDoc\IncompatiblePhpDocTypeRule
- PHPStan\Rules\PhpDoc\IncompatiblePropertyPhpDocTypeRule
Expand Down
3 changes: 3 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,9 @@ services:
-
class: PHPStan\Rules\Constants\LazyAlwaysUsedClassConstantsExtensionProvider

-
class: PHPStan\Rules\PhpDoc\ConditionalReturnTypeRuleHelper

-
class: PHPStan\Rules\PhpDoc\UnresolvableTypeHelper

Expand Down
76 changes: 76 additions & 0 deletions src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\ConditionalType;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
use function array_key_exists;
use function sprintf;
use function substr;

class ConditionalReturnTypeRuleHelper
{

/**
* @return RuleError[]
*/
public function check(ParametersAcceptor $acceptor): array
{
$templateTypeMap = $acceptor->getTemplateTypeMap();
$parametersByName = [];
foreach ($acceptor->getParameters() as $parameter) {
$parametersByName[$parameter->getName()] = $parameter;
}

$conditionalTypes = [];
TypeTraverser::map($acceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$conditionalTypes[] = $type;
}

return $traverse($type);
});

$errors = [];
foreach ($conditionalTypes as $conditionalType) {
if ($conditionalType instanceof ConditionalType) {
$subjectType = $conditionalType->getSubject();
if (!$subjectType instanceof TemplateType || $templateTypeMap->getType($subjectType->getName()) === null) {
$errors[] = RuleErrorBuilder::message(sprintf('Conditional return type uses subject type %s which is not part of PHPDoc @template tags.', $subjectType->describe(VerbosityLevel::typeOnly())))->build();
continue;
}
} else {
$parameterName = substr($conditionalType->getParameterName(), 1);
if (!array_key_exists($parameterName, $parametersByName)) {
$errors[] = RuleErrorBuilder::message(sprintf('Conditional return type references unknown parameter $%s.', $parameterName))->build();
continue;
}
$subjectType = $parametersByName[$parameterName]->getType();
}

$targetType = $conditionalType->getTarget();
$isTargetSuperType = $targetType->isSuperTypeOf($subjectType);
if ($isTargetSuperType->maybe()) {
continue;
}

$errors[] = RuleErrorBuilder::message(sprintf(
'Condition "%s" in conditional return type is always %s.',
sprintf('%s %s %s', $subjectType->describe(VerbosityLevel::typeOnly()), $conditionalType->isNegated() ? 'is not' : 'is', $targetType->describe(VerbosityLevel::typeOnly())),
$conditionalType->isNegated()
? ($isTargetSuperType->yes() ? 'false' : 'true')
: ($isTargetSuperType->yes() ? 'true' : 'false'),
))->build();
}

return $errors;
}

}
42 changes: 42 additions & 0 deletions src/Rules/PhpDoc/FunctionConditionalReturnTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InFunctionNode;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use function count;

/**
* @implements Rule<InFunctionNode>
*/
class FunctionConditionalReturnTypeRule implements Rule
{

public function __construct(private ConditionalReturnTypeRuleHelper $helper)
{
}

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

public function processNode(Node $node, Scope $scope): array
{
$method = $scope->getFunction();
if ($method === null) {
throw new ShouldNotHappenException();
}

$variants = $method->getVariants();
if (count($variants) !== 1) {
return [];
}

return $this->helper->check($variants[0]);
}

}
1 change: 0 additions & 1 deletion src/Rules/PhpDoc/IncompatiblePhpDocTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$functionName = null;
if ($node instanceof Node\Stmt\ClassMethod) {
$functionName = $node->name->name;
} elseif ($node instanceof Node\Stmt\Function_) {
Expand Down
42 changes: 42 additions & 0 deletions src/Rules/PhpDoc/MethodConditionalReturnTypeRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\InClassMethodNode;
use PHPStan\Rules\Rule;
use PHPStan\ShouldNotHappenException;
use function count;

/**
* @implements Rule<InClassMethodNode>
*/
class MethodConditionalReturnTypeRule implements Rule
{

public function __construct(private ConditionalReturnTypeRuleHelper $helper)
{
}

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

public function processNode(Node $node, Scope $scope): array
{
$method = $scope->getFunction();
if ($method === null) {
throw new ShouldNotHappenException();
}

$variants = $method->getVariants();
if (count($variants) !== 1) {
return [];
}

return $this->helper->check($variants[0]);
}

}
15 changes: 15 additions & 0 deletions src/Type/ConditionalType.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ public function __construct(
$this->else = $else;
}

public function getSubject(): Type
{
return $this->subject;
}

public function getTarget(): Type
{
return $this->target;
}

public function isNegated(): bool
{
return $this->negated;
}

public function getReferencedClasses(): array
{
return array_merge(
Expand Down
10 changes: 10 additions & 0 deletions src/Type/ConditionalTypeForParameter.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ public function getParameterName(): string
return $this->parameterName;
}

public function getTarget(): Type
{
return $this->target;
}

public function isNegated(): bool
{
return $this->negated;
}

public function changeParameterName(string $parameterName): self
{
return new self(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<FunctionConditionalReturnTypeRule>
*/
class FunctionConditionalReturnTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new FunctionConditionalReturnTypeRule(new ConditionalReturnTypeRuleHelper());
}

public function testRule(): void
{
require_once __DIR__ . '/data/function-conditional-return-type.php';
$this->analyse([__DIR__ . '/data/function-conditional-return-type.php'], [
[
'Conditional return type uses subject type stdClass which is not part of PHPDoc @template tags.',
37,
],
[
'Conditional return type references unknown parameter $j.',
45,
],
[
'Condition "int is int" in conditional return type is always true.',
53,
],
[
'Condition "T of int is int" in conditional return type is always true.',
63,
],
[
'Condition "T of int is int" in conditional return type is always true.',
73,
],
[
'Condition "int is not int" in conditional return type is always false.',
81,
],
[
'Condition "int is string" in conditional return type is always false.',
89,
],
[
'Condition "T of int is string" in conditional return type is always false.',
99,
],
[
'Condition "T of int is string" in conditional return type is always false.',
109,
],
[
'Condition "int is not string" in conditional return type is always true.',
117,
],
]);
}

}
69 changes: 69 additions & 0 deletions tests/PHPStan/Rules/PhpDoc/MethodConditionalReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\PhpDoc;

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<MethodConditionalReturnTypeRule>
*/
class MethodConditionalReturnTypeRuleTest extends RuleTestCase
{

protected function getRule(): Rule
{
return new MethodConditionalReturnTypeRule(new ConditionalReturnTypeRuleHelper());
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/method-conditional-return-type.php'], [
[
'Conditional return type uses subject type stdClass which is not part of PHPDoc @template tags.',
48,
],
[
'Conditional return type uses subject type TAboveClass which is not part of PHPDoc @template tags.',
57,
],
[
'Conditional return type references unknown parameter $j.',
65,
],
[
'Condition "int is int" in conditional return type is always true.',
73,
],
[
'Condition "T of int is int" in conditional return type is always true.',
83,
],
[
'Condition "T of int is int" in conditional return type is always true.',
93,
],
[
'Condition "int is not int" in conditional return type is always false.',
101,
],
[
'Condition "int is string" in conditional return type is always false.',
114,
],
[
'Condition "T of int is string" in conditional return type is always false.',
124,
],
[
'Condition "T of int is string" in conditional return type is always false.',
134,
],
[
'Condition "int is not string" in conditional return type is always true.',
142,
],
]);
}

}

0 comments on commit a8d9628

Please sign in to comment.