Skip to content

Commit 6e6c529

Browse files
staabmSeldaek
authored andcommitted
reflect flags behaviour
1 parent 50d11a3 commit 6e6c529

File tree

4 files changed

+118
-88
lines changed

4 files changed

+118
-88
lines changed

src/PHPStan/PregMatchFlags.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composer\Pcre\PHPStan;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\Type\Constant\ConstantIntegerType;
7+
use PHPStan\Type\TypeCombinator;
8+
use PHPStan\Type\Type;
9+
10+
final class PregMatchFlags
11+
{
12+
static public function getType(?Arg $flagsArg, Scope $scope): ?Type
13+
{
14+
if ($flagsArg === null) {
15+
return new ConstantIntegerType(PREG_UNMATCHED_AS_NULL);
16+
}
17+
18+
$flagsType = $scope->getType($flagsArg->value);
19+
20+
$constantScalars = $flagsType->getConstantScalarValues();
21+
if ($constantScalars === []) {
22+
return null;
23+
}
24+
25+
$internalFlagsTypes = [];
26+
foreach ($flagsType->getConstantScalarValues() as $constantScalarValue) {
27+
if (!is_int($constantScalarValue)) {
28+
return null;
29+
}
30+
31+
$internalFlagsTypes[] = $constantScalarValue | PREG_UNMATCHED_AS_NULL;
32+
}
33+
return TypeCombinator::union(...$internalFlagsTypes);
34+
}
35+
}
Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,57 @@
1-
<?php declare(strict_types = 1);
1+
<?php declare(strict_types=1);
22

33
namespace Composer\Pcre\PHPStan;
44

55
use Composer\Pcre\Preg;
6-
use PhpParser\Node\Expr\FuncCall;
7-
use PhpParser\Node\Expr\MethodCall;
86
use PhpParser\Node\Expr\StaticCall;
97
use PHPStan\Analyser\Scope;
10-
use PHPStan\Reflection\FunctionReflection;
118
use PHPStan\Reflection\MethodReflection;
129
use PHPStan\Reflection\ParameterReflection;
1310
use PHPStan\TrinaryLogic;
14-
use PHPStan\Type\MethodParameterOutTypeExtension;
1511
use PHPStan\Type\Php\RegexArrayShapeMatcher;
1612
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
1713
use PHPStan\Type\Type;
18-
use function in_array;
19-
use function strtolower;
2014

2115
final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension
2216
{
2317

2418
private RegexArrayShapeMatcher $regexShapeMatcher;
25-
public function __construct(
26-
RegexArrayShapeMatcher $regexShapeMatcher
27-
)
28-
{
19+
20+
public function __construct(
21+
RegexArrayShapeMatcher $regexShapeMatcher
22+
)
23+
{
2924
$this->regexShapeMatcher = $regexShapeMatcher;
30-
}
25+
}
3126

3227
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
3328
{
3429
return
3530
$methodReflection->getDeclaringClass()->getName() === Preg::class
3631
&& $methodReflection->getName() === 'match'
37-
&& $parameter->getName() === 'matches'
38-
;
32+
&& $parameter->getName() === 'matches';
3933
}
4034

4135
public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
4236
{
43-
$args = $methodCall->getArgs();
44-
$patternArg = $args[0] ?? null;
45-
$matchesArg = $args[2] ?? null;
46-
$flagsArg = $args[3] ?? null;
47-
48-
if (
49-
$patternArg === null || $matchesArg === null
50-
) {
51-
return null;
52-
}
53-
54-
$patternType = $scope->getType($patternArg->value);
55-
$flagsType = null;
56-
if ($flagsArg !== null) {
57-
$flagsType = $scope->getType($flagsArg->value);
58-
}
59-
60-
return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe());
61-
}
37+
$args = $methodCall->getArgs();
38+
$patternArg = $args[0] ?? null;
39+
$matchesArg = $args[2] ?? null;
40+
$flagsArg = $args[3] ?? null;
41+
42+
if (
43+
$patternArg === null || $matchesArg === null
44+
) {
45+
return null;
46+
}
47+
48+
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
49+
if ($flagsType === null) {
50+
return null;
51+
}
52+
$patternType = $scope->getType($patternArg->value);
53+
54+
return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe());
55+
}
6256

6357
}
Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
1-
<?php declare(strict_types = 1);
1+
<?php declare(strict_types=1);
22

33
namespace Composer\Pcre\PHPStan;
44

