diff --git a/conf/config.neon b/conf/config.neon index 4418c200c3..88a6568232 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1268,6 +1268,11 @@ services: tags: - phpstan.dynamicStaticMethodThrowTypeExtension + - + class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension + tags: + - phpstan.typeSpecifier.functionTypeSpecifyingExtension + - class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension tags: diff --git a/src/Type/Php/StrContainingTypeSpecifyingExtension.php b/src/Type/Php/StrContainingTypeSpecifyingExtension.php new file mode 100644 index 0000000000..dddbfe73e5 --- /dev/null +++ b/src/Type/Php/StrContainingTypeSpecifyingExtension.php @@ -0,0 +1,101 @@ + */ + private array $strContainingFunctions = [ + 'fnmatch' => [1, 0], + 'str_contains' => [0, 1], + 'str_starts_with' => [0, 1], + 'str_ends_with' => [0, 1], + 'strpos' => [0, 1], + 'strrpos' => [0, 1], + 'stripos' => [0, 1], + 'strripos' => [0, 1], + 'strstr' => [0, 1], + ]; + + private TypeSpecifier $typeSpecifier; + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool + { + return array_key_exists(strtolower($functionReflection->getName()), $this->strContainingFunctions) + && $context->truthy(); + } + + public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + + if (count($args) >= 2) { + [$hackstackArg, $needleArg] = $this->strContainingFunctions[strtolower($functionReflection->getName())]; + + $haystackType = $scope->getType($args[$hackstackArg]->value); + $needleType = $scope->getType($args[$needleArg]->value); + + if ($needleType->isNonEmptyString()->yes() && $haystackType->isString()->yes()) { + $accessories = [ + new StringType(), + new AccessoryNonEmptyStringType(), + ]; + + if ($haystackType->isLiteralString()->yes()) { + $accessories[] = new AccessoryLiteralStringType(); + } + if ($haystackType->isNumericString()->yes()) { + $accessories[] = new AccessoryNumericStringType(); + } + + return $this->typeSpecifier->create( + $args[$hackstackArg]->value, + new IntersectionType($accessories), + $context, + false, + $scope, + new BooleanAnd( + new NotIdentical( + $args[$needleArg]->value, + new String_(''), + ), + new FuncCall(new Name('FAUX_FUNCTION'), [ + new Arg($args[$needleArg]->value), + ]), + ), + ); + } + } + + return new SpecifiedTypes(); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index eff22135bc..92b3dd7059 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -902,6 +902,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7068.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7115.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/constant-array-type-identical.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-string-str-containing-fns.php'); } /** diff --git a/tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php b/tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php new file mode 100644 index 0000000000..ba98370913 --- /dev/null +++ b/tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php @@ -0,0 +1,123 @@ +analyse([__DIR__ . '/data/slevomat-cs-in-array.php'], []); } + public function testNonEmptySpecifiedString(): void + { + $this->checkAlwaysTrueCheckTypeFunctionCall = true; + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/non-empty-string-impossible-type.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php b/tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php new file mode 100644 index 0000000000..d2092228d2 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/non-empty-string-impossible-type.php @@ -0,0 +1,22 @@ +