Skip to content

Commit 26e6253

Browse files
authored
Narrow Strings::match/Strings::matchAll subject string type when match is truthy (#201)
1 parent 130b009 commit 26e6253

4 files changed

Lines changed: 109 additions & 1 deletion

File tree

extension.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,11 @@ services:
128128
tags:
129129
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
130130

131+
-
132+
class: PHPStan\Type\Nette\StringsMatchTypeSpecifiyingExtension
133+
tags:
134+
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension
135+
131136
-
132137
class: PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension
133138
tags:
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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\Analyser\SpecifiedTypes;
9+
use PHPStan\Analyser\TypeSpecifier;
10+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
11+
use PHPStan\Analyser\TypeSpecifierContext;
12+
use PHPStan\Reflection\MethodReflection;
13+
use PHPStan\Type\Php\RegexArrayShapeMatcher;
14+
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
15+
use function in_array;
16+
17+
class StringsMatchTypeSpecifiyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
18+
{
19+
20+
private RegexArrayShapeMatcher $regexArrayShapeMatcher;
21+
22+
private TypeSpecifier $typeSpecifier;
23+
24+
public function __construct(RegexArrayShapeMatcher $regexArrayShapeMatcher)
25+
{
26+
$this->regexArrayShapeMatcher = $regexArrayShapeMatcher;
27+
}
28+
29+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
30+
{
31+
$this->typeSpecifier = $typeSpecifier;
32+
}
33+
34+
public function getClass(): string
35+
{
36+
return Strings::class;
37+
}
38+
39+
public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool
40+
{
41+
return $context->true() && in_array($staticMethodReflection->getName(), ['match', 'matchAll'], true);
42+
}
43+
44+
public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
45+
{
46+
$args = $node->getArgs();
47+
$subjectArg = $args[0] ?? null;
48+
$patternArg = $args[1] ?? null;
49+
50+
$subjectTypes = new SpecifiedTypes();
51+
if ($patternArg === null || $subjectArg === null) {
52+
return $subjectTypes;
53+
}
54+
55+
if ($scope->getType($subjectArg->value)->isString()->yes()) {
56+
$subjectType = $this->regexArrayShapeMatcher->matchSubjectExpr($patternArg->value, $scope);
57+
if ($subjectType !== null) {
58+
$subjectTypes = $this->typeSpecifier->create(
59+
$subjectArg->value,
60+
$subjectType,
61+
$context,
62+
$scope,
63+
)->setRootExpr($node);
64+
}
65+
}
66+
67+
return $subjectTypes;
68+
}
69+
70+
}

tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php renamed to tests/Type/Nette/StringsMatchTypeInferenceExtensionTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

7-
class StringsMatchDynamicReturnTypeExtensionTest extends TypeInferenceTestCase
7+
class StringsMatchTypeInferenceExtensionTest extends TypeInferenceTestCase
88
{
99

1010
public function dataFileAsserts(): iterable
1111
{
1212
yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match.php');
1313
yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match-74.php');
14+
yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match-subject.php');
1415
}
1516

1617
/**
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
namespace StringsMatchSubject;
4+
5+
use Nette\Utils\Strings;
6+
use function PHPStan\Testing\assertType;
7+
8+
function (string $s): void {
9+
if (Strings::match($s, '/foo/')) {
10+
assertType("non-falsy-string", $s);
11+
} else {
12+
assertType("string", $s);
13+
}
14+
assertType("string", $s);
15+
16+
$matches = Strings::matchAll($s, '/foo/');
17+
if (count($matches) !== 0) {
18+
assertType("non-falsy-string", $s);
19+
} else {
20+
assertType("string", $s);
21+
}
22+
assertType("string", $s);
23+
};
24+
25+
function ($mixed): void {
26+
if (Strings::match($mixed, '/foo/')) {
27+
assertType("mixed", $mixed);
28+
} else {
29+
assertType("mixed", $mixed);
30+
}
31+
assertType("mixed", $mixed);
32+
};

0 commit comments

Comments
 (0)