Skip to content
Merged
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
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 === '{') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

() and [] are also valid reciprocal delimiters.
<> too but that's a shitty one to use really.. Still probably should support for completeness

https://www.php.net/manual/en/regexp.reference.delimiters.php

$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
Loading