Skip to content

Commit ea83653

Browse files
kamil-zacekondrejmirtes
authored andcommitted
String::match / String::matchAll support for nette 4
1 parent 32412c9 commit ea83653

File tree

3 files changed

+91
-11
lines changed

3 files changed

+91
-11
lines changed

src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,21 @@
33
namespace PHPStan\Type\Nette;
44

55
use Nette\Utils\Strings;
6+
use PhpParser\Node\Arg;
67
use PhpParser\Node\Expr\StaticCall;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Reflection\MethodReflection;
910
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
1013
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
1114
use PHPStan\Type\Php\RegexArrayShapeMatcher;
1215
use PHPStan\Type\Type;
16+
use function array_key_exists;
17+
use const PREG_OFFSET_CAPTURE;
18+
use const PREG_PATTERN_ORDER;
19+
use const PREG_SET_ORDER;
20+
use const PREG_UNMATCHED_AS_NULL;
1321

1422
class StringsMatchAllDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
1523
{
@@ -36,17 +44,47 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
3644
{
3745
$args = $methodCall->getArgs();
3846
$patternArg = $args[1] ?? null;
47+
3948
if ($patternArg === null) {
4049
return null;
4150
}
4251

43-
$flagsArg = $args[2] ?? null;
44-
$flagsType = null;
45-
if ($flagsArg !== null) {
46-
$flagsType = $scope->getType($flagsArg->value);
52+
return $this->regexArrayShapeMatcher->matchAllExpr(
53+
$patternArg->value,
54+
$this->resolveFlagsType($args, $scope),
55+
TrinaryLogic::createYes(),
56+
$scope
57+
);
58+
}
59+
60+
/**
61+
* @param array<Arg> $args
62+
*/
63+
private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType
64+
{
65+
if (!array_key_exists(2, $args)) {
66+
return new ConstantIntegerType(PREG_SET_ORDER);
67+
}
68+
69+
$captureOffsetType = $scope->getType($args[2]->value);
70+
71+
if ($captureOffsetType instanceof ConstantIntegerType) {
72+
$captureOffset = $captureOffsetType->getValue();
73+
$flags = ($captureOffset & PREG_PATTERN_ORDER) === PREG_PATTERN_ORDER ? $captureOffset : ($captureOffset | PREG_SET_ORDER);
74+
75+
return new ConstantIntegerType($flags);
4776
}
4877

49-
return $this->regexArrayShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
78+
$unmatchedAsNullType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false);
79+
$patternOrderType = array_key_exists(5, $args) ? $scope->getType($args[5]->value) : new ConstantBooleanType(false);
80+
81+
$captureOffset = $captureOffsetType->isTrue()->yes();
82+
$unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes();
83+
$patternOrder = $patternOrderType->isTrue()->yes();
84+
85+
return new ConstantIntegerType(
86+
($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0)
87+
);
5088
}
5189

5290
}

src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
namespace PHPStan\Type\Nette;
44

55
use Nette\Utils\Strings;
6+
use PhpParser\Node\Arg;
67
use PhpParser\Node\Expr\StaticCall;
78
use PHPStan\Analyser\Scope;
89
use PHPStan\Reflection\MethodReflection;
910
use PHPStan\TrinaryLogic;
11+
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
1013
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
1114
use PHPStan\Type\NullType;
1215
use PHPStan\Type\Php\RegexArrayShapeMatcher;
1316
use PHPStan\Type\Type;
1417
use PHPStan\Type\TypeCombinator;
18+
use function array_key_exists;
19+
use const PREG_OFFSET_CAPTURE;
20+
use const PREG_UNMATCHED_AS_NULL;
1521

1622
class StringsMatchDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
1723
{
@@ -38,22 +44,48 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection,
3844
{
3945
$args = $methodCall->getArgs();
4046
$patternArg = $args[1] ?? null;
47+
4148
if ($patternArg === null) {
4249
return null;
4350
}
4451

45-
$flagsArg = $args[2] ?? null;
46-
$flagsType = null;
47-
if ($flagsArg !== null) {
48-
$flagsType = $scope->getType($flagsArg->value);
49-
}
52+
$arrayShape = $this->regexArrayShapeMatcher->matchExpr(
53+
$patternArg->value,
54+
$this->resolveFlagsType($args, $scope),
55+
TrinaryLogic::createYes(),
56+
$scope
57+
);
5058

51-
$arrayShape = $this->regexArrayShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope);
5259
if ($arrayShape === null) {
5360
return null;
5461
}
5562

5663
return TypeCombinator::union($arrayShape, new NullType());
5764
}
5865

66+
/**
67+
* @param array<Arg> $args
68+
*/
69+
private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType
70+
{
71+
if (!array_key_exists(2, $args)) {
72+
return new ConstantIntegerType(0);
73+
}
74+
75+
$captureOffsetType = $scope->getType($args[2]->value);
76+
77+
if ($captureOffsetType instanceof ConstantIntegerType) {
78+
return $captureOffsetType;
79+
}
80+
81+
$unmatchedAsNullType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false);
82+
83+
$captureOffset = $captureOffsetType->isTrue()->yes();
84+
$unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes();
85+
86+
return new ConstantIntegerType(
87+
($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0)
88+
);
89+
}
90+
5991
}

tests/Type/Nette/data/strings-match.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,13 @@ function (string $s): void {
3333
$result = Strings::matchAll($s, '/ab(?P<num>\d+)(?P<suffix>ab)?/', PREG_PATTERN_ORDER);
3434
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $result);
3535
};
36+
37+
function (string $s): void {
38+
$result = Strings::matchAll($s, '/ab(?P<num>\d+)(?P<suffix>ab)?/', false, 0, false, true);
39+
assertType("array{0: list<string>, num: list<numeric-string>, 1: list<numeric-string>, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $result);
40+
};
41+
42+
function (string $s): void {
43+
$result = Strings::matchAll($lineContent, '~\[gallery ids=(„|")(?<allIds>([0-9]+,? ?)+)(“|")~');
44+
assertType('list<array{0: string, 1: non-empty-string, allIds: non-empty-string, 2: non-empty-string, 3: non-empty-string, 4: non-empty-string}>', $result);
45+
};

0 commit comments

Comments
 (0)