-
Notifications
You must be signed in to change notification settings - Fork 574
Recognize [$obj, $method] as callable when is_callable($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
Changes from 10 commits
819b1a1
868518b
62e36ad
9480606
8f297f0
766c820
085f943
f4e3d82
2dec7c6
cffe542
811afa8
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,11 @@ | |
| use PHPStan\Node\LiteralArrayItem; | ||
| use PHPStan\Node\LiteralArrayNode; | ||
| use PHPStan\Reflection\InitializerExprTypeResolver; | ||
| use PHPStan\Type\CallableType; | ||
| use PHPStan\Type\Type; | ||
| use PHPStan\Type\TypeCombinator; | ||
| use function array_merge; | ||
| use function count; | ||
|
|
||
| /** | ||
| * @implements ExprHandler<Array_> | ||
|
|
@@ -38,7 +44,23 @@ | |
|
|
||
| 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()) { | ||
|
Check warning on line 58 in src/Analyser/ExprHandler/ArrayHandler.php
|
||
|
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. Why would Just checking We should add a test with
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. All changes are pushed. Here's a summary of what was done: Root cause of Fix: Changed both Changes:
All 11,935 tests pass and static analysis reports no errors. |
||
| $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])) { | ||
|
staabm marked this conversation as resolved.
|
||
| 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.