diff --git a/src/Assert.php b/src/Assert.php index 31c861b9..8544ea91 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -1507,7 +1507,15 @@ public static function regex(mixed $value, mixed $pattern, string|callable $mess static::string($value); static::string($pattern); - if (!\preg_match($pattern, $value)) { + if (false === $result = @\preg_match($pattern, $value)) { + static::reportInvalidArgument(\sprintf( + 'The pattern %s could not be evaluated: %s.', + static::valueToString($pattern), + \preg_last_error_msg() + )); + } + + if (0 === $result) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'The value %s does not match the expected pattern.', diff --git a/tests/AssertTest.php b/tests/AssertTest.php index f0ec064d..1fc73a68 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -19,6 +19,7 @@ use DateTimeImmutable; use Error; use Exception; +use InvalidArgumentException; use LogicException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -991,6 +992,66 @@ public function testEnumAssertionErrorMessage(): void Assert::null(DummyEnum::CaseName, 'Expected null. Got: %s'); } + public function testRegexRejectsAnUncompilablePattern(): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('The pattern "/(/" could not be evaluated: Internal error.'); + + Assert::regex('abc', '/(/'); + } + + public function testRegexSuppressesTheWarningForAnUncompilablePattern(): void + { + $reported = []; + // PHPUnit masks E_WARNING out of error_reporting(); restore it here. + $errorReporting = error_reporting(E_ALL); + set_error_handler(static function (int $errno, string $message) use (&$reported): bool { + // This is what a well behaved error handler does with a suppressed diagnostic. + if (0 !== (error_reporting() & $errno)) { + $reported[] = $message; + } + + return true; + }); + + try { + Assert::regex('abc', '/(/'); + } catch (InvalidArgumentException) { + // The assertion is expected to fail; this test only inspects the reported errors. + } finally { + restore_error_handler(); + error_reporting($errorReporting); + } + + $this->assertSame([], $reported); + } + + public function testRegexDoesNotUseTheCustomMessageForAnUncompilablePattern(): void + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage('The pattern "/(/" could not be evaluated: Internal error.'); + + Assert::regex('abc', '/(/', 'The value %s is not a valid slug.'); + } + + public function testRegexRejectsAPatternExceedingTheBacktrackLimit(): void + { + $backtrackLimit = ini_get('pcre.backtrack_limit'); + ini_set('pcre.backtrack_limit', '100'); + + try { + Assert::regex(str_repeat('a', 30).'c', '/^(a+)+$/'); + $this->fail('Expected an InvalidArgumentException to be thrown.'); + } catch (InvalidArgumentException $e) { + $this->assertSame( + 'The pattern "/^(a+)+$/" could not be evaluated: Backtrack limit exhausted.', + $e->getMessage() + ); + } finally { + ini_set('pcre.backtrack_limit', $backtrackLimit); + } + } + #[DataProvider('getMethodsThatUseOtherMethods')] public function testMessageIsPassedToInternalCalls(string $method, array $args, string $exceptionMessage): void {