Skip to content

Commit

Permalink
Do not report non-existent function after function_exists() check
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 6, 2021
1 parent 6bb0fd6 commit 28e8c11
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -1306,6 +1306,11 @@ services:
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

-
class: PHPStan\Type\Php\FunctionExistsFunctionTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

-
class: PHPStan\Type\Php\InArrayFunctionTypeSpecifyingExtension
tags:
Expand Down
10 changes: 10 additions & 0 deletions src/Analyser/MutatingScope.php
Expand Up @@ -2765,6 +2765,16 @@ public function isInClassExists(string $className): bool
return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes();
}

/** @api */
public function isInFunctionExists(string $functionName): bool
{
$expr = new FuncCall(new FullyQualified('function_exists'), [
new Arg(new String_(ltrim($functionName, '\\'))),
]);

return (new ConstantBooleanType(true))->isSuperTypeOf($this->getType($expr))->yes();
}

/** @api */
public function enterClass(ClassReflection $classReflection): self
{
Expand Down
2 changes: 2 additions & 0 deletions src/Analyser/Scope.php
Expand Up @@ -89,6 +89,8 @@ public function isSpecified(Expr $node): bool;

public function isInClassExists(string $className): bool;

public function isInFunctionExists(string $functionName): bool;

public function isInClosureBind(): bool;

public function isParameterValueNullable(Param $parameter): bool;
Expand Down
4 changes: 4 additions & 0 deletions src/Rules/Functions/CallToNonExistentFunctionRule.php
Expand Up @@ -39,6 +39,10 @@ public function processNode(Node $node, Scope $scope): array
}

if (!$this->reflectionProvider->hasFunction($node->name, $scope)) {
if ($scope->isInFunctionExists($node->name->toString())) {
return [];
}

return [
RuleErrorBuilder::message(sprintf('Function %s not found.', (string) $node->name))->discoveringSymbolsTip()->build(),
];
Expand Down
56 changes: 56 additions & 0 deletions src/Type/Php/FunctionExistsFunctionTypeSpecifyingExtension.php
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;

class FunctionExistsFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function isFunctionSupported(
FunctionReflection $functionReflection,
FuncCall $node,
TypeSpecifierContext $context
): bool
{
return $functionReflection->getName() === 'function_exists' && isset($node->getArgs()[0]) && $context->truthy();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$argType = $scope->getType($node->getArgs()[0]->value);
if ($argType instanceof ConstantStringType) {
return $this->typeSpecifier->create(
new FuncCall(new FullyQualified('function_exists'), [
new Arg(new String_(ltrim($argType->getValue(), '\\'))),
]),
new ConstantBooleanType(true),
$context,
false,
$scope
);
}

return new SpecifiedTypes();
}

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

}
Expand Up @@ -121,4 +121,40 @@ public function testCreateFunctionPhp7(): void
$this->analyse([__DIR__ . '/data/create_function.php'], []);
}

public function testBug3576(): void
{
$this->analyse([__DIR__ . '/data/bug-3576.php'], [
[
'Function bug3576 not found.',
14,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Function bug3576 not found.',
17,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Function bug3576 not found.',
26,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Function bug3576 not found.',
29,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Function bug3576 not found.',
38,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
[
'Function bug3576 not found.',
41,
'Learn more at https://phpstan.org/user-guide/discovering-symbols',
],
]);
}

}
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-3576.php
@@ -0,0 +1,45 @@
<?php

namespace Bug3576;

class Foo
{

public function doFoo(): void
{
if (\function_exists('bug3576')) {
bug3576();
\bug3576();
} else {
bug3576();
}

bug3576();
}

public function doBar(): void
{
if (function_exists('bug3576')) {
bug3576();
\bug3576();
} else {
bug3576();
}

bug3576();
}

public function doBaz(): void
{
if (function_exists('\bug3576')) {
bug3576();
\bug3576();
} else {
bug3576();
}

bug3576();
}

}

0 comments on commit 28e8c11

Please sign in to comment.