Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Type/Regex/PHPRegexReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPRegex\Parser\Exception\RegexException;
use PHPRegex\Parser\Node\RegexNode;
use PHPRegex\Parser\Syntax\Delimiters;
use PHPRegex\Parser\PcreTarget;
use PHPRegex\Parser\RegexParser;
use PHPStan\DependencyInjection\AutowiredService;
Expand Down Expand Up @@ -45,6 +46,20 @@ public function read(string $rawRegex, string $modifiers, string $delimiter): Re
return $this->parser->parsePattern($rawRegex, $modifiers, $delimiter);
}

/**
* Whether the engine the judged target runs accepts the pattern: the
* parser validates for the target it reads for, so a construct the
* running engine takes but the analysed one predates is refused here.
*/
public function accepts(string $rawRegex, string $modifiers, string $delimiter): bool
{
$this->parser ??= RegexParser::create($this->targetOptions());

$regex = $delimiter . $rawRegex . Delimiters::closing($delimiter) . $modifiers;

return $this->parser->validate($regex)->isValid;
}

/**
* @return array<string, int|string>
*/
Expand Down
20 changes: 11 additions & 9 deletions src/Type/Regex/RegexGroupParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace PHPStan\Type\Regex;

use Nette\Utils\RegexpException;
use Nette\Utils\Strings;
use PHPRegex\Parser\Exception\RegexException;
use PHPRegex\Parser\Node\AlternationNode;
Expand Down Expand Up @@ -90,13 +89,6 @@ public function parseGroups(string $regex): ?RegexAstWalkResult

$modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? '';
} else {
try {
Strings::match('', $regex);
} catch (RegexpException) {
// pattern is invalid, so let the RegularExpressionPatternRule report it
return self::$parsedAst[$cacheKey] = null;
}

$modifiers = $this->regexExpressionHelper->getPatternModifiers($regex) ?? '';
foreach (self::NOT_SUPPORTED_MODIFIERS as $notSupportedModifier) {
if (str_contains($modifiers, $notSupportedModifier)) {
Expand All @@ -105,11 +97,21 @@ public function parseGroups(string $regex): ?RegexAstWalkResult
}

$rawRegex = $this->regexExpressionHelper->removeDelimitersAndModifiers($regex);
$delimiter = self::delimiterOf($regex);

// the pattern is judged for the analysed PHP: one the engine it
// runs refuses carries no shape, whatever the running engine
// says about it — the invalid-pattern rule reports on the
// running engine, and each part speaks for its own judge
if (!$this->reader->accepts($rawRegex, $modifiers, $delimiter)) {
return self::$parsedAst[$cacheKey] = null;
}

try {
$ast = self::$parsedAst[$cacheKey] = $this->reader->read(
$rawRegex,
$modifiers,
self::delimiterOf($regex),
$delimiter,
);
} catch (RegexException) {
return self::$parsedAst[$cacheKey] = null;
Expand Down
19 changes: 19 additions & 0 deletions tests/PHPStan/Type/Regex/RegexShapeReadingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,25 @@ public function testAPinAboveTheBundledEngineIsIgnored(): void
self::assertSame('10.44', $targetOf(new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.44'))), 'a pin at the bundled engine judges it');
}

public function testTargetInvalidPatternsGetNoShape(): void
{
// the pre-check judges for the analysed PHP, not the running one: a
// padded \x{ 41 } compiles on the runner's 10.43+ engine but the PHP
// an 8.2 project runs refuses it, so the shape is null and the
// invalid-pattern rule speaks about the running engine alone
$helper = (new ReflectionClass(RegexExpressionHelper::class))->newInstanceWithoutConstructor();
$php82 = new RegexGroupParser(new PhpVersion(80200), $helper, new PHPRegexReader(new PhpVersion(80200), PcreVersion::fromString('10.40')));
$php84 = new RegexGroupParser(new PhpVersion(80400), $helper, new PHPRegexReader(new PhpVersion(80400), PcreVersion::fromString('10.44')));

$this->resetParsedAstCache();
self::assertNull($php82->parseGroups('/(a\x{ 41 })/'), 'the engine PHP 8.2 bundles refuses the padded escape');

$this->resetParsedAstCache();
$result = $php84->parseGroups('/(a\x{ 41 })/');
self::assertNotNull($result, 'the engine PHP 8.4 bundles reads it');
self::assertNotInstanceOf(ConstantStringType::class, array_values($result->getCapturingGroups())[0]->getType());
}

public function testEngineRefusedPatternsGetNoShapeAndBracketDelimitersWalk(): void
{
$parser = $this->createParser();
Expand Down