Skip to content

Commit a38d64f

Browse files
committed
additional magic for preg_match, fixes #46
1 parent c55cfa5 commit a38d64f

File tree

3 files changed

+105
-0
lines changed

3 files changed

+105
-0
lines changed

phpstan-safe-rule.neon

+4
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ services:
1515
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchParameterOutTypeExtension
1616
tags:
1717
- phpstan.functionParameterOutTypeExtension
18+
-
19+
class: TheCodingMachine\Safe\PHPStan\Type\Php\PregMatchTypeSpecifyingExtension
20+
tags:
21+
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace TheCodingMachine\Safe\PHPStan\Type\Php;
4+
5+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Analyser\SpecifiedTypes;
9+
use PHPStan\Analyser\TypeSpecifier;
10+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\Reflection\FunctionReflection;
13+
use PHPStan\TrinaryLogic;
14+
use PHPStan\Type\FunctionTypeSpecifyingExtension;
15+
use function in_array;
16+
use function strtolower;
17+
18+
final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
19+
{
20+
21+
private TypeSpecifier $typeSpecifier;
22+
23+
public function __construct(
24+
private RegexArrayShapeMatcher $regexShapeMatcher,
25+
)
26+
{
27+
}
28+
29+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
30+
{
31+
$this->typeSpecifier = $typeSpecifier;
32+
}
33+
34+
public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
35+
{
36+
return in_array(strtolower($functionReflection->getName()), ['safe\preg_match', 'safe\preg_match_all'], true) && !$context->null();
37+
}
38+
39+
public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
40+
{
41+
$args = $node->getArgs();
42+
$patternArg = $args[0] ?? null;
43+
$matchesArg = $args[2] ?? null;
44+
$flagsArg = $args[3] ?? null;
45+
46+
if (
47+
$patternArg === null || $matchesArg === null
48+
) {
49+
return new SpecifiedTypes();
50+
}
51+
52+
$flagsType = null;
53+
if ($flagsArg !== null) {
54+
$flagsType = $scope->getType($flagsArg->value);
55+
}
56+
57+
if ($functionReflection->getName() === 'Safe\preg_match') {
58+
$matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
59+
} else {
60+
$matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope);
61+
}
62+
if ($matchedType === null) {
63+
return new SpecifiedTypes();
64+
}
65+
66+
$overwrite = false;
67+
if ($context->false()) {
68+
$overwrite = true;
69+
$context = $context->negate();
70+
}
71+
72+
$types = $this->typeSpecifier->create(
73+
$matchesArg->value,
74+
$matchedType,
75+
$context,
76+
$scope,
77+
)->setRootExpr($node);
78+
if ($overwrite) {
79+
$types = $types->setAlwaysOverwriteTypes();
80+
}
81+
82+
return $types;
83+
}
84+
85+
}

tests/Type/Php/data/preg.php

+16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// Checking that preg_match and Safe\preg_match are equivalent
66
$pattern = '/H(.)ll(o) (World)?/';
77
$string = 'Hello World';
8+
9+
// when return value isn't checked, we may-or-may-not have matches
810
$type = "array{0?: string, 1?: non-empty-string, 2?: 'o', 3?: 'World'}";
911

1012
// @phpstan-ignore-next-line - use of unsafe is intentional
@@ -13,3 +15,17 @@
1315

1416
\Safe\preg_match($pattern, $string, $matches);
1517
\PHPStan\Testing\assertType($type, $matches);
18+
19+
20+
// when the return value is checked, we should have matches,
21+
// unless the match-group itself is optional
22+
$type = "array{0: string, 1: non-empty-string, 2: 'o', 3?: 'World'}";
23+
24+
// @phpstan-ignore-next-line - use of unsafe is intentional
25+
if(\preg_match($pattern, $string, $matches)) {
26+
\PHPStan\Testing\assertType($type, $matches);
27+
}
28+
29+
if(\Safe\preg_match($pattern, $string, $matches)) {
30+
\PHPStan\Testing\assertType($type, $matches);
31+
}

0 commit comments

Comments
 (0)