From 4875f9f582aedfe99b827eda3dd049ea0c69560e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 11 Jul 2024 17:16:31 +0200 Subject: [PATCH 1/5] Trigger error on unsafe StrictGroups usages --- composer.json | 2 +- .../PregMatchTypeSpecifyingExtension.php | 14 +++----- tests/PHPStanTests/nsrt/preg-match.php | 32 +++++++++++++++---- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index f81a2e4..c7d1bc3 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ }, "require-dev": { "phpunit/phpunit": "^8 || ^9", - "phpstan/phpstan": "^1.11.6", + "phpstan/phpstan": "^1.11.6@dev", "phpstan/phpstan-strict-rules": "^1.1" }, "conflict": { diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php index af7700b..021a975 100644 --- a/src/PHPStan/PregMatchTypeSpecifyingExtension.php +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -78,15 +78,11 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod && count($matchedType->getConstantArrays()) === 1 ) { $matchedType = $matchedType->getConstantArrays()[0]; - $matchedType = new ConstantArrayType( - $matchedType->getKeyTypes(), - array_map(static function (Type $valueType): Type { - return TypeCombinator::removeNull($valueType); - }, $matchedType->getValueTypes()), - $matchedType->getNextAutoIndexes(), - [], - $matchedType->isList() - ); + foreach ($matchedType as $type) { + if ($type->containsNull()) { + // TODO trigger an error here that $methodReflection->getName() should not be used, or the pattern needs to make sure that $type->?? get key name is not nullable + } + } } $overwrite = false; diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 7ffb2c2..34a447b 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -15,25 +15,33 @@ function doMatch(string $s): void assertType('array{}|array{string}', $matches); if (Preg::match('/Price: (£|€)\d+/', $s, $matches)) { - assertType('array{string, string|null}', $matches); + assertType('array{string, string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{string, string|null}', $matches); + assertType('array{}|array{string, string}', $matches); if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) { - assertType('array{0: string, 1?: string|null}', $matches); + assertType('array{0: string, 1: string|null}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, 1?: string|null}', $matches); + assertType('array{}|array{0: string, 1: string|null}', $matches); + + // passing the PREG_UNMATCHED_AS_NULL should change nothing compared to above as it is always set + if (Preg::match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{0: string, 1: string|null}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{0: string, 1: string|null}', $matches); if (Preg::isMatch('/Price: (?£|€)\d+/', $s, $matches)) { - assertType('array{0: string, currency: string|null, 1: string|null}', $matches); + assertType('array{0: string, currency: string, 1: string}', $matches); } else { assertType('array{}', $matches); } - assertType('array{}|array{0: string, currency: string|null, 1: string|null}', $matches); + assertType('array{}|array{0: string, currency: string, 1: string}', $matches); } function doMatchStrictGroups(string $s): void @@ -60,6 +68,18 @@ function doMatchStrictGroups(string $s): void assertType('array{}|array{0: string, test: string, 1: string}', $matches); } +function doMatchStrictGroupsUnsafe(string $s): void +{ + if (Preg::isMatchStrictGroups('{Configure Command(?: *| *=> *)(.*)(?:|$)}m', $s, $matches)) { + // does not error because the match group might be empty but is not optional + assertType('array{string, string}', $matches); + } + + if (Preg::isMatchStrictGroups('{Configure Command(?: *| *=> *)(.*)?(?:|$)}m', $s, $matches)) { + // should error as it is unsafe due to the optional group + } +} + // disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved // //function identicalMatch(string $s): void From eca48c76f85735dae9acffb0e3bdf3aea9d0789a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 12 Jul 2024 10:21:48 +0200 Subject: [PATCH 2/5] Implement new rule to warn about unsafe strict groups calls --- extension.neon | 5 + .../PregMatchTypeSpecifyingExtension.php | 14 ++- src/PHPStan/UnsafeStrictGroupsCallRule.php | 113 ++++++++++++++++++ src/Regex.php | 2 + tests/PHPStanTests/TypeInferenceTest.php | 2 - .../UnsafeStrictGroupsCallRuleTest.php | 59 +++++++++ tests/PHPStanTests/nsrt/preg-match.php | 12 +- tests/PregTests/MatchAllTest.php | 7 +- tests/PregTests/MatchTest.php | 7 +- tests/RegexTests/MatchTest.php | 7 +- 10 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 src/PHPStan/UnsafeStrictGroupsCallRule.php create mode 100644 tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php diff --git a/extension.neon b/extension.neon index 282b8d4..2147431 100644 --- a/extension.neon +++ b/extension.neon @@ -8,9 +8,14 @@ conditionalTags: phpstan.staticMethodParameterOutTypeExtension: %featureToggles.narrowPregMatches% Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension: phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension: %featureToggles.narrowPregMatches% + Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule: + phpstan.rules.rule: %featureToggles.narrowPregMatches% services: - class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension - class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension + +rules: + - Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php index 021a975..af7700b 100644 --- a/src/PHPStan/PregMatchTypeSpecifyingExtension.php +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -78,11 +78,15 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod && count($matchedType->getConstantArrays()) === 1 ) { $matchedType = $matchedType->getConstantArrays()[0]; - foreach ($matchedType as $type) { - if ($type->containsNull()) { - // TODO trigger an error here that $methodReflection->getName() should not be used, or the pattern needs to make sure that $type->?? get key name is not nullable - } - } + $matchedType = new ConstantArrayType( + $matchedType->getKeyTypes(), + array_map(static function (Type $valueType): Type { + return TypeCombinator::removeNull($valueType); + }, $matchedType->getValueTypes()), + $matchedType->getNextAutoIndexes(), + [], + $matchedType->isList() + ); } $overwrite = false; diff --git a/src/PHPStan/UnsafeStrictGroupsCallRule.php b/src/PHPStan/UnsafeStrictGroupsCallRule.php new file mode 100644 index 0000000..4ac3888 --- /dev/null +++ b/src/PHPStan/UnsafeStrictGroupsCallRule.php @@ -0,0 +1,113 @@ + + */ +final class UnsafeStrictGroupsCallRule implements Rule +{ + /** + * @var RegexArrayShapeMatcher + */ + private $regexShapeMatcher; + + public function __construct(RegexArrayShapeMatcher $regexShapeMatcher) + { + $this->regexShapeMatcher = $regexShapeMatcher; + } + + public function getNodeType(): string + { + return StaticCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!$node->class instanceof FullyQualified) { + return []; + } + $isRegex = $node->class->toString() === Regex::class; + $isPreg = $node->class->toString() === Preg::class; + if (!$isRegex && !$isPreg) { + return []; + } + if (!$node->name instanceof Node\Identifier || !in_array($node->name->name, ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true)) { + return []; + } + + $args = $node->getArgs(); + if (!isset($args[0])) { + return []; + } + + $patternArg = $args[0] ?? null; + if ($isPreg) { + if (!isset($args[2])) { // no matches set, skip as the matches won't be used anyway + return []; + } + $flagsArg = $args[3] ?? null; + } else { + $flagsArg = $args[2] ?? null; + } + + if ($patternArg === null) { + return []; + } + + $flagsType = PregMatchFlags::getType($flagsArg, $scope); + if ($flagsType === null) { + return []; + } + $patternType = $scope->getType($patternArg->value); + + $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean(true)); + if ($matchedType === null) { + return [ + RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name)) + ->identifier('composerPcre.maybeUnsafeStrictGroups') + ->build(), + ]; + } + + if (count($matchedType->getConstantArrays()) === 1) { + $matchedType = $matchedType->getConstantArrays()[0]; + $nullableGroups = []; + foreach ($matchedType->getValueTypes() as $index => $type) { + if (TypeCombinator::containsNull($type)) { + $nullableGroups[] = $matchedType->getKeyTypes()[$index]->getValue(); + } + } + + if (\count($nullableGroups) > 0) { + return [ + RuleErrorBuilder::message(sprintf( + 'The %s call is unsafe as match group%s "%s" %s optional and may be null.', + $node->name->name, + \count($nullableGroups) > 1 ? 's' : '', + implode('", "', $nullableGroups), + \count($nullableGroups) > 1 ? 'are' : 'is' + ))->identifier('composerPcre.unsafeStrictGroups')->build(), + ]; + } + } + + return []; + } +} diff --git a/src/Regex.php b/src/Regex.php index 8c4158a..ed61f86 100644 --- a/src/Regex.php +++ b/src/Regex.php @@ -43,6 +43,7 @@ public static function match(string $pattern, string $subject, int $flags = 0, i */ public static function matchStrictGroups(string $pattern, string $subject, int $flags = 0, int $offset = 0): MatchStrictGroupsResult { + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups $count = Preg::matchStrictGroups($pattern, $subject, $matches, $flags, $offset); return new MatchStrictGroupsResult($count, $matches); @@ -87,6 +88,7 @@ public static function matchAllStrictGroups(string $pattern, string $subject, in self::checkOffsetCapture($flags, 'matchAllWithOffsets'); self::checkSetOrder($flags); + // @phpstan-ignore composerPcre.maybeUnsafeStrictGroups $count = Preg::matchAllStrictGroups($pattern, $subject, $matches, $flags, $offset); return new MatchAllStrictGroupsResult($count, $matches); diff --git a/tests/PHPStanTests/TypeInferenceTest.php b/tests/PHPStanTests/TypeInferenceTest.php index b669f0a..04ee357 100644 --- a/tests/PHPStanTests/TypeInferenceTest.php +++ b/tests/PHPStanTests/TypeInferenceTest.php @@ -17,8 +17,6 @@ * Run with "vendor/bin/phpunit --testsuite phpstan" * * This is excluded by default to avoid side effects with the library tests - * - * @group phpstan */ class TypeInferenceTest extends TypeInferenceTestCase { diff --git a/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php b/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php new file mode 100644 index 0000000..2b5e23e --- /dev/null +++ b/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Composer\Pcre\PHPStanTests; + +use PHPStan\Testing\RuleTestCase; +use Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule; +use PHPStan\Type\Php\RegexArrayShapeMatcher; +use PHPStan\Php\PhpVersion; + +/** + * Run with "vendor/bin/phpunit --testsuite phpstan" + * + * This is excluded by default to avoid side effects with the library tests + * + * @extends RuleTestCase + */ +class UnsafeStrictGruopsCallRuleTest extends RuleTestCase +{ + protected function getRule(): \PHPStan\Rules\Rule + { + // @phpstan-ignore phpstanApi.constructor,phpstanApi.constructor + return new UnsafeStrictGroupsCallRule(new RegexArrayShapeMatcher(new PhpVersion(PHP_VERSION_ID, PhpVersion::SOURCE_RUNTIME))); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/nsrt/preg-match.php'], [ + [ + 'The matchStrictGroups call is unsafe as match group "1" is optional and may be null.', + 80, + ], + [ + 'The matchAllStrictGroups call is unsafe as match groups "foo", "2" are optional and may be null.', + 82, + ], + [ + 'The isMatchStrictGroups call is potentially unsafe as $matches\' type could not be inferred.', + 86, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + 'phar://' . __DIR__ . '/../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon', + __DIR__ . '/../../extension.neon', + ]; + } +} diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 34a447b..f9480f8 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -3,6 +3,7 @@ namespace PregMatchShapes; use Composer\Pcre\Preg; +use Composer\Pcre\Regex; use function PHPStan\Testing\assertType; function doMatch(string $s): void @@ -75,8 +76,15 @@ function doMatchStrictGroupsUnsafe(string $s): void assertType('array{string, string}', $matches); } - if (Preg::isMatchStrictGroups('{Configure Command(?: *| *=> *)(.*)?(?:|$)}m', $s, $matches)) { - // should error as it is unsafe due to the optional group + // should error as it is unsafe due to the optional group 1 + Regex::matchStrictGroups('{Configure Command(?: *| *=> *)(.*)?(?:|$)}m', $s); + + if (Preg::matchAllStrictGroups('{((?.)?z)}m', $s, $matches)) { + // should error as it is unsafe due to the optional group foo/2 + } + + if (Preg::isMatchStrictGroups('{'.$s.'}', $s, $matches)) { + // should error as it is unsafe due not being introspectable with the dynamic string } } diff --git a/tests/PregTests/MatchAllTest.php b/tests/PregTests/MatchAllTest.php index 862d234..dada88d 100644 --- a/tests/PregTests/MatchAllTest.php +++ b/tests/PregTests/MatchAllTest.php @@ -44,7 +44,7 @@ public function testFailure(): void public function testSuccessStrictGroups(): void { - $count = Preg::matchAllStrictGroups('{(?P\d)(?a)?}', '3a', $matches); + $count = Preg::matchAllStrictGroups('{(?\d)(?a)}', '3a', $matches); self::assertSame(1, $count); self::assertSame(array(0 => ['3a'], 'm' => ['3'], 1 => ['3'], 'matched' => ['a'], 2 => ['a']), $matches); } @@ -52,8 +52,9 @@ public function testSuccessStrictGroups(): void public function testFailStrictGroups(): void { self::expectException(UnexpectedNullMatchException::class); - self::expectExceptionMessage('Pattern "{(?P\d)(?b)?}" had an unexpected unmatched group "unmatched", make sure the pattern always matches or use matchAll() instead.'); - Preg::matchAllStrictGroups('{(?P\d)(?b)?}', '123', $matches); + self::expectExceptionMessage('Pattern "{(?\d)(?b)?}" had an unexpected unmatched group "unmatched", make sure the pattern always matches or use matchAll() instead.'); + // @phpstan-ignore composerPcre.unsafeStrictGroups + Preg::matchAllStrictGroups('{(?\d)(?b)?}', '123', $matches); } public function testBadPatternThrowsIfWarningsAreNotThrowing(): void diff --git a/tests/PregTests/MatchTest.php b/tests/PregTests/MatchTest.php index 4038405..ae993d9 100644 --- a/tests/PregTests/MatchTest.php +++ b/tests/PregTests/MatchTest.php @@ -38,7 +38,7 @@ public function testSuccessWithInt(): void public function testSuccessStrictGroups(): void { - $count = Preg::matchStrictGroups('{(?P\d)(?a)?}', '3a', $matches); + $count = Preg::matchStrictGroups('{(?\d)(?a)}', '3a', $matches); self::assertSame(1, $count); self::assertSame(array(0 => '3a', 'm' => '3', 1 => '3', 'matched' => 'a', 2 => 'a'), $matches); } @@ -46,8 +46,9 @@ public function testSuccessStrictGroups(): void public function testFailStrictGroups(): void { self::expectException(UnexpectedNullMatchException::class); - self::expectExceptionMessage('Pattern "{(?P\d)(?b)?}" had an unexpected unmatched group "unmatched", make sure the pattern always matches or use match() instead.'); - Preg::matchStrictGroups('{(?P\d)(?b)?}', '123', $matches); + self::expectExceptionMessage('Pattern "{(?\d)(?b)?}" had an unexpected unmatched group "unmatched", make sure the pattern always matches or use match() instead.'); + // @phpstan-ignore composerPcre.unsafeStrictGroups + Preg::matchStrictGroups('{(?\d)(?b)?}', '123', $matches); } public function testTypeErrorWithNull(): void diff --git a/tests/RegexTests/MatchTest.php b/tests/RegexTests/MatchTest.php index 519e935..1dcdffe 100644 --- a/tests/RegexTests/MatchTest.php +++ b/tests/RegexTests/MatchTest.php @@ -40,15 +40,16 @@ public function testFailure(): void public function testSuccessStrictGroups(): void { - $result = Regex::matchStrictGroups('{(?P\d)(?a)?}', '3a'); + $result = Regex::matchStrictGroups('{(?\d)(?a)}', '3a'); self::assertSame(array(0 => '3a', 'm' => '3', 1 => '3', 'matched' => 'a', 2 => 'a'), $result->matches); } public function testFailStrictGroups(): void { self::expectException(UnexpectedNullMatchException::class); - self::expectExceptionMessage('Pattern "{(?P\d)(?b)?}" had an unexpected unmatched group "unmatched", make sure the pattern always matches or use match() instead.'); - Regex::matchStrictGroups('{(?P\d)(?b)?}', '123'); + self::expectExceptionMessage('Pattern "{(?\d)(?b)?}" had an unexpected unmatched group "unmatched", make sure the pattern always matches or use match() instead.'); + // @phpstan-ignore composerPcre.unsafeStrictGroups + Regex::matchStrictGroups('{(?\d)(?b)?}', '123'); } public function testBadPatternThrowsIfWarningsAreNotThrowing(): void From 7de896254aeaa0bba49eb0d268ad5edf1eb35e36 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 12 Jul 2024 11:32:43 +0200 Subject: [PATCH 3/5] Apply feedback --- src/PHPStan/UnsafeStrictGroupsCallRule.php | 2 +- .../UnsafeStrictGroupsCallRuleTest.php | 4 +-- tests/PHPStanTests/nsrt/preg-match.php | 28 +++++++++---------- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/PHPStan/UnsafeStrictGroupsCallRule.php b/src/PHPStan/UnsafeStrictGroupsCallRule.php index 4ac3888..5a1c492 100644 --- a/src/PHPStan/UnsafeStrictGroupsCallRule.php +++ b/src/PHPStan/UnsafeStrictGroupsCallRule.php @@ -77,7 +77,7 @@ public function processNode(Node $node, Scope $scope): array } $patternType = $scope->getType($patternArg->value); - $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean(true)); + $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createYes()); if ($matchedType === null) { return [ RuleErrorBuilder::message(sprintf('The %s call is potentially unsafe as $matches\' type could not be inferred.', $node->name->name)) diff --git a/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php b/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php index 2b5e23e..74eb9e0 100644 --- a/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php +++ b/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php @@ -14,7 +14,6 @@ use PHPStan\Testing\RuleTestCase; use Composer\Pcre\PHPStan\UnsafeStrictGroupsCallRule; use PHPStan\Type\Php\RegexArrayShapeMatcher; -use PHPStan\Php\PhpVersion; /** * Run with "vendor/bin/phpunit --testsuite phpstan" @@ -27,8 +26,7 @@ class UnsafeStrictGruopsCallRuleTest extends RuleTestCase { protected function getRule(): \PHPStan\Rules\Rule { - // @phpstan-ignore phpstanApi.constructor,phpstanApi.constructor - return new UnsafeStrictGroupsCallRule(new RegexArrayShapeMatcher(new PhpVersion(PHP_VERSION_ID, PhpVersion::SOURCE_RUNTIME))); + return new UnsafeStrictGroupsCallRule(self::getContainer()->getByType(RegexArrayShapeMatcher::class)); } public function testRule(): void diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index f9480f8..8a4458e 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -22,20 +22,20 @@ function doMatch(string $s): void } assertType('array{}|array{string, string}', $matches); - if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) { - assertType('array{0: string, 1: string|null}', $matches); - } else { - assertType('array{}', $matches); - } - assertType('array{}|array{0: string, 1: string|null}', $matches); - - // passing the PREG_UNMATCHED_AS_NULL should change nothing compared to above as it is always set - if (Preg::match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { - assertType('array{0: string, 1: string|null}', $matches); - } else { - assertType('array{}', $matches); - } - assertType('array{}|array{0: string, 1: string|null}', $matches); +// if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) { // temporarily disabled until https://github.com/phpstan/phpstan-src/pull/3229 is released +// assertType('array{0: string, 1: string|null}', $matches); +// } else { +// assertType('array{}', $matches); +// } +// assertType('array{}|array{0: string, 1: string|null}', $matches); +// +// // passing the PREG_UNMATCHED_AS_NULL should change nothing compared to above as it is always set +// if (Preg::match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { +// assertType('array{0: string, 1: string|null}', $matches); +// } else { +// assertType('array{}', $matches); +// } +// assertType('array{}|array{0: string, 1: string|null}', $matches); if (Preg::isMatch('/Price: (?£|€)\d+/', $s, $matches)) { assertType('array{0: string, currency: string, 1: string}', $matches); From 088f7529930ba6d04c7a06cb3e32861f7485a800 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 13 Jul 2024 13:14:07 +0200 Subject: [PATCH 4/5] Fix 7.2/7.3 builds --- src/PHPStan/PregMatchFlags.php | 5 +++-- tests/PHPStanTests/nsrt/preg-match.php | 28 +++++++++++++------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php index 9cd3598..ace3e8a 100644 --- a/src/PHPStan/PregMatchFlags.php +++ b/src/PHPStan/PregMatchFlags.php @@ -7,13 +7,14 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\Type; use PhpParser\Node\Arg; +use PHPStan\Type\Php\RegexArrayShapeMatcher; final class PregMatchFlags { static public function getType(?Arg $flagsArg, Scope $scope): ?Type { if ($flagsArg === null) { - return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL); + return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73); } $flagsType = $scope->getType($flagsArg->value); @@ -29,7 +30,7 @@ static public function getType(?Arg $flagsArg, Scope $scope): ?Type return null; } - $internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL); + $internalFlagsTypes[] = new ConstantIntegerType($constantScalarValue | PREG_UNMATCHED_AS_NULL | RegexArrayShapeMatcher::PREG_UNMATCHED_AS_NULL_ON_72_73); } return TypeCombinator::union(...$internalFlagsTypes); } diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index 8a4458e..5dd23db 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -22,20 +22,20 @@ function doMatch(string $s): void } assertType('array{}|array{string, string}', $matches); -// if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) { // temporarily disabled until https://github.com/phpstan/phpstan-src/pull/3229 is released -// assertType('array{0: string, 1: string|null}', $matches); -// } else { -// assertType('array{}', $matches); -// } -// assertType('array{}|array{0: string, 1: string|null}', $matches); -// -// // passing the PREG_UNMATCHED_AS_NULL should change nothing compared to above as it is always set -// if (Preg::match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { -// assertType('array{0: string, 1: string|null}', $matches); -// } else { -// assertType('array{}', $matches); -// } -// assertType('array{}|array{0: string, 1: string|null}', $matches); + if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) { + assertType('array{string, string|null}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, string|null}', $matches); + + // passing the PREG_UNMATCHED_AS_NULL should change nothing compared to above as it is always set + if (Preg::match('/Price: (£|€)?\d+/', $s, $matches, PREG_UNMATCHED_AS_NULL)) { + assertType('array{string, string|null}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, string|null}', $matches); if (Preg::isMatch('/Price: (?£|€)\d+/', $s, $matches)) { assertType('array{0: string, currency: string, 1: string}', $matches); From 5cdc186c0003cf87b357db5fa6f0ba53dea2e48b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 13 Jul 2024 13:20:44 +0200 Subject: [PATCH 5/5] Bump minimum PHPStan version --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index c7d1bc3..857feae 100644 --- a/composer.json +++ b/composer.json @@ -21,11 +21,11 @@ }, "require-dev": { "phpunit/phpunit": "^8 || ^9", - "phpstan/phpstan": "^1.11.6@dev", + "phpstan/phpstan": "^1.11.8@dev", "phpstan/phpstan-strict-rules": "^1.1" }, "conflict": { - "phpstan/phpstan": "<1.11.6" + "phpstan/phpstan": "<1.11.8" }, "autoload": { "psr-4": {