From aa04c0063bb3e2e8dc39a44cd10d8443e1b4435e Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Tue, 26 Sep 2023 08:51:21 +0200 Subject: [PATCH 01/43] Allow Stringable in sprintf() values --- stubs/Php80.phpstub | 11 +++++++++++ tests/CoreStubsTest.php | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 2f2d5bf0b9b..9a70506d912 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -247,6 +247,17 @@ function get_headers(string $url, bool $associative = false, $context = null) : */ function pack(string $format, mixed ...$values): string {} +/** + * @psalm-pure + * + * @param string|Stringable|int|float $values + * @return (PHP_MAJOR_VERSION is 8 ? string : string|false) + * @psalm-ignore-falsable-return + * + * @psalm-flow ($format, $values) -> return + */ +function sprintf(string $format, ...$values) {} + final class CurlHandle { private function __construct() diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index ed3d0b76327..b0189897090 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -125,6 +125,18 @@ function foo(string $foo): string '$a===' => 'string', ], ]; + yield 'sprintf accepts Stringable values' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; yield 'json_encode returns a non-empty-string provided JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE' => [ 'code' => ' Date: Thu, 28 Sep 2023 10:22:31 +0200 Subject: [PATCH 02/43] Allow passing `stringable-object`s to sprintf() in all PHP versions --- stubs/CoreGenericFunctions.phpstub | 2 +- stubs/Php80.phpstub | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 89b18d0a444..793150fbdfa 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1292,7 +1292,7 @@ function preg_quote(string $str, ?string $delimiter = null) : string {} /** * @psalm-pure * - * @param string|int|float $values + * @param string|stringable-object|int|float $values * @return (PHP_MAJOR_VERSION is 8 ? string : string|false) * @psalm-ignore-falsable-return * diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 9a70506d912..2f2d5bf0b9b 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -247,17 +247,6 @@ function get_headers(string $url, bool $associative = false, $context = null) : */ function pack(string $format, mixed ...$values): string {} -/** - * @psalm-pure - * - * @param string|Stringable|int|float $values - * @return (PHP_MAJOR_VERSION is 8 ? string : string|false) - * @psalm-ignore-falsable-return - * - * @psalm-flow ($format, $values) -> return - */ -function sprintf(string $format, ...$values) {} - final class CurlHandle { private function __construct() From c4c8ef53c4e16885d0c21da7948a1b4a20bc3353 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Fri, 29 Sep 2023 11:27:36 +0200 Subject: [PATCH 03/43] Delete an invalid test --- tests/ToStringTest.php | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index d010a365c43..9f498e96d59 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -261,19 +261,6 @@ function fooFoo(string $b): void {} fooFoo(new A());', 'error_message' => 'InvalidArgument', ], - 'implicitCastWithStrictTypesToEchoOrSprintf' => [ - 'code' => ' 'ImplicitToStringCast', - ], 'implicitCast' => [ 'code' => ' Date: Sat, 30 Sep 2023 10:36:21 +0700 Subject: [PATCH 04/43] fix: #10239 --- src/Psalm/Internal/Diff/FileStatementsDiffer.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Diff/FileStatementsDiffer.php b/src/Psalm/Internal/Diff/FileStatementsDiffer.php index 797b6f73012..665cb47cafd 100644 --- a/src/Psalm/Internal/Diff/FileStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/FileStatementsDiffer.php @@ -115,7 +115,11 @@ static function ( $b_code, ); - $keep = [...$keep, ...$class_keep[0]]; + if ($diff_elem->old->getDocComment() === $diff_elem->new->getDocComment()) { + $keep = [...$keep, ...$class_keep[0]]; + } else { + $keep_signature = [...$keep_signature, ...$class_keep[0]]; + } $keep_signature = [...$keep_signature, ...$class_keep[1]]; $add_or_delete = [...$add_or_delete, ...$class_keep[2]]; $diff_map = [...$diff_map, ...$class_keep[3]]; From c312c760503c4695331ee4893d0f81bce6f8e646 Mon Sep 17 00:00:00 2001 From: ging-dev Date: Sat, 30 Sep 2023 17:33:50 +0700 Subject: [PATCH 05/43] chore: add test --- tests/Cache/CacheTest.php | 49 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index 0efce1c6876..04f44eda644 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -199,5 +199,54 @@ class B { ], ], ]; + + yield 'classDocblockChange' => [ + [ + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + [], + ], + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + [ + '/src/A.php' => [ + "UndefinedDocblockClass: Docblock-defined class, interface or enum named T does not exist", + ], + ], + ], + ], + ]; } } From 480708637b8309ea7cd7675d827e05a7f50a6dab Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 1 Oct 2023 21:32:34 +0200 Subject: [PATCH 06/43] Fix https://psalm.dev/r/77be914054 --- src/Psalm/Issue/InternalClass.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Psalm/Issue/InternalClass.php b/src/Psalm/Issue/InternalClass.php index 827c9013419..93af91b4648 100644 --- a/src/Psalm/Issue/InternalClass.php +++ b/src/Psalm/Issue/InternalClass.php @@ -2,6 +2,7 @@ namespace Psalm\Issue; +use function array_unique; use function array_pop; use function count; use function implode; @@ -15,6 +16,7 @@ final class InternalClass extends ClassIssue /** @param non-empty-list $words */ public static function listToPhrase(array $words): string { + $words = array_unique($words); if (count($words) === 1) { return reset($words); } From 1306b62fed8afc0aed9fffa2aa51f704d2bc04a9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 1 Oct 2023 21:35:45 +0200 Subject: [PATCH 07/43] code style --- src/Psalm/Issue/InternalClass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Issue/InternalClass.php b/src/Psalm/Issue/InternalClass.php index 93af91b4648..087eece0201 100644 --- a/src/Psalm/Issue/InternalClass.php +++ b/src/Psalm/Issue/InternalClass.php @@ -2,8 +2,8 @@ namespace Psalm\Issue; -use function array_unique; use function array_pop; +use function array_unique; use function count; use function implode; use function reset; From 94a98ccddd68445665813c4989b5d75d555695ed Mon Sep 17 00:00:00 2001 From: cgocast Date: Mon, 2 Oct 2023 15:08:26 +0200 Subject: [PATCH 08/43] Allow tainted numerics except for 'html' and 'has_quotes' --- .../Expression/BinaryOpAnalyzer.php | 18 +++++ .../Expression/Call/ArgumentAnalyzer.php | 14 ++-- .../Statements/Expression/CastAnalyzer.php | 19 ++---- tests/TaintTest.php | 66 ++++++++++--------- 4 files changed, 64 insertions(+), 53 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index da8e3794b7c..46a57c6b5ac 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -170,6 +170,15 @@ public static function analyze( $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); if ($stmt_left_type && $stmt_left_type->parent_nodes) { + + // numeric types can't be tainted html or has_quotes, neither can bool + if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph + && $stmt_left_type->isSingle() + && ($stmt_left_type->isInt() || $stmt_left_type->isFloat() || $stmt_left_type->isBool()) + ) { + $removed_taints = array_merge($removed_taints, array('html', 'has_quotes')); + } + foreach ($stmt_left_type->parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, @@ -182,6 +191,15 @@ public static function analyze( } if ($stmt_right_type && $stmt_right_type->parent_nodes) { + + // numeric types can't be tainted html or has_quotes, neither can bool + if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph + && $stmt_right_type->isSingle() + && ($stmt_right_type->isInt() || $stmt_right_type->isFloat() || $stmt_right_type->isBool()) + ) { + $removed_taints = array_merge($removed_taints, array('html', 'has_quotes')); + } + foreach ($stmt_right_type->parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 43da070b4d6..3e0fb1865f9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -1490,19 +1490,19 @@ private static function processTaintedness( return; } - // numeric types can't be tainted, neither can bool + $event = new AddRemoveTaintsEvent($expr, $context, $statements_analyzer, $codebase); + + $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); + $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); + + // numeric types can't be tainted html or has_quotes, neither can bool if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && $input_type->isSingle() && ($input_type->isInt() || $input_type->isFloat() || $input_type->isBool()) ) { - return; + $removed_taints = array_merge($removed_taints, array('html', 'has_quotes')); } - $event = new AddRemoveTaintsEvent($expr, $context, $statements_analyzer, $codebase); - - $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); - $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - if ($function_param->type && $function_param->type->isString() && !$input_type->isString()) { $input_type = CastAnalyzer::castStringAttempt( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 83825307c32..9f082f3daeb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -142,14 +142,9 @@ public static function analyze( } } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type = new Union([new TBool()], [ - 'parent_nodes' => $maybe_type->parent_nodes ?? [], - ]); - } else { - $type = Type::getBool(); - } + $type = new Union([new TBool()], [ + 'parent_nodes' => $maybe_type->parent_nodes ?? [], + ]); $statements_analyzer->node_data->setType($stmt, $type); @@ -330,9 +325,7 @@ public static function castIntAttempt( $parent_nodes = []; - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { - $parent_nodes = $stmt_type->parent_nodes; - } + $parent_nodes = $stmt_type->parent_nodes; while ($atomic_types) { $atomic_type = array_pop($atomic_types); @@ -520,9 +513,7 @@ public static function castFloatAttempt( $parent_nodes = []; - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { - $parent_nodes = $stmt_type->parent_nodes; - } + $parent_nodes = $stmt_type->parent_nodes; while ($atomic_types) { $atomic_type = array_pop($atomic_types); diff --git a/tests/TaintTest.php b/tests/TaintTest.php index d5dd0e1dc34..faa299957d4 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -177,23 +177,6 @@ public function deleteUser(PDO $pdo, string $userId) : void { } }', ], - 'untaintedInputAfterIntCast' => [ - 'code' => 'getUserId(); - } - - public function deleteUser(PDO $pdo) : void { - $userId = $this->getAppendedUserId(); - $pdo->exec("delete from users where user_id = " . $userId); - } - }', - ], 'specializedCoreFunctionCall' => [ 'code' => ' [ - 'code' => ' [ 'code' => ' 'TaintedSql', ], + 'taintedInputAfterIntCast' => [ + 'code' => 'getUserId(); + } + + public function deleteUser(PDO $pdo) : void { + $userId = $this->getAppendedUserId(); + $pdo->exec("delete from users where user_id = " . $userId); + } + }', + 'error_message' => 'TaintedSql', + ], + 'TaintForIntTypeCastUsingAnnotatedSink' => [ + 'code' => ' 'TaintedSql', + ], 'taintedInputFromReturnTypeWithBranch' => [ 'code' => ' Date: Mon, 2 Oct 2023 15:22:57 +0200 Subject: [PATCH 09/43] Fix code style --- .../Analyzer/Statements/Expression/BinaryOpAnalyzer.php | 3 +-- .../Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index 46a57c6b5ac..bc799edd290 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -32,6 +32,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_merge; use function in_array; use function strlen; @@ -170,7 +171,6 @@ public static function analyze( $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); if ($stmt_left_type && $stmt_left_type->parent_nodes) { - // numeric types can't be tainted html or has_quotes, neither can bool if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && $stmt_left_type->isSingle() @@ -191,7 +191,6 @@ public static function analyze( } if ($stmt_right_type && $stmt_right_type->parent_nodes) { - // numeric types can't be tainted html or has_quotes, neither can bool if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && $stmt_right_type->isSingle() diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 3e0fb1865f9..2d8380ceb69 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -61,6 +61,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; +use function array_merge; use function count; use function explode; use function implode; From e0c24cbe7a037e0bbfa82eb723f08b53045ae758 Mon Sep 17 00:00:00 2001 From: cgocast Date: Mon, 2 Oct 2023 15:38:01 +0200 Subject: [PATCH 10/43] Remove unused parents_nodes --- .../Internal/Analyzer/Statements/Expression/CastAnalyzer.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 9f082f3daeb..12285e2432e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -323,8 +323,6 @@ public static function castIntAttempt( $atomic_types = $stmt_type->getAtomicTypes(); - $parent_nodes = []; - $parent_nodes = $stmt_type->parent_nodes; while ($atomic_types) { @@ -511,8 +509,6 @@ public static function castFloatAttempt( $atomic_types = $stmt_type->getAtomicTypes(); - $parent_nodes = []; - $parent_nodes = $stmt_type->parent_nodes; while ($atomic_types) { From b05ffeaf206135dbdddf4d6ea8b6c22d48972d1f Mon Sep 17 00:00:00 2001 From: tuqqu Date: Tue, 3 Oct 2023 02:17:08 +0200 Subject: [PATCH 11/43] Add socket_shutdown stream_socket_shutdown functions to impure list --- dictionaries/ImpureFunctionsList.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php index 70b1fad7d92..647587ad751 100644 --- a/dictionaries/ImpureFunctionsList.php +++ b/dictionaries/ImpureFunctionsList.php @@ -67,6 +67,8 @@ 'socket_set_block' => true, 'socket_set_nonblock' => true, 'socket_listen' => true, + 'stream_socket_shutdown' => true, + 'socket_shutdown' => true, // meta calls 'call_user_func' => true, 'call_user_func_array' => true, From 2baa094f58900cb24f74c485bf61f296568c36f5 Mon Sep 17 00:00:00 2001 From: tuqqu Date: Tue, 3 Oct 2023 02:17:26 +0200 Subject: [PATCH 12/43] Remove duplicate from impure list --- dictionaries/ImpureFunctionsList.php | 1 - 1 file changed, 1 deletion(-) diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php index 647587ad751..d3a3f7ce0a3 100644 --- a/dictionaries/ImpureFunctionsList.php +++ b/dictionaries/ImpureFunctionsList.php @@ -95,7 +95,6 @@ 'mcrypt_generic_deinit' => true, 'mcrypt_module_close' => true, // internal optimisation - 'opcache_compile_file' => true, 'clearstatcache' => true, // process-related 'pcntl_signal' => true, From 9f9e5f1e18208a72aa5c6dc8188ffdbf36aadf06 Mon Sep 17 00:00:00 2001 From: tuqqu Date: Tue, 3 Oct 2023 03:41:11 +0200 Subject: [PATCH 13/43] Emit MethodSignatureMismatch when descendant does not return by reference --- .../Internal/Analyzer/MethodComparator.php | 10 +++++ tests/MethodSignatureTest.php | 42 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 82e1c6c4863..d595df15803 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -313,6 +313,16 @@ private static function checkForObviousMethodMismatches( ); } + if ($guide_method_storage->returns_by_ref && !$implementer_method_storage->returns_by_ref) { + IssueBuffer::maybeAdd( + new MethodSignatureMismatch( + 'Method ' . $cased_implementer_method_id . ' must return by-reference', + $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); + } + if ($guide_method_storage->external_mutation_free && !$implementer_method_storage->external_mutation_free && !$guide_method_storage->mutation_free_inferred diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 4c65719ee5f..61cd41b9b99 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -929,6 +929,34 @@ public function __destruct() {} } ', ], + 'allowByRefReturn' => [ + 'code' => 'x; + } + } + ', + ], + 'descendantAddsByRefReturn' => [ + 'code' => 'x; + } + } + ', + ], ]; } @@ -1586,6 +1614,20 @@ public function jsonSerialize() { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'absentByRefReturnInDescendant' => [ + 'code' => ' 'MethodSignatureMismatch', + ], ]; } } From 413f1d6ce3e821d2d6676f781d904ca85b371365 Mon Sep 17 00:00:00 2001 From: tuqqu Date: Wed, 4 Oct 2023 20:51:31 +0200 Subject: [PATCH 14/43] Fix error message for returning function with never return type --- src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 5cfd1b17664..b41ba69ecdc 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -197,6 +197,7 @@ public static function verifyReturnType( ) ) && !$return_type->isVoid() + && !$return_type->isNever() && !$inferred_yield_types && (!$function_like_storage || !$function_like_storage->has_yield) && $function_returns_implicitly From 2a910d1f179a497524e0d6f64eeb36935a20d49a Mon Sep 17 00:00:00 2001 From: tuqqu Date: Wed, 4 Oct 2023 21:00:04 +0200 Subject: [PATCH 15/43] Changed error message for never return error --- src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index b41ba69ecdc..15ec85e3938 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -223,7 +223,7 @@ public static function verifyReturnType( ) { if (IssueBuffer::accepts( new InvalidReturnType( - $cased_method_id . ' is not expected to return any values but it does, ' + $cased_method_id . ' is not expected to return, but it does, ' . 'either implicitly or explicitly', $return_type_location, ), From 2bc330976f87092d4b39275a03168b92c4f2fefe Mon Sep 17 00:00:00 2001 From: tuqqu Date: Wed, 4 Oct 2023 21:18:59 +0200 Subject: [PATCH 16/43] Add tests for never return type --- tests/ReturnTypeTest.php | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 2557fdf8abe..3bc88b760f1 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1279,6 +1279,26 @@ function aggregate($type) { return $t; }', ], + 'neverReturnType' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1807,6 +1827,36 @@ public function process(): mixed 'ignored_issues' => [], 'php_version' => '8.0', ], + 'implicitReturnFromFunctionWithNeverReturnType' => [ + 'code' => <<<'PHP' + 'InvalidReturnType', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'implicitReturnFromFunctionWithNeverReturnType2' => [ + 'code' => <<<'PHP' + 'InvalidReturnType', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } From a93b35853e7458c43cc2552a0e83c79da552eb36 Mon Sep 17 00:00:00 2001 From: robchett Date: Sat, 7 Oct 2023 14:49:21 +0100 Subject: [PATCH 17/43] Allow names in callable docblocks --- src/Psalm/Internal/Type/ParseTreeCreator.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 9fdd3fcc482..ca2eea6cdff 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -553,24 +553,23 @@ private function handleSpace(): void $current_parent = $this->current_leaf->parent; - if ($current_parent instanceof CallableTree) { - return; - } - - while ($current_parent && !$current_parent instanceof MethodTree) { + //while ($current_parent && !$method_or_callable_parent) { + while ($current_parent && !$current_parent instanceof MethodTree && !$current_parent instanceof CallableTree) { $this->current_leaf = $current_parent; $current_parent = $current_parent->parent; } $next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; - if (!$current_parent instanceof MethodTree || !$next_token) { + if (!($current_parent instanceof MethodTree || $current_parent instanceof CallableTree) || !$next_token) { throw new TypeParseTreeException('Unexpected space'); } ++$this->t; - $this->createMethodParam($next_token, $current_parent); + if ($current_parent instanceof MethodTree) { + $this->createMethodParam($next_token, $current_parent); + } } private function handleQuestionMark(): void From c71a252deefdc1194b26dfa1801841a61bea682f Mon Sep 17 00:00:00 2001 From: robchett Date: Sat, 7 Oct 2023 16:04:48 +0100 Subject: [PATCH 18/43] Add tests for callable docblock parsing --- tests/TypeAnnotationTest.php | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 8cd1fb2189b..459cc803489 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -679,6 +679,56 @@ class Foo { '$output===' => 'callable():int', ], ], + 'callableFormats' => [ + 'code' => '): array + * @psalm-type H callable(array $e): array + * @psalm-type I \Closure(int, int): string + * + * @method ma(): A + * @method mb(): B + * @method mc(): C + * @method md(): D + * @method me(): E + * @method mf(): F + * @method mg(): G + * @method mh(): H + * @method mi(): I + */ + class Foo { + public function __call(string $method, array $params) { return 1; } + } + + $foo = new \Foo(); + $output_ma = $foo->ma(); + $output_mb = $foo->mb(); + $output_mc = $foo->mc(); + $output_md = $foo->md(); + $output_me = $foo->me(); + $output_mf = $foo->mf(); + $output_mg = $foo->mg(); + $output_mh = $foo->mh(); + $output_mi = $foo->mi(); + ', + 'assertions' => [ + '$output_ma===' => 'callable(int, int):string', + '$output_mb===' => 'callable(int, int=):string', + '$output_mc===' => 'callable(int, string):void', + '$output_md===' => 'callable(string):mixed', + '$output_me===' => 'callable(float...):(int|null)', + '$output_mf===' => 'callable(float...):(int|null)', + '$output_mg===' => 'callable(array):array', + '$output_mh===' => 'callable(array):array', + '$output_mi===' => 'Closure(int, int):string', + ], + ], 'unionOfStringsContainingBraceChar' => [ 'code' => ' Date: Sat, 7 Oct 2023 16:59:43 +0100 Subject: [PATCH 19/43] Fix test case for named variadic callable docblock --- src/Psalm/Internal/Type/ParseTreeCreator.php | 46 +++++++++++++++++++- tests/TypeAnnotationTest.php | 30 +++++++++---- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index ca2eea6cdff..0ea26080433 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -235,6 +235,46 @@ private function createMethodParam(array $current_token, ParseTree $current_pare $this->current_leaf = $new_parent_leaf; } + /** + * @param array{0: string, 1: int, 2?: string} $current_token + */ + private function parseCallableParam(array $current_token, ParseTree $current_parent): void + { + $variadic = false; + $has_default = false; + + if ($current_token[0] === '&') { + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '...') { + $variadic = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '=') { + $has_default = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } + + if (!$current_token || $current_token[0][0] !== '$') { + throw new TypeParseTreeException('Unexpected token after space'); + } + + $new_leaf = new CallableParamTree($current_parent); + $new_leaf->has_default = $has_default; + $new_leaf->variadic = $variadic; + + if ($current_parent !== $this->current_leaf) { + $new_leaf->children = [$this->current_leaf]; + array_pop($current_parent->children); + } + $current_parent->children[] = $new_leaf; + + $this->current_leaf = $new_leaf; + } + private function handleLessThan(): void { if (!$this->current_leaf instanceof FieldEllipsis) { @@ -565,11 +605,15 @@ private function handleSpace(): void throw new TypeParseTreeException('Unexpected space'); } - ++$this->t; if ($current_parent instanceof MethodTree) { + ++$this->t; $this->createMethodParam($next_token, $current_parent); } + if ($current_parent instanceof CallableTree) { + ++$this->t; + $this->parseCallableParam($next_token, $current_parent); + } } private function handleQuestionMark(): void diff --git a/tests/TypeAnnotationTest.php b/tests/TypeAnnotationTest.php index 459cc803489..29ee8f6581f 100644 --- a/tests/TypeAnnotationTest.php +++ b/tests/TypeAnnotationTest.php @@ -686,11 +686,14 @@ class Foo { * @psalm-type B callable(int, int=): string * @psalm-type C callable(int $a, string $b): void * @psalm-type D callable(string $c): mixed - * @psalm-type E callable(float...): (int|null) - * @psalm-type F callable(float ...$d): (int|null) - * @psalm-type G callable(array): array - * @psalm-type H callable(array $e): array - * @psalm-type I \Closure(int, int): string + * @psalm-type E callable(string $c): mixed + * @psalm-type F callable(float...): (int|null) + * @psalm-type G callable(float ...$d): (int|null) + * @psalm-type H callable(array): array + * @psalm-type I callable(array $e): array + * @psalm-type J callable(array ...): string + * @psalm-type K callable(array ...$e): string + * @psalm-type L \Closure(int, int): string * * @method ma(): A * @method mb(): B @@ -701,6 +704,9 @@ class Foo { * @method mg(): G * @method mh(): H * @method mi(): I + * @method mj(): J + * @method mk(): K + * @method ml(): L */ class Foo { public function __call(string $method, array $params) { return 1; } @@ -716,17 +722,23 @@ public function __call(string $method, array $params) { return 1; } $output_mg = $foo->mg(); $output_mh = $foo->mh(); $output_mi = $foo->mi(); + $output_mj = $foo->mj(); + $output_mk = $foo->mk(); + $output_ml = $foo->ml(); ', 'assertions' => [ '$output_ma===' => 'callable(int, int):string', '$output_mb===' => 'callable(int, int=):string', '$output_mc===' => 'callable(int, string):void', '$output_md===' => 'callable(string):mixed', - '$output_me===' => 'callable(float...):(int|null)', + '$output_me===' => 'callable(string):mixed', '$output_mf===' => 'callable(float...):(int|null)', - '$output_mg===' => 'callable(array):array', - '$output_mh===' => 'callable(array):array', - '$output_mi===' => 'Closure(int, int):string', + '$output_mg===' => 'callable(float...):(int|null)', + '$output_mh===' => 'callable(array):array', + '$output_mi===' => 'callable(array):array', + '$output_mj===' => 'callable(array...):string', + '$output_mk===' => 'callable(array...):string', + '$output_ml===' => 'Closure(int, int):string', ], ], 'unionOfStringsContainingBraceChar' => [ From c729fcd5c892ab7eeebc0ecf706c19ce6e19d38f Mon Sep 17 00:00:00 2001 From: robchett Date: Sun, 8 Oct 2023 15:38:45 +0100 Subject: [PATCH 20/43] Negated class_exist check on class-string converts to string instead of mixed --- .../Internal/Type/NegatedAssertionReconciler.php | 5 +++++ tests/ClassLikeStringTest.php | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index f4de672c99e..ab7da873005 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -122,6 +122,11 @@ public static function reconcile( $existing_var_type->removeType('array'); } + if ($assertion instanceof IsNotType && $assertion_type instanceof TClassString) { + $existing_var_type->removeType(TClassString::class); + $existing_var_type->addType(new TString); + } + if (!$is_equality && isset($existing_var_atomic_types['int']) && $existing_var_type->from_calculation diff --git a/tests/ClassLikeStringTest.php b/tests/ClassLikeStringTest.php index b3bcca56121..7970dae8400 100644 --- a/tests/ClassLikeStringTest.php +++ b/tests/ClassLikeStringTest.php @@ -611,6 +611,18 @@ class A {} new \RuntimeException(); }', ], + 'convertToStringClassExistsNegated' => [ + 'code' => ' [ + '$className===' => 'string', + ], + + ], 'createNewObjectFromGetClass' => [ 'code' => ' Date: Sun, 8 Oct 2023 17:44:32 +0100 Subject: [PATCH 21/43] Add alias support to psalm-check-type --- .../Internal/Analyzer/StatementsAnalyzer.php | 6 +++++- tests/CheckTypeTest.php | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index dfe109cc0b7..55e80d44643 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -678,7 +678,11 @@ private static function analyzeStatement( } else { try { $checked_type = $context->vars_in_scope[$checked_var_id]; - $check_type = Type::parseString($check_type_string); + $fq_check_type_string = Type::getFQCLNFromString( + $check_type_string, + $statements_analyzer->getAliases(), + ); + $check_type = Type::parseString($fq_check_type_string); /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; diff --git a/tests/CheckTypeTest.php b/tests/CheckTypeTest.php index a3f0aabbfe5..457c496db83 100644 --- a/tests/CheckTypeTest.php +++ b/tests/CheckTypeTest.php @@ -18,6 +18,26 @@ public function providerValidCodeParse(): iterable $foo = 1; ', ]; + yield 'allowNamespace' => [ + 'code' => ' [ + 'code' => ' Date: Sun, 8 Oct 2023 20:47:37 +0200 Subject: [PATCH 22/43] Disallow never type for parameters --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 11 +++++++++++ tests/FunctionCallTest.php | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 3c0b6f2bd54..4e93704e2c3 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1260,6 +1260,17 @@ private function processParams( ); } + if ($param_type->isNever()) { + IssueBuffer::maybeAdd( + new ReservedWord( + 'Parameter cannot be never', + $function_param->type_location, + 'never', + ), + $this->suppressed_issues, + ); + } + if ($param_type->check( $this->source, $function_param->type_location, diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 23f9dcb0865..f712af9dacd 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -3031,6 +3031,17 @@ function hasZeroByteOffset(string $s) : bool { }', 'error_message' => 'InvalidScalarArgument', ], + 'disallowNeverTypeForParam' => [ + 'code' => ' 'ReservedWord', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } From 56926ee4888d69fc19ecae84070e73afd74b31ff Mon Sep 17 00:00:00 2001 From: cgocast Date: Mon, 9 Oct 2023 14:27:36 +0200 Subject: [PATCH 23/43] Fix return of BadSqlTainter::afterExpressionAnalysis() --- docs/security_analysis/custom_taint_sources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/security_analysis/custom_taint_sources.md b/docs/security_analysis/custom_taint_sources.md index b7b140706e6..d3bdb0a3205 100644 --- a/docs/security_analysis/custom_taint_sources.md +++ b/docs/security_analysis/custom_taint_sources.md @@ -68,6 +68,7 @@ class BadSqlTainter implements AfterExpressionAnalysisInterface ); } } + return null; } } ``` From a3df6505f0bd58b812c4c40109361cb0bd4e22d4 Mon Sep 17 00:00:00 2001 From: klimick Date: Mon, 9 Oct 2023 16:49:38 +0300 Subject: [PATCH 24/43] Type check nested templates --- .../Internal/TypeVisitor/TypeChecker.php | 21 +++++++-- tests/CallableTest.php | 34 ++++++++++++++ tests/Template/FunctionTemplateTest.php | 47 +++++++++++++++++++ 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index 4c2e52c916b..2818b414c16 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -9,6 +9,9 @@ use Psalm\Internal\Analyzer\ClassLikeNameOptions; use Psalm\Internal\Analyzer\MethodAnalyzer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TemplateBound; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\DeprecatedClass; use Psalm\Issue\DeprecatedInterface; @@ -241,6 +244,7 @@ private function checkGenericParams(TGenericObject $atomic): void } $expected_type_param_keys = array_keys($expected_type_params); + $template_result = new TemplateResult($expected_type_params, []); foreach ($atomic->type_params as $i => $type_param) { $this->prevent_template_covariance = $this->source instanceof MethodAnalyzer @@ -251,12 +255,16 @@ private function checkGenericParams(TGenericObject $atomic): void $expected_template_name = $expected_type_param_keys[$i]; foreach ($expected_type_params[$expected_template_name] as $defining_class => $expected_type_param) { - $expected_type_param = TypeExpander::expandUnion( + $expected_type_param = TemplateInferredTypeReplacer::replace( + TypeExpander::expandUnion( + $codebase, + $expected_type_param, + $defining_class, + null, + null, + ), + $template_result, $codebase, - $expected_type_param, - $defining_class, - null, - null, ); $type_param = TypeExpander::expandUnion( @@ -279,6 +287,9 @@ private function checkGenericParams(TGenericObject $atomic): void ), $this->suppressed_issues, ); + } else { + $template_result->lower_bounds[$expected_template_name][$defining_class][] + = new TemplateBound($type_param); } } } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 5815222b1ad..605213b4a51 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1844,6 +1844,40 @@ function int_int_int_int(Closure $f): void {} 'ignored_issues' => [], 'php_version' => '8.0', ], + 'inferTypeWithNestedTemplatesAndExplicitTypeHint' => [ + 'code' => '> + */ + final class GetListOfNumbers implements Message {} + + /** + * @template TResult + * @template TMessage of Message + */ + final class Envelope {} + + /** + * @template TResult + * @template TMessage of Message + * @param class-string $_message + * @param callable(TMessage, Envelope): TResult $_handler + */ + function addHandler(string $_message, callable $_handler): void {} + + addHandler(GetListOfNumbers::class, function (Message $_message, Envelope $_envelope) { + /** + * @psalm-check-type-exact $_message = GetListOfNumbers + * @psalm-check-type-exact $_envelope = Envelope, GetListOfNumbers> + */ + return [1, 2, 3]; + });', + ], ]; } diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index ba72c2604d2..800e2956cd7 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -1673,6 +1673,29 @@ function foo(string $t): string return $t; }', ], + 'typeWithNestedTemplates' => [ + 'code' => ' + */ + final class BType {} + + /** + * @param BType> $_value + */ + function test1(BType $_value): void {} + + /** + * @param BType> $_value + */ + function test2(BType $_value): void {}', + ], ]; } @@ -2268,6 +2291,30 @@ function jsonFromEntityCollection(Container $c): void { }', 'error_message' => 'InvalidArgument', ], + 'catchInvalidTemplateTypeWithNestedTemplates' => [ + 'code' => ' + */ + final class BType {} + + /** + * @param BType> $_value + */ + function test1(BType $_value): void {} + + /** + * @param BType> $_value + */ + function test2(BType $_value): void {}', + 'error_message' => 'InvalidTemplateParam', + ], ]; } } From 6039e2be9b1ecffde979c58c7e3ca19cd07cba07 Mon Sep 17 00:00:00 2001 From: tuqqu Date: Tue, 10 Oct 2023 22:56:36 +0200 Subject: [PATCH 25/43] Fix for inferring enum case value from a class constant --- .../Expression/SimpleTypeInferer.php | 6 +-- tests/EnumTest.php | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index ab144621edb..cfd69cb3784 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -337,15 +337,13 @@ public static function infer( return Type::getLiteralClassString($const_fq_class_name, true); } - if ($existing_class_constants === null - && $file_source instanceof StatementsAnalyzer - ) { + if ($existing_class_constants === null || $existing_class_constants === []) { try { $foreign_class_constant = $codebase->classlikes->getClassConstantType( $const_fq_class_name, $stmt->name->name, ReflectionProperty::IS_PRIVATE, - $file_source, + $file_source instanceof StatementsAnalyzer ? $file_source : null, ); if ($foreign_class_constant) { diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 77df3b8d659..12322c66698 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -610,6 +610,26 @@ function f(Transport $e): void { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'backedEnumCaseValueFromClassConstant' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1030,6 +1050,36 @@ function f(string $state): void {} 'ignored_issues' => [], 'php_version' => '8.1', ], + 'stringBackedEnumCaseValueFromClassConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intBackedEnumCaseValueFromClassConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } From d0825b5fe2d850d7faea0d7dfac43661087abee7 Mon Sep 17 00:00:00 2001 From: tuqqu Date: Fri, 13 Oct 2023 00:03:53 +0200 Subject: [PATCH 26/43] Fix for inferring enum case value from a class constant, const test fix --- .../Analyzer/Statements/Expression/SimpleTypeInferer.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index cfd69cb3784..6d94391ffd2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -337,7 +337,10 @@ public static function infer( return Type::getLiteralClassString($const_fq_class_name, true); } - if ($existing_class_constants === null || $existing_class_constants === []) { + if ($existing_class_constants === null + || $existing_class_constants === [] + && $file_source !== null + ) { try { $foreign_class_constant = $codebase->classlikes->getClassConstantType( $const_fq_class_name, From 545e21b56b626beda237cf26a3e998ad4170e1cd Mon Sep 17 00:00:00 2001 From: Daniel Linjama Date: Fri, 13 Oct 2023 14:18:58 +0300 Subject: [PATCH 27/43] fix final class constant type --- .../Analyzer/Statements/Expression/ClassConstAnalyzer.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 2dec200fc9c..9335f658324 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -385,7 +385,11 @@ public static function analyzeFetch( ); } - if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock) { + if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock + || (isset($const_class_storage->constants[$stmt->name->name]) + && $const_class_storage->constants[$stmt->name->name]->final + ) + ) { $stmt_type = $class_constant_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); From 0162e75ee8ea97e687ecf55bec2b11a9f6ea6a68 Mon Sep 17 00:00:00 2001 From: ging-dev Date: Tue, 17 Oct 2023 02:43:22 +0700 Subject: [PATCH 28/43] fix: #10080 --- .../Internal/Diff/ClassStatementsDiffer.php | 16 ++++++- tests/Cache/CacheTest.php | 46 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index 5bc637b4624..65cccb3e60c 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -3,9 +3,11 @@ namespace Psalm\Internal\Diff; use PhpParser; +use UnexpectedValueException; use function count; use function get_class; +use function is_string; use function strpos; use function strtolower; use function substr; @@ -230,7 +232,19 @@ static function ( /** @var PhpParser\Node */ $affected_elem = $diff_elem->type === DiffElem::TYPE_REMOVE ? $diff_elem->old : $diff_elem->new; if ($affected_elem instanceof PhpParser\Node\Stmt\ClassMethod) { - $add_or_delete[] = $name_lc . '::' . strtolower((string) $affected_elem->name); + $method_name = strtolower((string) $affected_elem->name); + $add_or_delete[] = $name_lc . '::' . $method_name; + if ($method_name === '__construct') { + foreach ($affected_elem->getParams() as $param) { + if (!$param->flags || !$param->var instanceof PhpParser\Node\Expr\Variable) { + continue; + } + if ($param->var instanceof PhpParser\Node\Expr\Error || !is_string($param->var->name)) { + throw new UnexpectedValueException('Not expecting param name to be non-string'); + } + $add_or_delete[] = $name_lc . '::$' . $param->var->name; + } + } } elseif ($affected_elem instanceof PhpParser\Node\Stmt\Property) { foreach ($affected_elem->props as $prop) { $add_or_delete[] = $name_lc . '::$' . $prop->name; diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index 04f44eda644..8e883c65eb3 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -248,5 +248,51 @@ public function foo($baz): void ], ], ]; + + yield 'constructorPropertyPromotionChange' => [ + [ + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + foo; + } + } + PHP, + ], + 'issues' => [], + ], + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + foo; + } + } + PHP, + ], + 'issues' => [ + '/src/A.php' => [ + "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", + "MixedReturnStatement: Could not infer a return type", + "MixedInferredReturnType: Could not verify return type 'string' for A::bar", + ], + ], + ], + ], + ]; } } From 54a31b64a4abc5be67ff8da1b5dc5d4a7a9ebef3 Mon Sep 17 00:00:00 2001 From: Mathieu Rochette Date: Wed, 11 Oct 2023 17:42:55 +0200 Subject: [PATCH 29/43] str_replace / substr_replace signature improvements --- dictionaries/CallMap.php | 13 ++++++++++--- dictionaries/CallMap_80_delta.php | 8 ++++++-- dictionaries/CallMap_historical.php | 13 ++++++++++--- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index eedbac5dea9..782ed5d8fdf 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -12886,10 +12886,16 @@ 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_getcsv' => ['non-empty-list', 'string'=>'string', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], +'str_ireplace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], +'str_ireplace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], +'str_ireplace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], +'str_ireplace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_pad' => ['string', 'string'=>'string', 'length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'string'=>'string', 'times'=>'int'], -'str_replace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], +'str_replace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], +'str_replace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], +'str_replace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], +'str_replace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_rot13' => ['string', 'string'=>'string'], 'str_shuffle' => ['string', 'string'=>'string'], 'str_split' => ['list', 'string'=>'string', 'length='=>'positive-int'], @@ -13015,7 +13021,8 @@ 'substr' => ['string', 'string'=>'string', 'offset'=>'int', 'length='=>'?int'], 'substr_compare' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset'=>'int', 'length='=>'?int', 'case_insensitive='=>'bool'], 'substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'?int'], -'substr_replace' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], +'substr_replace' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], +'substr_replace\'1' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], 'suhosin_encrypt_cookie' => ['string|false', 'name'=>'string', 'value'=>'string'], 'suhosin_get_raw_cookies' => ['array'], 'SVM::__construct' => ['void'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index a9cde9ff992..29227b632bc 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -2589,8 +2589,12 @@ 'new' => ['string', 'string'=>'string', 'offset'=>'int', 'length='=>'?int'], ], 'substr_replace' => [ - 'old' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], - 'new' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], + 'old' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'new' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], + ], + 'substr_replace\'1' => [ + 'old' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'new' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], ], 'tidy_parse_file' => [ 'old' => ['tidy', 'filename'=>'string', 'config='=>'array|string', 'encoding='=>'string', 'useIncludePath='=>'bool'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 9565bfa203d..5ff4033aff0 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14303,10 +14303,16 @@ 'stomp_unsubscribe' => ['bool', 'link'=>'resource', 'destination'=>'string', 'headers='=>'?array'], 'stomp_version' => ['string'], 'str_getcsv' => ['non-empty-list', 'string'=>'string', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], - 'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], + 'str_ireplace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], + 'str_ireplace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], + 'str_ireplace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], + 'str_ireplace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_pad' => ['string', 'string'=>'string', 'length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'string'=>'string', 'times'=>'int'], - 'str_replace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], + 'str_replace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], + 'str_replace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], + 'str_replace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], + 'str_replace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_rot13' => ['string', 'string'=>'string'], 'str_shuffle' => ['string', 'string'=>'string'], 'str_split' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'], @@ -14430,7 +14436,8 @@ 'substr' => ['string|false', 'string'=>'string', 'offset'=>'int', 'length='=>'int'], 'substr_compare' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset'=>'int', 'length='=>'int', 'case_insensitive='=>'bool'], 'substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], - 'substr_replace' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'substr_replace' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'substr_replace\'1' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], 'suhosin_encrypt_cookie' => ['string|false', 'name'=>'string', 'value'=>'string'], 'suhosin_get_raw_cookies' => ['array'], 'svm::crossvalidate' => ['float', 'problem'=>'array', 'number_of_folds'=>'int'], From 8ee875086f356369cad7376fc0b295239becd227 Mon Sep 17 00:00:00 2001 From: ging-dev Date: Tue, 17 Oct 2023 20:17:15 +0700 Subject: [PATCH 30/43] chore: add failing test --- tests/Cache/CacheTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index 04f44eda644..f2d8443a711 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -219,6 +219,16 @@ public function foo($baz): void } } PHP, + '/src/B.php' => <<<'PHP' + foo(1); + } + } + PHP, ], 'issues' => [], ], @@ -244,6 +254,9 @@ public function foo($baz): void '/src/A.php' => [ "UndefinedDocblockClass: Docblock-defined class, interface or enum named T does not exist", ], + '/src/B.php' => [ + "InvalidArgument: Argument 1 of A::foo expects T, but 1 provided", + ], ], ], ], From 18c037ec6eeccbcaf95ab7b004507f7bb3be29c4 Mon Sep 17 00:00:00 2001 From: ging-dev Date: Tue, 17 Oct 2023 20:23:11 +0700 Subject: [PATCH 31/43] fix: bug fixes for test cases --- src/Psalm/Internal/Diff/FileStatementsDiffer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Diff/FileStatementsDiffer.php b/src/Psalm/Internal/Diff/FileStatementsDiffer.php index 665cb47cafd..1fb58c710c5 100644 --- a/src/Psalm/Internal/Diff/FileStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/FileStatementsDiffer.php @@ -118,7 +118,7 @@ static function ( if ($diff_elem->old->getDocComment() === $diff_elem->new->getDocComment()) { $keep = [...$keep, ...$class_keep[0]]; } else { - $keep_signature = [...$keep_signature, ...$class_keep[0]]; + $add_or_delete = [...$add_or_delete, ...$class_keep[0]]; } $keep_signature = [...$keep_signature, ...$class_keep[1]]; $add_or_delete = [...$add_or_delete, ...$class_keep[2]]; From e2d1e83b8715fd2e8f4a3811cf7174d9f9ea25a0 Mon Sep 17 00:00:00 2001 From: robchett Date: Tue, 17 Oct 2023 18:49:28 +0100 Subject: [PATCH 32/43] Fix memory explosion with calls to method_exists --- .../Method/AtomicMethodCallAnalysisResult.php | 2 +- .../Call/Method/AtomicMethodCallAnalyzer.php | 9 +++++---- .../Method/ExistingAtomicMethodCallAnalyzer.php | 3 ++- .../Call/Method/MissingMethodCallHandler.php | 10 +++++----- tests/MethodCallTest.php | 16 ++++++++++++++++ 5 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php index e83742e070b..94703207fc6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php @@ -26,7 +26,7 @@ class AtomicMethodCallAnalysisResult public array $invalid_method_call_types = []; /** - * @var array + * @var array */ public array $existent_method_ids = []; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 4e6d3188211..64567213200 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -338,7 +338,7 @@ public static function analyze( $all_intersection_return_type = null; $all_intersection_existent_method_ids = []; - // insersection types are also fun, they also complicate matters + // intersection types are also fun, they also complicate matters if ($intersection_types) { [$all_intersection_return_type, $all_intersection_existent_method_ids] = self::getIntersectionReturnType( @@ -525,7 +525,7 @@ public static function analyze( /** * @param TNamedObject|TTemplateParam $lhs_type_part * @param array $intersection_types - * @return array{?Union, array} + * @return array{?Union, array} */ private static function getIntersectionReturnType( StatementsAnalyzer $statements_analyzer, @@ -646,7 +646,8 @@ private static function handleInvalidClass( && $stmt->name instanceof PhpParser\Node\Identifier && isset($lhs_type_part->methods[strtolower($stmt->name->name)]) ) { - $result->existent_method_ids[] = $lhs_type_part->methods[strtolower($stmt->name->name)]; + $method_id = $lhs_type_part->methods[strtolower($stmt->name->name)]; + $result->existent_method_ids[$method_id] = true; } elseif (!$is_intersection) { if ($stmt->name instanceof PhpParser\Node\Identifier) { $codebase->analyzer->addMixedMemberName( @@ -915,7 +916,7 @@ private static function handleCallableObject( ?TemplateResult $inferred_template_result = null ): void { $method_id = 'object::__invoke'; - $result->existent_method_ids[] = $method_id; + $result->existent_method_ids[$method_id] = true; $result->has_valid_method_call_type = true; if ($lhs_type_part_callable !== null) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index 35fda473155..05210818b64 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -87,7 +87,8 @@ public static function analyze( $cased_method_id = $fq_class_name . '::' . $stmt_name->name; - $result->existent_method_ids[] = $method_id->__toString(); + + $result->existent_method_ids[$method_id->__toString()] = true; if ($context->collect_initializations && $context->calling_method_id) { [$calling_method_class] = explode('::', $context->calling_method_id); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 8e9346d803d..1f287b98d57 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -52,7 +52,7 @@ public static function handleMagicMethod( if ($stmt->isFirstClassCallable()) { if (isset($class_storage->pseudo_methods[$method_name_lc])) { $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; $result->return_type = self::createFirstClassCallableReturnType( $class_storage->pseudo_methods[$method_name_lc], ); @@ -110,7 +110,7 @@ public static function handleMagicMethod( if ($found_method_and_class_storage) { $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; [$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage; @@ -198,7 +198,7 @@ public static function handleMagicMethod( } $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; $array_values = array_map( static fn(PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem => new VirtualArrayItem( @@ -235,7 +235,7 @@ public static function handleMagicMethod( } /** - * @param array $all_intersection_existent_method_ids + * @param array $all_intersection_existent_method_ids */ public static function handleMissingOrMagicMethod( StatementsAnalyzer $statements_analyzer, @@ -267,7 +267,7 @@ public static function handleMissingOrMagicMethod( && $found_method_and_class_storage ) { $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; [$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage; diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 1f00196809e..9b8ba5b72b2 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -556,6 +556,22 @@ function foo(DateTime $d1, DateTime $d2) : void { ); }', ], + 'methodExistsDoesntExhaustMemory' => [ + 'code' => 'a() : []; + method_exists($c, \'b\') ? $c->b() : []; + method_exists($c, \'c\') ? $c->c() : []; + method_exists($c, \'d\') ? $c->d() : []; + method_exists($c, \'e\') ? $c->e() : []; + method_exists($c, \'f\') ? $c->f() : []; + method_exists($c, \'g\') ? $c->g() : []; + method_exists($c, \'h\') ? $c->h() : []; + method_exists($c, \'i\') ? $c->i() : []; + }', + ], 'callMethodAfterCheckingExistence' => [ 'code' => ' Date: Thu, 19 Oct 2023 11:54:03 +0400 Subject: [PATCH 33/43] Update IssueBuffer.php --- src/Psalm/IssueBuffer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index e7dfdda9145..1460c53f7c8 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -831,7 +831,7 @@ public static function printSuccessMessage(ProjectAnalyzer $project_analyzer): v $foreground = "30"; // text style, 1 = bold - $style = "1"; + $style = "2"; if ($project_analyzer->stdout_report_options->use_color) { echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n"; From 576e4d2bc46a674d8566b7e6f6e3d72302a827c2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 19 Oct 2023 11:16:20 +0200 Subject: [PATCH 34/43] Fix method calls and property accesses after extension_loaded --- .../Internal/Analyzer/ClassLikeAnalyzer.php | 6 +++- .../Expression/AssignmentAnalyzer.php | 18 +++++------- .../Call/Method/AtomicMethodCallAnalyzer.php | 1 + .../Expression/Call/MethodCallAnalyzer.php | 15 ---------- .../Expression/Call/StaticCallAnalyzer.php | 3 +- .../Fetch/StaticPropertyFetchAnalyzer.php | 3 +- tests/UnusedCodeTest.php | 29 +++++++++++++++++++ 7 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 2c32967f805..3ee68c6582f 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -203,7 +203,8 @@ public static function checkFullyQualifiedClassLikeName( ?string $calling_fq_class_name, ?string $calling_method_id, array $suppressed_issues, - ?ClassLikeNameOptions $options = null + ?ClassLikeNameOptions $options = null, + bool $check_classes = true ): ?bool { if ($options === null) { $options = new ClassLikeNameOptions(); @@ -276,6 +277,9 @@ public static function checkFullyQualifiedClassLikeName( && !($interface_exists && $options->allow_interface) && !($enum_exists && $options->allow_enum) ) { + if (!$check_classes) { + return null; + } if (!$options->allow_trait || !$codebase->classlikes->traitExists($fq_class_name, $code_location)) { if ($options->from_docblock) { if (IssueBuffer::accepts( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index 76c7073c12e..c59a554d9ab 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -671,16 +671,14 @@ private static function analyzeAssignment( return false; } - if ($context->check_classes) { - if (StaticPropertyAssignmentAnalyzer::analyze( - $statements_analyzer, - $assign_var, - $assign_value, - $assign_value_type, - $context, - ) === false) { - return false; - } + if (StaticPropertyAssignmentAnalyzer::analyze( + $statements_analyzer, + $assign_var, + $assign_value, + $assign_value_type, + $context, + ) === false) { + return false; } if ($var_id) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 64567213200..fca21662783 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -184,6 +184,7 @@ public static function analyze( $context->calling_method_id, $statements_analyzer->getSuppressedIssues(), new ClassLikeNameOptions(true, false, true, true, $lhs_type_part->from_docblock), + $context->check_classes, ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 437f65d7510..72c343d1073 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -122,21 +122,6 @@ public static function analyze( $statements_analyzer->node_data->setType($stmt, Type::getMixed()); } - if (!$context->check_classes) { - if (ArgumentsAnalyzer::analyze( - $statements_analyzer, - $stmt->getArgs(), - null, - null, - true, - $context, - ) === false) { - return false; - } - - return true; - } - if ($class_type && $stmt->name instanceof PhpParser\Node\Identifier && ($class_type->isNull() || $class_type->isVoid()) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index fa7cc498184..6a0a95ff2da 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -103,7 +103,7 @@ public static function analyze( if ($context->isPhantomClass($fq_class_name)) { return true; } - } elseif ($context->check_classes) { + } else { $aliases = $statements_analyzer->getAliases(); if ($context->calling_method_id @@ -153,6 +153,7 @@ public static function analyze( : null, $statements_analyzer->getSuppressedIssues(), new ClassLikeNameOptions(false, false, false, true), + $context->check_classes, ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index 46a4cf0f414..35bc7061427 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -154,7 +154,6 @@ public static function analyze( } if (!$fq_class_name - || !$context->check_classes || !$context->check_variables || ExpressionAnalyzer::isMock($fq_class_name) ) { @@ -249,7 +248,7 @@ public static function analyze( : null, ) ) { - if ($context->inside_isset) { + if ($context->inside_isset || !$context->check_classes) { return true; } diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index 82fe731886a..ef570f8a6f0 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -467,6 +467,35 @@ public function __construct() {} new A(); }', ], + 'useMethodPropertiesAfterExtensionLoaded' => [ + 'code' => 'test(); + } + if (\extension_loaded("fdsfdsfd")) { + return a::$a; + } + if (\extension_loaded("fdsfdsfd")) { + return a::get(); + } + return $handler->test(); + }', + ], 'usedParamInIf' => [ 'code' => ' Date: Thu, 19 Oct 2023 11:23:24 +0200 Subject: [PATCH 35/43] Fix --- .../Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 6a0a95ff2da..8302dcfca44 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -54,8 +54,6 @@ public static function analyze( $config = $codebase->config; if ($stmt->class instanceof PhpParser\Node\Name) { - $fq_class_name = null; - if (count($stmt->class->getParts()) === 1 && in_array(strtolower($stmt->class->getFirst()), ['self', 'static', 'parent'], true) ) { From cddf6a9a5789cf011224b9c988d3351a0659f550 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 21 Oct 2023 20:44:04 +0200 Subject: [PATCH 36/43] Rector fixes --- src/Psalm/Codebase.php | 3 +-- src/Psalm/Internal/Analyzer/CommentAnalyzer.php | 3 +-- .../Statements/Expression/CastAnalyzer.php | 9 +++------ src/Psalm/Internal/Codebase/Populator.php | 16 ++++++++-------- .../FunctionDocblockManipulator.php | 2 +- .../Type/TemplateInferredTypeReplacer.php | 2 +- .../Event/AfterMethodCallAnalysisEvent.php | 2 +- src/Psalm/Type/Atomic/TClosure.php | 4 +--- src/Psalm/Type/Atomic/TGenericObject.php | 3 +-- src/Psalm/Type/UnionTrait.php | 5 ++--- 10 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index d2b5d54ceae..e1c2f017090 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -69,7 +69,6 @@ use UnexpectedValueException; use function array_combine; -use function array_merge; use function array_pop; use function array_reverse; use function array_values; @@ -1917,7 +1916,7 @@ public function getCompletionItemsForClassishThing( ); } - $completion_items = array_merge($completion_items, array_values($pseudo_property_types)); + $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { $property_storage = $this->properties->getStorage( diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 9668f292417..65915d1f34c 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -25,7 +25,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_merge; use function count; use function is_string; use function preg_match; @@ -400,7 +399,7 @@ public static function splitDocLine(string $return_block): array $remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); if ($remaining) { - return array_merge([rtrim($type)], preg_split('/\s+/', $remaining) ?: []); + return [rtrim($type), ...preg_split('/\s+/', $remaining) ?: []]; } return [$type]; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 12285e2432e..99f045d71c6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -476,7 +476,7 @@ public static function castIntAttempt( // todo: emit error here } - $valid_types = array_merge($valid_ints, $castable_types); + $valid_types = [...$valid_ints, ...$castable_types]; if (!$valid_types) { $int_type = Type::getInt(); @@ -661,7 +661,7 @@ public static function castFloatAttempt( // todo: emit error here } - $valid_types = array_merge($valid_floats, $castable_types); + $valid_types = [...$valid_floats, ...$castable_types]; if (!$valid_types) { $float_type = Type::getFloat(); @@ -804,10 +804,7 @@ public static function castStringAttempt( $parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes); } - $castable_types = array_merge( - $castable_types, - array_values($return_type->getAtomicTypes()), - ); + $castable_types = [...$castable_types, ...array_values($return_type->getAtomicTypes())]; continue 2; } diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 022cc684e46..69aa208044e 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -931,10 +931,10 @@ protected function inheritMethodsFromParent( if ($parent_storage->is_trait && $storage->trait_alias_map ) { - $aliased_method_names = array_merge( - $aliased_method_names, - array_keys($storage->trait_alias_map, $method_name_lc, true), - ); + $aliased_method_names = [ + ...$aliased_method_names, + ...array_keys($storage->trait_alias_map, $method_name_lc, true), + ]; } foreach ($aliased_method_names as $aliased_method_name) { @@ -1001,10 +1001,10 @@ protected function inheritMethodsFromParent( if ($parent_storage->is_trait && $storage->trait_alias_map ) { - $aliased_method_names = array_merge( - $aliased_method_names, - array_keys($storage->trait_alias_map, $method_name_lc, true), - ); + $aliased_method_names = [ + ...$aliased_method_names, + ...array_keys($storage->trait_alias_map, $method_name_lc, true), + ]; } foreach ($aliased_method_names as $aliased_method_name) { diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index b04952c0581..940e43c2349 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -44,7 +44,7 @@ class FunctionDocblockManipulator private static array $manipulators = []; /** @var Closure|Function_|ClassMethod|ArrowFunction */ - private $stmt; + private FunctionLike $stmt; private int $docblock_start; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index fc8573ab418..259aedd9521 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -226,7 +226,7 @@ public static function replace( )->freeze(); } - $atomic_types = array_merge($types, $new_types); + $atomic_types = [...$types, ...$new_types]; if (!$atomic_types) { throw new UnexpectedValueException('This array should be full'); } diff --git a/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php b/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php index b0ad1ef31d5..f699a6d4e03 100644 --- a/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/AfterMethodCallAnalysisEvent.php @@ -16,7 +16,7 @@ final class AfterMethodCallAnalysisEvent /** * @var MethodCall|StaticCall */ - private $expr; + private Expr $expr; private string $method_id; private string $appearing_method_id; private string $declaring_method_id; diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index 97949adc967..94ee9446d44 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -9,8 +9,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; - /** * Represents a closure where we know the return type and params * @@ -131,6 +129,6 @@ public function replaceTemplateTypesWithStandins( protected function getChildNodeKeys(): array { - return array_merge(parent::getChildNodeKeys(), $this->getCallableChildNodeKeys()); + return [...parent::getChildNodeKeys(), ...$this->getCallableChildNodeKeys()]; } } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 45aa50b8cee..c362085f565 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -8,7 +8,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; use function count; use function implode; use function strrpos; @@ -127,7 +126,7 @@ public function getAssertionString(): string protected function getChildNodeKeys(): array { - return array_merge(parent::getChildNodeKeys(), ['type_params']); + return [...parent::getChildNodeKeys(), 'type_params']; } /** diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index 3361c2ba873..a500789cf41 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -43,7 +43,6 @@ use Psalm\Type\Atomic\TTrue; use function array_filter; -use function array_merge; use function array_unique; use function count; use function get_class; @@ -273,13 +272,13 @@ public function toNamespacedString( } if (count($literal_ints) <= 3 && !$has_non_literal_int) { - $other_types = array_merge($other_types, $literal_ints); + $other_types = [...$other_types, ...$literal_ints]; } else { $other_types[] = 'int'; } if (count($literal_strings) <= 3 && !$has_non_literal_string) { - $other_types = array_merge($other_types, $literal_strings); + $other_types = [...$other_types, ...$literal_strings]; } else { $other_types[] = 'string'; } From 3b66272aa0e38389daa5bc72003839d85380ee12 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 21 Oct 2023 20:45:09 +0200 Subject: [PATCH 37/43] More rector fixes --- .../scripts/update_signaturemap_from_other_tool.php | 2 +- examples/plugins/ClassUnqualifier.php | 2 +- src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php | 2 +- .../Internal/Analyzer/Statements/Block/ForeachAnalyzer.php | 2 +- .../Statements/Expression/BinaryOp/ConcatAnalyzer.php | 2 +- .../Expression/Call/Method/AtomicMethodCallAnalyzer.php | 1 + .../Expression/Fetch/AtomicPropertyFetchAnalyzer.php | 3 ++- src/Psalm/Internal/Analyzer/StatementsAnalyzer.php | 2 +- .../FileManipulation/FunctionDocblockManipulator.php | 2 +- src/Psalm/Internal/LanguageServer/LanguageServer.php | 4 ++-- .../ReturnTypeProvider/ArrayMapReturnTypeProvider.php | 4 ++-- src/Psalm/Internal/Type/SimpleAssertionReconciler.php | 4 ++-- src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php | 1 + src/Psalm/Internal/Type/TypeParser.php | 3 +-- src/Psalm/IssueBuffer.php | 6 +++--- src/Psalm/Report/ByIssueLevelAndTypeReport.php | 2 +- src/Psalm/Report/CountReport.php | 2 +- src/Psalm/Storage/FunctionLikeStorage.php | 4 ++-- src/Psalm/Type/Atomic/TValueOf.php | 5 +++-- tests/AsyncTestCase.php | 2 +- tests/CodebaseTest.php | 2 +- tests/Config/ConfigTest.php | 2 +- .../Plugin/Hook/CustomArrayMapFunctionStorageProvider.php | 3 +-- tests/DocumentationTest.php | 2 +- tests/FileDiffTest.php | 4 ++-- tests/TaintTest.php | 2 +- tests/TestCase.php | 2 +- tests/TypeComparatorTest.php | 2 +- tests/fixtures/DestructiveAutoloader/autoloader.php | 2 +- tests/fixtures/SuicidalAutoloader/autoloader.php | 4 +--- 30 files changed, 40 insertions(+), 40 deletions(-) diff --git a/dictionaries/scripts/update_signaturemap_from_other_tool.php b/dictionaries/scripts/update_signaturemap_from_other_tool.php index 21fb61166d5..d50e88ee0a8 100644 --- a/dictionaries/scripts/update_signaturemap_from_other_tool.php +++ b/dictionaries/scripts/update_signaturemap_from_other_tool.php @@ -30,7 +30,7 @@ $removed_foreign_functions ); -uksort($new_local, fn($a, $b) => strtolower($a) <=> strtolower($b)); +uksort($new_local, static fn($a, $b) => strtolower($a) <=> strtolower($b)); foreach ($new_local as $name => $data) { if (!is_array($data)) { diff --git a/examples/plugins/ClassUnqualifier.php b/examples/plugins/ClassUnqualifier.php index 3f4f14f517a..57a9f86da08 100644 --- a/examples/plugins/ClassUnqualifier.php +++ b/examples/plugins/ClassUnqualifier.php @@ -44,7 +44,7 @@ public static function afterClassLikeExistenceCheck( $new_candidate_type = implode( '', array_map( - fn($f) => $f[0], + static fn($f) => $f[0], $type_tokens, ), ); diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 3ee68c6582f..bc20c08f8ae 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -707,7 +707,7 @@ protected function checkTemplateParams( && $storage->template_types && $storage->template_covariants && ($local_offset - = array_search($t->param_name, array_keys($storage->template_types))) + = array_search($t->param_name, array_keys($storage->template_types), true)) !== false && !empty($storage->template_covariants[$local_offset]) ) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 905dc3d8a80..128eaada6ef 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -1096,7 +1096,7 @@ private static function getExtendedType( ): ?Union { if ($calling_class === $template_class) { if (isset($class_template_types[$template_name]) && $calling_type_params) { - $offset = array_search($template_name, array_keys($class_template_types)); + $offset = array_search($template_name, array_keys($class_template_types), true); if ($offset !== false && isset($calling_type_params[$offset])) { return $calling_type_params[$offset]; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index b90ffbc387d..c9dcbafdb09 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -180,7 +180,7 @@ public static function analyze( if ($literal_concat) { // Bypass opcache bug: https://github.com/php/php-src/issues/10635 - (function (int $_): void { + (static function (int $_) : void { })($combinations); if (count($result_type_parts) === 0) { throw new AssertionError("The number of parts cannot be 0!"); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index fca21662783..5997ac82efa 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -751,6 +751,7 @@ private static function handleTemplatedMixins( $param_position = array_search( $mixin->param_name, $template_type_keys, + true, ); if ($param_position !== false diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index d6289b6687b..ff14ed24273 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -788,6 +788,7 @@ public static function localizePropertyType( $position = array_search( $param_name, array_keys($property_class_storage->template_types), + true, ); } @@ -1000,7 +1001,7 @@ private static function handleEnumName( empty($relevant_enum_case_names) ? Type::getNonEmptyString() : new Union(array_map( - fn(string $name): TString => Type::getAtomicStringFromLiteral($name), + static fn(string $name): TString => Type::getAtomicStringFromLiteral($name), $relevant_enum_case_names, )), ); diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 55e80d44643..fc75e1631eb 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -869,7 +869,7 @@ public function checkUnreferencedVars(array $stmts, Context $context): void } if ($function_storage) { - $param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup)); + $param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup), true); if ($param_index !== false) { $param = $function_storage->params[$param_index]; diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 940e43c2349..f10a42379e2 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -415,7 +415,7 @@ private function getDocblock(): string $modified_docblock = true; $inferredThrowsClause = array_reduce( $this->throwsExceptions, - fn(string $throwsClause, string $exception) => $throwsClause === '' + static fn(string $throwsClause, string $exception) => $throwsClause === '' ? $exception : $throwsClause.'|'.$exception, '', diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index ed6b879dcbe..48c0de835a4 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -237,7 +237,7 @@ function (Message $msg): Generator { $this->protocolReader->on( 'readMessageGroup', - function (): void { + static function () : void { //$this->verboseLog('Received message group'); //$this->doAnalysis(); }, @@ -765,7 +765,7 @@ function (IssueData $issue_data): Diagnostic { return $diagnostic; }, array_filter( - array_map(function (IssueData $issue_data) use (&$issue_baseline) { + array_map(static function (IssueData $issue_data) use (&$issue_baseline) { if (empty($issue_baseline)) { return $issue_data; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 92397ebb69f..910c5d5c9e6 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -113,9 +113,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $array_arg_types = array_map(null, ...$array_arg_types); $array_arg_types = array_map( /** @param non-empty-array $sub */ - function (array $sub) use ($null) { + static function (array $sub) use ($null) { $sub = array_map( - fn(?Union $t) => $t ?? $null, + static fn(?Union $t) => $t ?? $null, $sub, ); return new Union([new TKeyedArray($sub, null, null, true)]); diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 5e0d0a827ed..aa606ec8f89 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -686,7 +686,7 @@ private static function reconcileNonEmptyCountable( $existing_var_type->removeType('array'); $existing_var_type->addType($array_atomic_type->setProperties( array_map( - fn(Union $union) => $union->setPossiblyUndefined(false), + static fn(Union $union) => $union->setPossiblyUndefined(false), $array_atomic_type->properties, ), )); @@ -806,7 +806,7 @@ private static function reconcileExactlyCountable( $existing_var_type->removeType('array'); $existing_var_type->addType($array_atomic_type->setProperties( array_map( - fn(Union $union) => $union->setPossiblyUndefined(false), + static fn(Union $union) => $union->setPossiblyUndefined(false), $array_atomic_type->properties, ), )); diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 9fa51c420cd..bbab4e24b7b 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1343,6 +1343,7 @@ public static function getMappedGenericTypeParams( $old_params_offset = (int) array_search( $template->param_name, array_keys($input_class_storage->template_types), + true, ); $candidate_param_types[] = ($input_type_params[$old_params_offset] ?? Type::getMixed()) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 99cf82a4d51..fc66827c82d 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -958,7 +958,7 @@ private static function getTypeFromGenericTree( } assert(count($parse_tree->children) === 2); - $get_int_range_bound = function (ParseTree $parse_tree, Union $generic_param, string $bound_name): ?int { + $get_int_range_bound = static function (ParseTree $parse_tree, Union $generic_param, string $bound_name) : ?int { if (!$parse_tree instanceof Value || count($generic_param->getAtomicTypes()) > 1 || (!$generic_param->getSingleAtomic() instanceof TLiteralInt @@ -970,7 +970,6 @@ private static function getTypeFromGenericTree( "Invalid type \"{$generic_param->getId()}\" as int $bound_name boundary", ); } - $generic_param_atomic = $generic_param->getSingleAtomic(); return $generic_param_atomic instanceof TLiteralInt ? $generic_param_atomic->value : null; }; diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 1460c53f7c8..9ca29ef701b 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -190,7 +190,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) return true; } - $suppressed_issue_position = array_search($issue_type, $suppressed_issues); + $suppressed_issue_position = array_search($issue_type, $suppressed_issues, true); if ($suppressed_issue_position !== false) { if (is_int($suppressed_issue_position)) { @@ -203,7 +203,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) $parent_issue_type = Config::getParentIssueType($issue_type); if ($parent_issue_type) { - $suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues); + $suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues, true); if ($suppressed_issue_position !== false) { if (is_int($suppressed_issue_position)) { @@ -216,7 +216,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) $suppress_all_position = $config->disable_suppress_all ? false - : array_search('all', $suppressed_issues); + : array_search('all', $suppressed_issues, true); if ($suppress_all_position !== false) { if (is_int($suppress_all_position)) { diff --git a/src/Psalm/Report/ByIssueLevelAndTypeReport.php b/src/Psalm/Report/ByIssueLevelAndTypeReport.php index c657b415749..36aeb60221b 100644 --- a/src/Psalm/Report/ByIssueLevelAndTypeReport.php +++ b/src/Psalm/Report/ByIssueLevelAndTypeReport.php @@ -181,7 +181,7 @@ private function sortIssuesByLevelAndType(): void { usort( $this->issues_data, - fn(IssueData $left, IssueData $right): int => [$left->error_level > 0, -$left->error_level, + static fn(IssueData $left, IssueData $right): int => [$left->error_level > 0, -$left->error_level, $left->type, $left->file_path, $left->file_name, $left->line_from] <=> [$right->error_level > 0, -$right->error_level, $right->type, $right->file_path, $right->file_name, $right->line_from], diff --git a/src/Psalm/Report/CountReport.php b/src/Psalm/Report/CountReport.php index b044d851651..f4f3c428762 100644 --- a/src/Psalm/Report/CountReport.php +++ b/src/Psalm/Report/CountReport.php @@ -21,7 +21,7 @@ public function create(): string $issue_type_counts[$issue_data->type] = 1; } } - uksort($issue_type_counts, function (string $a, string $b) use ($issue_type_counts): int { + uksort($issue_type_counts, static function (string $a, string $b) use ($issue_type_counts) : int { $cmp_result = $issue_type_counts[$a] <=> $issue_type_counts[$b]; if ($cmp_result === 0) { return $a <=> $b; diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 929e2b84d7a..33bff92dd67 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -254,7 +254,7 @@ public function getHoverMarkdown(): string $params = count($this->params) > 0 ? "\n" . implode( ",\n", array_map( - function (FunctionLikeParameter $param): string { + static function (FunctionLikeParameter $param) : string { $realType = $param->type ?: 'mixed'; return " {$realType} \${$param->name}"; }, @@ -289,7 +289,7 @@ public function getCompletionSignature(): string $symbol_text = 'function ' . $this->cased_name . '(' . implode( ',', array_map( - fn(FunctionLikeParameter $param): string => ($param->type ?: 'mixed') . ' $' . $param->name, + static fn(FunctionLikeParameter $param): string => ($param->type ?: 'mixed') . ' $' . $param->name, $this->params, ), ) . ') : ' . ($this->return_type ?: 'mixed'); diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 9220c079dee..a202c12e1fc 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -42,8 +42,9 @@ private static function getValueTypeForNamedObject(array $cases, TNamedObject $a } return new Union(array_map( - function (EnumCaseStorage $case): Atomic { - assert($case->value !== null); // Backed enum must have a value + static function (EnumCaseStorage $case) : Atomic { + assert($case->value !== null); + // Backed enum must have a value return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value); }, array_values($cases), diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index e9834d7af89..b459c42414b 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -152,7 +152,7 @@ public static function assertArrayKeysAreStrings(array $array, string $message = */ public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { - $isZeroOrString = /** @param mixed $key */ fn($key): bool => $key === 0 || is_string($key); + $isZeroOrString = /** @param mixed $key */ static fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } diff --git a/tests/CodebaseTest.php b/tests/CodebaseTest.php index 81e7a7786b8..f5291649826 100644 --- a/tests/CodebaseTest.php +++ b/tests/CodebaseTest.php @@ -159,7 +159,7 @@ public static function afterClassLikeVisit(AfterClassLikeVisitEvent $event) ? (string)$stmt->extends->getAttribute('resolvedName') : ''; $storage->custom_metadata['implements'] = array_map( - fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'), + static fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'), $stmt->implements, ); $storage->custom_metadata['a'] = 'b'; diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 17d6a2e5c5e..88273122b36 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -1106,7 +1106,7 @@ public function testAllPossibleIssues(): void * @param string $issue_name * @return string */ - fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n", + static fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n", IssueHandler::getAllIssueTypes(), ), ); diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index a491d9ecf1b..87da0a93636 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -54,11 +54,10 @@ public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $e $custom_array_map_storage->return_type = self::createReturnType($all_expected_callables); $custom_array_map_storage->params = [ ...array_map( - function (TCallable $expected, int $offset) { + static function (TCallable $expected, int $offset) { $t = new Union([$expected]); $param = new FunctionLikeParameter('fn' . $offset, false, $t, $t); $param->is_optional = false; - return $param; }, $all_expected_callables, diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 653e25f2004..26b734c803a 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -342,7 +342,7 @@ public function testShortcodesAreUnique(): void $duplicate_shortcodes = array_filter( $all_shortcodes, - fn($issues): bool => count($issues) > 1 + static fn($issues): bool => count($issues) > 1 ); $this->assertEquals( diff --git a/tests/FileDiffTest.php b/tests/FileDiffTest.php index acc84f404bb..345e1d98b3b 100644 --- a/tests/FileDiffTest.php +++ b/tests/FileDiffTest.php @@ -62,7 +62,7 @@ public function testCode( * @param array{0: int, 1: int, 2: int, 3: int} $arr * @return array{0: int, 1: int} */ - fn(array $arr): array => [$arr[2], $arr[3]], + static fn(array $arr): array => [$arr[2], $arr[3]], $diff[3], ); @@ -133,7 +133,7 @@ public function testPartialAstDiff( * @param array{0: int, 1: int, 2: int, 3: int} $arr * @return array{0: int, 1: int} */ - fn(array $arr): array => [$arr[2], $arr[3]], + static fn(array $arr): array => [$arr[2], $arr[3]], $diff[3], ); diff --git a/tests/TaintTest.php b/tests/TaintTest.php index faa299957d4..9f62160edb2 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -2528,7 +2528,7 @@ public function multipleTaintIssuesAreDetected(string $code, array $expectedIssu $this->analyzeFile($filePath, new Context(), false); $actualIssueTypes = array_map( - fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', + static fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', IssueBuffer::getIssuesDataForFile($filePath), ); self::assertSame($expectedIssuesTypes, $actualIssueTypes); diff --git a/tests/TestCase.php b/tests/TestCase.php index 00739cbccb0..d0d285eb9e3 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -149,7 +149,7 @@ public static function assertArrayKeysAreStrings(array $array, string $message = public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { - $isZeroOrString = /** @param mixed $key */ fn($key): bool => $key === 0 || is_string($key); + $isZeroOrString = /** @param mixed $key */ static fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } diff --git a/tests/TypeComparatorTest.php b/tests/TypeComparatorTest.php index 6a2d093f511..0e21e5e7880 100644 --- a/tests/TypeComparatorTest.php +++ b/tests/TypeComparatorTest.php @@ -88,7 +88,7 @@ public function getAllBasicTypes(): array $basic_types['list{123}'] = true; return array_map( - fn($type) => [$type], + static fn($type) => [$type], array_keys($basic_types), ); } diff --git a/tests/fixtures/DestructiveAutoloader/autoloader.php b/tests/fixtures/DestructiveAutoloader/autoloader.php index 17cb0a152c5..37a8624de36 100644 --- a/tests/fixtures/DestructiveAutoloader/autoloader.php +++ b/tests/fixtures/DestructiveAutoloader/autoloader.php @@ -9,7 +9,7 @@ $GLOBALS[$key] = new Exception; } -spl_autoload_register(function() { +spl_autoload_register(static function () { // and destroy vars again // this will run during scanning (?) foreach ($GLOBALS as $key => $_) { diff --git a/tests/fixtures/SuicidalAutoloader/autoloader.php b/tests/fixtures/SuicidalAutoloader/autoloader.php index 365fa7b723e..d506219c141 100644 --- a/tests/fixtures/SuicidalAutoloader/autoloader.php +++ b/tests/fixtures/SuicidalAutoloader/autoloader.php @@ -3,7 +3,7 @@ use React\Promise\PromiseInterface as ReactPromise; use Composer\InstalledVersions; -spl_autoload_register(function (string $className) { +spl_autoload_register(static function (string $className) { $knownBadClasses = [ ReactPromise::class, // amphp/amp ResourceBundle::class, // symfony/polyfill-php73 @@ -25,11 +25,9 @@ 'Symfony\Component\String\s', 'Symfony\Component\Translation\t', ]; - if (in_array($className, $knownBadClasses)) { return; } - $ex = new RuntimeException('Attempted to load ' . $className); echo $ex->__toString() . "\n\n" . $ex->getTraceAsString() . "\n\n"; exit(70); From 596ee11d0dd0f0bc60fc2f03d06b3841021b2e99 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 21 Oct 2023 20:48:06 +0200 Subject: [PATCH 38/43] cs-fix --- .../Statements/Expression/BinaryOp/ConcatAnalyzer.php | 2 +- src/Psalm/Internal/LanguageServer/LanguageServer.php | 2 +- src/Psalm/Internal/Type/TypeParser.php | 6 +++++- src/Psalm/Report/CountReport.php | 2 +- src/Psalm/Storage/FunctionLikeStorage.php | 2 +- src/Psalm/Type/Atomic/TValueOf.php | 2 +- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index c9dcbafdb09..05bfb3fc387 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -180,7 +180,7 @@ public static function analyze( if ($literal_concat) { // Bypass opcache bug: https://github.com/php/php-src/issues/10635 - (static function (int $_) : void { + (static function (int $_): void { })($combinations); if (count($result_type_parts) === 0) { throw new AssertionError("The number of parts cannot be 0!"); diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 48c0de835a4..fde6b541395 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -237,7 +237,7 @@ function (Message $msg): Generator { $this->protocolReader->on( 'readMessageGroup', - static function () : void { + static function (): void { //$this->verboseLog('Received message group'); //$this->doAnalysis(); }, diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index fc66827c82d..0287ab4d6bd 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -958,7 +958,11 @@ private static function getTypeFromGenericTree( } assert(count($parse_tree->children) === 2); - $get_int_range_bound = static function (ParseTree $parse_tree, Union $generic_param, string $bound_name) : ?int { + $get_int_range_bound = static function ( + ParseTree $parse_tree, + Union $generic_param, + string $bound_name + ): ?int { if (!$parse_tree instanceof Value || count($generic_param->getAtomicTypes()) > 1 || (!$generic_param->getSingleAtomic() instanceof TLiteralInt diff --git a/src/Psalm/Report/CountReport.php b/src/Psalm/Report/CountReport.php index f4f3c428762..4321789d44b 100644 --- a/src/Psalm/Report/CountReport.php +++ b/src/Psalm/Report/CountReport.php @@ -21,7 +21,7 @@ public function create(): string $issue_type_counts[$issue_data->type] = 1; } } - uksort($issue_type_counts, static function (string $a, string $b) use ($issue_type_counts) : int { + uksort($issue_type_counts, static function (string $a, string $b) use ($issue_type_counts): int { $cmp_result = $issue_type_counts[$a] <=> $issue_type_counts[$b]; if ($cmp_result === 0) { return $a <=> $b; diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index 33bff92dd67..7be91f80434 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -254,7 +254,7 @@ public function getHoverMarkdown(): string $params = count($this->params) > 0 ? "\n" . implode( ",\n", array_map( - static function (FunctionLikeParameter $param) : string { + static function (FunctionLikeParameter $param): string { $realType = $param->type ?: 'mixed'; return " {$realType} \${$param->name}"; }, diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index a202c12e1fc..59941bc61e5 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -42,7 +42,7 @@ private static function getValueTypeForNamedObject(array $cases, TNamedObject $a } return new Union(array_map( - static function (EnumCaseStorage $case) : Atomic { + static function (EnumCaseStorage $case): Atomic { assert($case->value !== null); // Backed enum must have a value return ConstantTypeResolver::getLiteralTypeFromScalarValue($case->value); From 4c656f0a2b2e577a4910047198de39199b0497bb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sat, 21 Oct 2023 20:50:08 +0200 Subject: [PATCH 39/43] Revert --- .../Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index 05bfb3fc387..b90ffbc387d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -180,7 +180,7 @@ public static function analyze( if ($literal_concat) { // Bypass opcache bug: https://github.com/php/php-src/issues/10635 - (static function (int $_): void { + (function (int $_): void { })($combinations); if (count($result_type_parts) === 0) { throw new AssertionError("The number of parts cannot be 0!"); From e8b7b30043c55e878653fb7fdded656fa5d2fa7c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 20:11:28 +0200 Subject: [PATCH 40/43] Fixes --- UPGRADING.md | 8 ++++ src/Psalm/Config.php | 43 +++++++++---------- src/Psalm/Plugin/ArgTypeInferer.php | 15 +++---- src/Psalm/Report/CodeClimateReport.php | 6 +-- src/Psalm/Type/Atomic/TTypeAlias.php | 57 ++------------------------ tests/EnumTest.php | 2 +- tests/PsalmPluginTest.php | 4 +- tests/ReportOutputTest.php | 20 ++++----- tests/TestConfig.php | 3 +- 9 files changed, 58 insertions(+), 100 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index 55cb4f65119..2bf88c584a4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -44,6 +44,14 @@ - [BC] `Psalm\CodeLocation\Raw`, `Psalm\CodeLocation\ParseErrorLocation`, `Psalm\CodeLocation\DocblockTypeLocation`, `Psalm\Report\CountReport`, `Psalm\Type\Atomic\TNonEmptyArray` are now all final. +- [BC] `Psalm\Config` is now final. + +- [BC] The return type of `Psalm\Plugin\ArgTypeInferer::infer` changed from `Union|false` to `Union|null` + +- [BC] The `extra_types` property and `setIntersectionTypes` method of `Psalm\Type\Atomic\TTypeAlias` were removed. + +- [BC] Methods `convertSeverity` and `calculateFingerprint` of `Psalm\Report\CodeClimateReport` were removed. + # Upgrading from Psalm 4 to Psalm 5 ## Changed diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d86d787c73b..8759c0ddbcd 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -69,7 +69,6 @@ use function flock; use function fopen; use function function_exists; -use function get_class; use function get_defined_constants; use function get_defined_functions; use function getcwd; @@ -98,7 +97,9 @@ use function scandir; use function sha1; use function simplexml_import_dom; +use function str_contains; use function str_replace; +use function str_starts_with; use function strlen; use function strpos; use function strrpos; @@ -127,13 +128,13 @@ * @psalm-suppress PropertyNotSetInConstructor * @psalm-consistent-constructor */ -class Config +final class Config { private const DEFAULT_FILE_NAME = 'psalm.xml'; - public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; - public const REPORT_INFO = 'info'; - public const REPORT_ERROR = 'error'; - public const REPORT_SUPPRESS = 'suppress'; + final public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; + final public const REPORT_INFO = 'info'; + final public const REPORT_ERROR = 'error'; + final public const REPORT_SUPPRESS = 'suppress'; /** * @var array @@ -172,7 +173,7 @@ class Config * * @var array */ - protected array $universal_object_crates; + private array $universal_object_crates; /** * @var static|null @@ -222,7 +223,7 @@ class Config protected ?ProjectFileFilter $project_files = null; - protected ?ProjectFileFilter $extra_files = null; + private ?ProjectFileFilter $extra_files = null; /** * The base directory of this config file @@ -426,7 +427,7 @@ class Config private ?IncludeCollector $include_collector = null; - protected ?TaintAnalysisFileFilter $taint_analysis_ignored_files = null; + private ?TaintAnalysisFileFilter $taint_analysis_ignored_files = null; /** * @var bool whether to emit a backtrace of emitted issues to stderr @@ -874,7 +875,6 @@ private static function processConfigDeprecations( /** * @param non-empty-string $file_contents * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArgument * @psalm-suppress MixedPropertyFetch * @throws ConfigException */ @@ -963,15 +963,15 @@ private static function fromXmlAndPaths( if (file_exists($composer_json_path)) { $composer_json_contents = file_get_contents($composer_json_path); assert($composer_json_contents !== false); - $composer_json = json_decode($composer_json_contents, true); + $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); if (!is_array($composer_json)) { throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); } } $required_extensions = []; foreach (($composer_json["require"] ?? []) as $required => $_) { - if (strpos($required, "ext-") === 0) { - $required_extensions[strtolower(substr($required, 4))] = true; + if (str_starts_with((string) $required, "ext-")) { + $required_extensions[strtolower(substr((string) $required, 4))] = true; } } foreach ($required_extensions as $required_ext => $_) { @@ -1649,7 +1649,7 @@ public function reportIssueInFile(string $issue_type, string $file_path): bool try { $file_storage = $codebase->file_storage_provider->get($file_path); $dependent_files += $file_storage->required_by_file_paths; - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // do nothing } } @@ -1700,7 +1700,7 @@ public function trackTaintsInPath(string $file_path): bool public function getReportingLevelForIssue(CodeIssue $e): string { - $fqcn_parts = explode('\\', get_class($e)); + $fqcn_parts = explode('\\', $e::class); $issue_type = array_pop($fqcn_parts); $reporting_level = null; @@ -1765,17 +1765,17 @@ public static function getParentIssueType(string $issue_type): ?string return null; } - if (strpos($issue_type, 'Possibly') === 0) { + if (str_starts_with($issue_type, 'Possibly')) { $stripped_issue_type = (string) preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); - if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) { + if (!str_contains($stripped_issue_type, 'Invalid') && !str_starts_with($stripped_issue_type, 'Un')) { $stripped_issue_type = 'Invalid' . $stripped_issue_type; } return $stripped_issue_type; } - if (strpos($issue_type, 'Tainted') === 0) { + if (str_starts_with($issue_type, 'Tainted')) { return 'TaintedInput'; } @@ -2298,7 +2298,7 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P $codebase->classlikes->forgetMissingClassLikes(); $this->include_collector->runAndCollect( - [$this, 'requireAutoloader'], + $this->requireAutoloader(...), ); } @@ -2324,7 +2324,8 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P } } - public function getComposerFilePathForClassLike(string $fq_classlike_name): string|false + /** @return string|false */ + public function getComposerFilePathForClassLike(string $fq_classlike_name): string|bool { if (!$this->composer_class_loader) { return false; @@ -2502,7 +2503,7 @@ public function getPHPVersionFromComposerJson(): ?string $composer_json_contents = file_get_contents($composer_json_path); assert($composer_json_contents !== false); $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { + } catch (JsonException) { $composer_json = null; } diff --git a/src/Psalm/Plugin/ArgTypeInferer.php b/src/Psalm/Plugin/ArgTypeInferer.php index 0347eead24a..109769c8024 100644 --- a/src/Psalm/Plugin/ArgTypeInferer.php +++ b/src/Psalm/Plugin/ArgTypeInferer.php @@ -13,19 +13,16 @@ final class ArgTypeInferer { - private Context $context; - private StatementsAnalyzer $statements_analyzer; - /** * @internal */ - public function __construct(Context $context, StatementsAnalyzer $statements_analyzer) - { - $this->context = $context; - $this->statements_analyzer = $statements_analyzer; + public function __construct( + private readonly Context $context, + private readonly StatementsAnalyzer $statements_analyzer, + ) { } - public function infer(PhpParser\Node\Arg $arg): false|Union + public function infer(PhpParser\Node\Arg $arg): null|Union { $already_inferred_type = $this->statements_analyzer->node_data->getType($arg->value); @@ -34,7 +31,7 @@ public function infer(PhpParser\Node\Arg $arg): false|Union } if (ExpressionAnalyzer::analyze($this->statements_analyzer, $arg->value, $this->context) === false) { - return false; + return null; } return $this->statements_analyzer->node_data->getType($arg->value) ?? Type::getMixed(); diff --git a/src/Psalm/Report/CodeClimateReport.php b/src/Psalm/Report/CodeClimateReport.php index fb0cbf28689..61027d6371b 100644 --- a/src/Psalm/Report/CodeClimateReport.php +++ b/src/Psalm/Report/CodeClimateReport.php @@ -28,7 +28,7 @@ public function create(): string $options = $this->pretty ? Json::PRETTY : Json::DEFAULT; $issues_data = array_map( - [$this, 'mapToNewStructure'], + $this->mapToNewStructure(...), $this->issues_data, ); @@ -39,7 +39,7 @@ public function create(): string * convert our own severity to CodeClimate format * Values can be : info, minor, major, critical, or blocker */ - protected function convertSeverity(string $input): string + private function convertSeverity(string $input): string { if (Config::REPORT_INFO === $input) { return 'info'; @@ -58,7 +58,7 @@ protected function convertSeverity(string $input): string /** * calculate a unique fingerprint for a given issue */ - protected function calculateFingerprint(IssueData $issue): string + private function calculateFingerprint(IssueData $issue): string { return md5($issue->type.$issue->message.$issue->file_name.$issue->from.$issue->to); } diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 52a2fe9037a..ebda8e4118b 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -6,55 +6,17 @@ use Psalm\Type\Atomic; -use function array_map; -use function implode; - /** * @psalm-immutable */ final class TTypeAlias extends Atomic { - /** - * @var array|null - * @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the - * referencing type(s) are part of other intersection types. The intersection types are not set anymore - * and with v6 this property along with its related methods will get removed. - */ - public ?array $extra_types = null; - - public string $declaring_fq_classlike_name; - - public string $alias_name; - - /** - * @param array|null $extra_types - */ - public function __construct(string $declaring_fq_classlike_name, string $alias_name, ?array $extra_types = null) - { - $this->declaring_fq_classlike_name = $declaring_fq_classlike_name; - $this->alias_name = $alias_name; - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - $this->extra_types = $extra_types; + public function __construct( + public string $declaring_fq_classlike_name, + public string $alias_name, + ) { parent::__construct(true); } - /** - * @param array|null $extra_types - * @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the - * referencing type(s) are part of other intersection types. This method will get removed with v6. - * @psalm-suppress PossiblyUnusedMethod For backwards compatibility, we have to keep this here. - */ - public function setIntersectionTypes(?array $extra_types): self - { - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - if ($extra_types === $this->extra_types) { - return $this; - } - return new self( - $this->declaring_fq_classlike_name, - $this->alias_name, - $extra_types, - ); - } public function getKey(bool $include_extra = true): string { @@ -63,17 +25,6 @@ public function getKey(bool $include_extra = true): string public function getId(bool $exact = true, bool $nested = false): string { - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - if ($this->extra_types) { - return $this->getKey() . '&' . implode( - '&', - array_map( - static fn(Atomic $type): string => $type->getId($exact, true), - $this->extra_types, - ), - ); - } - return $this->getKey(); } diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 4015c3f7735..ebc67a05e1e 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -632,7 +632,7 @@ function noop(string $s): string $foo = FooEnum::Foo->value; noop($foo); noop(FooEnum::Foo->value); - PHP, + PHP, 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.1', diff --git a/tests/PsalmPluginTest.php b/tests/PsalmPluginTest.php index 64311a30122..5b97dc23136 100644 --- a/tests/PsalmPluginTest.php +++ b/tests/PsalmPluginTest.php @@ -25,9 +25,9 @@ class PsalmPluginTest extends TestCase { use MockeryPHPUnitIntegration; - private PluginList&MockInterface $plugin_list; + private MockInterface $plugin_list; - private PluginListFactory&MockInterface $plugin_list_factory; + private MockInterface $plugin_list_factory; private Application $app; diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index bee44b19d37..c17370a8106 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -712,6 +712,7 @@ public function testJsonReport(): void $issue_data = [ [ + 'link' => 'https://psalm.dev/024', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -727,13 +728,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => -1, 'shortcode' => 24, - 'link' => 'https://psalm.dev/024', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/138', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -749,13 +750,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'error_level' => 1, 'shortcode' => 138, - 'link' => 'https://psalm.dev/138', + 'error_level' => 1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/047', 'severity' => 'error', 'line_from' => 2, 'line_to' => 2, @@ -771,13 +772,13 @@ public function testJsonReport(): void 'snippet_to' => 56, 'column_from' => 42, 'column_to' => 49, - 'error_level' => 1, 'shortcode' => 47, - 'link' => 'https://psalm.dev/047', + 'error_level' => 1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/020', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, @@ -793,13 +794,13 @@ public function testJsonReport(): void 'snippet_to' => 172, 'column_from' => 6, 'column_to' => 15, - 'error_level' => -1, 'shortcode' => 20, - 'link' => 'https://psalm.dev/020', + 'error_level' => -1, 'taint_trace' => null, 'other_references' => null, ], [ + 'link' => 'https://psalm.dev/126', 'severity' => 'info', 'line_from' => 17, 'line_to' => 17, @@ -815,9 +816,8 @@ public function testJsonReport(): void 'snippet_to' => 277, 'column_from' => 6, 'column_to' => 8, - 'error_level' => 3, 'shortcode' => 126, - 'link' => 'https://psalm.dev/126', + 'error_level' => 3, 'taint_trace' => null, 'other_references' => null, ], diff --git a/tests/TestConfig.php b/tests/TestConfig.php index 572deabbce6..87433cc67c6 100644 --- a/tests/TestConfig.php +++ b/tests/TestConfig.php @@ -61,7 +61,8 @@ protected function getContents(): string '; } - public function getComposerFilePathForClassLike(string $fq_classlike_name): string|false + /** @return false */ + public function getComposerFilePathForClassLike(string $fq_classlike_name): bool { return false; } From 292ed063233e29d59d0bde61bb6daedee94d55bb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 20:13:31 +0200 Subject: [PATCH 41/43] Fix --- composer.json | 1 + phpunit.xml.dist | 2 +- src/Psalm/Internal/Type/TypeExpander.php | 22 ---------------------- tests/autoload.php | 9 +++++++++ 4 files changed, 11 insertions(+), 23 deletions(-) create mode 100644 tests/autoload.php diff --git a/composer.json b/composer.json index 0a56c517c06..0a36e057c1f 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", + "dg/bypass-finals": "^1.5", "mockery/mockery": "^1.5", "nunomaduro/mock-final-classes": "^1.1", "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 748be83439b..4f2c25cff79 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ extra_types ?? [] as $alias) { - $more_recursively_fleshed_out_types = self::expandAtomic( - $codebase, - $alias, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $recursively_fleshed_out_types = [ - ...$more_recursively_fleshed_out_types, - ...$recursively_fleshed_out_types, - ]; - } - return $recursively_fleshed_out_types; } diff --git a/tests/autoload.php b/tests/autoload.php new file mode 100644 index 00000000000..449bca68efa --- /dev/null +++ b/tests/autoload.php @@ -0,0 +1,9 @@ + Date: Sun, 22 Oct 2023 20:17:39 +0200 Subject: [PATCH 42/43] cs-fix --- psalm-baseline.xml | 15 ++++++++++++++- src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php | 2 +- src/Psalm/Internal/Type/TypeParser.php | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index d04ba234885..abd3dbf4b71 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -16,6 +16,9 @@ $deprecated_element_xml + + $this + @@ -622,6 +625,16 @@ hasLowercaseString + + + Config + + + public function __construct() + public function getComposerFilePathForClassLike(string $fq_classlike_name): bool + public function getProjectDirectories(): array + + diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index a6bff08e5b7..2e89585d8f6 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -206,7 +206,7 @@ public static function checkFullyQualifiedClassLikeName( ?string $calling_method_id, array $suppressed_issues, ?ClassLikeNameOptions $options = null, - bool $check_classes = true + bool $check_classes = true, ): ?bool { if ($options === null) { $options = new ClassLikeNameOptions(); diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 5063728eaa8..5b3bb664fe7 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -962,7 +962,7 @@ private static function getTypeFromGenericTree( $get_int_range_bound = static function ( ParseTree $parse_tree, Union $generic_param, - string $bound_name + string $bound_name, ): ?int { if (!$parse_tree instanceof Value || count($generic_param->getAtomicTypes()) > 1 From e72fb5a2b31e606abd525f867696c5ba5bf7451b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 22 Oct 2023 20:22:01 +0200 Subject: [PATCH 43/43] Fix --- tests/ReportOutputTest.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index c17370a8106..bee44b19d37 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -712,7 +712,6 @@ public function testJsonReport(): void $issue_data = [ [ - 'link' => 'https://psalm.dev/024', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -728,13 +727,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'shortcode' => 24, 'error_level' => -1, + 'shortcode' => 24, + 'link' => 'https://psalm.dev/024', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/138', 'severity' => 'error', 'line_from' => 3, 'line_to' => 3, @@ -750,13 +749,13 @@ public function testJsonReport(): void 'snippet_to' => 83, 'column_from' => 10, 'column_to' => 26, - 'shortcode' => 138, 'error_level' => 1, + 'shortcode' => 138, + 'link' => 'https://psalm.dev/138', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/047', 'severity' => 'error', 'line_from' => 2, 'line_to' => 2, @@ -772,13 +771,13 @@ public function testJsonReport(): void 'snippet_to' => 56, 'column_from' => 42, 'column_to' => 49, - 'shortcode' => 47, 'error_level' => 1, + 'shortcode' => 47, + 'link' => 'https://psalm.dev/047', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/020', 'severity' => 'error', 'line_from' => 8, 'line_to' => 8, @@ -794,13 +793,13 @@ public function testJsonReport(): void 'snippet_to' => 172, 'column_from' => 6, 'column_to' => 15, - 'shortcode' => 20, 'error_level' => -1, + 'shortcode' => 20, + 'link' => 'https://psalm.dev/020', 'taint_trace' => null, 'other_references' => null, ], [ - 'link' => 'https://psalm.dev/126', 'severity' => 'info', 'line_from' => 17, 'line_to' => 17, @@ -816,8 +815,9 @@ public function testJsonReport(): void 'snippet_to' => 277, 'column_from' => 6, 'column_to' => 8, - 'shortcode' => 126, 'error_level' => 3, + 'shortcode' => 126, + 'link' => 'https://psalm.dev/126', 'taint_trace' => null, 'other_references' => null, ],