diff --git a/conf/config.neon b/conf/config.neon index 20cdfde763..c85cce945d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -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 diff --git a/src/Rules/Regexp/RegularExpressionPatternRule.php b/src/Rules/Regexp/RegularExpressionPatternRule.php index 8d46915f1a..fde058e62a 100644 --- a/src/Rules/Regexp/RegularExpressionPatternRule.php +++ b/src/Rules/Regexp/RegularExpressionPatternRule.php @@ -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; diff --git a/src/Rules/Regexp/RegularExpressionQuotingRule.php b/src/Rules/Regexp/RegularExpressionQuotingRule.php index c46b7ce72c..e6a5839fdb 100644 --- a/src/Rules/Regexp/RegularExpressionQuotingRule.php +++ b/src/Rules/Regexp/RegularExpressionQuotingRule.php @@ -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; diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 2e647c4f8f..7e48fa124c 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -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; @@ -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) { @@ -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 @@ -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; @@ -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]); } } @@ -243,9 +248,9 @@ private function getOnlyOptionalTopLevelGroup(array $captureGroups): ?RegexCaptu /** * @param array $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; @@ -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; } /** diff --git a/src/Type/Regex/RegexAlternation.php b/src/Type/Regex/RegexAlternation.php new file mode 100644 index 0000000000..ecf6d0fb39 --- /dev/null +++ b/src/Type/Regex/RegexAlternation.php @@ -0,0 +1,39 @@ +> */ + 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> + */ + public function getGroupCombinations(): array + { + return $this->groupCombinations; + } + +} diff --git a/src/Type/Php/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php similarity index 81% rename from src/Type/Php/RegexCapturingGroup.php rename to src/Type/Regex/RegexCapturingGroup.php index e5ddac62e2..983a823bef 100644 --- a/src/Type/Php/RegexCapturingGroup.php +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -1,6 +1,6 @@ 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 diff --git a/src/Type/Php/RegexExpressionHelper.php b/src/Type/Regex/RegexExpressionHelper.php similarity index 99% rename from src/Type/Php/RegexExpressionHelper.php rename to src/Type/Regex/RegexExpressionHelper.php index 8b763f1f0e..2b94fda6e2 100644 --- a/src/Type/Php/RegexExpressionHelper.php +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -1,6 +1,6 @@ , array>, list}|null + * @return array{array, list}|null */ public function parseGroups(string $regex): ?array { @@ -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 $capturingGroups - * @param array> $groupCombinations * @param list $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, @@ -124,7 +119,7 @@ private function walkRegexAst( $group = new RegexCapturingGroup( $captureGroupId++, null, - $inAlternation ? $alternationId : null, + $alternation, $inOptionalQuantification, $parentGroup, $this->createGroupType( @@ -139,7 +134,7 @@ private function walkRegexAst( $group = new RegexCapturingGroup( $captureGroupId++, $name, - $inAlternation ? $alternationId : null, + $alternation, $inOptionalQuantification, $parentGroup, $this->createGroupType( @@ -151,7 +146,7 @@ private function walkRegexAst( $parentGroup = $group; } elseif ($ast->getId() === '#noncapturing') { $group = new RegexNonCapturingGroup( - $inAlternation ? $alternationId : null, + $alternation, $inOptionalQuantification, $parentGroup, false, @@ -159,7 +154,7 @@ private function walkRegexAst( $parentGroup = $group; } elseif ($ast->getId() === '#noncapturingreset') { $group = new RegexNonCapturingGroup( - $inAlternation ? $alternationId : null, + $alternation, $inOptionalQuantification, $parentGroup, true, @@ -182,7 +177,7 @@ private function walkRegexAst( if ($ast->getId() === '#alternation') { $alternationId++; - $inAlternation = true; + $alternation = new RegexAlternation($alternationId); } if ($ast->getId() === '#mark') { @@ -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, diff --git a/src/Type/Php/RegexNonCapturingGroup.php b/src/Type/Regex/RegexNonCapturingGroup.php similarity index 82% rename from src/Type/Php/RegexNonCapturingGroup.php rename to src/Type/Regex/RegexNonCapturingGroup.php index 1f3fd26d4c..13bc6dabf6 100644 --- a/src/Type/Php/RegexNonCapturingGroup.php +++ b/src/Type/Regex/RegexNonCapturingGroup.php @@ -1,12 +1,12 @@ 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 diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php index aaf2e44f78..b1c964fada 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\Php\RegexExpressionHelper; +use PHPStan\Type\Regex\RegexExpressionHelper; use function sprintf; use const PHP_VERSION_ID; diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php index 1bffbf1a1d..38197c1aa6 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php @@ -4,7 +4,7 @@ use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; -use PHPStan\Type\Php\RegexExpressionHelper; +use PHPStan\Type\Regex\RegexExpressionHelper; use const PHP_VERSION_ID; /**