fix: distinguish PCRE failure from a mismatch in Assert::regex()

preg_match() has three outcomes -- 1, 0 and false -- but regex() folded
false into the "no match" branch. An uncompilable pattern or an exhausted
PCRE limit therefore leaked a raw PHP warning out of the library and then
reported the misleading "The value ... does not match the expected pattern."

Check for false separately and report the pattern together with
preg_last_error_msg(), following the existing false === $x = nativeCall()
handling in Assert::strlen(). The caller's custom message is intentionally
not reused for that branch, since it describes a mismatch that never
happened. Matching values and genuine mismatches are unaffected.
This commit is contained in:
dualfroz committed 2026-09-06 00:59:17 +02:00
1 parent f6badd5191
commit 24cce423ab
2 files changed
+70 -1

No files matched your search

+9 -1
View File
@@ -1507,7 +1507,15 @@ class Assert
static::string($value); static::string($value);
static::string($pattern); 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); $message = self::resolveMessage($message);
static::reportInvalidArgument(\sprintf( static::reportInvalidArgument(\sprintf(
$message ?: 'The value %s does not match the expected pattern.', $message ?: 'The value %s does not match the expected pattern.',
+61
View File
@@ -19,6 +19,7 @@ use DateTime;
use DateTimeImmutable; use DateTimeImmutable;
use Error; use Error;
use Exception; use Exception;
use InvalidArgumentException;
use LogicException; use LogicException;
use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -991,6 +992,66 @@ class AssertTest extends TestCase
Assert::null(DummyEnum::CaseName, 'Expected null. Got: %s'); 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')] #[DataProvider('getMethodsThatUseOtherMethods')]
public function testMessageIsPassedToInternalCalls(string $method, array $args, string $exceptionMessage): void public function testMessageIsPassedToInternalCalls(string $method, array $args, string $exceptionMessage): void
{ {