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
58 changes: 50 additions & 8 deletions src/Type/Php/RegexGroupParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
use PHPStan\Type\TypeCombinator;
use function array_key_exists;
use function count;
use function implode;
use function in_array;
use function is_int;
use function rtrim;
Expand Down Expand Up @@ -284,10 +283,22 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
$inOptionalQuantification = false;
$onlyLiterals = [];

$this->walkGroupAst($group, $isNonEmpty, $isNumeric, $inOptionalQuantification, $onlyLiterals);
$this->walkGroupAst(
$group,
$isNonEmpty,
$isNumeric,
$inOptionalQuantification,
$onlyLiterals,
false,
);

if ($maybeConstant && $onlyLiterals !== null && $onlyLiterals !== []) {
return new ConstantStringType(implode('', $onlyLiterals));
$result = [];
foreach ($onlyLiterals as $literal) {
$result[] = new ConstantStringType($literal);

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

if ($isNumeric->yes()) {
Expand All @@ -306,7 +317,14 @@ private function createGroupType(TreeNode $group, bool $maybeConstant): Type
/**
* @param array<string>|null $onlyLiterals
*/
private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryLogic &$isNumeric, bool &$inOptionalQuantification, ?array &$onlyLiterals): void
private function walkGroupAst(
TreeNode $ast,
TrinaryLogic &$isNonEmpty,
TrinaryLogic &$isNumeric,
bool &$inOptionalQuantification,
?array &$onlyLiterals,
bool $inClass,
): void
{
$children = $ast->getChildren();

Expand All @@ -327,8 +345,24 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
}

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

$newLiterals = [];
foreach ($children as $child) {
$oldLiterals = $onlyLiterals;

if ($child->getId() === 'token') {
$this->getLiteralValue($child, $oldLiterals, true);
}

foreach ($oldLiterals ?? [] as $oldLiteral) {
$newLiterals[] = $oldLiteral;
}
}
$onlyLiterals = $newLiterals;
} elseif ($ast->getId() === 'token') {
$literalValue = $this->getLiteralValue($ast, $onlyLiterals);
$literalValue = $this->getLiteralValue($ast, $onlyLiterals, !$inClass);
if ($literalValue !== null) {
if (Strings::match($literalValue, '/^\d+$/') === null) {
$isNumeric = TrinaryLogic::createNo();
Expand Down Expand Up @@ -360,14 +394,15 @@ private function walkGroupAst(TreeNode $ast, TrinaryLogic &$isNonEmpty, TrinaryL
$isNumeric,
$inOptionalQuantification,
$onlyLiterals,
$inClass,
);
}
}

/**
* @param array<string>|null $onlyLiterals
*/
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals, bool $appendLiterals): ?string
{
if ($node->getId() !== 'token') {
return null;
Expand All @@ -381,11 +416,18 @@ private function getLiteralValue(TreeNode $node, ?array &$onlyLiterals): ?string
if (strlen($value) > 1 && $value[0] === '\\') {
return substr($value, 1);
} elseif (
$token === 'literal'
$appendLiterals
&& $token === 'literal'
&& $onlyLiterals !== null
&& !in_array($value, ['.'], true)
) {
$onlyLiterals[] = $value;
if ($onlyLiterals === []) {
$onlyLiterals = [$value];
} else {
foreach ($onlyLiterals as &$literal) {
$literal .= $value;
}
}
}

return $value;
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-11311.php
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ function (string $s): void {

function (string $s): void {
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches, PREG_UNMATCHED_AS_NULL) === 1) {
assertType('array{string, non-empty-string|null, non-empty-string}', $matches);
assertType("array{string, non-empty-string|null, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches);
}
};

Expand Down
40 changes: 35 additions & 5 deletions tests/PHPStan/Analyser/nsrt/preg_match_shapes.php
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ function (string $s, $mixed): void {

function (string $s): void {
if (preg_match('/^%([0-9]*\$)?[0-9]*\.?[0-9]*([sbdeEfFgGhHouxX])$/', $s, $matches) === 1) {
assertType('array{string, string, non-empty-string}', $matches);
assertType("array{string, string, 'b'|'d'|'E'|'e'|'F'|'f'|'G'|'g'|'H'|'h'|'o'|'s'|'u'|'X'|'x'}", $matches);
}
};

Expand All @@ -453,13 +453,13 @@ function (string $s): void {

function (string $s): void {
if (preg_match('~^([157])$~', $s, $matches) === 1) {
assertType("array{string, numeric-string}", $matches);
assertType("array{string, '1'|'5'|'7'}", $matches);
}
};

function (string $s): void {
if (preg_match('~^([157XY])$~', $s, $matches) === 1) {
assertType("array{string, non-empty-string}", $matches);
assertType("array{string, '1'|'5'|'7'|'X'|'Y'}", $matches);
}
};

Expand Down Expand Up @@ -519,10 +519,10 @@ function bug11323(string $s): void {
assertType('array{string, non-empty-string}', $matches);
}
if (preg_match("{([\r\n]+)(\n)([\n])}", $s, $matches)) {
assertType('array{string, non-empty-string, "\n", non-empty-string}', $matches);
assertType('array{string, non-empty-string, "\n", "\n"}', $matches);
}
if (preg_match('/foo(*:first)|bar(*:second)([x])/', $s, $matches)) {
assertType("array{0: string, 1?: non-empty-string, MARK?: 'first'|'second'}", $matches);
assertType("array{0: string, 1?: 'x', MARK?: 'first'|'second'}", $matches);
}
}

Expand Down Expand Up @@ -570,3 +570,33 @@ function (string $s): void {
assertType("array{0: string, 1: non-empty-string, 2?: numeric-string, 3?: 'x'}", $matches);
}
};

function (string $s): void {
if (preg_match('/Price: ([a-z])/i', $s, $matches)) {
assertType("array{string, non-empty-string}", $matches);
}
};

function (string $s): void {
if (preg_match('/Price: ([0-9])/i', $s, $matches)) {
assertType("array{string, numeric-string}", $matches);
}
};

function (string $s): void {
if (preg_match('/Price: ([xXa])/i', $s, $matches)) {
assertType("array{string, 'a'|'X'|'x'}", $matches);
}
};

function (string $s): void {
if (preg_match('/Price: (ba[rz])/i', $s, $matches)) {
assertType("array{string, 'bar'|'baz'}", $matches);
Copy link

Choose a reason for hiding this comment

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

i modifier must be honored, ie. 'baR' must be present in the expected type

Copy link
Member

Choose a reason for hiding this comment

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

At that point I'd fall back to not have literal strings at all. Too many combinations -> performance problems

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, had the same thing in mind. Will fix.

Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fix in #3288

}
};

function (string $s): void {
if (preg_match('/Price: (b[ao][mn])/i', $s, $matches)) {
assertType("array{string, 'bam'|'ban'|'bom'|'bon'}", $matches);
}
};
Loading