Skip to content

Commit

Permalink
RegexArrayShapeMatcher - Support 'n' modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm authored Jul 27, 2024
1 parent 224af32 commit bfef6da
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 46 deletions.
6 changes: 6 additions & 0 deletions src/Php/PhpVersion.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ public function supportsPregUnmatchedAsNull(): bool
return $this->versionId >= 70400;
}

public function supportsPregCaptureOnlyNamedGroups(): bool
{
// https://php.watch/versions/8.2/preg-n-no-capture-modifier
return $this->versionId >= 80200;
}

public function hasDateTimeExceptions(): bool
{
return $this->versionId >= 80300;
Expand Down
44 changes: 6 additions & 38 deletions src/Rules/Regexp/RegularExpressionQuotingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,25 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Php\RegexExpressionHelper;
use function array_filter;
use function array_merge;
use function array_values;
use function count;
use function in_array;
use function sprintf;
use function strlen;
use function substr;

/**
* @implements Rule<Node\Expr\FuncCall>
*/
class RegularExpressionQuotingRule implements Rule
{

public function __construct(private ReflectionProvider $reflectionProvider)
public function __construct(
private ReflectionProvider $reflectionProvider,
private RegexExpressionHelper $regexExpressionHelper,
)
{
}

Expand Down Expand Up @@ -76,7 +78,7 @@ public function processNode(Node $node, Scope $scope): array
return [];
}

$patternDelimiters = $this->getDelimitersFromConcat($normalizedArgs[0]->value, $scope);
$patternDelimiters = $this->regexExpressionHelper->getPatternDelimiters($normalizedArgs[0]->value, $scope);
return $this->validateQuoteDelimiters($normalizedArgs[0]->value, $scope, $patternDelimiters);
}

Expand Down Expand Up @@ -193,40 +195,6 @@ private function validatePregQuote(FuncCall $pregQuote, Scope $scope, array $pat
return null;
}

/**
* Get delimiters from non-constant patterns, if possible.
*
* @return string[]
*/
private function getDelimitersFromConcat(Concat $concat, Scope $scope): array
{
if ($concat->left instanceof Concat) {
return $this->getDelimitersFromConcat($concat->left, $scope);
}

$left = $scope->getType($concat->left);

$delimiters = [];
foreach ($left->getConstantStrings() as $leftString) {
$delimiter = $this->getDelimiterFromString($leftString);
if ($delimiter === null) {
continue;
}

$delimiters[] = $delimiter;
}
return $delimiters;
}

private function getDelimiterFromString(ConstantStringType $string): ?string
{
if ($string->getValue() === '') {
return null;
}

return substr($string->getValue(), 0, 1);
}

