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
4 changes: 2 additions & 2 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1506,10 +1506,10 @@ services:
class: PHPStan\Type\Php\RegexArrayShapeMatcher

-
class: PHPStan\Type\Php\RegexGroupParser
class: PHPStan\Type\Regex\RegexGroupParser

-
class: PHPStan\Type\Php\RegexExpressionHelper
class: PHPStan\Type\Regex\RegexExpressionHelper

-
class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Regexp/RegularExpressionPatternRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Php\RegexExpressionHelper;
use PHPStan\Type\Regex\RegexExpressionHelper;
use function in_array;
use function sprintf;
use function str_starts_with;
Expand Down
2 changes: 1 addition & 1 deletion src/Rules/Regexp/RegularExpressionQuotingRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Php\RegexExpressionHelper;
use PHPStan\Type\Regex\RegexExpressionHelper;
use function array_filter;
use function array_merge;
use function array_values;
Expand Down
31 changes: 18 additions & 13 deletions src/Type/Php/RegexArrayShapeMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\Regex\RegexAlternation;
use PHPStan\Type\Regex\RegexCapturingGroup;
use PHPStan\Type\Regex\RegexExpressionHelper;
use PHPStan\Type\Regex\RegexGroupParser;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function array_reverse;
use function count;
use function in_array;
Expand Down Expand Up @@ -117,7 +120,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
// regex could not be parsed by Hoa/Regex
return null;
}
[$groupList, $groupCombinations, $markVerbs] = $parseResult;
[$groupList, $markVerbs] = $parseResult;

$trailingOptionals = 0;
foreach (array_reverse($groupList) as $captureGroup) {
Expand All @@ -128,7 +131,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
}

$onlyOptionalTopLevelGroup = $this->getOnlyOptionalTopLevelGroup($groupList);
$onlyTopLevelAlternationId = $this->getOnlyTopLevelAlternationId($groupList);
$onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList);

