From d57c246c9309df7c70e1f8d03f7463bda592a420 Mon Sep 17 00:00:00 2001 From: ralila <> Date: Mon, 29 Nov 2021 14:21:58 +0100 Subject: [PATCH 1/8] Add stub for preg_match --- stubs/CoreGenericFunctions.phpstub | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index ea26ca5c8..ca73f6857 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -963,6 +963,24 @@ function preg_replace_callback($pattern, $callback, $subject, int $limit = -1, & */ function preg_match_all($pattern, $subject, &$matches = [], int $flags = 1, int $offset = 0) {} +/** + * @psalm-pure + * @template TFlags as int-mask<0, 256, 512> + * + * @param string $pattern + * @param string $subject + * @param mixed $matches + * @param TFlags $flags + * @param-out (TFlags is 256 ? array : + * TFlags is 512 ? array : + * TFlags is 768 ? array : + * array + * ) $matches + * @return 1|0|false + * @psalm-ignore-falsable-return + */ +function preg_match($pattern, $subject, &$matches = [], int $flags = 0, int $offset = 0) {} + /** * @psalm-pure * From 15eb8127ee72f26895e752c508a420f6431875e7 Mon Sep 17 00:00:00 2001 From: ralila <> Date: Mon, 29 Nov 2021 14:23:33 +0100 Subject: [PATCH 2/8] Reverse parameter list vor preg_match --- .../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 4792a5458..52b8ae94f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -635,7 +635,7 @@ class ArgumentsAnalyzer } } - if ($method_id === 'preg_match_all' && count($args) > 3) { + if (($method_id === 'preg_match_all' || $method_id === 'preg_match') && count($args) > 3) { $args = array_reverse($args, true); } From 2c94ce01855c1cf6a0d8dc38a979d578af38bb97 Mon Sep 17 00:00:00 2001 From: ralila Date: Mon, 29 Nov 2021 14:23:57 +0100 Subject: [PATCH 3/8] Adjust callmaps --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 11c010ef7..bf61327c5 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -10273,7 +10273,7 @@ return [ 'preg_filter' => ['null|string|string[]', 'pattern'=>'mixed', 'replacement'=>'mixed', 'subject'=>'mixed', 'limit='=>'int', '&w_count='=>'int'], 'preg_grep' => ['array|false', 'pattern'=>'string', 'array'=>'array', 'flags='=>'int'], 'preg_last_error' => ['int'], -'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'string[]', 'flags='=>'0|', 'offset='=>'int'], +'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'string[]', 'flags='=>'0', 'offset='=>'int'], 'preg_match\'1' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_match_all' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_quote' => ['string', 'str'=>'string', 'delimiter='=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 18f614ade..3e61d63f3 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14454,7 +14454,7 @@ return [ 'preg_filter' => ['null|string|string[]', 'pattern'=>'mixed', 'replacement'=>'mixed', 'subject'=>'mixed', 'limit='=>'int', '&w_count='=>'int'], 'preg_grep' => ['array|false', 'pattern'=>'string', 'array'=>'array', 'flags='=>'int'], 'preg_last_error' => ['int'], - 'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'string[]', 'flags='=>'0|', 'offset='=>'int'], + 'preg_match' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'string[]', 'flags='=>'0', 'offset='=>'int'], 'preg_match\'1' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_match_all' => ['int|false', 'pattern'=>'string', 'subject'=>'string', '&w_matches='=>'array', 'flags='=>'int', 'offset='=>'int'], 'preg_quote' => ['string', 'str'=>'string', 'delimiter='=>'string'], From e5c1a35e31308529d904b762555d3cce8306fa5c Mon Sep 17 00:00:00 2001 From: ralila <> Date: Mon, 29 Nov 2021 14:24:15 +0100 Subject: [PATCH 4/8] Adjust tests --- tests/FunctionCallTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index a70bd3aea..518a15dba 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1305,10 +1305,6 @@ class FunctionCallTest extends TestCase function takesInt(int $i) : void {} if (preg_match("{foo}", "this is foo", $matches, PREG_OFFSET_CAPTURE)) { - /** - * @psalm-suppress MixedArrayAccess - * @psalm-suppress MixedArgument - */ takesInt($matches[0][1]); }', ], @@ -1449,7 +1445,10 @@ class FunctionCallTest extends TestCase ], 'writeArgsAllowed' => [ ' Date: Tue, 30 Nov 2021 03:19:27 +0100 Subject: [PATCH 5/8] Add some tests --- tests/FunctionCallTest.php | 47 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 518a15dba..091261250 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1282,6 +1282,13 @@ class FunctionCallTest extends TestCase takesInt(preg_match("{foo}", "foo"));', ], + 'pregMatch2' => [ + ' [ + '$r===' => '0|1|false', + ], + ], 'pregMatchWithMatches' => [ ' [ + ' [ + '$r===' => '0|1|false', + '$matches===' => 'array', + ], + ], 'pregMatchWithOffset' => [ ' [ + ' [ + '$r===' => '0|1|false', + '$matches===' => 'array', + ], + ], 'pregMatchWithFlags' => [ ' [ + ' [ + '$r===' => '0|1|false', + '$matches===' => 'array', + ], + ], + 'pregMatchWithFlagUnMatchedAsNull' => [ + ' [ + '$r===' => '0|1|false', + '$matches===' => 'array', + ], + ], + 'pregMatchWithFlagOffsetCaptureAndUnMatchedAsNull' => [ + ' [ + '$r===' => '0|1|false', + '$matches===' => 'array', + ], + ], 'pregReplaceCallback' => [ ' Date: Tue, 30 Nov 2021 03:25:39 +0100 Subject: [PATCH 6/8] Allow skipping tests for PHP <= 7.2 --- tests/Traits/ValidCodeAnalysisTestTrait.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/Traits/ValidCodeAnalysisTestTrait.php b/tests/Traits/ValidCodeAnalysisTestTrait.php index df70554e8..2840eb509 100644 --- a/tests/Traits/ValidCodeAnalysisTestTrait.php +++ b/tests/Traits/ValidCodeAnalysisTestTrait.php @@ -35,14 +35,18 @@ trait ValidCodeAnalysisTestTrait string $php_version = '7.3' ): void { $test_name = $this->getTestName(); - if (strpos($test_name, 'PHP73-') !== false) { - if (version_compare(PHP_VERSION, '7.3.0', '<')) { - $this->markTestSkipped('Test case requires PHP 7.3.'); - } - } elseif (strpos($test_name, 'PHP71-') !== false) { + if (strpos($test_name, 'PHP71-') !== false) { if (version_compare(PHP_VERSION, '7.1.0', '<')) { $this->markTestSkipped('Test case requires PHP 7.1.'); } + } elseif (strpos($test_name, 'PHP72-') !== false) { + if (version_compare(PHP_VERSION, '7.2.0', '<')) { + $this->markTestSkipped('Test case requires PHP 7.2.'); + } + } elseif (strpos($test_name, 'PHP73-') !== false) { + if (version_compare(PHP_VERSION, '7.3.0', '<')) { + $this->markTestSkipped('Test case requires PHP 7.3.'); + } } elseif (strpos($test_name, 'PHP80-') !== false) { if (version_compare(PHP_VERSION, '8.0.0', '<')) { $this->markTestSkipped('Test case requires PHP 8.0.'); From 9a7d0809d2e69a3bd0fe6dbc37700e88d33524b8 Mon Sep 17 00:00:00 2001 From: ralila <> Date: Tue, 30 Nov 2021 03:28:38 +0100 Subject: [PATCH 7/8] Restrict tests with PREG_UNMATCHED_AS_NULL to PHP >= 7.2 --- tests/FunctionCallTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 091261250..19b40baf1 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1339,7 +1339,7 @@ class FunctionCallTest extends TestCase '$matches===' => 'array', ], ], - 'pregMatchWithFlagUnMatchedAsNull' => [ + 'PHP72-pregMatchWithFlagUnmatchedAsNull' => [ ' [ @@ -1347,9 +1347,9 @@ class FunctionCallTest extends TestCase '$matches===' => 'array', ], ], - 'pregMatchWithFlagOffsetCaptureAndUnMatchedAsNull' => [ + 'PHP72-pregMatchWithFlagOffsetCaptureAndUnmatchedAsNull' => [ ' [ '$r===' => '0|1|false', '$matches===' => 'array', From 98b0b052ce365247cb0664f117ce12117dbc2b50 Mon Sep 17 00:00:00 2001 From: ralila <> Date: Tue, 30 Nov 2021 05:08:56 +0100 Subject: [PATCH 8/8] Remove some now superfluous casts --- src/Psalm/CodeLocation.php | 6 +++--- .../PhpVisitor/Reflector/ClassLikeDocblockParser.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php index e41304441..17f56cd1c 100644 --- a/src/Psalm/CodeLocation.php +++ b/src/Psalm/CodeLocation.php @@ -259,14 +259,14 @@ class CodeLocation } if (preg_match($regex, $preview_snippet, $matches, PREG_OFFSET_CAPTURE)) { - if (!isset($matches[1]) || (int)$matches[1][1] === -1) { + if (!isset($matches[1]) || $matches[1][1] === -1) { throw new \LogicException( "Failed to match anything to 1st capturing group, " . "or regex doesn't contain 1st capturing group, regex type " . $this->regex_type ); } - $this->selection_start = $this->selection_start + (int)$matches[1][1]; - $this->selection_end = $this->selection_start + strlen((string)$matches[1][0]); + $this->selection_start = $this->selection_start + $matches[1][1]; + $this->selection_end = $this->selection_start + strlen($matches[1][0]); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 8c62963a2..d50ad8dca 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -325,7 +325,7 @@ class ClassLikeDocblockParser $end_of_method_regex = '/(?