-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
083c9b5
commit 30ff227
Showing
10 changed files
with
362 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Classes; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Node\InstantiationCallableNode; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
|
||
/** | ||
* @implements \PHPStan\Rules\Rule<InstantiationCallableNode> | ||
*/ | ||
class InstantiationCallableRule implements \PHPStan\Rules\Rule | ||
{ | ||
|
||
public function getNodeType(): string | ||
{ | ||
return InstantiationCallableNode::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
return [ | ||
RuleErrorBuilder::message('Cannot create callable from the new operator.')->nonIgnorable()->build(), | ||
]; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Functions; | ||
|
||
use PhpParser\Node; | ||
use PHPStan\Analyser\NullsafeOperatorHelper; | ||
use PHPStan\Analyser\Scope; | ||
use PHPStan\Node\FunctionCallableNode; | ||
use PHPStan\Php\PhpVersion; | ||
use PHPStan\Reflection\ReflectionProvider; | ||
use PHPStan\Rules\RuleErrorBuilder; | ||
use PHPStan\Rules\RuleLevelHelper; | ||
use PHPStan\Type\ErrorType; | ||
use PHPStan\Type\Type; | ||
use PHPStan\Type\VerbosityLevel; | ||
|
||
/** | ||
* @implements \PHPStan\Rules\Rule<FunctionCallableNode> | ||
*/ | ||
class FunctionCallableRule implements \PHPStan\Rules\Rule | ||
{ | ||
|
||
private ReflectionProvider $reflectionProvider; | ||
|
||
private RuleLevelHelper $ruleLevelHelper; | ||
|
||
private PhpVersion $phpVersion; | ||
|
||
private bool $checkFunctionNameCase; | ||
|
||
private bool $reportMaybes; | ||
|
||
public function __construct(ReflectionProvider $reflectionProvider, RuleLevelHelper $ruleLevelHelper, PhpVersion $phpVersion, bool $checkFunctionNameCase, bool $reportMaybes) | ||
{ | ||
$this->reflectionProvider = $reflectionProvider; | ||
$this->ruleLevelHelper = $ruleLevelHelper; | ||
$this->phpVersion = $phpVersion; | ||
$this->checkFunctionNameCase = $checkFunctionNameCase; | ||
$this->reportMaybes = $reportMaybes; | ||
} | ||
|
||
public function getNodeType(): string | ||
{ | ||
return FunctionCallableNode::class; | ||
} | ||
|
||
public function processNode(Node $node, Scope $scope): array | ||
{ | ||
if (!$this->phpVersion->supportsFirstClassCallables()) { | ||
return [ | ||
RuleErrorBuilder::message('First-class callables are supported only on PHP 8.1 and later.') | ||
->nonIgnorable() | ||
->build(), | ||
]; | ||
} | ||
|
||
$functionName = $node->getName(); | ||
if ($functionName instanceof Node\Name) { | ||
$functionNameName = $functionName->toString(); | ||
if ($this->reflectionProvider->hasFunction($functionName, $scope)) { | ||
if ($this->checkFunctionNameCase) { | ||
$function = $this->reflectionProvider->getFunction($functionName, $scope); | ||
|
||
/** @var string $calledFunctionName */ | ||
$calledFunctionName = $this->reflectionProvider->resolveFunctionName($functionName, $scope); | ||
if ( | ||
strtolower($function->getName()) === strtolower($calledFunctionName) | ||
&& $function->getName() !== $calledFunctionName | ||
) { | ||
return [ | ||
RuleErrorBuilder::message(sprintf( | ||
'Call to function %s() with incorrect case: %s', | ||
$function->getName(), | ||
$functionNameName | ||
))->build(), | ||
]; | ||
} | ||
} | ||
|
||
return []; | ||
} | ||
|
||
if ($scope->isInFunctionExists($functionNameName)) { | ||
return []; | ||
} | ||
|
||
return [ | ||
RuleErrorBuilder::message(sprintf('Function %s not found.', $functionNameName)) | ||
->build(), | ||
]; | ||
} | ||
|
||
$typeResult = $this->ruleLevelHelper->findTypeToCheck( | ||
$scope, | ||
NullsafeOperatorHelper::getNullsafeShortcircuitedExprRespectingScope($scope, $functionName), | ||
'Creating callable from an unknown class %s.', | ||
static function (Type $type): bool { | ||
return $type->isCallable()->yes(); | ||
} | ||
); | ||
$type = $typeResult->getType(); | ||
if ($type instanceof ErrorType) { | ||
return $typeResult->getUnknownClassErrors(); | ||
} | ||
|
||
$isCallable = $type->isCallable(); | ||
if ($isCallable->no()) { | ||
return [ | ||
RuleErrorBuilder::message( | ||
sprintf('Trying to create callable from %s but it\'s not a callable.', $type->describe(VerbosityLevel::value())) | ||
)->build(), | ||
]; | ||
} | ||
if ($this->reportMaybes && $isCallable->maybe()) { | ||
return [ | ||
RuleErrorBuilder::message( | ||
sprintf('Trying to create callable from %s but it might not be a callable.', $type->describe(VerbosityLevel::value())) | ||
)->build(), | ||
]; | ||
} | ||
|
||
return []; | ||
} | ||
|
||
} |
32 changes: 32 additions & 0 deletions
32
tests/PHPStan/Rules/Classes/InstantiationCallableRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Classes; | ||
|
||
use PHPStan\Rules\Rule; | ||
use PHPStan\Testing\RuleTestCase; | ||
|
||
/** | ||
* @extends RuleTestCase<InstantiationCallableRule> | ||
*/ | ||
class InstantiationCallableRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
return new InstantiationCallableRule(); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
if (!self::$useStaticReflectionProvider) { | ||
self::markTestSkipped('Test requires static reflection.'); | ||
} | ||
$this->analyse([__DIR__ . '/data/instantiation-callable.php'], [ | ||
[ | ||
'Cannot create callable from the new operator.', | ||
11, | ||
], | ||
]); | ||
} | ||
|
||
} |
14 changes: 14 additions & 0 deletions
14
tests/PHPStan/Rules/Classes/data/instantiation-callable.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace InstantiationCallable; | ||
|
||
class Foo | ||
{ | ||
|
||
public function doFoo() | ||
{ | ||
$a = new self(); | ||
$f = new self(...); | ||
} | ||
|
||
} |
80 changes: 80 additions & 0 deletions
80
tests/PHPStan/Rules/Functions/FunctionCallableRuleTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
<?php declare(strict_types = 1); | ||
|
||
namespace PHPStan\Rules\Functions; | ||
|
||
use PHPStan\Php\PhpVersion; | ||
use PHPStan\Rules\Rule; | ||
use PHPStan\Rules\RuleLevelHelper; | ||
use PHPStan\Testing\RuleTestCase; | ||
|
||
/** | ||
* @extends RuleTestCase<FunctionCallableRule> | ||
*/ | ||
class FunctionCallableRuleTest extends RuleTestCase | ||
{ | ||
|
||
protected function getRule(): Rule | ||
{ | ||
$reflectionProvider = $this->createReflectionProvider(); | ||
|
||
return new FunctionCallableRule( | ||
$reflectionProvider, | ||
new RuleLevelHelper($reflectionProvider, true, false, true, false), | ||
new PhpVersion(PHP_VERSION_ID), | ||
true, | ||
true | ||
); | ||
} | ||
|
||
public function testNotSupportedOnOlderVersions(): void | ||
{ | ||
if (PHP_VERSION_ID >= 80100) { | ||
self::markTestSkipped('Test runs on PHP < 8.1.'); | ||
} | ||
if (!self::$useStaticReflectionProvider) { | ||
self::markTestSkipped('Test requires static reflection.'); | ||
} | ||
|
||
$this->analyse([__DIR__ . '/data/function-callable-not-supported.php'], [ | ||
[ | ||
'First-class callables are supported only on PHP 8.1 and later.', | ||
10, | ||
], | ||
]); | ||
} | ||
|
||
public function testRule(): void | ||
{ | ||
if (PHP_VERSION_ID < 80100) { | ||
self::markTestSkipped('Test requires PHP 8.1.'); | ||
} | ||
|
||
$this->analyse([__DIR__ . '/data/function-callable.php'], [ | ||
[ | ||
'Function nonexistent not found.', | ||
13, | ||
], | ||
[ | ||
'Trying to create callable from string but it might not be a callable.', | ||
19, | ||
], | ||
[ | ||
'Trying to create callable from 1 but it\'s not a callable.', | ||
33, | ||
], | ||
[ | ||
'Call to function strlen() with incorrect case: StrLen', | ||
38, | ||
], | ||
[ | ||
'Trying to create callable from 1|(callable(): mixed) but it might not be a callable.', | ||
47, | ||
], | ||
[ | ||
'Creating callable from an unknown class FunctionCallable\Nonexistent.', | ||
52, | ||
], | ||
]); | ||
} | ||
|
||
} |
13 changes: 13 additions & 0 deletions
13
tests/PHPStan/Rules/Functions/data/function-callable-not-supported.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php // lint >= 8.1 | ||
|
||
namespace FunctionCallableNotSupported; | ||
|
||
class Foo | ||
{ | ||
|
||
public function doFoo(): void | ||
{ | ||
$f = json_encode(...); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
<?php // lint >= 8.1 | ||
|
||
namespace FunctionCallable; | ||
|
||
use function function_exists; | ||
|
||
class Foo | ||
{ | ||
|
||
public function doFoo(string $s): void | ||
{ | ||
strlen(...); | ||
nonexistent(...); | ||
|
||
if (function_exists('blabla')) { | ||
blabla(...); | ||
} | ||
|
||
$s(...); | ||
if (function_exists($s)) { | ||
$s(...); | ||
} | ||
} | ||
|
||
public function doBar(): void | ||
{ | ||
$f = function (): void { | ||
|
||
}; | ||
$f(...); | ||
|
||
$i = 1; | ||
$i(...); | ||
} | ||
|
||
public function doBaz(): void | ||
{ | ||
StrLen(...); | ||
} | ||
|
||
public function doLorem(callable $cb): void | ||
{ | ||
if (rand(0, 1)) { | ||
$cb = 1; | ||
} | ||
|
||
$f = $cb(...); | ||
} | ||
|
||
public function doIpsum(Nonexistent $obj): void | ||
{ | ||
$f = $obj(...); | ||
} | ||
|
||
} |