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
9 changes: 0 additions & 9 deletions src/Type/Regex/RegexExpressionHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function ltrim;
use function str_ends_with;
use function str_starts_with;
use function strrpos;
use function substr;

Expand Down Expand Up @@ -91,13 +89,6 @@ public function getPatternModifiers(string $pattern): ?string
return substr($pattern, $endDelimiterPos + 1);
}

public function isAnchoredPattern(string $pattern): bool
{
$cleanedPattern = $this->removeDelimitersAndModifiers($pattern);

return str_starts_with($cleanedPattern, '^') && str_ends_with($cleanedPattern, '$');
}

public function removeDelimitersAndModifiers(string $pattern): string
{
$pattern = ltrim($pattern);
Expand Down
66 changes: 21 additions & 45 deletions src/Type/Regex/RegexGroupParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
use PHPStan\Php\PhpVersion;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
Expand Down Expand Up @@ -125,14 +125,8 @@
);

if (!$subjectAsGroupResult->mightContainEmptyStringLiteral() && !$this->containsEscapeK($ast)) {
if (
$subjectAsGroupResult->isDecimalInteger()->yes()
&& $this->regexExpressionHelper->isAnchoredPattern($regex)
) {
$astWalkResult = $astWalkResult->withSubjectBaseType(
new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]),
);
} elseif ($subjectAsGroupResult->isNonFalsy()->yes()) {
// we could handle numeric-string, in case we know the regex is delimited by ^ and $
if ($subjectAsGroupResult->isNonFalsy()->yes()) {

Check warning on line 129 in src/Type/Regex/RegexGroupParser.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (!$subjectAsGroupResult->mightContainEmptyStringLiteral() && !$this->containsEscapeK($ast)) { // we could handle numeric-string, in case we know the regex is delimited by ^ and $ - if ($subjectAsGroupResult->isNonFalsy()->yes()) { + if (!$subjectAsGroupResult->isNonFalsy()->no()) { $astWalkResult = $astWalkResult->withSubjectBaseType( new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), );

Check warning on line 129 in src/Type/Regex/RegexGroupParser.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if (!$subjectAsGroupResult->mightContainEmptyStringLiteral() && !$this->containsEscapeK($ast)) { // we could handle numeric-string, in case we know the regex is delimited by ^ and $ - if ($subjectAsGroupResult->isNonFalsy()->yes()) { + if (!$subjectAsGroupResult->isNonFalsy()->no()) { $astWalkResult = $astWalkResult->withSubjectBaseType( new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]), );
$astWalkResult = $astWalkResult->withSubjectBaseType(
new IntersectionType([new StringType(), new AccessoryNonFalsyStringType()]),
);
Expand Down Expand Up @@ -426,16 +420,16 @@
return TypeCombinator::union(...$result);
}

if ($walkResult->isDecimalInteger()->yes()) {
if ($walkResult->isNumeric()->yes()) {

Check warning on line 423 in src/Type/Regex/RegexGroupParser.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return TypeCombinator::union(...$result); } - if ($walkResult->isNumeric()->yes()) { + if (!$walkResult->isNumeric()->no()) { if ($walkResult->isNonFalsy()->yes()) { return new IntersectionType([ new StringType(),

Check warning on line 423 in src/Type/Regex/RegexGroupParser.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ return TypeCombinator::union(...$result); } - if ($walkResult->isNumeric()->yes()) { + if (!$walkResult->isNumeric()->no()) { if ($walkResult->isNonFalsy()->yes()) { return new IntersectionType([ new StringType(),
if ($walkResult->isNonFalsy()->yes()) {
return new IntersectionType([
new StringType(),
new AccessoryDecimalIntegerStringType(),
new AccessoryNumericStringType(),
new AccessoryNonFalsyStringType(),
]);
}

$result = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]);
$result = new IntersectionType([new StringType(), new AccessoryNumericStringType()]);
if (!$walkResult->isNonEmpty()->yes()) {
return new UnionType([new ConstantStringType(''), $result]);
}
Expand Down Expand Up @@ -488,17 +482,12 @@
$meaningfulTokens = 0;
foreach ($children as $child) {
$nonFalsy = false;
$isNonDecimal = false;
if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy, $isNonDecimal)) {
if ($this->isMaybeEmptyNode($child, $patternModifiers, $nonFalsy)) {
continue;
}

$meaningfulTokens++;

if ($isNonDecimal) {
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo());
}

if (!$nonFalsy) {
continue;
}
Expand Down Expand Up @@ -552,19 +541,10 @@
$walkResult = $walkResult->onlyLiterals($onlyLiterals);

if ($literalValue !== null) {
if (Strings::match($literalValue, '/^\d+$/') !== null) {
if ($walkResult->isDecimalInteger()->maybe()) {
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createYes());
}
} elseif (
$literalValue === '-'
&& $walkResult->isDecimalInteger()->maybe()
&& !$walkResult->hasSeenDecimalIntegerSign()
) {
// a single leading minus sign keeps the string a decimal integer (e.g. "-1")
$walkResult = $walkResult->seenDecimalIntegerSign(true);
} elseif ($literalValue !== '') {
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo());
if (Strings::match($literalValue, '/^\d+$/') === null) {
$walkResult = $walkResult->numeric(TrinaryLogic::createNo());
} elseif ($walkResult->isNumeric()->maybe()) {
$walkResult = $walkResult->numeric(TrinaryLogic::createYes());
}

if (!$walkResult->isInOptionalQuantification() && $literalValue !== '') {
Expand All @@ -583,7 +563,7 @@
$newLiterals = [];
$nonEmpty = TrinaryLogic::createYes();
$nonFalsy = TrinaryLogic::createYes();
$decimalInteger = TrinaryLogic::createYes();
$numeric = TrinaryLogic::createYes();
foreach ($children as $child) {
$childResult = $this->walkGroupAst(
$child,
Expand All @@ -592,13 +572,12 @@
$walkResult->onlyLiterals([])
->nonEmpty(TrinaryLogic::createMaybe())
->nonFalsy(TrinaryLogic::createMaybe())
->decimalInteger(TrinaryLogic::createMaybe())
->seenDecimalIntegerSign(false),
->numeric(TrinaryLogic::createMaybe()),
);

$nonEmpty = $nonEmpty->and($childResult->isNonEmpty());
$nonFalsy = $nonFalsy->and($childResult->isNonFalsy());
$decimalInteger = $decimalInteger->and($childResult->isDecimalInteger());
$numeric = $numeric->and($childResult->isNumeric());

if ($newLiterals === null) {
continue;
Expand All @@ -617,14 +596,14 @@
->onlyLiterals($newLiterals)
->nonEmpty($walkResult->isNonEmpty()->or($nonEmpty))
->nonFalsy($walkResult->isNonFalsy()->or($nonFalsy))
->decimalInteger(TrinaryLogic::maxMin($walkResult->isDecimalInteger(), $decimalInteger));
->numeric($walkResult->isNumeric()->and($numeric));
}

// [^0-9] should not parse as decimal-int-string, and [^list-everything-but-numbers] is technically
// doable but really silly compared to just \d so we can safely assume the string is not a decimal
// integer for negative classes
// [^0-9] should not parse as numeric-string, and [^list-everything-but-numbers] is technically
// 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') {
$walkResult = $walkResult->decimalInteger(TrinaryLogic::createNo());
$walkResult = $walkResult->numeric(TrinaryLogic::createNo());
}

foreach ($children as $child) {
Expand All @@ -639,7 +618,7 @@
return $walkResult;
}

private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy, bool &$isNonDecimal): bool
private function isMaybeEmptyNode(TreeNode $node, string $patternModifiers, bool &$isNonFalsy): bool
{
if ($node->getId() === '#quantification') {
[$min] = $this->getQuantificationRange($node);
Expand All @@ -658,14 +637,11 @@
if ($literal !== '' && $literal !== '0') {
$isNonFalsy = true;
}
if (Strings::match($literal, '/^\d+$/') === null) {
$isNonDecimal = true;
}
return $literal === '';
}

foreach ($node->getChildren() as $child) {
if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy, $isNonDecimal)) {
if (!$this->isMaybeEmptyNode($child, $patternModifiers, $isNonFalsy)) {
return false;
}
}
Expand Down
43 changes: 9 additions & 34 deletions src/Type/Regex/RegexGroupWalkResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ public function __construct(
private ?array $onlyLiterals,
private TrinaryLogic $isNonEmpty,
private TrinaryLogic $isNonFalsy,
private TrinaryLogic $isDecimalInteger,
private bool $seenDecimalIntegerSign,
private TrinaryLogic $isNumeric,
)
{
}
Expand All @@ -30,7 +29,6 @@ public static function createEmpty(): self
TrinaryLogic::createMaybe(),
TrinaryLogic::createMaybe(),
TrinaryLogic::createMaybe(),
false,
);
}

Expand All @@ -41,8 +39,7 @@ public function inOptionalQuantification(bool $inOptionalQuantification): self
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
$this->isNumeric,
);
}

Expand All @@ -56,8 +53,7 @@ public function onlyLiterals(?array $onlyLiterals): self
$onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
$this->isNumeric,
);
}

Expand All @@ -68,8 +64,7 @@ public function nonEmpty(TrinaryLogic $nonEmpty): self
$this->onlyLiterals,
$nonEmpty,
$this->isNonFalsy,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
$this->isNumeric,
);
}

Expand All @@ -80,33 +75,18 @@ public function nonFalsy(TrinaryLogic $nonFalsy): self
$this->onlyLiterals,
$this->isNonEmpty,
$nonFalsy,
$this->isDecimalInteger,
$this->seenDecimalIntegerSign,
$this->isNumeric,
);
}