/**
* @param string[] $delimiters
*
Expand Down
15 changes: 14 additions & 1 deletion src/Type/Php/RegexArrayShapeMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use function is_string;
use function rtrim;
use function sscanf;
use function str_contains;
use function str_replace;
use function strlen;
use function substr;
Expand Down Expand Up @@ -411,6 +412,12 @@ private function parseGroups(string $regex): ?array
return null;
}

$captureOnlyNamed = false;
if ($this->phpVersion->supportsPregCaptureOnlyNamedGroups()) {
$modifiers = $this->regexExpressionHelper->getPatternModifiers($regex);
$captureOnlyNamed = str_contains($modifiers ?? '', 'n');
}

$capturingGroups = [];
$groupCombinations = [];
$alternationId = -1;
Expand All @@ -427,6 +434,7 @@ private function parseGroups(string $regex): ?array
$capturingGroups,
$groupCombinations,
$markVerbs,
$captureOnlyNamed,
);

return [$capturingGroups, $groupCombinations, $markVerbs];
Expand All @@ -448,6 +456,7 @@ private function walkRegexAst(
array &$capturingGroups,
array &$groupCombinations,
array &$markVerbs,
bool $captureOnlyNamed,
): void
{
$group = null;
Expand Down Expand Up @@ -509,7 +518,10 @@ private function walkRegexAst(
return;
}

if ($group instanceof RegexCapturingGroup) {
if (
$group instanceof RegexCapturingGroup &&
(!$captureOnlyNamed || $group->isNamed())
) {
$capturingGroups[$group->getId()] = $group;

if (!array_key_exists($alternationId, $groupCombinations)) {
Expand All @@ -533,6 +545,7 @@ private function walkRegexAst(
$capturingGroups,
$groupCombinations,
$markVerbs,
$captureOnlyNamed,
);

if ($ast->getId() !== '#alternation') {
Expand Down
62 changes: 60 additions & 2 deletions src/Type/Php/RegexExpressionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
namespace PHPStan\Type\Php;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function strrpos;
use function substr;

final class RegexExpressionHelper
{
Expand All @@ -26,7 +29,7 @@ public function __construct(
*
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
*/
public function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
public function resolvePatternConcat(Concat $concat, Scope $scope): Type
{
$resolver = new class($scope) {

Expand All @@ -44,7 +47,7 @@ public function resolve(Expr $expr): Type
return new ConstantStringType('');
}

if ($expr instanceof Expr\BinaryOp\Concat) {
if ($expr instanceof Concat) {
$left = $this->resolve($expr->left);
$right = $this->resolve($expr->right);

Expand All @@ -66,4 +69,59 @@ public function resolve(Expr $expr): Type
return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
}

public function getPatternModifiers(string $pattern): ?string
{
$delimiter = $this->getDelimiterFromString(new ConstantStringType($pattern));
if ($delimiter === null) {
return null;
}

if ($delimiter === '{') {
$endDelimiterPos = strrpos($pattern, '}');
} else {
// same start and end delimiter
$endDelimiterPos = strrpos($pattern, $delimiter);
}

if ($endDelimiterPos === false) {
return null;
}

return substr($pattern, $endDelimiterPos + 1);
}

/**
* Get delimiters from non-constant patterns, if possible.
*
* @return string[]
*/
public function getPatternDelimiters(Concat $concat, Scope $scope): array
{
if ($concat->left instanceof Concat) {
return $this->getPatternDelimiters($concat->left, $scope);
}

$left = $scope->getType($concat->left);

$delimiters = [];
foreach ($left->getConstantStrings() as $leftString) {
$delimiter = $this->getDelimiterFromString($leftString);
if ($delimiter === null) {
continue;
}

$delimiters[] = $delimiter;
}
return $delimiters;
}

private function getDelimiterFromString(ConstantStringType $string): ?string
{
if ($string->getValue() === '') {
return null;
}

return substr($string->getValue(), 0, 1);
}

}
13 changes: 9 additions & 4 deletions tests/PHPStan/Analyser/nsrt/preg_match_shapes_php82.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@
// https://php.watch/versions/8.2/preg-n-no-capture-modifier
function doNonAutoCapturingFlag(string $s): void {
if (preg_match('/(\d+)/n', $s, $matches)) {
assertType('array{string, numeric-string}', $matches); // should be 'array{string}'
assertType('array{string}', $matches);
}
assertType('array{}|array{string, numeric-string}', $matches);
assertType('array{}|array{string}', $matches);

if (preg_match('/(\d+)(?P<num>\d+)/n', $s, $matches)) {
assertType('array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches);
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
}
assertType('array{}|array{0: string, 1: numeric-string, num: numeric-string, 2: numeric-string}', $matches);
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);

if (preg_match('/(\w)-(?P<num>\d+)-(\w)/n', $s, $matches)) {
assertType('array{0: string, num: numeric-string, 1: numeric-string}', $matches);
}
assertType('array{}|array{0: string, num: numeric-string, 1: numeric-string}', $matches);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Php\RegexExpressionHelper;
use const PHP_VERSION_ID;

/**
Expand All @@ -14,7 +15,10 @@ class RegularExpressionQuotingRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new RegularExpressionQuotingRule($this->createReflectionProvider());
return new RegularExpressionQuotingRule(
$this->createReflectionProvider(),
self::getContainer()->getByType(RegexExpressionHelper::class),
);
}

public function testRule(): void
Expand Down

0 comments on commit bfef6da

Please sign in to comment.