Skip to content

Commit 4017da4

Browse files
staabmSeldaek
authored andcommitted
Implement array shapes for Preg::match $matches by-ref parameter
1 parent b13ea67 commit 4017da4

File tree

7 files changed

+241
-3
lines changed

7 files changed

+241
-3
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"require-dev": {
2323
"symfony/phpunit-bridge": "^7",
24-
"phpstan/phpstan": "^1.3",
24+
"phpstan/phpstan": "^1.11",
2525
"phpstan/phpstan-strict-rules": "^1.1"
2626
},
2727
"autoload": {

extension.neon

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# composer/pcre PHPStan extensions
2+
#
3+
# These can be reused by third party packages by including 'vendor/composer/pcre/extension.neon'
4+
# in your phpstan config
5+
6+
services:
7+
-
8+
class: Composer\Pcre\PHPStan\PregMatchParameterOutTypeExtension
9+
tags:
10+
- phpstan.staticMethodParameterOutTypeExtension
11+
-
12+
class: Composer\Pcre\PHPStan\PregMatchTypeSpecifyingExtension
13+
tags:
14+
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension

phpstan.neon.dist

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ parameters:
77
reportUnmatchedIgnoredErrors: false
88
treatPhpDocTypesAsCertain: false
99

10-
bootstrapFiles:
11-
- tests/phpstan-locate-phpunit-autoloader.php
10+
ignoreErrors:
11+
- '#Test::data[a-zA-Z0-9_]+\(\) return type has no value type specified in iterable type#'
12+
13+
excludePaths:
14+
- tests/PHPStanTests/nsrt/*
1215

1316
includes:
17+
- extension.neon
1418
- vendor/phpstan/phpstan/conf/bleedingEdge.neon
1519
- vendor/phpstan/phpstan-strict-rules/rules.neon
1620
- phpstan-baseline.neon
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Composer\Pcre\PHPStan;
4+
5+
use Composer\Pcre\Preg;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Reflection\FunctionReflection;
11+
use PHPStan\Reflection\MethodReflection;
12+
use PHPStan\Reflection\ParameterReflection;
13+
use PHPStan\TrinaryLogic;
14+
use PHPStan\Type\MethodParameterOutTypeExtension;
15+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
16+
use PHPStan\Type\StaticMethodParameterOutTypeExtension;
17+
use PHPStan\Type\Type;
18+
use function in_array;
19+
use function strtolower;
20+
21+
final class PregMatchParameterOutTypeExtension implements StaticMethodParameterOutTypeExtension
22+
{
23+
24+
private RegexArrayShapeMatcher $regexShapeMatcher;
25+
public function __construct(
26+
RegexArrayShapeMatcher $regexShapeMatcher
27+
)
28+
{
29+
$this->regexShapeMatcher = $regexShapeMatcher;
30+
}
31+
32+
public function isStaticMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
33+
{
34+
return
35+
$methodReflection->getDeclaringClass()->getName() === Preg::class
36+
&& $methodReflection->getName() === 'match'
37+
&& $parameter->getName() === 'matches'
38+
;
39+
}
40+
41+
public function getParameterOutTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, ParameterReflection $parameter, Scope $scope): ?Type
42+
{
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+
}
62+
63+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Composer\Pcre\PHPStan;
4+
5+
use Composer\Pcre\Preg;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Analyser\SpecifiedTypes;
11+
use PHPStan\Analyser\TypeSpecifier;
12+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
13+
use PHPStan\Analyser\TypeSpecifierContext;
14+
use PHPStan\Reflection\FunctionReflection;
15+
use PHPStan\Reflection\MethodReflection;
16+
use PHPStan\TrinaryLogic;
17+
use PHPStan\Type\FunctionTypeSpecifyingExtension;
18+
use PHPStan\Type\MethodTypeSpecifyingExtension;
19+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
20+
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
21+
use function in_array;
22+
use function strtolower;
23+
24+
final class PregMatchTypeSpecifyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
25+
{
26+
27+
private TypeSpecifier $typeSpecifier;
28+
29+
private RegexArrayShapeMatcher $regexShapeMatcher;
30+
31+
public function __construct(
32+
RegexArrayShapeMatcher $regexShapeMatcher
33+
)
34+
{
35+
$this->regexShapeMatcher = $regexShapeMatcher;
36+
}
37+
38+
39+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
40+
{
41+
$this->typeSpecifier = $typeSpecifier;
42+
}
43+
44+
public function getClass(): string {
45+
return Preg::class;
46+
}
47+
48+
public function isStaticMethodSupported(MethodReflection $methodReflection, StaticCall $node, TypeSpecifierContext $context) : bool
49+
{
50+
return $methodReflection->getName() === 'match' && !$context->null();
51+
}
52+
53+
public function specifyTypes(MethodReflection $methodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context) : SpecifiedTypes
54+
{
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+
}
92+
93+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
/*
4+
* This file is part of composer/pcre.
5+
*
6+
* (c) Composer <https://github.com/composer>
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace Composer\Pcre\PHPStanTests;
13+
14+
use PHPStan\Testing\TypeInferenceTestCase;
15+
16+
class TypeInferenceTest extends TypeInferenceTestCase
17+
{
18+
public function dataFileAsserts(): iterable
19+
{
20+
yield from $this->gatherAssertTypesFromDirectory(__DIR__ . '/nsrt');
21+
22+
}
23+
24+
/**
25+
* @dataProvider dataFileAsserts
26+
* @param mixed ...$args
27+
*/
28+
public function testFileAsserts(
29+
string $assertType,
30+
string $file,
31+
...$args
32+
): void
33+
{
34+
$this->assertFileAsserts($assertType, $file, ...$args);
35+
}
36+
37+
public static function getAdditionalConfigFiles(): array
38+
{
39+
return [
40+
__DIR__ . '/../../extension.neon',
41+
];
42+
}
43+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace PregMatchShapes;
4+
5+
use Composer\Pcre\Preg;
6+
use function PHPStan\Testing\assertType;
7+
8+
function doMatch(string $s): void
9+
{
10+
if (Preg::match('/Price: /i', $s, $matches)) {
11+
assertType('array{string}', $matches);
12+
}
13+
assertType('array{}|array{string}', $matches);
14+
15+
if (Preg::match('/Price: (£|€)\d+/', $s, $matches)) {
16+
assertType('array{string, string}', $matches);
17+
} else {
18+
assertType('array{}', $matches);
19+
}
20+
assertType('array{}|array{string, string}', $matches);
21+
}

0 commit comments

Comments
 (0)