From 1d6ae16504de98af0705a4a34a107fbfd108a896 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 21:22:47 +0100 Subject: [PATCH 01/17] Add Exception code return type provider --- .../ExceptionCodeReturnTypeProvider.php | 34 +++++++++++ .../ReturnTypeProvider/ExceptionCodeTest.php | 61 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php create mode 100644 tests/ReturnTypeProvider/ExceptionCodeTest.php diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php new file mode 100644 index 00000000000..bf55e08e357 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php @@ -0,0 +1,34 @@ +getMethodNameLowercase(); + $fqcn = $event->getCalledFqClasslikeName(); + + if ($method_name_lowercase !== 'getcode') { + return null; + } + + if ($fqcn === 'Exception' || $fqcn === 'Throwable') { + return null; + } + + if (is_a($fqcn, \PDOException::class, true)) { + return Type::parseString('string'); + } + + return Type::parseString('int'); + } +} diff --git a/tests/ReturnTypeProvider/ExceptionCodeTest.php b/tests/ReturnTypeProvider/ExceptionCodeTest.php new file mode 100644 index 00000000000..ca607b7e685 --- /dev/null +++ b/tests/ReturnTypeProvider/ExceptionCodeTest.php @@ -0,0 +1,61 @@ + [ + 'getCode(); + return $code; + } + ', + ['$code' => 'int'], + ]; + yield 'LogicException' => [ + 'getCode(); + return $code; + } + ', + ['$code' => 'int'], + ]; + yield 'PDOException' => [ + 'getCode(); + return $code; + } + ', + ['$code' => 'string'], + ]; + yield 'Exception' => [ + 'getCode(); + return $code; + } + ', + ['$code' => 'int|string'], + ]; + yield 'Throwable' => [ + 'getCode(); + return $code; + } + ', + ['$code' => 'int|string'], + ]; + } +} From 31c5845a3039f382ed29496947d545fc551a2395 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 21:46:01 +0100 Subject: [PATCH 02/17] Register provider --- src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 8b124ea5532..35d9d406524 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -23,6 +23,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ArraySpliceReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ArrayUniqueReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ArrayValuesReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\ExceptionCodeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ExplodeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider; @@ -109,6 +110,7 @@ public function __construct() $this->registerClass(TriggerErrorReturnTypeProvider::class); $this->registerClass(RandReturnTypeProvider::class); $this->registerClass(InArrayReturnTypeProvider::class); + $this->registerClass(ExceptionCodeReturnTypeProvider::class); } /** From b1285bd0584618d1f388d34172459da42d97d78a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 21:48:20 +0100 Subject: [PATCH 03/17] Fix cs --- .../ExceptionCodeReturnTypeProvider.php | 10 +++++++--- tests/ReturnTypeProvider/ExceptionCodeTest.php | 9 +++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php index bf55e08e357..cbe0ed3f88f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php @@ -3,11 +3,15 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent; +use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\Type; +use PDOException; -class ExceptionCodeReturnTypeProvider implements \Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface +use function is_a; + +class ExceptionCodeReturnTypeProvider implements MethodReturnTypeProviderInterface { - public static function getClassLikeNames() : array + public static function getClassLikeNames(): array { return ['Throwable']; } @@ -25,7 +29,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - if (is_a($fqcn, \PDOException::class, true)) { + if (is_a($fqcn, PDOException::class, true)) { return Type::parseString('string'); } diff --git a/tests/ReturnTypeProvider/ExceptionCodeTest.php b/tests/ReturnTypeProvider/ExceptionCodeTest.php index ca607b7e685..8739fec78a9 100644 --- a/tests/ReturnTypeProvider/ExceptionCodeTest.php +++ b/tests/ReturnTypeProvider/ExceptionCodeTest.php @@ -3,7 +3,6 @@ namespace Psalm\Tests\ReturnTypeProvider; use Psalm\Tests\TestCase; -use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; class ExceptionCodeTest extends TestCase @@ -14,7 +13,7 @@ public function providerValidCodeParse(): iterable { yield 'RuntimeException' => [ 'getCode(); return $code; } @@ -23,7 +22,7 @@ function f(\RuntimeException $e) { ]; yield 'LogicException' => [ 'getCode(); return $code; } @@ -32,7 +31,7 @@ function f(\LogicException $e) { ]; yield 'PDOException' => [ 'getCode(); return $code; } @@ -41,6 +40,7 @@ function f(\PDOException $e) { ]; yield 'Exception' => [ 'getCode(); return $code; @@ -50,6 +50,7 @@ function f(\Exception $e) { ]; yield 'Throwable' => [ 'getCode(); return $code; From f2138ace7bda0eb3ab04fec7aa3809b16ce11a50 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 22:12:39 +0100 Subject: [PATCH 04/17] Avoid calling autoloader --- .../ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php index cbe0ed3f88f..391c644eae5 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php @@ -5,9 +5,6 @@ use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; use Psalm\Type; -use PDOException; - -use function is_a; class ExceptionCodeReturnTypeProvider implements MethodReturnTypeProviderInterface { @@ -29,7 +26,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - if (is_a($fqcn, PDOException::class, true)) { + if ($fqcn === 'PDOException') { return Type::parseString('string'); } From b376a8d4d718e0440052441613a83ee1112d982d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 22:23:41 +0100 Subject: [PATCH 05/17] Wip --- .../ReturnTypeProvider/ExceptionCodeTest.php | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/ReturnTypeProvider/ExceptionCodeTest.php b/tests/ReturnTypeProvider/ExceptionCodeTest.php index 8739fec78a9..ccc8ea10db5 100644 --- a/tests/ReturnTypeProvider/ExceptionCodeTest.php +++ b/tests/ReturnTypeProvider/ExceptionCodeTest.php @@ -14,47 +14,38 @@ public function providerValidCodeParse(): iterable yield 'RuntimeException' => [ 'getCode(); - return $code; + return $e->getCode(); } ', - ['$code' => 'int'], + [], ]; yield 'LogicException' => [ 'getCode(); - return $code; + return $e->getCode(); } ', - ['$code' => 'int'], + [], ]; yield 'PDOException' => [ 'getCode(); - return $code; + return $e->getCode(); } ', - ['$code' => 'string'], + [], ]; yield 'Exception' => [ 'getCode(); - return $code; - } + /** @var \Throwable $e */ + $code = $e->getCode(); ', ['$code' => 'int|string'], ]; yield 'Throwable' => [ 'getCode(); - return $code; - } + /** @var \Exception $e */ + $code = $e->getCode(); ', ['$code' => 'int|string'], ]; From cbfa6a10dadb67c112fa35ba8ee94d212b823507 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 22:43:57 +0100 Subject: [PATCH 06/17] Fix --- src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php | 1 - src/Psalm/Internal/Provider/MethodReturnTypeProvider.php | 2 ++ .../ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 35d9d406524..70d8119aaf7 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -110,7 +110,6 @@ public function __construct() $this->registerClass(TriggerErrorReturnTypeProvider::class); $this->registerClass(RandReturnTypeProvider::class); $this->registerClass(InArrayReturnTypeProvider::class); - $this->registerClass(ExceptionCodeReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index f29dc77d6cc..7e42a861340 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -8,6 +8,7 @@ use Psalm\Context; use Psalm\Internal\Provider\ReturnTypeProvider\ClosureFromCallableReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\DomNodeAppendChild; +use Psalm\Internal\Provider\ReturnTypeProvider\ExceptionCodeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ImagickPixelColorReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\PdoStatementReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\SimpleXmlElementAsXml; @@ -58,6 +59,7 @@ public function __construct() $this->registerClass(SimpleXmlElementAsXml::class); $this->registerClass(PdoStatementReturnTypeProvider::class); $this->registerClass(ClosureFromCallableReturnTypeProvider::class); + $this->registerClass(ExceptionCodeReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php index 391c644eae5..e2c5f447522 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php @@ -16,7 +16,7 @@ public static function getClassLikeNames(): array public static function getMethodReturnType(MethodReturnTypeProviderEvent $event): ?Type\Union { $method_name_lowercase = $event->getMethodNameLowercase(); - $fqcn = $event->getCalledFqClasslikeName(); + $fqcn = $event->getFqClasslikeName(); if ($method_name_lowercase !== 'getcode') { return null; From 9f84da4d6f74ebfd14c2d3fe776a52e57212cc01 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 13 Jan 2022 23:36:46 +0100 Subject: [PATCH 07/17] Try another strategy --- .../Method/MethodCallReturnTypeFetcher.php | 13 +++++++ .../Provider/FunctionReturnTypeProvider.php | 1 - .../Provider/MethodReturnTypeProvider.php | 2 -- .../ExceptionCodeReturnTypeProvider.php | 35 ------------------- 4 files changed, 13 insertions(+), 38 deletions(-) delete mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index cc99d748ec5..4e48a48bd4b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -2,6 +2,8 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call\Method; +use Exception; +use PDOException; use PhpParser; use Psalm\CodeLocation; use Psalm\Codebase; @@ -27,6 +29,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; +use Throwable; use UnexpectedValueException; use function array_filter; @@ -92,6 +95,16 @@ public static function fetch( } } + if ($premixin_method_id->method_name === 'getcode' + && $premixin_method_id->fq_class_name !== Exception::class + && in_array(Throwable::class, $class_storage->class_implements)) { + if ($premixin_method_id->fq_class_name === PDOException::class) { + return Type::getString(); + } else { + return Type::getInt(); + } + } + if ($declaring_method_id && $declaring_method_id !== $method_id) { $declaring_fq_class_name = $declaring_method_id->fq_class_name; $declaring_method_name = $declaring_method_id->method_name; diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 70d8119aaf7..8b124ea5532 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -23,7 +23,6 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ArraySpliceReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ArrayUniqueReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ArrayValuesReturnTypeProvider; -use Psalm\Internal\Provider\ReturnTypeProvider\ExceptionCodeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ExplodeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\FilterVarReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\FirstArgStringReturnTypeProvider; diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index 7e42a861340..f29dc77d6cc 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -8,7 +8,6 @@ use Psalm\Context; use Psalm\Internal\Provider\ReturnTypeProvider\ClosureFromCallableReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\DomNodeAppendChild; -use Psalm\Internal\Provider\ReturnTypeProvider\ExceptionCodeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ImagickPixelColorReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\PdoStatementReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\SimpleXmlElementAsXml; @@ -59,7 +58,6 @@ public function __construct() $this->registerClass(SimpleXmlElementAsXml::class); $this->registerClass(PdoStatementReturnTypeProvider::class); $this->registerClass(ClosureFromCallableReturnTypeProvider::class); - $this->registerClass(ExceptionCodeReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php deleted file mode 100644 index e2c5f447522..00000000000 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ExceptionCodeReturnTypeProvider.php +++ /dev/null @@ -1,35 +0,0 @@ -getMethodNameLowercase(); - $fqcn = $event->getFqClasslikeName(); - - if ($method_name_lowercase !== 'getcode') { - return null; - } - - if ($fqcn === 'Exception' || $fqcn === 'Throwable') { - return null; - } - - if ($fqcn === 'PDOException') { - return Type::parseString('string'); - } - - return Type::parseString('int'); - } -} From 09fc43a4d85b5c86c0c59317bb958fe1d22429e8 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 17 Jan 2022 19:26:42 +0100 Subject: [PATCH 08/17] Add support of docblock method using parent keyword (fix #7411) --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 133 ++++++++++-------- tests/MagicMethodAnnotationTest.php | 33 +++++ 2 files changed, 110 insertions(+), 56 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 0130f030696..5f4b459cec0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -445,37 +445,16 @@ private static function handleNamedCall( $class_storage->final ); - $old_data_provider = $statements_analyzer->node_data; - - $statements_analyzer->node_data = clone $statements_analyzer->node_data; - $context->vars_in_scope['$tmp_mixin_var'] = $new_lhs_type; - $fake_method_call_expr = new VirtualMethodCall( - new VirtualVariable( - 'tmp_mixin_var', - $stmt->class->getAttributes() - ), + return self::forwardCallToInstanceMethod( + $statements_analyzer, + $stmt, $stmt_name, - $stmt->getArgs(), - $stmt->getAttributes() + $context, + 'tmp_mixin_var', + true ); - - if (MethodCallAnalyzer::analyze( - $statements_analyzer, - $fake_method_call_expr, - $context - ) === false) { - return false; - } - - $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr); - - $statements_analyzer->node_data = $old_data_provider; - - $statements_analyzer->node_data->setType($stmt, $fake_method_call_type ?? Type::getMixed()); - - return true; } } } @@ -671,6 +650,18 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { if ($pseudo_method_storage->return_type) { return true; } + } elseif ($stmt->class instanceof PhpParser\Node\Name && $stmt->class->parts[0] === 'parent' + && !$codebase->methodExists($method_id) + && !$statements_analyzer->isStatic() + ) { + // In case of parent::xxx() call on instance method context (i.e. not static context) + // with nonexistent method, we try to forward to instance method call for resolve pseudo method. + return self::forwardCallToInstanceMethod( + $statements_analyzer, + $stmt, + $stmt_name, + $context + ); } if (!$context->check_methods) { @@ -781,37 +772,12 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { } if ($is_dynamic_this_method) { - $old_data_provider = $statements_analyzer->node_data; - - $statements_analyzer->node_data = clone $statements_analyzer->node_data; - - $fake_method_call_expr = new VirtualMethodCall( - new VirtualVariable( - 'this', - $stmt->class->getAttributes() - ), - $stmt_name, - $stmt->getArgs(), - $stmt->getAttributes() - ); - - if (MethodCallAnalyzer::analyze( + return self::forwardCallToInstanceMethod( $statements_analyzer, - $fake_method_call_expr, + $stmt, + $stmt_name, $context - ) === false) { - return false; - } - - $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr); - - $statements_analyzer->node_data = $old_data_provider; - - if ($fake_method_call_type) { - $statements_analyzer->node_data->setType($stmt, $fake_method_call_type); - } - - return true; + ); } } @@ -1046,4 +1012,59 @@ private static function findPseudoMethodAndClassStorages( return null; } + + /** + * Forward static call to instance call, using `VirtualMethodCall` and `MethodCallAnalyzer::analyze()` + * The resolved method return type will be set as type of the $stmt node. + * + * @param StatementsAnalyzer $statements_analyzer + * @param PhpParser\Node\Expr\StaticCall $stmt + * @param PhpParser\Node\Identifier $stmt_name + * @param Context $context + * @param string $virtual_var_name Temporary var name to use for create the fake MethodCall statement. + * @param bool $always_set_node_type If true, when the method has no declared typed, mixed will be set on node. + * + * @return bool Result of analysis. False if the call is invalid. + * + * @see MethodCallAnalyzer::analyze() + */ + private static function forwardCallToInstanceMethod( + StatementsAnalyzer $statements_analyzer, + PhpParser\Node\Expr\StaticCall $stmt, + PhpParser\Node\Identifier $stmt_name, + Context $context, + string $virtual_var_name = 'this', + bool $always_set_node_type = false + ): bool { + $old_data_provider = $statements_analyzer->node_data; + + $statements_analyzer->node_data = clone $statements_analyzer->node_data; + + $fake_method_call_expr = new VirtualMethodCall( + new VirtualVariable($virtual_var_name, $stmt->class->getAttributes()), + $stmt_name, + $stmt->getArgs(), + $stmt->getAttributes() + ); + + if (MethodCallAnalyzer::analyze( + $statements_analyzer, + $fake_method_call_expr, + $context + ) === false) { + return false; + } + + $fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr); + + $statements_analyzer->node_data = $old_data_provider; + + if ($fake_method_call_type) { + $statements_analyzer->node_data->setType($stmt, $fake_method_call_type); + } elseif ($always_set_node_type) { + $statements_analyzer->node_data->setType($stmt, Type::getMixed()); + } + + return true; + } } diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index e2c4afdfedd..26c32327f6c 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -819,6 +819,39 @@ function consumeInt(int $i): void {} consumeInt(B::bar());' ], + 'callUsingParent' => [ + 'create([])); + + $d = new BlahModel(); + consumeBlah($d->create([]));' + ], ]; } From 33f0de073f4a86cd0ff1e042ba0389f02f755bbe Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 18 Jan 2022 09:34:05 +0100 Subject: [PATCH 09/17] Add test case for parent method call without declared pseudo method --- tests/MethodCallTest.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 8676075fd61..0996184cbc4 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1004,6 +1004,34 @@ function fooOrNull(): ?Foo { [], '8.0' ], + 'parentMagicMethodCall' => [ + 'create([]);', + [ + '$n' => 'BlahModel', + ] + ], ]; } From 4cfe1a07738356cd75834e42579fa555c6d2561c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 19 Jan 2022 13:24:26 +0100 Subject: [PATCH 10/17] Add configuration option to disable `@psalm-suppress all` --- config.xsd | 1 + docs/running_psalm/configuration.md | 9 +++++++++ src/Psalm/Config.php | 6 ++++++ src/Psalm/IssueBuffer.php | 4 +++- 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/config.xsd b/config.xsd index afd6069e5b0..35432530932 100644 --- a/config.xsd +++ b/config.xsd @@ -109,6 +109,7 @@ + diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index 21a253e0be0..b94c848059e 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -154,6 +154,15 @@ This flag is deprecated and will be removed in Psalm 5 ``` When `true`, strings can be used as classes, meaning `$some_string::someMethod()` is allowed. If `false`, only class constant strings (of the form `Foo\Bar::class`) can stand in for classes, otherwise an `InvalidStringClass` issue is emitted. Defaults to `false`. +#### disableSuppressAll + +```xml + +``` +When `true`, disables wildcard suppression of all issues with `@psalm-suppress all`. Defaults to `false`. + #### memoizeMethodCallResults ```xml diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 6392d72d449..896c827d7a4 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -326,6 +326,11 @@ class Config */ public $allow_string_standin_for_class = false; + /** + * @var bool + */ + public $disable_suppress_all = false; + /** * @var bool */ @@ -910,6 +915,7 @@ private static function fromXmlAndPaths( 'rememberPropertyAssignmentsAfterCall' => 'remember_property_assignments_after_call', 'allowPhpStormGenerics' => 'allow_phpstorm_generics', 'allowStringToStandInForClass' => 'allow_string_standin_for_class', + 'disableSuppressAll' => 'disable_suppress_all', 'usePhpDocMethodsWithoutMagicCall' => 'use_phpdoc_method_without_magic_or_parent', 'usePhpDocPropertiesWithoutMagicCall' => 'use_phpdoc_property_without_magic_or_parent', 'memoizeMethodCallResults' => 'memoize_method_calls', diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index b9c5308d872..9de67613558 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -212,7 +212,9 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) } } - $suppress_all_position = array_search('all', $suppressed_issues); + $suppress_all_position = $config->disable_suppress_all + ? false + : array_search('all', $suppressed_issues); if ($suppress_all_position !== false) { if (is_int($suppress_all_position)) { From 280de4bc987a395781b6332ba8ba90ff3566322c Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 24 Jan 2022 09:17:29 +0100 Subject: [PATCH 11/17] Fix undefined parent method call (ref #7414) --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 32 +++++++++++++++++-- tests/MagicMethodAnnotationTest.php | 3 +- tests/MethodCallTest.php | 12 ++++++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 5f4b459cec0..3ae8dea1052 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -656,12 +656,38 @@ function (PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem { ) { // In case of parent::xxx() call on instance method context (i.e. not static context) // with nonexistent method, we try to forward to instance method call for resolve pseudo method. - return self::forwardCallToInstanceMethod( + + // Use parent type as static type for the method call + $context->vars_in_scope['$tmp_parent_var'] = new Union([$lhs_type_part]); + + if (self::forwardCallToInstanceMethod( $statements_analyzer, $stmt, $stmt_name, - $context - ); + $context, + 'tmp_parent_var' + ) === false) { + return false; + } + + // Resolve actual static return type according to caller (i.e. $this) static type + if (isset($context->vars_in_scope['$this']) + && $method_call_type = $statements_analyzer->node_data->getType($stmt) + ) { + $method_call_type = clone $method_call_type; + + foreach ($method_call_type->getAtomicTypes() as $name => $type) { + if ($type instanceof TNamedObject && $type->was_static && $type->value === $fq_class_name) { + // Replace parent&static type to actual static type + $method_call_type->removeType($name); + $method_call_type->addType($context->vars_in_scope['$this']->getSingleAtomic()); + } + } + + $statements_analyzer->node_data->setType($stmt, $method_call_type); + } + + return true; } if (!$context->check_methods) { diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php index 373bebcc0ff..46f8e0db842 100644 --- a/tests/MagicMethodAnnotationTest.php +++ b/tests/MagicMethodAnnotationTest.php @@ -825,7 +825,7 @@ function consumeInt(int $i): void {} * @method static create(array $data) */ class Model { - public function __call() { + public function __call(string $name, array $arguments) { /** @psalm-suppress UnsafeInstantiation */ return new static; } @@ -834,6 +834,7 @@ public function __call() { class BlahModel extends Model { /** * @param mixed $input + * @return static */ public function create($input): BlahModel { diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 27d009ae6fe..843cce11941 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1011,7 +1011,7 @@ class Model { /** * @return static */ - public function __call() { + public function __call(string $method, array $args) { /** @psalm-suppress UnsafeInstantiation */ return new static; } @@ -1566,6 +1566,16 @@ function fooOrNull(): ?Foo { false, '8.0' ], + 'undefinedMethodOnParentCallWithMethodExistsOnSelf' => [ + ' 'UndefinedMethod', + ], ]; } } From 9905baeceb909175c7e9654023d60c49e8fde8c0 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 24 Jan 2022 09:27:57 +0100 Subject: [PATCH 12/17] Add flagfrom_calculation --- .../Expression/Call/Method/MethodCallReturnTypeFetcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index 4e48a48bd4b..c4779156a9e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -101,7 +101,7 @@ public static function fetch( if ($premixin_method_id->fq_class_name === PDOException::class) { return Type::getString(); } else { - return Type::getInt(); + return Type::getInt(true); // TODO: Remove the flag in Psalm 5 } } From 9f01c16ae9119749315180ec524d2bd9e483044d Mon Sep 17 00:00:00 2001 From: ElisDN Date: Tue, 25 Jan 2022 14:43:28 +0300 Subject: [PATCH 13/17] Fix array_replace type --- .../ReturnTypeProvider/ArrayMergeReturnTypeProvider.php | 8 +++++++- tests/ArrayFunctionCallTest.php | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 63059484798..a461dac8dac 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -44,6 +44,8 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } + $is_replace = mb_strcut($event->getFunctionId(), 6, 7) === 'replace'; + $inner_value_types = []; $inner_key_types = []; @@ -104,7 +106,11 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev foreach ($unpacked_type_part->properties as $key => $type) { if (!is_string($key)) { - $generic_properties[] = $type; + if ($is_replace) { + $generic_properties[$key] = $type; + } else { + $generic_properties[] = $type; + } continue; } diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index b06a524a54b..3c63a41a602 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -210,9 +210,9 @@ function getInts(): array{ return []; } ], 'arrayMergeIntArrays' => [ ' [ - '$d' => 'array{0: string, 1: string, 2: string, 3: int, 4: int, 5: int}', + '$d' => 'array{0: string, 1: string, 2: string, 3: string, 4: int, 5: int, 6: int}', ], ], 'arrayMergePossiblyUndefined' => [ @@ -266,9 +266,9 @@ public function merge($a, $b): array ], 'arrayReplaceIntArrays' => [ ' [ - '$d' => 'array{0: string, 1: string, 2: string, 3: int, 4: int, 5: int}', + '$d' => 'array{0: int, 1: int, 2: int, 3: string}', ], ], 'arrayReplacePossiblyUndefined' => [ From 3f799fc5fda6ee9cd9884ed4cdc89ad63198e974 Mon Sep 17 00:00:00 2001 From: orklah Date: Tue, 25 Jan 2022 20:58:40 +0100 Subject: [PATCH 14/17] fix CS issue --- .../Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index a461dac8dac..c4002a35fa8 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -23,6 +23,7 @@ use function count; use function is_string; use function max; +use function mb_strcut; class ArrayMergeReturnTypeProvider implements FunctionReturnTypeProviderInterface { From 6bf34b5b97d1e1d3bd692ea61a737f60dde117eb Mon Sep 17 00:00:00 2001 From: Fabien Villepinte Date: Tue, 25 Jan 2022 21:35:27 +0100 Subject: [PATCH 15/17] Add missing parameter to Phar::getMetadata() --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_80_delta.php | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 1873918d54e..9857a3b7399 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -10054,7 +10054,7 @@ 'Phar::delMetadata' => ['bool'], 'Phar::extractTo' => ['bool', 'pathto'=>'string', 'files='=>'string|array|null', 'overwrite='=>'bool'], 'Phar::getAlias' => ['string'], -'Phar::getMetadata' => ['mixed'], +'Phar::getMetadata' => ['mixed', 'unserializeOptions='=>'array'], 'Phar::getModified' => ['bool'], 'Phar::getPath' => ['string'], 'Phar::getSignature' => ['array{hash:string, hash_type:string}'], @@ -10122,7 +10122,7 @@ 'PharFileInfo::getCompressedSize' => ['int'], 'PharFileInfo::getContent' => ['string'], 'PharFileInfo::getCRC32' => ['int'], -'PharFileInfo::getMetadata' => ['mixed'], +'PharFileInfo::getMetadata' => ['mixed', 'unserializeOptions='=>'array'], 'PharFileInfo::getPharFlags' => ['int'], 'PharFileInfo::hasMetadata' => ['bool'], 'PharFileInfo::isCompressed' => ['bool', 'compression_type='=>'int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index f561a75ea15..5453c3585b6 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -93,6 +93,14 @@ 'old' => ['bool', 'mode'=>'int'], 'new' => ['bool', 'mode'=>'int', '...args='=>'mixed'], ], + 'Phar::getMetadata' => [ + 'old' => ['mixed'], + 'new' => ['mixed', 'unserializeOptions='=>'array'], + ], + 'PharFileInfo::getMetadata' => [ + 'old' => ['mixed'], + 'new' => ['mixed', 'unserializeOptions='=>'array'], + ], 'ReflectionClass::getConstants' => [ 'old' => ['array'], 'new' => ['array', 'filter='=>'?int'], From b30121ba1ca4e72a8f5fbbb8180480bdd5084638 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 26 Jan 2022 20:21:00 +0100 Subject: [PATCH 16/17] Deprecate template_extended_count --- src/Psalm/Storage/ClassLikeStorage.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index 0119b950aea..78681da1b46 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -361,6 +361,7 @@ class ClassLikeStorage public $template_extended_params; /** + * @deprecated Will be replaced with $template_type_extends_count in Psalm v5 * @var ?int */ public $template_extended_count; From d2d151c0d8192eadbb2f6a354e9875dd0bb1c006 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 26 Jan 2022 20:24:15 +0100 Subject: [PATCH 17/17] Update baseline --- psalm-baseline.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index b04ad4de319..dcd0433d3dd 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -51,7 +51,7 @@ - + $codebase->php_major_version $codebase->php_major_version $codebase->php_major_version @@ -59,6 +59,8 @@ $codebase->php_minor_version $codebase->php_minor_version $codebase->php_minor_version + $storage->template_extended_count + $storage->template_extended_count $comments[0] @@ -501,7 +503,8 @@ - + + $storage->template_extended_count $this->codebase->php_major_version $this->codebase->php_minor_version