/** A decimal integer string is composed only of digits, optionally preceded by a single leading minus sign. */
public function decimalInteger(TrinaryLogic $decimalInteger): self
public function numeric(TrinaryLogic $numeric): self
{
return new self(
$this->inOptionalQuantification,
$this->onlyLiterals,
$this->isNonEmpty,
$this->isNonFalsy,
$decimalInteger,
$this->seenDecimalIntegerSign,
);
}

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

Expand Down Expand Up @@ -147,14 +127,9 @@ public function isNonFalsy(): TrinaryLogic
return $this->isNonFalsy;
}

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

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

}
18 changes: 9 additions & 9 deletions tests/PHPStan/Analyser/nsrt/bug-11293.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,54 +9,54 @@ class HelloWorld
public function sayHello(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) > 0) {
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
}
}

public function sayHello2(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) === 1) {
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
}
}

public function sayHello3(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) >= 1) {
assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
}
}

public function sayHello4(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) <= 0) {
assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches);

return;
}

assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
}

public function sayHello5(string $s): void
{
if (preg_match('/data-(\d{6})\.json$/', $s, $matches) < 1) {
assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches);

return;
}

assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
}

public function sayHello6(string $s): void
{
if (1 > preg_match('/data-(\d{6})\.json$/', $s, $matches)) {
assertType('array{}|array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{}|array{non-falsy-string, non-falsy-string&numeric-string}', $matches);

return;
}

assertType('array{non-falsy-string, decimal-int-string&non-falsy-string}', $matches);
assertType('array{non-falsy-string, non-falsy-string&numeric-string}', $matches);
}
}
Loading
Loading