Skip to content

Commit 3e68a5d

Browse files
committed
Understand the exact array shape coming from Nette\Utils\Strings::match() based on pattern
1 parent 9eebad8 commit 3e68a5d

5 files changed

+128
-1
lines changed

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
],
88
"require": {
99
"php": "^7.2 || ^8.0",
10-
"phpstan/phpstan": "^1.10"
10+
"phpstan/phpstan": "^1.11.6"
1111
},
1212
"conflict": {
1313
"nette/application": "<2.3.0",

extension.neon

+7
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ parameters:
5252
- terminate
5353
- forward
5454

55+
conditionalTags:
56+
PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension:
57+
phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches%
58+
5559
services:
5660
-
5761
class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension
@@ -114,3 +118,6 @@ services:
114118
class: PHPStan\Rule\Nette\PresenterInjectedPropertiesExtension
115119
tags:
116120
- phpstan.properties.readWriteExtension
121+
122+
-
123+
class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Nette;
4+
5+
use Nette\Utils\Strings;
6+
use PhpParser\Node\Expr\StaticCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\TrinaryLogic;
10+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
11+
use PHPStan\Type\NullType;
12+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
13+
use PHPStan\Type\Type;
14+
use PHPStan\Type\TypeCombinator;
15+
16+
class StringsMatchDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
17+
{
18+
19+
/** @var RegexArrayShapeMatcher */
20+
private $regexArrayShapeMatcher;
21+
22+
public function __construct(RegexArrayShapeMatcher $regexArrayShapeMatcher)
23+
{
24+
$this->regexArrayShapeMatcher = $regexArrayShapeMatcher;
25+
}
26+
27+
public function getClass(): string
28+
{
29+
return Strings::class;
30+
}
31+
32+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
33+
{
34+
return $methodReflection->getName() === 'match';
35+
}
36+
37+
public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type
38+
{
39+
$args = $methodCall->getArgs();
40+
$patternArg = $args[1] ?? null;
41+
if ($patternArg === null) {
42+
return null;
43+
}
44+
45+
$patternType = $scope->getType($patternArg->value);
46+
$flagsArg = $args[2] ?? null;
47+
$flagsType = null;
48+
if ($flagsArg !== null) {
49+
$flagsType = $scope->getType($flagsArg->value);
50+
}
51+
52+
$arrayShape = $this->regexArrayShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createYes());
53+
if ($arrayShape === null) {
54+
return null;
55+
}
56+
57+
return TypeCombinator::union($arrayShape, new NullType());
58+
}
59+
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Nette;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class StringsMatchDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
10+
/**
11+
* @return iterable<string, mixed[]>
12+
*/
13+
public function dataFileAsserts(): iterable
14+
{
15+
yield from $this->gatherAssertTypes(__DIR__ . '/data/strings-match.php');
16+
}
17+
18+
/**
19+
* @dataProvider dataFileAsserts
20+
* @param mixed ...$args
21+
*/
22+
public function testFileAsserts(
23+
string $assertType,
24+
string $file,
25+
...$args
26+
): void
27+
{
28+
$this->assertFileAsserts($assertType, $file, ...$args);
29+
}
30+
31+
public static function getAdditionalConfigFiles(): array
32+
{
33+
return [
34+
'phar://' . __DIR__ . '/../../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon',
35+
__DIR__ . '/phpstan.neon',
36+
];
37+
}
38+
39+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace StringsMatch;
4+
5+
use Nette\Utils\Strings;
6+
use function PHPStan\Testing\assertType;
7+
use const PREG_OFFSET_CAPTURE;
8+
9+
function (string $s): void {
10+
$result = Strings::match($s, '/%env\((.*)\:.*\)%/U');
11+
assertType('array{string, string}|null', $result);
12+
13+
$result = Strings::match($s, '/%env\((.*)\:.*\)%/U');
14+
assertType('array{string, string}|null', $result);
15+
16+
$result = Strings::match($s, '/(foo)(bar)(baz)/', PREG_OFFSET_CAPTURE);
17+
assertType('array{array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}}|null', $result);
18+
19+
$result = Strings::match($s, '/(foo)(bar)(baz)/');
20+
assertType('array{string, string, string, string}|null', $result);
21+
};

0 commit comments

Comments
 (0)