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
89 changes: 37 additions & 52 deletions src/Type/Regex/RegexGroupParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -304,35 +304,25 @@ private function createGroupType(TreeNode $group, bool $maybeConstant, string $p
return TypeCombinator::union(...$types);
}

$isNonEmpty = TrinaryLogic::createMaybe();
$isNonFalsy = TrinaryLogic::createMaybe();
$isNumeric = TrinaryLogic::createMaybe();
$inOptionalQuantification = false;
$onlyLiterals = [];

$this->walkGroupAst(
$walkResult = $this->walkGroupAst(
$group,
false,
$isNonEmpty,
$isNonFalsy,
$isNumeric,
$inOptionalQuantification,
$onlyLiterals,
false,
$patternModifiers,
RegexGroupWalkResult::createEmpty(),
);

if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) {
if ($maybeConstant && $walkResult->getOnlyLiterals() !== null && $walkResult->getOnlyLiterals() !== []) {
$result = [];
foreach ($onlyLiterals as $literal) {
foreach ($walkResult->getOnlyLiterals() as $literal) {
$result[] = new ConstantStringType($literal);

}
return TypeCombinator::union(...$result);
}

if ($isNumeric->yes()) {
if ($isNonFalsy->yes()) {
if ($walkResult->isNumeric()->yes()) {
if ($walkResult->isNonFalsy()->yes()) {
return new IntersectionType([
new StringType(),
new AccessoryNumericStringType(),
Expand All @@ -341,13 +331,13 @@ private function createGroupType(TreeNode $group, bool $maybeConstant, string $p
}

$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
if (!$isNonEmpty->yes()) {
if (!$walkResult->isNonEmpty()->yes()) {
return TypeCombinator::union(new ConstantStringType(''), $result);
}
return $result;
} elseif ($isNonFalsy->yes()) {
} elseif ($walkResult->isNonFalsy()->yes()) {
return new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]);
} elseif ($isNonEmpty->yes()) {
} elseif ($walkResult->isNonEmpty()->yes()) {
return new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]);
}

Expand Down Expand Up @@ -376,20 +366,13 @@ private function getRootAlternation(TreeNode $group): ?TreeNode
return null;
}

/**
* @param array<string>|null $onlyLiterals
*/
private function walkGroupAst(
TreeNode $ast,
bool $inAlternation,
TrinaryLogic &$isNonEmpty,
TrinaryLogic &$isNonFalsy,
TrinaryLogic &$isNumeric,
bool &$inOptionalQuantification,
?array &$onlyLiterals,
Comment on lines -385 to -389
Copy link
Contributor Author

Choose a reason for hiding this comment

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

essentially I moved these by-ref parameters into a separate class without anymore by-ref semantics

bool $inClass,
string $patternModifiers,
): void
RegexGroupWalkResult $walkResult,
): RegexGroupWalkResult
{
$children = $ast->getChildren();

Expand All @@ -411,61 +394,65 @@ private function walkGroupAst(
}

// a single token non-falsy on its own
$isNonFalsy = TrinaryLogic::createYes();
$walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes());
break;
}

if ($meaningfulTokens > 0) {
$isNonEmpty = TrinaryLogic::createYes();
$walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes());

// two non-empty tokens concatenated results in a non-falsy string
if ($meaningfulTokens > 1 && !$inAlternation) {
$isNonFalsy = TrinaryLogic::createYes();
$walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes());
}
}
} elseif ($ast->getId() === '#quantification') {
[$min] = $this->getQuantificationRange($ast);

if ($min === 0) {
$inOptionalQuantification = true;
$walkResult = $walkResult->inOptionalQuantification(true);
}
if ($min >= 1) {
$isNonEmpty = TrinaryLogic::createYes();
$inOptionalQuantification = false;
$walkResult = $walkResult
->nonEmpty(TrinaryLogic::createYes())
->inOptionalQuantification(false);
}
if ($min >= 2 && !$inAlternation) {
$isNonFalsy = TrinaryLogic::createYes();
$walkResult = $walkResult->nonFalsy(TrinaryLogic::createYes());
}

$onlyLiterals = null;
} elseif ($ast->getId() === '#class' && $onlyLiterals !== null) {
$walkResult = $walkResult->onlyLiterals(null);
} elseif ($ast->getId() === '#class' && $walkResult->getOnlyLiterals() !== null) {
$inClass = true;

$newLiterals = [];
foreach ($children as $child) {
$oldLiterals = $onlyLiterals;
$oldLiterals = $walkResult->getOnlyLiterals();

$this->getLiteralValue($child, $oldLiterals, true, $patternModifiers, true);
foreach ($oldLiterals ?? [] as $oldLiteral) {
$newLiterals[] = $oldLiteral;
}
}
$onlyLiterals = $newLiterals;
$walkResult = $walkResult->onlyLiterals($newLiterals);
} elseif ($ast->getId() === 'token') {
$onlyLiterals = $walkResult->getOnlyLiterals();
$literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass, $patternModifiers, false);
$walkResult = $walkResult->onlyLiterals($onlyLiterals);

