Skip to content

Commit 71cb3dc

Browse files
committed
Type hints for Safe\preg_match, fixes #40
1 parent b35841a commit 71cb3dc

File tree

4 files changed

+108
-0
lines changed

4 files changed

+108
-0
lines changed

phpstan-safe-rule.neon

+4
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ services:
1111
class: TheCodingMachine\Safe\PHPStan\Type\Php\ReplaceSafeFunctionsDynamicReturnTypeExtension
1212
tags:
1313
- phpstan.broker.dynamicFunctionReturnTypeExtension
14+
-
15+
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchParameterOutTypeExtension
16+
tags:
17+
- phpstan.functionParameterOutTypeExtension
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php declare(strict_types = 1);
2+
3+
/*
4+
Blatantly copy-pasted from PHPStan's source code but with isFunctionSupported changed
5+
6+
https://github.com/phpstan/phpstan-src/blob/e664bed7b62e2a58d571fb631ddf47030914a2b5/src/Type/Php/PregMatchParameterOutTypeExtension.php
7+
*/
8+
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
9+
10+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
11+
use PhpParser\Node\Expr\FuncCall;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Reflection\FunctionReflection;
14+
use PHPStan\Reflection\ParameterReflection;
15+
use PHPStan\TrinaryLogic;
16+
use PHPStan\Type\FunctionParameterOutTypeExtension;
17+
use PHPStan\Type\Type;
18+
use function in_array;
19+
20+
final class PregMatchParameterOutTypeExtension implements FunctionParameterOutTypeExtension
21+
{
22+
23+
public function __construct(
24+
private RegexArrayShapeMatcher $regexShapeMatcher,
25+
) {
26+
}
27+
28+
public function isFunctionSupported(FunctionReflection $functionReflection, ParameterReflection $parameter): bool
29+
{
30+
return in_array($functionReflection->getName(), ['Safe\preg_match', 'Safe\preg_match_all'], true)
31+
// the parameter is named different, depending on PHP version.
32+
&& in_array($parameter->getName(), ['subpatterns', 'matches'], true);
33+
}
34+
35+
public function getParameterOutTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $funcCall, ParameterReflection $parameter, Scope $scope): ?Type
36+
{
37+
$args = $funcCall->getArgs();
38+
$patternArg = $args[0] ?? null;
39+
$matchesArg = $args[2] ?? null;
40+
$flagsArg = $args[3] ?? null;
41+
42+
if ($patternArg === null || $matchesArg === null
43+
) {
44+
return null;
45+
}
46+
47+
$flagsType = null;
48+
if ($flagsArg !== null) {
49+
$flagsType = $scope->getType($flagsArg->value);
50+
}
51+
52+
if ($functionReflection->getName() === 'Safe\preg_match') {
53+
return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
54+
}
55+
return $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope);
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
4+
5+
use PHPStan\Testing\TypeInferenceTestCase;
6+
7+
class PregMatchParameterOutTypeExtensionTest extends TypeInferenceTestCase
8+
{
9+
/**
10+
* @return iterable<mixed>
11+
*/
12+
public static function dataFileAsserts(): iterable
13+
{
14+
yield from self::gatherAssertTypes(__DIR__ . '/data/preg.php');
15+
}
16+
17+
/**
18+
* @dataProvider dataFileAsserts
19+
*/
20+
public function testFileAsserts(
21+
string $assertType,
22+
string $file,
23+
mixed ...$args
24+
): void {
25+
$this->assertFileAsserts($assertType, $file, ...$args);
26+
}
27+
28+
public static function getAdditionalConfigFiles(): array
29+
{
30+
return [__DIR__ . '/../../../phpstan-safe-rule.neon'];
31+
}
32+
}

tests/Type/Php/data/preg.php

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace TheCodingMachine\Safe\PHPStan\Type\Php\data;
4+
5+
// Checking that preg_match and Safe\preg_match are equivalent
6+
$pattern = '/H(.)ll(o) (World)?/';
7+
$string = 'Hello World';
8+
$type = "array{0?: string, 1?: non-empty-string, 2?: 'o', 3?: 'World'}";
9+
10+
// @phpstan-ignore-next-line - use of unsafe is intentional
11+
\preg_match($pattern, $string, $matches);
12+
\PHPStan\Testing\assertType($type, $matches);
13+
14+
\Safe\preg_match($pattern, $string, $matches);
15+
\PHPStan\Testing\assertType($type, $matches);

0 commit comments

Comments
 (0)