From 083584d115b3a06c734c2264be3a39b8c0f1e4c1 Mon Sep 17 00:00:00 2001 From: cgocast Date: Wed, 22 Nov 2023 11:10:50 +0100 Subject: [PATCH 01/22] TaintedExtract --- UPGRADING.md | 2 +- config.xsd | 1 + docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + docs/running_psalm/issues/TaintedExtract.md | 10 ++++++++++ .../Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/TaintFlowGraph.php | 10 ++++++++++ src/Psalm/Issue/TaintedExtract.php | 10 ++++++++++ src/Psalm/Type/TaintKind.php | 1 + src/Psalm/Type/TaintKindGroup.php | 1 + stubs/CoreGenericFunctions.phpstub | 5 +++++ tests/TaintTest.php | 11 +++++++++++ 12 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 docs/running_psalm/issues/TaintedExtract.md create mode 100644 src/Psalm/Issue/TaintedExtract.php diff --git a/UPGRADING.md b/UPGRADING.md index 9b3665d4dcf..767e293871e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -17,7 +17,7 @@ - [BC] Class `Psalm\Issue\MixedInferredReturnType` was removed -- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well. +- [BC] Value of constant `Psalm\Type\TaintKindGroup::ALL_INPUT` changed to reflect new `TaintKind::INPUT_EXTRACT`, `TaintKind::INPUT_SLEEP` and `TaintKind::INPUT_XPATH` have been added. Accordingly, default values for `$taint` parameters of `Psalm\Codebase::addTaintSource()` and `Psalm\Codebase::addTaintSink()` have been changed as well. - [BC] Property `Config::$shepherd_host` was replaced with `Config::$shepherd_endpoint` diff --git a/config.xsd b/config.xsd index 6a6d182dca3..c97e3198ad9 100644 --- a/config.xsd +++ b/config.xsd @@ -433,6 +433,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 2d9c35ced37..a7b61ee78a1 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -286,6 +286,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [TaintedCookie](issues/TaintedCookie.md) - [TaintedCustom](issues/TaintedCustom.md) - [TaintedEval](issues/TaintedEval.md) + - [TaintedExtract](issues/TaintedExtract.md) - [TaintedFile](issues/TaintedFile.md) - [TaintedHeader](issues/TaintedHeader.md) - [TaintedHtml](issues/TaintedHtml.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 95f3839593b..364541ee439 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -234,6 +234,7 @@ - [TaintedCookie](issues/TaintedCookie.md) - [TaintedCustom](issues/TaintedCustom.md) - [TaintedEval](issues/TaintedEval.md) + - [TaintedExtract](issues/TaintedExtract.md) - [TaintedFile](issues/TaintedFile.md) - [TaintedHeader](issues/TaintedHeader.md) - [TaintedHtml](issues/TaintedHtml.md) diff --git a/docs/running_psalm/issues/TaintedExtract.md b/docs/running_psalm/issues/TaintedExtract.md new file mode 100644 index 00000000000..7b0fa27d85a --- /dev/null +++ b/docs/running_psalm/issues/TaintedExtract.md @@ -0,0 +1,10 @@ +# TaintedExtract + +Emitted when user-controlled array can be passed into an `extract` call. + +```php +vars_in_scope[$var_id])) diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 5c5f72173eb..1cab3ea6dd8 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -14,6 +14,7 @@ use Psalm\Issue\TaintedCookie; use Psalm\Issue\TaintedCustom; use Psalm\Issue\TaintedEval; +use Psalm\Issue\TaintedExtract; use Psalm\Issue\TaintedFile; use Psalm\Issue\TaintedHeader; use Psalm\Issue\TaintedHtml; @@ -471,6 +472,15 @@ private function getChildNodes( ); break; + case TaintKind::INPUT_EXTRACT: + $issue = new TaintedExtract( + 'Detected tainted extract', + $issue_location, + $issue_trace, + $path, + ); + break; + default: $issue = new TaintedCustom( 'Detected tainted ' . $matching_taint, diff --git a/src/Psalm/Issue/TaintedExtract.php b/src/Psalm/Issue/TaintedExtract.php new file mode 100644 index 00000000000..60eef6b9271 --- /dev/null +++ b/src/Psalm/Issue/TaintedExtract.php @@ -0,0 +1,10 @@ + 'TaintedSleep', ], + 'taintedExtract' => [ + 'code' => ' 'TaintedExtract', + ], + 'extractPost' => [ + 'code' => ' 'TaintedExtract', + ], ]; } From c75e6da8665b83ae30fe5f0174368b26f7581e92 Mon Sep 17 00:00:00 2001 From: cgocast Date: Tue, 28 Nov 2023 10:24:01 +0100 Subject: [PATCH 02/22] Fix coding style --- .../Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index acdd6c272fd..d265e22d0bb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -1270,7 +1270,7 @@ private static function handleByRefFunctionArg( $builtin_array_functions = [ 'ksort', 'asort', 'krsort', 'arsort', 'natcasesort', 'natsort', - 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'extract' + 'reset', 'end', 'next', 'prev', 'array_pop', 'array_shift', 'extract', ]; if (($var_id && isset($context->vars_in_scope[$var_id])) From bfd167515b47323cfe823adea6a696fe15c7372e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:25:03 +0100 Subject: [PATCH 03/22] the new version has no changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7e41dd27eeb..c4dda05c87e 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "dnoegel/php-xdg-base-dir": "^0.1.1", "felixfbecker/advanced-json-rpc": "^3.1", "felixfbecker/language-server-protocol": "^1.5.2", - "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", "nikic/php-parser": "^4.16", "sebastian/diff": "^4.0 || ^5.0", From 5fccb339380a9c78492fb9e8c363dee71f4e0a86 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 16:12:03 +0100 Subject: [PATCH 04/22] dont combine empty string with numeric-string Fix https://github.com/vimeo/psalm/issues/6646 --- src/Psalm/Internal/Type/TypeCombiner.php | 3 +++ tests/TypeCombinationTest.php | 32 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 773e3f71a47..e21d41b0559 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1080,6 +1080,9 @@ private static function scrapeStringProperties( if ($has_only_numeric_strings) { $combination->value_types['string'] = $type; + } elseif (count($combination->strings) === 1 && !$has_only_non_empty_strings) { + $combination->value_types['string'] = $type; + return; } elseif ($has_only_non_empty_strings) { $combination->value_types['string'] = new TNonEmptyString(); } else { diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index c3371ef2a6b..b891e84dcc2 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -88,6 +88,38 @@ function expectsTraversableOrArray($_a): void } ', ], + 'emptyStringNumericStringDontCombine' => [ + 'code' => ' [ + 'code' => ' Date: Thu, 7 Dec 2023 15:22:56 -0700 Subject: [PATCH 05/22] Fixed docblock spacing in supported_annotations.md `@psalm-internal` example --- docs/annotating_code/supported_annotations.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md index ba14e7d67c9..070f3ddd682 100644 --- a/docs/annotating_code/supported_annotations.md +++ b/docs/annotating_code/supported_annotations.md @@ -263,9 +263,9 @@ is not within the given namespace. Date: Tue, 12 Dec 2023 07:51:21 +0100 Subject: [PATCH 06/22] fix psalm v4 hardcoded in tests --- tests/Config/ConfigTest.php | 5 +++-- tests/Config/PluginTest.php | 5 +++-- tests/ProjectCheckerTest.php | 5 +++-- tests/ReportOutputTest.php | 3 ++- tests/StubTest.php | 5 +++-- tests/TestCase.php | 5 +++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 88273122b36..e8fe9a94c40 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -17,6 +17,7 @@ use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Scanner\FileScanner; +use Psalm\Internal\VersionUtils; use Psalm\Issue\TooManyArguments; use Psalm\Issue\UndefinedFunction; use Psalm\Tests\Config\Plugin\FileTypeSelfRegisteringPlugin; @@ -58,11 +59,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php index 130fd2d3304..bfd98cffc0b 100644 --- a/tests/Config/PluginTest.php +++ b/tests/Config/PluginTest.php @@ -12,6 +12,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\AfterEveryFunctionCallAnalysisInterface; @@ -46,11 +47,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index cce7da9d697..3e7bf7af31c 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -8,6 +8,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Plugin\EventHandler\AfterCodebasePopulatedInterface; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; @@ -43,11 +44,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index 8864247abe3..67aeedaca4b 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -10,6 +10,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Report; use Psalm\Report\JsonReport; @@ -109,7 +110,7 @@ public function testSarifReport(): void 'driver' => [ 'name' => 'Psalm', 'informationUri' => 'https://psalm.dev', - 'version' => '4.0.0', + 'version' => VersionUtils::getPsalmVersion(), 'rules' => [ [ 'id' => '246', diff --git a/tests/StubTest.php b/tests/StubTest.php index 6c835a65d8d..f12fb943ed8 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -13,6 +13,7 @@ use Psalm\Internal\Provider\FakeFileProvider; use Psalm\Internal\Provider\Providers; use Psalm\Internal\RuntimeCaches; +use Psalm\Internal\VersionUtils; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use function define; @@ -37,11 +38,11 @@ public static function setUpBeforeClass(): void self::$config = new TestConfig(); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 2da89b3558c..39f02d46703 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,6 +12,7 @@ use Psalm\Internal\RuntimeCaches; use Psalm\Internal\Type\TypeParser; use Psalm\Internal\Type\TypeTokenizer; +use Psalm\Internal\VersionUtils; use Psalm\IssueBuffer; use Psalm\Tests\Internal\Provider\FakeParserCacheProvider; use Psalm\Type\Union; @@ -56,11 +57,11 @@ public static function setUpBeforeClass(): void ini_set('memory_limit', '-1'); if (!defined('PSALM_VERSION')) { - define('PSALM_VERSION', '4.0.0'); + define('PSALM_VERSION', VersionUtils::getPsalmVersion()); } if (!defined('PHP_PARSER_VERSION')) { - define('PHP_PARSER_VERSION', '4.0.0'); + define('PHP_PARSER_VERSION', VersionUtils::getPhpParserVersion()); } parent::setUpBeforeClass(); From 0fd789cdcc2afd0784d8dd87eccd08482e2cbac9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:44:17 +0100 Subject: [PATCH 07/22] Fix type not equal when parent parent nodes are only populated if taint/unused variable analysis is enabled --- src/Psalm/Type/Atomic/TArray.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 06477607592..54dfef34117 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -84,7 +84,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } From 679a492609100e586fce6e356d1b378f98d9da06 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 13:54:35 +0100 Subject: [PATCH 08/22] other atomics --- src/Psalm/Type/Atomic/TClassStringMap.php | 2 +- src/Psalm/Type/Atomic/TGenericObject.php | 2 +- src/Psalm/Type/Atomic/TIterable.php | 2 +- src/Psalm/Type/Atomic/TKeyedArray.php | 6 +++--- src/Psalm/Type/Atomic/TList.php | 2 +- src/Psalm/Type/Atomic/TObjectWithProperties.php | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index d15d297f10e..56b43ccf597 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -216,7 +216,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality)) { + if (!$this->value_param->equals($other_type->value_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index c362085f565..415458de135 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -111,7 +111,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index 6b6c9ea32ab..1f67bfb5602 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -115,7 +115,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } foreach ($this->type_params as $i => $type_param) { - if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality)) { + if (!$type_param->equals($other_type->type_params[$i], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index dfddbe81334..3bd7e2a65e4 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -658,11 +658,11 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool } if ($this->fallback_params !== null && $other_type->fallback_params !== null) { - if (!$this->fallback_params[0]->equals($other_type->fallback_params[0])) { + if (!$this->fallback_params[0]->equals($other_type->fallback_params[0], false, false)) { return false; } - if (!$this->fallback_params[1]->equals($other_type->fallback_params[1])) { + if (!$this->fallback_params[1]->equals($other_type->fallback_params[1], false, false)) { return false; } } @@ -672,7 +672,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 13c44e5b453..7d102709919 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -205,7 +205,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$this->type_param->equals($other_type->type_param, $ensure_source_equality)) { + if (!$this->type_param->equals($other_type->type_param, $ensure_source_equality, false)) { return false; } diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index cae5be7e7f7..b681459bfaa 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -207,7 +207,7 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return false; } - if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality)) { + if (!$property_type->equals($other_type->properties[$property_name], $ensure_source_equality, false)) { return false; } } From 3c045b30a7a2aad2f2e9395ba179a0d136351391 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:05:15 +0100 Subject: [PATCH 09/22] fix false positive ArgumentTypeCoercion for callback param when unsealed and all optional --- .../Type/Comparator/ArrayTypeComparator.php | 13 +++++++++++++ tests/CallableTest.php | 16 ++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index c440526fea5..122bc65d70e 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -49,6 +49,19 @@ public static function isContainedBy( return true; } + if ($container_type_part instanceof TKeyedArray + && $input_type_part instanceof TArray + && !$container_type_part->is_list + && !$container_type_part->isNonEmpty() + && !$container_type_part->isSealed() + && $input_type_part->equals( + $container_type_part->getGenericArrayType($container_type_part->isNonEmpty()), + false, + ) + ) { + return true; + } + if ($container_type_part instanceof TKeyedArray && $input_type_part instanceof TArray ) { diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 23054bf9a6e..fc8c36d212f 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1892,6 +1892,22 @@ function addHandler(string $_message, callable $_handler): void {} return [1, 2, 3]; });', ], + 'unsealedAllOptionalCbParam' => [ + 'code' => ') $arg + * @return void + */ + function foo($arg) {} + + /** + * @param array{a?: string}&array $cb_arg + * @return void + */ + function bar($cb_arg) {} + + foo("bar");', + ], ]; } From 761f390d9bb4803d8cf35fb2934db389838b66d4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 12 Dec 2023 18:51:31 +0100 Subject: [PATCH 10/22] Use same parameter names in stubs --- stubs/CoreGenericClasses.phpstub | 18 +++++------ stubs/CoreGenericIterators.phpstub | 18 +++++------ stubs/SPL.phpstub | 52 +++++++++++++++--------------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 2b7b76da7e4..62e0ff33750 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -158,46 +158,46 @@ class ArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Count * Returns whether the requested index exists * @link http://php.net/manual/en/arrayobject.offsetexists.php * - * @param TKey $index The index being checked. + * @param TKey $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.0.0 */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** * Returns the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetget.php * - * @param TKey $index The index with the value. + * @param TKey $offset The index with the value. * @return TValue The value at the specified index or false. * * @since 5.0.0 */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** * Sets the value at the specified index to newval * @link http://php.net/manual/en/arrayobject.offsetset.php * - * @param TKey $index The index being set. - * @param TValue $newval The new value for the index. + * @param TKey $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.0.0 */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** * Unsets the value at the specified index * @link http://php.net/manual/en/arrayobject.offsetunset.php * - * @param TKey $index The index being unset. + * @param TKey $offset The index being unset. * @return void * * @since 5.0.0 */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * Appends the value diff --git a/stubs/CoreGenericIterators.phpstub b/stubs/CoreGenericIterators.phpstub index 43a7bb1f85c..48abad51dea 100644 --- a/stubs/CoreGenericIterators.phpstub +++ b/stubs/CoreGenericIterators.phpstub @@ -185,30 +185,30 @@ class ArrayIterator implements SeekableIterator, ArrayAccess, Serializable, Coun public function __construct($array = array(), $flags = 0) { } /** - * @param TKey $index The offset being checked. + * @param TKey $offset The offset being checked. * @return bool true if the offset exists, otherwise false */ - public function offsetExists($index) { } + public function offsetExists($offset) { } /** - * @param TKey $index The offset to get the value from. + * @param TKey $offset The offset to get the value from. * @return TValue|null The value at offset index, null when accessing invalid indexes * @psalm-ignore-nullable-return */ - public function offsetGet($index) { } + public function offsetGet($offset) { } /** - * @param TKey $index The index to set for. - * @param TValue $newval The new value to store at the index. + * @param TKey $offset The index to set for. + * @param TValue $value The new value to store at the index. * @return void */ - public function offsetSet($index, $newval) { } + public function offsetSet($offset, $value) { } /** - * @param TKey $index The offset to unset. + * @param TKey $offset The offset to unset. * @return void */ - public function offsetUnset($index) { } + public function offsetUnset($offset) { } /** * @param TValue $value The value to append. diff --git a/stubs/SPL.phpstub b/stubs/SPL.phpstub index 288ceba770a..7a623c933a0 100644 --- a/stubs/SPL.phpstub +++ b/stubs/SPL.phpstub @@ -15,14 +15,14 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa /** * Add/insert a new value at the specified index * - * @param int $index The index where the new value is to be inserted. - * @param TValue $newval The new value for the index. + * @param int $offset The index where the new value is to be inserted. + * @param TValue $value The new value for the index. * @return void * * @link https://php.net/spldoublylinkedlist.add * @since 5.5.0 */ - public function add($index, $newval) {} + public function add($offset, $value) {} /** * Pops a node from the end of the doubly linked list @@ -107,49 +107,49 @@ class SplDoublyLinkedList implements Iterator, Countable, ArrayAccess, Serializa public function isEmpty() {} /** - * Returns whether the requested $index exists + * Returns whether the requested $offset exists * @link https://php.net/manual/en/spldoublylinkedlist.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, otherwise false * * @since 5.3.0 */ - public function offsetExists($index) {} + public function offsetExists($offset) {} /** - * Returns the value at the specified $index + * Returns the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetget.php * - * @param int $index The index with the value. + * @param int $offset The index with the value. * @return TValue The value at the specified index. * * @since 5.3.0 */ - public function offsetGet($index) {} + public function offsetGet($offset) {} /** - * Sets the value at the specified $index to $newval + * Sets the value at the specified $offset to $value * @link https://php.net/manual/en/spldoublylinkedlist.offsetset.php * - * @param int $index The index being set. - * @param TValue $newval The new value for the index. + * @param int $offset The index being set. + * @param TValue $value The new value for the index. * @return void * * @since 5.3.0 */ - public function offsetSet($index, $newval) {} + public function offsetSet($offset, $value) {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/spldoublylinkedlist.offsetunset.php * - * @param int $index The index being unset. + * @param int $offset The index being unset. * @return void * * @since 5.3.0 */ - public function offsetUnset($index) {} + public function offsetUnset($offset) {} /** * Return current array entry @@ -297,46 +297,46 @@ class SplFixedArray implements Iterator, ArrayAccess, Countable { * Returns whether the specified index exists * @link https://php.net/manual/en/splfixedarray.offsetexists.php * - * @param int $index The index being checked. + * @param int $offset The index being checked. * @return bool true if the requested index exists, and false otherwise. * * @since 5.3.0 */ - public function offsetExists(int $index): bool {} + public function offsetExists(int $offset): bool {} /** * Sets a new value at a specified index * @link https://php.net/manual/en/splfixedarray.offsetset.php * - * @param int $index The index being sent. - * @param TValue $newval The new value for the index + * @param int $offset The index being sent. + * @param TValue $value The new value for the index * @return void * * @since 5.3.0 */ - public function offsetSet(int $index, $newval): void {} + public function offsetSet(int $offset, $value): void {} /** - * Unsets the value at the specified $index + * Unsets the value at the specified $offset * @link https://php.net/manual/en/splfixedarray.offsetunset.php * - * @param int $index The index being unset + * @param int $offset The index being unset * @return void * * @since 5.3.0 */ - public function offsetUnset(int $index): void {} + public function offsetUnset(int $offset): void {} /** * Returns the value at the specified index * @link https://php.net/manual/en/splfixedarray.offsetget.php * - * @param int $index The index with the value + * @param int $offset The index with the value * @return TValue The value at the specified index * * @since 5.3.0 */ - public function offsetGet(int $index) {} + public function offsetGet(int $offset) {} } From 82ff58228092cb15f6bd17adfba5aa607179a4a9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 12 Dec 2023 23:29:54 +0100 Subject: [PATCH 11/22] add error for invalid array key type in docblock --- src/Psalm/Internal/Type/TypeParser.php | 58 ++++++++++++++++++++++++++ tests/AnnotationTest.php | 10 ++++- tests/ArrayAssignmentTest.php | 9 ++-- tests/KeyOfArrayTest.php | 18 ++++---- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 770e7efc958..b498c9944e5 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -50,11 +50,13 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPropertiesOf; +use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateIndexedAccess; use Psalm\Type\Atomic\TTemplateKeyOf; use Psalm\Type\Atomic\TTemplateParam; @@ -643,6 +645,34 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TArray($generic_params, $from_docblock); } @@ -671,6 +701,34 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } + foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TInt + || $atomic_type instanceof TString + || $atomic_type instanceof TArrayKey + || $atomic_type instanceof TClassConstant // @todo resolve and check types + || $atomic_type instanceof TMixed + || $atomic_type instanceof TNever + || $atomic_type instanceof TTemplateParam + || $atomic_type instanceof TValueOf + ) { + continue; + } + + if ($codebase->register_stub_files || $codebase->register_autoload_files) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + + if (count($generic_params[0]->getAtomicTypes()) <= 1) { + $builder = $builder->addType(new TArrayKey($from_docblock)); + } + + $generic_params[0] = $builder->freeze(); + continue; + } + + throw new TypeParseTreeException('Invalid array key type ' . $atomic_type->getKey()); + } + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index b06bd217e7f..e0855c3bdc8 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1366,7 +1366,15 @@ public function barBar() { }', 'error_message' => 'MissingDocblockType', ], - + 'invalidArrayKeyType' => [ + 'code' => ' $arg + * @return void + */ + function foo($arg) {}', + 'error_message' => 'InvalidDocblock', + ], 'invalidClassMethodReturnBrackets' => [ 'code' => ' [ 'code' => ' 'InvalidArrayOffset', + 'error_message' => 'MixedArrayAccess', + 'ignored_issues' => ['InvalidDocblock'], ], 'unpackTypedIterableWithStringKeysIntoArray' => [ 'code' => ' [ 'code' => '|array> + * @return key-of|array> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asString) { + if ($asString) { + return "42"; } return 42; } @@ -194,14 +194,14 @@ public function getKey() { ', 'error_message' => 'InvalidReturnStatement', ], - 'noStringAllowedInKeyOfIntFloatArray' => [ + 'noStringAllowedInKeyOfIntFloatStringArray' => [ 'code' => '|array> + * @return key-of|array<"42.0", string>> */ - function getKey(bool $asFloat) { - if ($asFloat) { - return 42.0; + function getKey(bool $asInt) { + if ($asInt) { + return 42; } return "42"; } From 9be7fceb594eb1fdfe9886c39c54db48a815afca Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 00:17:43 +0100 Subject: [PATCH 12/22] Fix literal string keys int not handled as int as PHP does Fix https://github.com/vimeo/psalm/issues/8680 See also https://github.com/vimeo/psalm/issues/9295 --- src/Psalm/Internal/Type/TypeParser.php | 29 ++++++++++++++ tests/ArrayAssignmentTest.php | 23 +++++++++++ tests/ArrayKeysTest.php | 55 ++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index b498c9944e5..58718eae2dd 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -48,6 +48,7 @@ use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; +use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; @@ -85,6 +86,7 @@ use function defined; use function end; use function explode; +use function filter_var; use function get_class; use function in_array; use function is_int; @@ -98,6 +100,9 @@ use function strtolower; use function strtr; use function substr; +use function trim; + +use const FILTER_VALIDATE_INT; /** * @psalm-suppress InaccessibleProperty Allowed during construction @@ -646,6 +651,18 @@ private static function getTypeFromGenericTree( } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + if ($atomic_type instanceof TInt || $atomic_type instanceof TString || $atomic_type instanceof TArrayKey @@ -702,6 +719,18 @@ private static function getTypeFromGenericTree( } foreach ($generic_params[0]->getAtomicTypes() as $key => $atomic_type) { + // PHP 8 values with whitespace after number are counted as numeric + // and filter_var treats them as such too + if ($atomic_type instanceof TLiteralString + && trim($atomic_type->value) === $atomic_type->value + && ($string_to_int = filter_var($atomic_type->value, FILTER_VALIDATE_INT)) !== false + ) { + $builder = $generic_params[0]->getBuilder(); + $builder->removeType($key); + $generic_params[0] = $builder->addType(new TLiteralInt($string_to_int, $from_docblock))->freeze(); + continue; + } + if ($atomic_type instanceof TInt || $atomic_type instanceof TString || $atomic_type instanceof TArrayKey diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 769ebadc290..fa65da8223c 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -2115,6 +2115,29 @@ function getQueryParams(): array return $queryParams; }', ], + 'stringIntKeys' => [ + 'code' => ' $arg + * @return bool + */ + function foo($arg) { + foreach ($arg as $k => $v) { + if ( $k === 15 ) { + return true; + } + + if ( $k === 17 ) { + return false; + } + } + + return true; + } + + $x = ["15" => "a", 17 => "b"]; + foo($x);', + ], ]; } diff --git a/tests/ArrayKeysTest.php b/tests/ArrayKeysTest.php index 070ea52873b..e3f9cc897a4 100644 --- a/tests/ArrayKeysTest.php +++ b/tests/ArrayKeysTest.php @@ -97,6 +97,33 @@ function getKey() { } ', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + ], ]; } @@ -126,6 +153,34 @@ function getKeys() { ', 'error_message' => 'InvalidReturnStatement', ], + 'literalStringAsIntArrayKey' => [ + 'code' => ' [ + "from" => "79268724911", + "to" => "74950235931", + ], + "b" => [ + "from" => "79313044964", + "to" => "78124169167", + ], + ]; + + private const SIP_FORMAT = "sip:%s@voip.test.com:9090"; + + /** @return array */ + public function test(): array { + $redirects = []; + foreach (self::REDIRECTS as $redirect) { + $redirects[$redirect["from"]] = sprintf(self::SIP_FORMAT, $redirect["to"]); + } + + return $redirects; + } + }', + 'error_message' => 'InvalidReturnStatement', + ], ]; } } From 108f6267124377263ac1c99e5f22e105367b0842 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:59:26 +0100 Subject: [PATCH 13/22] fix literal int/string comparisons only using one literal Fix https://github.com/vimeo/psalm/issues/9552 --- .../Statements/Expression/AssertionFinder.php | 9 ++++++-- tests/TypeReconciliation/ConditionalTest.php | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index de4d2022aaa..740994506ed 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -555,8 +555,13 @@ private static function scrapeEqualityAssertions( $var_assertion_different = $var_type->getId() !== $intersection_type->getId(); + $all_assertions = []; + foreach ($intersection_type->getAtomicTypes() as $atomic_type) { + $all_assertions[] = new IsIdentical($atomic_type); + } + if ($var_name_left && $var_assertion_different) { - $if_types[$var_name_left] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_left] = [$all_assertions]; } $var_name_right = ExpressionIdentifier::getExtendedVarId( @@ -568,7 +573,7 @@ private static function scrapeEqualityAssertions( $other_assertion_different = $other_type->getId() !== $intersection_type->getId(); if ($var_name_right && $other_assertion_different) { - $if_types[$var_name_right] = [[new IsIdentical($intersection_type->getSingleAtomic())]]; + $if_types[$var_name_right] = [$all_assertions]; } return $if_types ? [$if_types] : []; diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index ca69a70a358..819d871eebb 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -580,6 +580,29 @@ function foo($a, string $b) : void { } }', ], + 'reconcileMultipleLiteralStrings' => [ + 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 14:10:22 +0100 Subject: [PATCH 14/22] Fix https://psalm.dev/r/aada187f50 where 2 union types are not intersected and the condition contains both types --- .../Statements/Expression/AssertionFinder.php | 2 +- tests/TypeReconciliation/ConditionalTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 740994506ed..4b81cb4adb9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -544,7 +544,7 @@ private static function scrapeEqualityAssertions( // both side of the Identical can be asserted to the intersection of both $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); - if ($intersection_type !== null && $intersection_type->isSingle()) { + if ($intersection_type !== null) { $if_types = []; $var_name_left = ExpressionIdentifier::getExtendedVarId( diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 819d871eebb..c8f3f9ab7bb 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -603,6 +603,21 @@ function foo($param, $param2) { } }', ], + 'reconcileMultipleUnionIntersection' => [ + 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 14:43:55 +0100 Subject: [PATCH 15/22] fix bug equality assertion with int and float setting wrong type - required so previous commit works --- .../Statements/Expression/AssertionFinder.php | 2 +- src/Psalm/Type.php | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 4b81cb4adb9..8c6aaa03ce3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -542,7 +542,7 @@ private static function scrapeEqualityAssertions( ); } else { // both side of the Identical can be asserted to the intersection of both - $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase); + $intersection_type = Type::intersectUnionTypes($var_type, $other_type, $codebase, false, false); if ($intersection_type !== null) { $if_types = []; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 6903c94094a..1215799f785 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -712,7 +712,9 @@ public static function combineUnionTypes( public static function intersectUnionTypes( ?Union $type_1, ?Union $type_2, - Codebase $codebase + Codebase $codebase, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Union { if ($type_2 === null && $type_1 === null) { throw new UnexpectedValueException('At least one type must be provided to combine'); @@ -766,6 +768,8 @@ public static function intersectUnionTypes( $type_2_atomic, $codebase, $intersection_performed, + $allow_interface_equality, + $allow_float_int_equality, ); if (null !== $intersection_atomic) { @@ -838,7 +842,9 @@ private static function intersectAtomicTypes( Atomic $type_1_atomic, Atomic $type_2_atomic, Codebase $codebase, - bool &$intersection_performed + bool &$intersection_performed, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true ): ?Atomic { $intersection_atomic = null; $wider_type = null; @@ -884,6 +890,8 @@ private static function intersectAtomicTypes( $codebase, $type_2_atomic, $type_1_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_2_atomic; $wider_type = $type_1_atomic; @@ -892,6 +900,8 @@ private static function intersectAtomicTypes( $codebase, $type_1_atomic, $type_2_atomic, + $allow_interface_equality, + $allow_float_int_equality, )) { $intersection_atomic = $type_1_atomic; $wider_type = $type_2_atomic; From af3978281edc7e4feec1205b72fb31cbf725d27c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:05:48 +0100 Subject: [PATCH 16/22] remove previously broken test https://github.com/vimeo/psalm/issues/10487 --- tests/TypeReconciliation/ValueTest.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index 10df002174c..527b4f6f76f 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -272,17 +272,6 @@ function foo($f) : void { if ($s === "a") {} }', ], - 'moreValueReconciliation' => [ - 'code' => ' [ 'code' => ' Date: Wed, 13 Dec 2023 15:10:15 +0100 Subject: [PATCH 17/22] add missing phpdoc in new tests --- tests/TypeReconciliation/ConditionalTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index c8f3f9ab7bb..99650002db3 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -608,6 +608,7 @@ function foo($param, $param2) { /** * @param int|string $param * @param float|string $param2 + * @return void */ function foo($param, $param2) { if ($param === $param2) { @@ -616,7 +617,7 @@ function foo($param, $param2) { } } - function takesString(string $arg) {}', + function takesString(string $arg): void {}', ], 'reconcileNullableStringWithWeakEquality' => [ 'code' => ' Date: Wed, 13 Dec 2023 15:30:43 +0100 Subject: [PATCH 18/22] Fix https://github.com/vimeo/psalm/issues/9267 --- .../Statements/Expression/Call/ArgumentAnalyzer.php | 2 +- tests/FunctionCallTest.php | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 706534fbbe5..a1df71add81 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -902,7 +902,7 @@ public static function verifyType( $input_type, $param_type, true, - true, + !isset($param_type->getAtomicTypes()['true']), $union_comparison_results, ); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index f712af9dacd..2b73d655018 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -2358,6 +2358,17 @@ function fooFoo(int $a): void {} fooFoo("string");', 'error_message' => 'InvalidArgument', ], + 'invalidArgumentFalseTrueExpected' => [ + 'code' => ' 'InvalidArgument', + ], 'builtinFunctioninvalidArgumentWithWeakTypes' => [ 'code' => ' Date: Thu, 14 Dec 2023 09:44:28 +0300 Subject: [PATCH 19/22] Fix Uncaught RuntimeException: PHP Error: Uninitialized string offset 0 when $pattern is empty --- .../Call/FunctionCallReturnTypeFetcher.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index e15fd22a71d..8695053c6f8 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -636,17 +636,19 @@ private static function taintReturnType( $first_arg_value = $first_stmt_type->getSingleStringLiteral()->value; $pattern = substr($first_arg_value, 1, -1); + if (strlen(trim($pattern)) > 0) { + $pattern = trim($pattern); + if ($pattern[0] === '[' + && $pattern[1] === '^' + && substr($pattern, -1) === ']' + ) { + $pattern = substr($pattern, 2, -1); - if ($pattern[0] === '[' - && $pattern[1] === '^' - && substr($pattern, -1) === ']' - ) { - $pattern = substr($pattern, 2, -1); - - if (self::simpleExclusion($pattern, $first_arg_value[0])) { - $removed_taints[] = 'html'; - $removed_taints[] = 'has_quotes'; - $removed_taints[] = 'sql'; + if (self::simpleExclusion($pattern, $first_arg_value[0])) { + $removed_taints[] = 'html'; + $removed_taints[] = 'has_quotes'; + $removed_taints[] = 'sql'; + } } } } From c8748dc5c976cc52940b93b6c3689869c1363978 Mon Sep 17 00:00:00 2001 From: mu3ic Date: Thu, 14 Dec 2023 09:54:32 +0300 Subject: [PATCH 20/22] Add trim() in global use --- .../Statements/Expression/Call/FunctionCallReturnTypeFetcher.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index 8695053c6f8..55557576e6b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -51,6 +51,7 @@ use function strpos; use function strtolower; use function substr; +use function trim; /** * @internal From d6cf9faebbfc0c21710b99eae01e9d4188aae76e Mon Sep 17 00:00:00 2001 From: Antonio del Olmo Date: Fri, 15 Dec 2023 11:14:53 +0100 Subject: [PATCH 21/22] Add support for Override attribute --- stubs/CoreGenericAttributes.phpstub | 6 ++++++ tests/AttributeTest.php | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/stubs/CoreGenericAttributes.phpstub b/stubs/CoreGenericAttributes.phpstub index 7aa6400df06..92abe9542f8 100644 --- a/stubs/CoreGenericAttributes.phpstub +++ b/stubs/CoreGenericAttributes.phpstub @@ -6,6 +6,12 @@ final class AllowDynamicProperties public function __construct() {} } +#[Attribute(Attribute::TARGET_METHOD)] +final class Override +{ + public function __construct() {} +} + #[Attribute(Attribute::TARGET_PARAMETER)] final class SensitiveParameter { diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index f1051773882..ddb5b1f5fd9 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -293,6 +293,22 @@ class Foo 'ignored_issues' => [], 'php_version' => '8.2', ], + 'override' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], 'sensitiveParameter' => [ 'code' => ' Date: Sun, 17 Dec 2023 16:06:03 +0100 Subject: [PATCH 22/22] strtok always returns a non-empty-string when it does not return false --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 5f3dba9c880..853b78ed8a0 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -12923,8 +12923,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'?int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], -'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], -'strtok\'1' => ['string|false', 'string'=>'string'], +'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], +'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'?int'], 'strtoupper' => ['string', 'string'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 864b668bd0b..60e798e09cb 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14333,8 +14333,8 @@ 'strrpos' => ['int|false', 'haystack'=>'string', 'needle'=>'string|int', 'offset='=>'int'], 'strspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'], 'strstr' => ['string|false', 'haystack'=>'string', 'needle'=>'string|int', 'before_needle='=>'bool'], - 'strtok' => ['string|false', 'string'=>'string', 'token'=>'string'], - 'strtok\'1' => ['string|false', 'string'=>'string'], + 'strtok' => ['non-empty-string|false', 'string'=>'string', 'token'=>'string'], + 'strtok\'1' => ['non-empty-string|false', 'string'=>'string'], 'strtolower' => ['lowercase-string', 'string'=>'string'], 'strtotime' => ['int|false', 'datetime'=>'string', 'baseTimestamp='=>'int'], 'strtoupper' => ['string', 'string'=>'string'],