if ($literalValue !== null) {
if (Strings::match($literalValue, '/^\d+$/') === null) {
$isNumeric = TrinaryLogic::createNo();
} elseif ($isNumeric->maybe()) {
$isNumeric = TrinaryLogic::createYes();
$walkResult = $walkResult->numeric(TrinaryLogic::createNo());
} elseif ($walkResult->isNumeric()->maybe()) {
$walkResult = $walkResult->numeric(TrinaryLogic::createYes());
}

if (!$inOptionalQuantification && $literalValue !== '') {
$isNonEmpty = TrinaryLogic::createYes();
if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') {
$walkResult = $walkResult->nonEmpty(TrinaryLogic::createYes());
}
}
} elseif (!in_array($ast->getId(), ['#capturing', '#namedcapturing', '#alternation'], true)) {
$onlyLiterals = null;
$walkResult = $walkResult->onlyLiterals(null);
}

if ($ast->getId() === '#alternation') {
Expand All @@ -476,22 +463,20 @@ private function walkGroupAst(
// doable but really silly compared to just \d so we can safely assume the string is not numeric
// for negative classes
if ($ast->getId() === '#negativeclass') {
$isNumeric = TrinaryLogic::createNo();
$walkResult = $walkResult->numeric(TrinaryLogic::createNo());
}

foreach ($children as $child) {
$this->walkGroupAst(
$walkResult = $this->walkGroupAst(
$child,
$inAlternation,
$isNonEmpty,
$isNonFalsy,
$isNumeric,
$inOptionalQuantification,
$onlyLiterals,
$inClass,
$patternModifiers,
$walkResult,
);
}

return $walkResult;
}

private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool
Expand Down
121 changes: 121 additions & 0 deletions src/Type/Regex/RegexGroupWalkResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Regex;

use PHPStan\TrinaryLogic;

/** @immutable */
final class RegexGroupWalkResult
{

/**
* @param array<string>|null $onlyLiterals
*/
public function __construct(
private bool $inOptionalQuantification,
private ?array $onlyLiterals,
private TrinaryLogic $isNonEmpty,
private TrinaryLogic $isNonFalsy,
private TrinaryLogic $isNumeric,
)
{
}

public static function createEmpty(): self
{
return new self(
false,
[],
TrinaryLogic::createMaybe(),
TrinaryLogic::createMaybe(),
TrinaryLogic::createMaybe(),
);
}

public function inOptionalQuantification(bool $inOptionalQuantification): self
{
return new self(
$inOptionalQuantification,
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isNumeric,
);
}

/**
* @param array<string>|null $onlyLiterals
*/
public function onlyLiterals(?array $onlyLiterals): self
{
return new self(
$this->inOptionalQuantification,
$onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isNumeric,
);
}

public function nonEmpty(TrinaryLogic $nonEmpty): self
{
return new self(
$this->inOptionalQuantification,
$this->onlyLiterals,
$nonEmpty,
$this->isNonFalsy,
$this->isNumeric,
);
}

public function nonFalsy(TrinaryLogic $nonFalsy): self
{
return new self(
$this->inOptionalQuantification,
$this->onlyLiterals,
$this->isNonEmpty,
$nonFalsy,
$this->isNumeric,
);
}

public function numeric(TrinaryLogic $numeric): self
{
return new self(
$this->inOptionalQuantification,
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$numeric,
);
}

public function isInOptionalQuantification(): bool
{
return $this->inOptionalQuantification;
}

/**
* @return array<string>|null
*/
public function getOnlyLiterals(): ?array
{
return $this->onlyLiterals;
}

public function isNonEmpty(): TrinaryLogic
{
return $this->isNonEmpty;
}

public function isNonFalsy(): TrinaryLogic
{
return $this->isNonFalsy;
}

public function isNumeric(): TrinaryLogic
{
return $this->isNumeric;
}

}
Loading