-
Notifications
You must be signed in to change notification settings - Fork 574
Recognize [$obj, $method] as callable when method_exists($obj, $method) is known true in scope
#5547
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 2.1.x
Are you sure you want to change the base?
Recognize [$obj, $method] as callable when method_exists($obj, $method) is known true in scope
#5547
Changes from all commits
819b1a1
868518b
62e36ad
9480606
8f297f0
766c820
085f943
f4e3d82
2dec7c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,11 @@ | |
|
|
||
| namespace PHPStan\Analyser\ExprHandler; | ||
|
|
||
| use PhpParser\Node\Arg; | ||
| use PhpParser\Node\Expr; | ||
| use PhpParser\Node\Expr\Array_; | ||
| use PhpParser\Node\Expr\FuncCall; | ||
| use PhpParser\Node\Name\FullyQualified; | ||
| use PhpParser\Node\Stmt; | ||
| use PHPStan\Analyser\ExpressionContext; | ||
| use PHPStan\Analyser\ExpressionResult; | ||
|
|
@@ -15,8 +18,12 @@ | |
| use PHPStan\Node\LiteralArrayItem; | ||
| use PHPStan\Node\LiteralArrayNode; | ||
| use PHPStan\Reflection\InitializerExprTypeResolver; | ||
| use PHPStan\Type\CallableType; | ||
| use PHPStan\Type\Constant\ConstantBooleanType; | ||
| use PHPStan\Type\Type; | ||
| use PHPStan\Type\TypeCombinator; | ||
| use function array_merge; | ||
| use function count; | ||
|
|
||
| /** | ||
| * @implements ExprHandler<Array_> | ||
|
|
@@ -38,7 +45,26 @@ public function supports(Expr $expr): bool | |
|
|
||
| public function resolveType(MutatingScope $scope, Expr $expr): Type | ||
| { | ||
| return $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); | ||
| $type = $this->initializerExprTypeResolver->getArrayType($expr, static fn (Expr $expr): Type => $scope->getType($expr)); | ||
|
|
||
| if ( | ||
| count($expr->items) === 2 | ||
| && isset($expr->items[0], $expr->items[1]) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Also, how are indexed the item ?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pushed. Here's a summary of the changes addressing VincentLanglet's review:
All tests (11,927) and static analysis pass. |
||
| && $type->isCallable()->maybe() | ||
| ) { | ||
| $isCallableCall = new FuncCall( | ||
| new FullyQualified('is_callable'), | ||
| [new Arg($expr->items[0]->value), new Arg($expr->items[1]->value)], | ||
| ); | ||
| if ( | ||
| $scope->hasExpressionType($isCallableCall)->yes() | ||
| && (new ConstantBooleanType(true))->isSuperTypeOf($scope->getType($isCallableCall))->yes() | ||
| ) { | ||
| $type = TypeCombinator::intersect($type, new CallableType()); | ||
| } | ||
| } | ||
|
|
||
| return $type; | ||
| } | ||
|
|
||
| public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| <?php declare(strict_types = 1); | ||
|
|
||
| namespace Bug4510; | ||
|
|
||
| use function PHPStan\Testing\assertType; | ||
|
|
||
| class HelloWorld | ||
| { | ||
| public function existingMethod(): void {} | ||
|
|
||
| public function doSomething(string $method): void { | ||
| if (!method_exists($this, $method)) { | ||
| return; | ||
| } | ||
|
|
||
| [$this, $method](); // error - method_exists doesn't imply callable | ||
| } | ||
| } | ||
|
|
||
| function testMethodExists(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (!method_exists($instance, $method)) { | ||
| return; | ||
| } | ||
|
|
||
| assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]); | ||
| [$instance, $method](); // error - method_exists doesn't imply callable | ||
| } | ||
|
|
||
| function testIsCallableInlineArray(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (!is_callable([$instance, $method])) { | ||
| return; | ||
| } | ||
|
|
||
| assertType('list{Bug4510\HelloWorld, string}&callable(): mixed', [$instance, $method]); | ||
| [$instance, $method](); // ok - is_callable verifies callability | ||
| } | ||
|
|
||
| function testMethodExistsWithClassString(string $method): void { | ||
| if (!method_exists(HelloWorld::class, $method)) { | ||
| return; | ||
| } | ||
|
|
||
| assertType("array{'Bug4510\\\\HelloWorld', string}", [HelloWorld::class, $method]); | ||
| [HelloWorld::class, $method](); // error - method_exists doesn't imply callable | ||
| } | ||
|
|
||
| function testIsCallableWithClassString(string $method): void { | ||
| if (!is_callable([HelloWorld::class, $method])) { | ||
| return; | ||
| } | ||
|
|
||
| assertType("list{'Bug4510\\\\HelloWorld', string}&callable(): mixed", [HelloWorld::class, $method]); | ||
| [HelloWorld::class, $method](); // ok - is_callable verifies callability | ||
| } | ||
|
|
||
| function testIsCallableExplicitKeys(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (!is_callable([0 => $instance, 1 => $method])) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seeing this makes me think we should also test with named arguments |
||
| return; | ||
| } | ||
|
|
||
| assertType('list{Bug4510\HelloWorld, string}&callable(): mixed', [0 => $instance, 1 => $method]); | ||
| [0 => $instance, 1 => $method](); // ok - is_callable verifies callability | ||
| } | ||
|
|
||
| function testIsCallableExplicitKeysWithClassString(string $method): void { | ||
| if (!is_callable([0 => HelloWorld::class, 1 => $method])) { | ||
| return; | ||
| } | ||
|
|
||
| assertType("list{'Bug4510\\\\HelloWorld', string}&callable(): mixed", [0 => HelloWorld::class, 1 => $method]); | ||
| [0 => HelloWorld::class, 1 => $method](); // ok - is_callable verifies callability | ||
| } | ||
|
|
||
| function testWithDynamicMethodExistsAndVariable(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| $callable = [$instance, $method]; | ||
| if (!is_callable($callable)) { | ||
| return; | ||
| } | ||
|
|
||
| $callable(); // ok - is_callable on variable already worked | ||
| } | ||
|
|
||
| function testMethodExistsInElseBranch(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (method_exists($instance, $method)) { | ||
| [$instance, $method](); // error - method_exists doesn't imply callable | ||
| } | ||
| } | ||
|
|
||
| function testIsCallableInElseBranch(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (is_callable([$instance, $method])) { | ||
| [$instance, $method](); // ok - is_callable verifies callability | ||
| } | ||
| } | ||
|
|
||
| function testIsCallableNamedArg(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (!is_callable(value: [$instance, $method])) { | ||
| return; | ||
| } | ||
|
|
||
| assertType('list{Bug4510\HelloWorld, string}&callable(): mixed', [$instance, $method]); | ||
| [$instance, $method](); // ok - is_callable verifies callability | ||
| } | ||
|
|
||
| function testMethodExistsNamedArgs(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| if (!method_exists(object_or_class: $instance, method: $method)) { | ||
| return; | ||
| } | ||
|
|
||
| assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]); | ||
| [$instance, $method](); // error - method_exists doesn't imply callable | ||
| } | ||
|
|
||
| function testNoMethodExists(string $method): void { | ||
| $instance = new HelloWorld(); | ||
| assertType('array{Bug4510\HelloWorld, string}', [$instance, $method]); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we limit this to 2 item arrays only?
that way we would not need to check any literal array we find, whether its callable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Added
count($expr->items) === 2as an early guard before theisCallable()->maybe()check, so we only evaluate callable-related logic on arrays that could actually be[$obj, $method]pairs. Tests and static analysis both pass.