Skip to content

Commit

Permalink
Scope - universal constant booleans for type-specified methods and st…
Browse files Browse the repository at this point in the history
…atic methods, related ImpossibleCheckType rules
  • Loading branch information
ondrejmirtes committed Apr 1, 2018
1 parent 49aa301 commit 43ebec3
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 17 deletions.
14 changes: 14 additions & 0 deletions conf/config.level4.neon
Expand Up @@ -25,6 +25,20 @@ services:
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Comparison\ImpossibleCheckTypeMethodCallRule
arguments:
checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Comparison\ImpossibleCheckTypeStaticMethodCallRule
arguments:
checkAlwaysTrueCheckTypeFunctionCall: %checkAlwaysTrueCheckTypeFunctionCall%
tags:
- phpstan.rules.rule

-
class: PHPStan\Rules\Comparison\StrictComparisonOfDifferentTypesRule
arguments:
Expand Down
68 changes: 52 additions & 16 deletions src/Analyser/Scope.php
Expand Up @@ -25,6 +25,7 @@
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
use PHPStan\Reflection\PropertyReflection;
Expand Down Expand Up @@ -861,6 +862,17 @@ private function resolveType(Expr $node): Type
}

$methodReflection = $methodClassReflection->getMethod($node->name, $this);
foreach ($this->typeSpecifier->getMethodTypeSpecifyingExtensionsForClass($methodClassReflection->getName()) as $functionTypeSpecifyingExtension) {
if (!$functionTypeSpecifyingExtension->isMethodSupported($methodReflection, $node, TypeSpecifierContext::createTruthy())) {
continue;
}

$specifiedType = $this->findSpecifiedType($node, $methodReflection);
if ($specifiedType !== null) {
return $specifiedType;
}
}

