From 0776d8cf60e41be695106a5b59584423bef4c48f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 May 2024 08:05:20 +0200 Subject: [PATCH 1/4] Avoid false positive about non-callable --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 13 +++++++++++++ tests/CallableTest.php | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 5442bfae87a..7b20cb156b1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -955,12 +955,25 @@ public static function verifyType( ) { $potential_method_ids = []; + $builder = $param_type->getBuilder(); + $builder->removeType('callable'); + $param_type_without_callable = $builder->freeze(); + foreach ($input_type->getAtomicTypes() as $input_type_part) { if ($input_type_part instanceof TList) { $input_type_part = $input_type_part->getKeyedArray(); } if ($input_type_part instanceof TKeyedArray) { + // If the param accept an array, we don't report arrays as wrong callbacks. + if (UnionTypeComparator::isContainedBy( + $codebase, + $input_type, + $param_type_without_callable, + )) { + continue; + } + $potential_method_id = CallableTypeComparator::getCallableMethodIdFromTKeyedArray( $input_type_part, $codebase, diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 5ca5f06986d..7e3744f0944 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -2210,6 +2210,15 @@ function foo($arg): void {} foo(["a", "b"]);', ], + 'notCallableArray' => [ + 'code' => ' [ 'code' => ' Date: Fri, 3 May 2024 08:25:12 +0200 Subject: [PATCH 2/4] Fix --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 7b20cb156b1..74a84c94bed 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -955,9 +955,12 @@ public static function verifyType( ) { $potential_method_ids = []; - $builder = $param_type->getBuilder(); - $builder->removeType('callable'); - $param_type_without_callable = $builder->freeze(); + $param_type_without_callable = new Union(array_filter( + $param_type->getAtomicTypes(), + static function (Atomic $atomic) { + return !$atomic instanceof Atomic\TCallableInterface; + }) + ); foreach ($input_type->getAtomicTypes() as $input_type_part) { if ($input_type_part instanceof TList) { From 3b512cedc3af14618da01475df01167bb4164373 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 May 2024 08:30:14 +0200 Subject: [PATCH 3/4] Fix --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 74a84c94bed..d577e3c6fa1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -63,6 +63,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_filter; use function count; use function explode; use function implode; @@ -955,12 +956,13 @@ public static function verifyType( ) { $potential_method_ids = []; - $param_type_without_callable = new Union(array_filter( + $param_types_without_callable = array_filter( $param_type->getAtomicTypes(), - static function (Atomic $atomic) { - return !$atomic instanceof Atomic\TCallableInterface; - }) + static fn(Atomic $atomic) => !$atomic instanceof Atomic\TCallableInterface, ); + $param_type_without_callable = [] !== $param_types_without_callable + ? new Union($param_types_without_callable) + : null; foreach ($input_type->getAtomicTypes() as $input_type_part) { if ($input_type_part instanceof TList) { @@ -969,7 +971,7 @@ static function (Atomic $atomic) { if ($input_type_part instanceof TKeyedArray) { // If the param accept an array, we don't report arrays as wrong callbacks. - if (UnionTypeComparator::isContainedBy( + if (null !== $param_type_without_callable && UnionTypeComparator::isContainedBy( $codebase, $input_type, $param_type_without_callable, From e97dbab4063d15c8010247c527a8c0d9133ddad0 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 3 May 2024 17:10:29 +0200 Subject: [PATCH 4/4] Feedback --- .../Expression/Call/ArgumentAnalyzer.php | 9 +++++++++ tests/CallableTest.php | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index d577e3c6fa1..2fd7cd1b5a7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -1024,6 +1024,15 @@ public static function verifyType( } elseif ($input_type_part instanceof TLiteralString && strpos($input_type_part->value, '::') ) { + // If the param also accept a string, we don't report string as wrong callbacks. + if (null !== $param_type_without_callable && UnionTypeComparator::isContainedBy( + $codebase, + $input_type, + $param_type_without_callable, + )) { + continue; + } + $parts = explode('::', $input_type_part->value); /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ $potential_method_id = new MethodIdentifier( diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 7e3744f0944..f59707faa35 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -2219,6 +2219,15 @@ function foo($arg): void {} foo([\DateTime::class, "format"]);', ], + 'notCallableString' => [ + 'code' => ' [ 'code' => ' 'ParentNotFound', ], + 'wrongCallableInUnion' => [ + 'code' => ' 'InvalidArgument', + ], ]; } }