if (
!$matchesAll
Expand Down Expand Up @@ -160,12 +163,11 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
} elseif (
!$matchesAll
&& $wasMatched->yes()
&& $onlyTopLevelAlternationId !== null
&& array_key_exists($onlyTopLevelAlternationId, $groupCombinations)
&& $onlyTopLevelAlternation !== null
) {
$combiTypes = [];
$isOptionalAlternation = false;
foreach ($groupCombinations[$onlyTopLevelAlternationId] as $groupCombo) {
foreach ($onlyTopLevelAlternation->getGroupCombinations() as $groupCombo) {
$comboList = $groupList;

$beforeCurrentCombo = true;
Expand All @@ -176,7 +178,10 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched
$beforeCurrentCombo = false;
} elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) {
$group->forceNonOptional();
} elseif ($group->getAlternationId() === $onlyTopLevelAlternationId && !$this->containsUnmatchedAsNull($flags ?? 0, $matchesAll)) {
} elseif (
$group->getAlternationId() === $onlyTopLevelAlternation->getId()
&& !$this->containsUnmatchedAsNull($flags ?? 0, $matchesAll)
) {
unset($comboList[$groupId]);
}
}
Expand Down Expand Up @@ -243,9 +248,9 @@ private function getOnlyOptionalTopLevelGroup(array $captureGroups): ?RegexCaptu
/**
* @param array<int, RegexCapturingGroup> $captureGroups
*/
private function getOnlyTopLevelAlternationId(array $captureGroups): ?int
private function getOnlyTopLevelAlternation(array $captureGroups): ?RegexAlternation
{
$alternationId = null;
$alternation = null;
foreach ($captureGroups as $captureGroup) {
if (!$captureGroup->isTopLevel()) {
continue;
Expand All @@ -255,14 +260,14 @@ private function getOnlyTopLevelAlternationId(array $captureGroups): ?int
return null;
}

if ($alternationId === null) {
$alternationId = $captureGroup->getAlternationId();
} elseif ($alternationId !== $captureGroup->getAlternationId()) {
if ($alternation === null) {
$alternation = $captureGroup->getAlternation();
} elseif ($alternation->getId() !== $captureGroup->getAlternation()->getId()) {
return null;
}
}

return $alternationId;
return $alternation;
}

/**
Expand Down
39 changes: 39 additions & 0 deletions src/Type/Regex/RegexAlternation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Regex;

use function array_key_exists;

final class RegexAlternation
{

/** @var array<int, list<int>> */
private array $groupCombinations = [];

public function __construct(private int $alternationId)
{
}

public function getId(): int
{
return $this->alternationId;
}

public function pushGroup(int $combinationIndex, RegexCapturingGroup $group): void
{
if (!array_key_exists($combinationIndex, $this->groupCombinations)) {
$this->groupCombinations[$combinationIndex] = [];
}

$this->groupCombinations[$combinationIndex][] = $group->getId();
}

/**
* @return array<int, list<int>>
*/
public function getGroupCombinations(): array
{
return $this->groupCombinations;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;
namespace PHPStan\Type\Regex;

use PHPStan\Type\Type;

Expand All @@ -12,7 +12,7 @@ class RegexCapturingGroup
public function __construct(
private int $id,
private ?string $name,
private ?int $alternationId,
private ?RegexAlternation $alternation,
private bool $inOptionalQuantification,
private RegexCapturingGroup|RegexNonCapturingGroup|null $parent,
private Type $type,
Expand Down Expand Up @@ -40,15 +40,27 @@ public function resetsGroupCounter(): bool
return $this->parent instanceof RegexNonCapturingGroup && $this->parent->resetsGroupCounter();
}

/** @phpstan-assert-if-true !null $this->getAlternationId() */
/**
* @phpstan-assert-if-true !null $this->getAlternationId()
* @phpstan-assert-if-true !null $this->getAlternation()
*/
public function inAlternation(): bool
{
return $this->alternationId !== null;
return $this->alternation !== null;
}

public function getAlternation(): ?RegexAlternation
{
return $this->alternation;
}

public function getAlternationId(): ?int
{
return $this->alternationId;
if ($this->alternation === null) {
return null;
}

return $this->alternation->getId();
}

public function isOptional(): bool
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;
namespace PHPStan\Type\Regex;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Concat;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;
namespace PHPStan\Type\Regex;

use Hoa\Compiler\Llk\Llk;
use Hoa\Compiler\Llk\Parser;
Expand All @@ -20,7 +20,6 @@
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function count;
use function in_array;
use function is_int;
Expand All @@ -45,7 +44,7 @@ public function __construct(
}

/**
* @return array{array<int, RegexCapturingGroup>, array<int, array<int, int[]>>, list<string>}|null
* @return array{array<int, RegexCapturingGroup>, list<string>}|null
*/
public function parseGroups(string $regex): ?array
{
Expand Down Expand Up @@ -75,44 +74,40 @@ public function parseGroups(string $regex): ?array
}

$capturingGroups = [];
$groupCombinations = [];
$alternationId = -1;
$captureGroupId = 100;
$markVerbs = [];
$this->walkRegexAst(
$ast,
false,
null,
$alternationId,
0,
false,
null,
$captureGroupId,
$capturingGroups,
$groupCombinations,
$markVerbs,
$captureOnlyNamed,
false,
$modifiers,
);

return [$capturingGroups, $groupCombinations, $markVerbs];
return [$capturingGroups, $markVerbs];
}

/**
* @param array<int, RegexCapturingGroup> $capturingGroups
* @param array<int, array<int, int[]>> $groupCombinations
* @param list<string> $markVerbs
*/
private function walkRegexAst(
TreeNode $ast,
bool $inAlternation,
?RegexAlternation $alternation,
int &$alternationId,
int $combinationIndex,
bool $inOptionalQuantification,
RegexCapturingGroup|RegexNonCapturingGroup|null $parentGroup,
int &$captureGroupId,
array &$capturingGroups,
array &$groupCombinations,
array &$markVerbs,
bool $captureOnlyNamed,
bool $repeatedMoreThanOnce,
Expand All @@ -124,7 +119,7 @@ private function walkRegexAst(
$group = new RegexCapturingGroup(
$captureGroupId++,
null,
$inAlternation ? $alternationId : null,
$alternation,
$inOptionalQuantification,
$parentGroup,
$this->createGroupType(
Expand All @@ -139,7 +134,7 @@ private function walkRegexAst(
$group = new RegexCapturingGroup(
$captureGroupId++,
$name,
$inAlternation ? $alternationId : null,
$alternation,
$inOptionalQuantification,
$parentGroup,
$this->createGroupType(
Expand All @@ -151,15 +146,15 @@ private function walkRegexAst(
$parentGroup = $group;
} elseif ($ast->getId() === '#noncapturing') {
$group = new RegexNonCapturingGroup(
$inAlternation ? $alternationId : null,
$alternation,
$inOptionalQuantification,
$parentGroup,
false,
);
$parentGroup = $group;
} elseif ($ast->getId() === '#noncapturingreset') {
$group = new RegexNonCapturingGroup(
$inAlternation ? $alternationId : null,
$alternation,
$inOptionalQuantification,
$parentGroup,
true,
Expand All @@ -182,7 +177,7 @@ private function walkRegexAst(

if ($ast->getId() === '#alternation') {
$alternationId++;
$inAlternation = true;
$alternation = new RegexAlternation($alternationId);
}

if ($ast->getId() === '#mark') {
Expand All @@ -196,26 +191,21 @@ private function walkRegexAst(
) {
$capturingGroups[$group->getId()] = $group;

if (!array_key_exists($alternationId, $groupCombinations)) {
$groupCombinations[$alternationId] = [];
}
if (!array_key_exists($combinationIndex, $groupCombinations[$alternationId])) {
$groupCombinations[$alternationId][$combinationIndex] = [];
if ($alternation !== null) {
$alternation->pushGroup($combinationIndex, $group);
}
$groupCombinations[$alternationId][$combinationIndex][] = $group->getId();
}

foreach ($ast->getChildren() as $child) {
$this->walkRegexAst(
$child,
$inAlternation,
$alternation,
$alternationId,
$combinationIndex,
$inOptionalQuantification,
$parentGroup,
$captureGroupId,
$capturingGroups,
$groupCombinations,
$markVerbs,
$captureOnlyNamed,
$repeatedMoreThanOnce,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;
namespace PHPStan\Type\Regex;

class RegexNonCapturingGroup
{

public function __construct(
private ?int $alternationId,
private ?RegexAlternation $alternation,
private bool $inOptionalQuantification,
private RegexCapturingGroup|RegexNonCapturingGroup|null $parent,
private bool $resetGroupCounter,
Expand All @@ -17,12 +17,16 @@ public function __construct(
/** @phpstan-assert-if-true !null $this->getAlternationId() */
public function inAlternation(): bool
{
return $this->alternationId !== null;
return $this->alternation !== null;
}

public function getAlternationId(): ?int
{
return $this->alternationId;
if ($this->alternation === null) {
return null;
}

return $this->alternation->getId();
}

public function isOptional(): bool
Expand Down
Loading
Loading