foreach ($this->broker->getDynamicMethodReturnTypeExtensionsForClass($methodClassReflection->getName()) as $dynamicMethodReturnTypeExtension) {
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
continue;
Expand Down Expand Up @@ -907,6 +919,17 @@ private function resolveType(Expr $node): Type
&& $this->broker->hasClass($referencedClasses[0])
) {
$staticMethodClassReflection = $this->broker->getClass($referencedClasses[0]);
foreach ($this->typeSpecifier->getStaticMethodTypeSpecifyingExtensionsForClass($staticMethodClassReflection->getName()) as $functionTypeSpecifyingExtension) {
if (!$functionTypeSpecifyingExtension->isStaticMethodSupported($staticMethodReflection, $node, TypeSpecifierContext::createTruthy())) {
continue;
}

$specifiedType = $this->findSpecifiedType($node, $staticMethodReflection);
if ($specifiedType !== null) {
return $specifiedType;
}
}

foreach ($this->broker->getDynamicStaticMethodReturnTypeExtensionsForClass($staticMethodClassReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) {
if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($staticMethodReflection)) {
continue;
Expand Down Expand Up @@ -971,22 +994,9 @@ private function resolveType(Expr $node): Type
continue;
}

$sureTypes = $this->typeSpecifier->specifyTypesInCondition($this, $node, TypeSpecifierContext::createTruthy())->getSureTypes();
if (count($sureTypes) === 1) {
$sureType = reset($sureTypes);
$argumentType = $this->getType($sureType[0]);

/** @var \PHPStan\Type\Type $resultType */
$resultType = $sureType[1];

$isSuperType = $resultType->isSuperTypeOf($argumentType);
if ($isSuperType->yes()) {
return new ConstantBooleanType(true);
} elseif ($isSuperType->no()) {
return new ConstantBooleanType(false);
}

return $functionReflection->getReturnType();
$specifiedType = $this->findSpecifiedType($node, $functionReflection);
if ($specifiedType !== null) {
return $specifiedType;
}
}

Expand All @@ -1004,6 +1014,32 @@ private function resolveType(Expr $node): Type
return new MixedType();
}

private function findSpecifiedType(
Expr $node,
ParametersAcceptor $parametersAcceptor
): ?Type
{
$sureTypes = $this->typeSpecifier->specifyTypesInCondition($this, $node, TypeSpecifierContext::createTruthy())->getSureTypes();
if (count($sureTypes) === 1) {
$sureType = reset($sureTypes);
$argumentType = $this->getType($sureType[0]);

/** @var \PHPStan\Type\Type $resultType */
$resultType = $sureType[1];

$isSuperType = $resultType->isSuperTypeOf($argumentType);
if ($isSuperType->yes()) {
return new ConstantBooleanType(true);
} elseif ($isSuperType->no()) {
return new ConstantBooleanType(false);
}

return $parametersAcceptor->getReturnType();
}

return null;
}

public function resolveName(Name $name): string
{
$originalClass = (string) $name;
Expand Down
80 changes: 80 additions & 0 deletions src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php
@@ -0,0 +1,80 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;

class ImpossibleCheckTypeMethodCallRule implements \PHPStan\Rules\Rule
{

/** @var bool */
private $checkAlwaysTrueCheckTypeFunctionCall;

public function __construct(
bool $checkAlwaysTrueCheckTypeFunctionCall
)
{
$this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall;
}

public function getNodeType(): string
{
return \PhpParser\Node\Expr\MethodCall::class;
}

/**
* @param \PhpParser\Node\Expr\MethodCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[] errors
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}

$nodeType = $scope->getType($node);
if (!$nodeType instanceof ConstantBooleanType) {
return [];
}

if (!$nodeType->getValue()) {
$method = $this->getMethod($node->var, $node->name, $scope);

return [sprintf(
'Call to method %s::%s() will always evaluate to false.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
)];
} elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) {
$method = $this->getMethod($node->var, $node->name, $scope);

return [sprintf(
'Call to method %s::%s() will always evaluate to true.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
)];
}

return [];
}

private function getMethod(
Expr $var,
string $methodName,
Scope $scope
): MethodReflection
{
$calledOnType = $scope->getType($var);
if (!$calledOnType->hasMethod($methodName)) {
throw new \PHPStan\ShouldNotHappenException();
}

return $calledOnType->getMethod($methodName, $scope);
}

}
93 changes: 93 additions & 0 deletions src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php
@@ -0,0 +1,93 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PhpParser\Node;
use PhpParser\Node\Expr;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\ObjectType;

class ImpossibleCheckTypeStaticMethodCallRule implements \PHPStan\Rules\Rule
{

/** @var bool */
private $checkAlwaysTrueCheckTypeFunctionCall;

public function __construct(
bool $checkAlwaysTrueCheckTypeFunctionCall
)
{
$this->checkAlwaysTrueCheckTypeFunctionCall = $checkAlwaysTrueCheckTypeFunctionCall;
}

public function getNodeType(): string
{
return \PhpParser\Node\Expr\StaticCall::class;
}

/**
* @param \PhpParser\Node\Expr\StaticCall $node
* @param \PHPStan\Analyser\Scope $scope
* @return string[] errors
*/
public function processNode(Node $node, Scope $scope): array
{
if (!is_string($node->name)) {
return [];
}

$nodeType = $scope->getType($node);
if (!$nodeType instanceof ConstantBooleanType) {
return [];
}

if (!$nodeType->getValue()) {
$method = $this->getMethod($node->class, $node->name, $scope);

return [sprintf(
'Call to static method %s::%s() will always evaluate to false.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
)];
} elseif ($this->checkAlwaysTrueCheckTypeFunctionCall) {
$method = $this->getMethod($node->class, $node->name, $scope);

return [sprintf(
'Call to static method %s::%s() will always evaluate to true.',
$method->getDeclaringClass()->getDisplayName(),
$method->getName()
)];
}

return [];
}

/**
* @param Node\Name|Expr $class
* @param string $methodName
* @param Scope $scope
* @return MethodReflection
* @throws \PHPStan\ShouldNotHappenException
*/
private function getMethod(
$class,
string $methodName,
Scope $scope
): MethodReflection
{
if ($class instanceof Node\Name) {
$calledOnType = new ObjectType($scope->resolveName($class));
} else {
$calledOnType = $scope->getType($class);
}

if (!$calledOnType->hasMethod($methodName)) {
throw new \PHPStan\ShouldNotHappenException();
}

return $calledOnType->getMethod($methodName, $scope);
}

}
23 changes: 22 additions & 1 deletion src/Testing/RuleTestCase.php
Expand Up @@ -29,7 +29,12 @@ private function getAnalyser(): Analyser
$broker = $this->createBroker();
$printer = new \PhpParser\PrettyPrinter\Standard();
$fileHelper = $this->getFileHelper();
$typeSpecifier = $this->createTypeSpecifier($printer, $broker);
$typeSpecifier = $this->createTypeSpecifier(
$printer,
$broker,
$this->getMethodTypeSpecifyingExtensions(),
$this->getStaticMethodTypeSpecifyingExtensions()
);
$this->analyser = new Analyser(
$broker,
$this->getParser(),
Expand Down Expand Up @@ -58,6 +63,22 @@ private function getAnalyser(): Analyser
return $this->analyser;
}

/**
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
*/
protected function getMethodTypeSpecifyingExtensions(): array
{
return [];
}

/**
* @return \PHPStan\Type\StaticMethodTypeSpecifyingExtension[]
*/
protected function getStaticMethodTypeSpecifyingExtensions(): array
{
return [];
}

public function analyse(array $files, array $expectedErrors): void
{
$files = array_map([$this->getFileHelper(), 'normalizePath'], $files);
Expand Down
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PHPStan\Tests\AssertionClassMethodTypeSpecifyingExtension;

class ImpossibleCheckTypeMethodCallRuleTest extends \PHPStan\Testing\RuleTestCase
{

public function getRule(): \PHPStan\Rules\Rule
{
return new ImpossibleCheckTypeMethodCallRule(true);
}

/**
* @return \PHPStan\Type\MethodTypeSpecifyingExtension[]
*/
protected function getMethodTypeSpecifyingExtensions(): array
{
return [
new AssertionClassMethodTypeSpecifyingExtension(null),
];
}

public function testRule(): void
{
$this->analyse([__DIR__ . '/data/impossible-method-call.php'], [
[
'Call to method PHPStan\Tests\AssertionClass::assertString() will always evaluate to true.',
14,
],
[
'Call to method PHPStan\Tests\AssertionClass::assertString() will always evaluate to false.',
15,
],
]);
}

}

0 comments on commit 43ebec3

Please sign in to comment.