Skip to content

Commit a6a4fed

Browse files
committed
implemented str_contains FunctionTypeSpecifyingExtension
1 parent 1911a92 commit a6a4fed

File tree

4 files changed

+146
-0
lines changed

4 files changed

+146
-0
lines changed

conf/config.neon

+5
Original file line numberDiff line numberDiff line change
@@ -1178,6 +1178,11 @@ services:
11781178
tags:
11791179
- phpstan.dynamicStaticMethodThrowTypeExtension
11801180

1181+
-
1182+
class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension
1183+
tags:
1184+
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
1185+
11811186
-
11821187
class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension
11831188
tags:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Analyser\SpecifiedTypes;
8+
use PHPStan\Analyser\TypeSpecifier;
9+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
10+
use PHPStan\Analyser\TypeSpecifierContext;
11+
use PHPStan\Reflection\FunctionReflection;
12+
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
13+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
14+
use PHPStan\Type\Accessory\AccessoryNumericStringType;
15+
use PHPStan\Type\FunctionTypeSpecifyingExtension;
16+
use PHPStan\Type\IntersectionType;
17+
use PHPStan\Type\StringType;
18+
use function count;
19+
use function in_array;
20+
use function strtolower;
21+
22+
final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
23+
{
24+
25+
/** @var string[] */
26+
private array $strContainingFunctions = [
27+
'str_contains',
28+
'str_starts_with',
29+
'str_ends_with',
30+
];
31+
32+
private TypeSpecifier $typeSpecifier;
33+
34+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
35+
{
36+
$this->typeSpecifier = $typeSpecifier;
37+
}
38+
39+
public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
40+
{
41+
return in_array(strtolower($functionReflection->getName()), $this->strContainingFunctions, true)
42+
&& $context->true();
43+
}
44+
45+
public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
46+
{
47+
$args = $node->getArgs();
48+
49+
if (count($args) >= 2) {
50+
$haystackType = $scope->getType($args[0]->value);
51+
$needleType = $scope->getType($args[1]->value);
52+
53+
if ($needleType->isNonEmptyString()->yes() && $haystackType->isString()->yes()) {
54+
$accessories = [
55+
new StringType(),
56+
new AccessoryNonEmptyStringType(),
57+
];
58+
59+
if ($haystackType->isLiteralString()->yes()) {
60+
$accessories[] = new AccessoryLiteralStringType();
61+
}
62+
if ($haystackType->isNumericString()->yes()) {
63+
$accessories[] = new AccessoryNumericStringType();
64+
}
65+
66+
return $this->typeSpecifier->create(
67+
$args[0]->value,
68+
new IntersectionType($accessories),
69+
$context,
70+
false,
71+
$scope,
72+
);
73+
}
74+
}
75+
76+
return new SpecifiedTypes();
77+
}
78+
79+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+2
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,8 @@ public function dataFileAsserts(): iterable
791791
}
792792

793793
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6584.php');
794+
795+
yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-str-contains.php');
794796
}
795797

796798
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
namespace NonEmptyStringStrContains;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo {
8+
/**
9+
* @param non-empty-string $nonES
10+
* @param numeric-string $numS
11+
* @param literal-string $literalS
12+
* @param non-empty-string&numeric-string $nonEAndNumericS
13+
*/
14+
public function sayHello(string $s, string $s2, $nonES, $numS, $literalS, $nonEAndNumericS, int $i): void
15+
{
16+
if (str_contains($s, ':')) {
17+
assertType('non-empty-string', $s);
18+
}
19+
assertType('string', $s);
20+
21+
if (str_contains($s, $s2)) {
22+
assertType('string', $s);
23+
}
24+
25+
if (str_contains($s, $nonES)) {
26+
assertType('non-empty-string', $s);
27+
}
28+
29+
if (str_contains($s, $numS)) {
30+
assertType('non-empty-string', $s);
31+
}
32+
33+
if (str_contains($s, $literalS)) {
34+
assertType('string', $s);
35+
}
36+
37+
if (str_contains($s, $nonEAndNumericS)) {
38+
assertType('non-empty-string', $s);
39+
}
40+
if (str_contains($numS, $nonEAndNumericS)) {
41+
assertType('non-empty-string&numeric-string', $numS);
42+
}
43+
44+
if (str_contains($i, $s2)) {
45+
assertType('int', $i);
46+
}
47+
}
48+
49+
public function variants(string $s) {
50+
if (str_starts_with($s, ':')) {
51+
assertType('non-empty-string', $s);
52+
}
53+
assertType('string', $s);
54+
55+
if (str_ends_with($s, ':')) {
56+
assertType('non-empty-string', $s);
57+
}
58+
assertType('string', $s);
59+
}
60+
}

0 commit comments

Comments
 (0)