From e6c5d51a78fd085520bc2f737f7b4c33e57d2d60 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Mar 2025 08:56:25 +0100 Subject: [PATCH 1/6] RegexArrayShapeMatcher - turn more details immutable --- src/Type/Php/RegexArrayShapeMatcher.php | 35 ++++++++++--------------- src/Type/Regex/RegexCapturingGroup.php | 18 ++++++------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index cd91c0aa80..9ec66093f7 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -123,19 +123,18 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $trailingOptionals++; } - $onlyOptionalTopLevelGroup = $this->getOnlyOptionalTopLevelGroup($groupList); + $onlyOptionalTopLevelGroupIndex = $this->getOnlyOptionalTopLevelGroupIndex($groupList); $onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList); $flags ??= 0; if ( !$matchesAll && $wasMatched->yes() - && $onlyOptionalTopLevelGroup !== null + && $onlyOptionalTopLevelGroupIndex !== null ) { // if only one top level capturing optional group exists // we build a more precise tagged union of a empty-match and a match with the group - - $onlyOptionalTopLevelGroup->forceNonOptional(); + $groupList[$onlyOptionalTopLevelGroupIndex] = $groupList[$onlyOptionalTopLevelGroupIndex]->forceNonOptional(); $combiType = $this->buildArrayType( $groupList, @@ -154,12 +153,10 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ); } - $onlyOptionalTopLevelGroup->clearOverrides(); - return $combiType; } elseif ( !$matchesAll - && $onlyOptionalTopLevelGroup === null + && $onlyOptionalTopLevelGroupIndex === null && $onlyTopLevelAlternation !== null && !$wasMatched->no() ) { @@ -174,13 +171,14 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched foreach ($comboList as $groupId => $group) { if (in_array($groupId, $groupCombo, true)) { $isOptionalAlternation = $group->inOptionalAlternation(); - $group->forceNonOptional(); + $forcedGroup = $group->forceNonOptional(); $beforeCurrentCombo = false; + $comboList[$groupId] = $forcedGroup; } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { - $group->forceNonOptional(); - $group->forceType( + $forcedGroup = $group->forceNonOptional()->forceType( $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), ); + $comboList[$groupId] = $forcedGroup; } elseif ( $group->getAlternationId() === $onlyTopLevelAlternation->getId() && !$this->containsUnmatchedAsNull($flags, $matchesAll) @@ -199,11 +197,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ); $combiTypes[] = $combiType; - - foreach ($groupCombo as $groupId) { - $group = $comboList[$groupId]; - $group->clearOverrides(); - } } if ( @@ -235,10 +228,10 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched /** * @param array $captureGroups */ - private function getOnlyOptionalTopLevelGroup(array $captureGroups): ?RegexCapturingGroup + private function getOnlyOptionalTopLevelGroupIndex(array $captureGroups): ?int { - $group = null; - foreach ($captureGroups as $captureGroup) { + $groupIndex = null; + foreach ($captureGroups as $i => $captureGroup) { if (!$captureGroup->isTopLevel()) { continue; } @@ -247,14 +240,14 @@ private function getOnlyOptionalTopLevelGroup(array $captureGroups): ?RegexCaptu return null; } - if ($group !== null) { + if ($groupIndex !== null) { return null; } - $group = $captureGroup; + $groupIndex = $i; } - return $group; + return $groupIndex; } /** diff --git a/src/Type/Regex/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php index 51a1fc9d85..cf512a7fad 100644 --- a/src/Type/Regex/RegexCapturingGroup.php +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -27,20 +27,18 @@ public function getId(): int return $this->id; } - public function forceNonOptional(): void + public function forceNonOptional(): self { - $this->forceNonOptional = true; + $new = clone $this; + $new->forceNonOptional = true; + return $new; } - public function forceType(Type $type): void + public function forceType(Type $type): self { - $this->forceType = $type; - } - - public function clearOverrides(): void - { - $this->forceNonOptional = false; - $this->forceType = null; + $new = clone $this; + $new->forceType = $type; + return $new; } public function resetsGroupCounter(): bool From 9adf2dda78684617bf4a5837de0018edad59069d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Mar 2025 09:16:34 +0100 Subject: [PATCH 2/6] wip --- src/Type/Php/RegexArrayShapeMatcher.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 9ec66093f7..7155340914 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -123,18 +123,18 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $trailingOptionals++; } - $onlyOptionalTopLevelGroupIndex = $this->getOnlyOptionalTopLevelGroupIndex($groupList); + $onlyOptionalTopLevelGroupId = $this->getOnlyOptionalTopLevelGroupId($groupList); $onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList); $flags ??= 0; if ( !$matchesAll && $wasMatched->yes() - && $onlyOptionalTopLevelGroupIndex !== null + && $onlyOptionalTopLevelGroupId !== null ) { // if only one top level capturing optional group exists // we build a more precise tagged union of a empty-match and a match with the group - $groupList[$onlyOptionalTopLevelGroupIndex] = $groupList[$onlyOptionalTopLevelGroupIndex]->forceNonOptional(); + $groupList[$onlyOptionalTopLevelGroupId] = $groupList[$onlyOptionalTopLevelGroupId]->forceNonOptional(); $combiType = $this->buildArrayType( $groupList, @@ -156,7 +156,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched return $combiType; } elseif ( !$matchesAll - && $onlyOptionalTopLevelGroupIndex === null + && $onlyOptionalTopLevelGroupId === null && $onlyTopLevelAlternation !== null && !$wasMatched->no() ) { @@ -228,10 +228,10 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched /** * @param array $captureGroups */ - private function getOnlyOptionalTopLevelGroupIndex(array $captureGroups): ?int + private function getOnlyOptionalTopLevelGroupId(array $captureGroups): ?int { $groupIndex = null; - foreach ($captureGroups as $i => $captureGroup) { + foreach ($captureGroups as $captureGroup) { if (!$captureGroup->isTopLevel()) { continue; } @@ -244,7 +244,7 @@ private function getOnlyOptionalTopLevelGroupIndex(array $captureGroups): ?int return null; } - $groupIndex = $i; + $groupIndex = $captureGroup->getId(); } return $groupIndex; From 43964bbc5ba4640783bcf7df433e1a12d761c61d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Mar 2025 10:09:50 +0100 Subject: [PATCH 3/6] cleanup --- src/Type/Php/RegexArrayShapeMatcher.php | 31 +++---- src/Type/Regex/RegexCapturingGroup.php | 14 ++- src/Type/Regex/RegexGroupList.php | 118 ++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 src/Type/Regex/RegexGroupList.php diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 7155340914..e4ec6ac2b1 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -18,11 +18,11 @@ use PHPStan\Type\Regex\RegexAlternation; use PHPStan\Type\Regex\RegexCapturingGroup; use PHPStan\Type\Regex\RegexExpressionHelper; +use PHPStan\Type\Regex\RegexGroupList; use PHPStan\Type\Regex\RegexGroupParser; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function array_reverse; use function count; use function in_array; use function is_string; @@ -115,13 +115,8 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched } [$groupList, $markVerbs] = $parseResult; - $trailingOptionals = 0; - foreach (array_reverse($groupList) as $captureGroup) { - if (!$captureGroup->isOptional()) { - break; - } - $trailingOptionals++; - } + $regexGroupList = new RegexGroupList($groupList); + $trailingOptionals = $regexGroupList->countTrailingOptionals(); $onlyOptionalTopLevelGroupId = $this->getOnlyOptionalTopLevelGroupId($groupList); $onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList); @@ -134,10 +129,10 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ) { // if only one top level capturing optional group exists // we build a more precise tagged union of a empty-match and a match with the group - $groupList[$onlyOptionalTopLevelGroupId] = $groupList[$onlyOptionalTopLevelGroupId]->forceNonOptional(); + $regexGroupList = $regexGroupList->forceGroupIdNonOptional($onlyOptionalTopLevelGroupId); $combiType = $this->buildArrayType( - $groupList, + $regexGroupList, $wasMatched, $trailingOptionals, $flags, @@ -165,25 +160,24 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $combiTypes = []; $isOptionalAlternation = false; foreach ($onlyTopLevelAlternation->getGroupCombinations() as $groupCombo) { - $comboList = $groupList; + $comboList = new RegexGroupList($groupList); $beforeCurrentCombo = true; foreach ($comboList as $groupId => $group) { if (in_array($groupId, $groupCombo, true)) { $isOptionalAlternation = $group->inOptionalAlternation(); - $forcedGroup = $group->forceNonOptional(); + $comboList = $comboList->forceGroupIdNonOptional($group->getId()); $beforeCurrentCombo = false; - $comboList[$groupId] = $forcedGroup; } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { - $forcedGroup = $group->forceNonOptional()->forceType( + $comboList = $comboList->forceGroupIdTypeAndNonOptional( + $group->getId(), $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), ); - $comboList[$groupId] = $forcedGroup; } elseif ( $group->getAlternationId() === $onlyTopLevelAlternation->getId() && !$this->containsUnmatchedAsNull($flags, $matchesAll) ) { - unset($comboList[$groupId]); + $comboList = $comboList->removeGroup($groupId); } } @@ -216,7 +210,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched // the general case, which should work in all cases but does not yield the most // precise result possible in some cases return $this->buildArrayType( - $groupList, + $regexGroupList, $wasMatched, $trailingOptionals, $flags, @@ -280,11 +274,10 @@ private function getOnlyTopLevelAlternation(array $captureGroups): ?RegexAlterna } /** - * @param array $captureGroups * @param list $markVerbs */ private function buildArrayType( - array $captureGroups, + RegexGroupList $captureGroups, TrinaryLogic $wasMatched, int $trailingOptionals, int $flags, diff --git a/src/Type/Regex/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php index cf512a7fad..0fbd9750d1 100644 --- a/src/Type/Regex/RegexCapturingGroup.php +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -16,7 +16,7 @@ public function __construct( private readonly ?string $name, private readonly ?RegexAlternation $alternation, private readonly bool $inOptionalQuantification, - private readonly RegexCapturingGroup|RegexNonCapturingGroup|null $parent, + private RegexCapturingGroup|RegexNonCapturingGroup|null $parent, private readonly Type $type, ) { @@ -41,6 +41,13 @@ public function forceType(Type $type): self return $new; } + public function withParent(RegexCapturingGroup|RegexNonCapturingGroup $parent): self + { + $new = clone $this; + $new->parent = $parent; + return $new; + } + public function resetsGroupCounter(): bool { return $this->parent instanceof RegexNonCapturingGroup && $this->parent->resetsGroupCounter(); @@ -126,4 +133,9 @@ public function getType(): Type return $this->type; } + public function getParent(): RegexCapturingGroup|RegexNonCapturingGroup|null + { + return $this->parent; + } + } diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php new file mode 100644 index 0000000000..917ed52904 --- /dev/null +++ b/src/Type/Regex/RegexGroupList.php @@ -0,0 +1,118 @@ + + */ +final class RegexGroupList implements Countable, IteratorAggregate +{ + + /** + * @param array $groups + */ + public function __construct( + private readonly array $groups, + ) + { + } + + public function countTrailingOptionals(): int + { + $trailingOptionals = 0; + foreach (array_reverse($this->groups) as $captureGroup) { + if (!$captureGroup->isOptional()) { + break; + } + $trailingOptionals++; + } + return $trailingOptionals; + } + + public function forceGroupIdNonOptional(int $id): self + { + return $this->cloneAndReParentList($id); + } + + public function forceGroupIdTypeAndNonOptional(int $id, Type $type): self + { + return $this->cloneAndReParentList($id, $type); + } + + private function cloneAndReParentList(int $id, ?Type $type = null): self + { + $groups = []; + $forcedGroup = null; + foreach ($this->groups as $i => $group) { + if ($group->getId() === $id) { + $forcedGroup = $group->forceNonOptional(); + if ($type !== null) { + $forcedGroup = $forcedGroup->forceType($type); + } + $groups[$i] = $forcedGroup; + + continue; + } + + $groups[$i] = $group; + } + + if ($forcedGroup === null) { + throw new ShouldNotHappenException(); + } + + foreach ($groups as $i => $group) { + $parent = $group->getParent(); + + while ($parent !== null) { + if ($parent instanceof RegexNonCapturingGroup) { + $parent = $parent->getParent(); + continue; + } + + if ($parent->getId() === $id) { + $groups[$i] = $groups[$i]->withParent($forcedGroup); + } + $parent = $parent->getParent(); + } + } + + return new self($groups); + } + + public function removeGroup(int $id): self + { + $groups = []; + foreach ($this->groups as $i => $group) { + if ($group->getId() === $id) { + continue; + } + + $groups[$i] = $group; + } + + return new self($groups); + } + + public function count(): int + { + return count($this->groups); + } + + /** + * @return ArrayIterator + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->groups); + } + +} From 473dff1e78deb170ba6c94cb780abd9e0df9c3fb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Mar 2025 10:12:49 +0100 Subject: [PATCH 4/6] refactor --- src/Type/Php/RegexArrayShapeMatcher.php | 60 +------------------------ src/Type/Regex/RegexGroupList.php | 48 ++++++++++++++++++++ 2 files changed, 50 insertions(+), 58 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index e4ec6ac2b1..324767995a 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -15,7 +15,6 @@ use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; -use PHPStan\Type\Regex\RegexAlternation; use PHPStan\Type\Regex\RegexCapturingGroup; use PHPStan\Type\Regex\RegexExpressionHelper; use PHPStan\Type\Regex\RegexGroupList; @@ -117,9 +116,8 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $regexGroupList = new RegexGroupList($groupList); $trailingOptionals = $regexGroupList->countTrailingOptionals(); - - $onlyOptionalTopLevelGroupId = $this->getOnlyOptionalTopLevelGroupId($groupList); - $onlyTopLevelAlternation = $this->getOnlyTopLevelAlternation($groupList); + $onlyOptionalTopLevelGroupId = $regexGroupList->getOnlyOptionalTopLevelGroupId(); + $onlyTopLevelAlternation = $regexGroupList->getOnlyTopLevelAlternation(); $flags ??= 0; if ( @@ -219,60 +217,6 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched ); } - /** - * @param array $captureGroups - */ - private function getOnlyOptionalTopLevelGroupId(array $captureGroups): ?int - { - $groupIndex = null; - foreach ($captureGroups as $captureGroup) { - if (!$captureGroup->isTopLevel()) { - continue; - } - - if (!$captureGroup->isOptional()) { - return null; - } - - if ($groupIndex !== null) { - return null; - } - - $groupIndex = $captureGroup->getId(); - } - - return $groupIndex; - } - - /** - * @param array $captureGroups - */ - private function getOnlyTopLevelAlternation(array $captureGroups): ?RegexAlternation - { - $alternation = null; - foreach ($captureGroups as $captureGroup) { - if (!$captureGroup->isTopLevel()) { - continue; - } - - if (!$captureGroup->inAlternation()) { - return null; - } - - if ($captureGroup->inOptionalQuantification()) { - return null; - } - - if ($alternation === null) { - $alternation = $captureGroup->getAlternation(); - } elseif ($alternation->getId() !== $captureGroup->getAlternation()->getId()) { - return null; - } - } - - return $alternation; - } - /** * @param list $markVerbs */ diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php index 917ed52904..975d50f317 100644 --- a/src/Type/Regex/RegexGroupList.php +++ b/src/Type/Regex/RegexGroupList.php @@ -102,6 +102,54 @@ public function removeGroup(int $id): self return new self($groups); } + public function getOnlyOptionalTopLevelGroupId(): ?int + { + $groupIndex = null; + foreach ($this->groups as $captureGroup) { + if (!$captureGroup->isTopLevel()) { + continue; + } + + if (!$captureGroup->isOptional()) { + return null; + } + + if ($groupIndex !== null) { + return null; + } + + $groupIndex = $captureGroup->getId(); + } + + return $groupIndex; + } + + public function getOnlyTopLevelAlternation(): ?RegexAlternation + { + $alternation = null; + foreach ($this->groups as $captureGroup) { + if (!$captureGroup->isTopLevel()) { + continue; + } + + if (!$captureGroup->inAlternation()) { + return null; + } + + if ($captureGroup->inOptionalQuantification()) { + return null; + } + + if ($alternation === null) { + $alternation = $captureGroup->getAlternation(); + } elseif ($alternation->getId() !== $captureGroup->getAlternation()->getId()) { + return null; + } + } + + return $alternation; + } + public function count(): int { return count($this->groups); From 9af73506650d0cdbe722a99694852a6934424681 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Mar 2025 10:18:35 +0100 Subject: [PATCH 5/6] readonly props --- src/Type/Regex/RegexCapturingGroup.php | 47 ++++++++++++++++++-------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/Type/Regex/RegexCapturingGroup.php b/src/Type/Regex/RegexCapturingGroup.php index 0fbd9750d1..3cc16fa182 100644 --- a/src/Type/Regex/RegexCapturingGroup.php +++ b/src/Type/Regex/RegexCapturingGroup.php @@ -7,17 +7,15 @@ final class RegexCapturingGroup { - private bool $forceNonOptional = false; - - private ?Type $forceType = null; - public function __construct( private readonly int $id, private readonly ?string $name, private readonly ?RegexAlternation $alternation, private readonly bool $inOptionalQuantification, - private RegexCapturingGroup|RegexNonCapturingGroup|null $parent, + private readonly RegexCapturingGroup|RegexNonCapturingGroup|null $parent, private readonly Type $type, + private readonly bool $forceNonOptional = false, + private readonly ?Type $forceType = null, ) { } @@ -29,23 +27,44 @@ public function getId(): int public function forceNonOptional(): self { - $new = clone $this; - $new->forceNonOptional = true; - return $new; + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $this->type, + true, + $this->forceType, + ); } public function forceType(Type $type): self { - $new = clone $this; - $new->forceType = $type; - return $new; + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $this->parent, + $type, + $this->forceNonOptional, + $this->forceType, + ); } public function withParent(RegexCapturingGroup|RegexNonCapturingGroup $parent): self { - $new = clone $this; - $new->parent = $parent; - return $new; + return new self( + $this->id, + $this->name, + $this->alternation, + $this->inOptionalQuantification, + $parent, + $this->type, + $this->forceNonOptional, + $this->forceType, + ); } public function resetsGroupCounter(): bool From 21d53c7442a709f3cb7e1dd165a3e3be4f18c129 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 21 Mar 2025 10:34:46 +0100 Subject: [PATCH 6/6] stronger types --- src/Type/Php/RegexArrayShapeMatcher.php | 20 +++++++++--------- src/Type/Regex/RegexGroupList.php | 28 ++++++++++++------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 324767995a..da6a44e735 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -116,18 +116,18 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $regexGroupList = new RegexGroupList($groupList); $trailingOptionals = $regexGroupList->countTrailingOptionals(); - $onlyOptionalTopLevelGroupId = $regexGroupList->getOnlyOptionalTopLevelGroupId(); + $onlyOptionalTopLevelGroup = $regexGroupList->getOnlyOptionalTopLevelGroup(); $onlyTopLevelAlternation = $regexGroupList->getOnlyTopLevelAlternation(); $flags ??= 0; if ( !$matchesAll && $wasMatched->yes() - && $onlyOptionalTopLevelGroupId !== null + && $onlyOptionalTopLevelGroup !== null ) { // if only one top level capturing optional group exists // we build a more precise tagged union of a empty-match and a match with the group - $regexGroupList = $regexGroupList->forceGroupIdNonOptional($onlyOptionalTopLevelGroupId); + $regexGroupList = $regexGroupList->forceGroupNonOptional($onlyOptionalTopLevelGroup); $combiType = $this->buildArrayType( $regexGroupList, @@ -149,7 +149,7 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched return $combiType; } elseif ( !$matchesAll - && $onlyOptionalTopLevelGroupId === null + && $onlyOptionalTopLevelGroup === null && $onlyTopLevelAlternation !== null && !$wasMatched->no() ) { @@ -161,21 +161,21 @@ private function matchRegex(string $regex, ?int $flags, TrinaryLogic $wasMatched $comboList = new RegexGroupList($groupList); $beforeCurrentCombo = true; - foreach ($comboList as $groupId => $group) { - if (in_array($groupId, $groupCombo, true)) { + foreach ($comboList as $group) { + if (in_array($group->getId(), $groupCombo, true)) { $isOptionalAlternation = $group->inOptionalAlternation(); - $comboList = $comboList->forceGroupIdNonOptional($group->getId()); + $comboList = $comboList->forceGroupNonOptional($group); $beforeCurrentCombo = false; } elseif ($beforeCurrentCombo && !$group->resetsGroupCounter()) { - $comboList = $comboList->forceGroupIdTypeAndNonOptional( - $group->getId(), + $comboList = $comboList->forceGroupTypeAndNonOptional( + $group, $this->containsUnmatchedAsNull($flags, $matchesAll) ? new NullType() : new ConstantStringType(''), ); } elseif ( $group->getAlternationId() === $onlyTopLevelAlternation->getId() && !$this->containsUnmatchedAsNull($flags, $matchesAll) ) { - $comboList = $comboList->removeGroup($groupId); + $comboList = $comboList->removeGroup($group); } } diff --git a/src/Type/Regex/RegexGroupList.php b/src/Type/Regex/RegexGroupList.php index 975d50f317..d5f624f5df 100644 --- a/src/Type/Regex/RegexGroupList.php +++ b/src/Type/Regex/RegexGroupList.php @@ -37,22 +37,22 @@ public function countTrailingOptionals(): int return $trailingOptionals; } - public function forceGroupIdNonOptional(int $id): self + public function forceGroupNonOptional(RegexCapturingGroup $group): self { - return $this->cloneAndReParentList($id); + return $this->cloneAndReParentList($group); } - public function forceGroupIdTypeAndNonOptional(int $id, Type $type): self + public function forceGroupTypeAndNonOptional(RegexCapturingGroup $group, Type $type): self { - return $this->cloneAndReParentList($id, $type); + return $this->cloneAndReParentList($group, $type); } - private function cloneAndReParentList(int $id, ?Type $type = null): self + private function cloneAndReParentList(RegexCapturingGroup $target, ?Type $type = null): self { $groups = []; $forcedGroup = null; foreach ($this->groups as $i => $group) { - if ($group->getId() === $id) { + if ($group->getId() === $target->getId()) { $forcedGroup = $group->forceNonOptional(); if ($type !== null) { $forcedGroup = $forcedGroup->forceType($type); @@ -78,7 +78,7 @@ private function cloneAndReParentList(int $id, ?Type $type = null): self continue; } - if ($parent->getId() === $id) { + if ($parent->getId() === $target->getId()) { $groups[$i] = $groups[$i]->withParent($forcedGroup); } $parent = $parent->getParent(); @@ -88,11 +88,11 @@ private function cloneAndReParentList(int $id, ?Type $type = null): self return new self($groups); } - public function removeGroup(int $id): self + public function removeGroup(RegexCapturingGroup $remove): self { $groups = []; foreach ($this->groups as $i => $group) { - if ($group->getId() === $id) { + if ($group->getId() === $remove->getId()) { continue; } @@ -102,9 +102,9 @@ public function removeGroup(int $id): self return new self($groups); } - public function getOnlyOptionalTopLevelGroupId(): ?int + public function getOnlyOptionalTopLevelGroup(): ?RegexCapturingGroup { - $groupIndex = null; + $group = null; foreach ($this->groups as $captureGroup) { if (!$captureGroup->isTopLevel()) { continue; @@ -114,14 +114,14 @@ public function getOnlyOptionalTopLevelGroupId(): ?int return null; } - if ($groupIndex !== null) { + if ($group !== null) { return null; } - $groupIndex = $captureGroup->getId(); + $group = $captureGroup; } - return $groupIndex; + return $group; } public function getOnlyTopLevelAlternation(): ?RegexAlternation