55
use Composer\Pcre\Preg;
6-
use PhpParser\Node\Expr\FuncCall;
7-
use PhpParser\Node\Expr\MethodCall;
86
use PhpParser\Node\Expr\StaticCall;
97
use PHPStan\Analyser\Scope;
108
use PHPStan\Analyser\SpecifiedTypes;
119
use PHPStan\Analyser\TypeSpecifier;
1210
use PHPStan\Analyser\TypeSpecifierAwareExtension;
1311
use PHPStan\Analyser\TypeSpecifierContext;
14-
use PHPStan\Reflection\FunctionReflection;
1512
use PHPStan\Reflection\MethodReflection;
1613
use PHPStan\TrinaryLogic;
17-
use PHPStan\Type\FunctionTypeSpecifyingExtension;
18-
use PHPStan\Type\MethodTypeSpecifyingExtension;
1914
use PHPStan\Type\Php\RegexArrayShapeMatcher;
2015
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
21-
use function in_array;
22-
use function strtolower;
2316

2417
final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
2518
{
2619

27-
private TypeSpecifier $typeSpecifier;
20+
private TypeSpecifier $typeSpecifier;
2821

2922
private RegexArrayShapeMatcher $regexShapeMatcher;
3023

@@ -36,58 +29,59 @@ public function __construct(
3629
}
3730

3831

39-
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
40-
{
41-
$this->typeSpecifier = $typeSpecifier;
42-
}
32+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
33+
{
34+
$this->typeSpecifier = $typeSpecifier;
35+
}
4336

44-
public function getClass(): string {
37+
public function getClass(): string
38+
{
4539
return Preg::class;
4640
}
4741

48-
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context) : bool
42+
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context): bool
4943
{
5044
return $methodReflection->getName() === 'match' && !$context->null();
5145
}
5246

53-
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes
47+
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
5448
{
55-
$args = $node->getArgs();
56-
$patternArg = $args[0] ?? null;
57-
$matchesArg = $args[2] ?? null;
58-
$flagsArg = $args[3] ?? null;
59-
60-
if (
61-
$patternArg === null || $matchesArg === null
62-
) {
63-
return new SpecifiedTypes();
64-
}
65-
66-
$patternType = $scope->getType($patternArg->value);
67-
$flagsType = null;
68-
if ($flagsArg !== null) {
69-
$flagsType = $scope->getType($flagsArg->value);
70-
}
71-
72-
$matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true()));
73-
if ($matchedType === null) {
74-
return new SpecifiedTypes();
75-
}
76-
77-
$overwrite = false;
78-
if ($context->false()) {
79-
$overwrite = true;
80-
$context = $context->negate();
81-
}
82-
83-
return $this->typeSpecifier->create(
84-
$matchesArg->value,
85-
$matchedType,
86-
$context,
87-
$overwrite,
88-
$scope,
89-
$node,
90-
);
91-
}
49+
$args = $node->getArgs();
50+
$patternArg = $args[0] ?? null;
51+
$matchesArg = $args[2] ?? null;
52+
$flagsArg = $args[3] ?? null;
53+
54+
if (
55+
$patternArg === null || $matchesArg === null
56+
) {
57+
return new SpecifiedTypes();
58+
}
59+
60+
$flagsType = PregMatchFlags::getType($flagsArg, $scope);
61+
if ($flagsType === null) {
62+
return new SpecifiedTypes();
63+
}
64+
$patternType = $scope->getType($patternArg->value);
65+
66+
$matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true()));
67+
if ($matchedType === null) {
68+
return new SpecifiedTypes();
69+
}
70+
71+
$overwrite = false;
72+
if ($context->false()) {
73+
$overwrite = true;
74+
$context = $context->negate();
75+
}
76+
77+
return $this->typeSpecifier->create(
78+
$matchesArg->value,
79+
$matchedType,
80+
$context,
81+
$overwrite,
82+
$scope,
83+
$node,
84+
);
85+
}
9286

9387
}

tests/PHPStanTests/nsrt/preg-match.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@ function doMatch(string $s): void
1515
assertType('array{}|array{string}', $matches);
1616

1717
if (Preg::match('/Price: (£|€)\d+/', $s, $matches)) {
18-
assertType('array{string, string}', $matches);
18+
assertType('array{string, string|null}', $matches);
1919
} else {
2020
assertType('array{}', $matches);
2121
}
22-
assertType('array{}|array{string, string}', $matches);
22+
assertType('array{}|array{string, string|null}', $matches);
23+
24+
if (Preg::match('/Price: (£|€)?\d+/', $s, $matches)) {
25+
assertType('array{0: string, 1?: string|null}', $matches);
26+
} else {
27+
assertType('array{}', $matches);
28+
}
29+
assertType('array{}|array{0: string, 1?: string|null}', $matches);
2330
}
2431

2532
function identicalMatch(string $s): void

0 commit comments

Comments
 (0)