Skip to content

Commit

Permalink
implemented str_contains FunctionTypeSpecifyingExtension
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Apr 28, 2022
1 parent 7e1e51d commit 49b8b26
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1268,6 +1268,11 @@ services:
tags:
- phpstan.dynamicStaticMethodThrowTypeExtension

-
class: PHPStan\Type\Php\StrContainingTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension

-
class: PHPStan\Type\Php\SimpleXMLElementClassPropertyReflectionExtension
tags:
Expand Down
101 changes: 101 additions & 0 deletions src/Type/Php/StrContainingTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Arg;
use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\StringType;
use function array_key_exists;
use function count;
use function strtolower;

final class StrContainingTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

/** @var array<string, array{0: int, 1: int}> */
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();
}

}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}

/**
Expand Down
123 changes: 123 additions & 0 deletions tests/PHPStan/Analyser/data/non-empty-string-str-containing-fns.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace NonEmptyStringStrContains;

use function PHPStan\Testing\assertType;

class Foo {
/**
* @param non-empty-string $nonES
* @param numeric-string $numS
* @param literal-string $literalS
* @param non-empty-string&numeric-string $nonEAndNumericS
*/
public function strContains(string $s, string $s2, $nonES, $numS, $literalS, $nonEAndNumericS, int $i): void
{
if (str_contains($s, ':')) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (str_contains($s, $s2)) {
assertType('string', $s);
}

if (str_contains($s, $nonES)) {
assertType('non-empty-string', $s);
}
if (str_contains($s, $numS)) {
assertType('non-empty-string', $s);
}
if (str_contains($s, $literalS)) {
assertType('string', $s);
}

if (str_contains($s, $nonEAndNumericS)) {
assertType('non-empty-string', $s);
}
if (str_contains($numS, $nonEAndNumericS)) {
assertType('non-empty-string&numeric-string', $numS);
}

if (str_contains($nonES, $s)) {
assertType('non-empty-string', $nonES);
}
if (str_contains($nonEAndNumericS, $s)) {
assertType('non-empty-string&numeric-string', $nonEAndNumericS);
}

if (str_contains($i, $s2)) {
assertType('int', $i);
}
}

public function variants(string $s) {
if (fnmatch("*gr[ae]y", $s)) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (str_starts_with($s, ':')) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (str_ends_with($s, ':')) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (strpos($s, ':') !== false) {
assertType('non-empty-string', $s);
}
assertType('string', $s);
if (strpos($s, ':') === false) {
assertType('string', $s);
}
assertType('string', $s);

if (strpos($s, ':') === 5) {
assertType('string', $s); // could be non-empty-string
}
assertType('string', $s);
if (strpos($s, ':') !== 5) {
assertType('string', $s);
}
assertType('string', $s);

if (strrpos($s, ':') !== false) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (stripos($s, ':') !== false) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (strripos($s, ':') !== false) {
assertType('non-empty-string', $s);
}
assertType('string', $s);

if (strstr($s, ':') === 'hallo') {
assertType('string', $s); // could be non-empty-string
}
assertType('string', $s);
if (strstr($s, ':', true) === 'hallo') {
assertType('string', $s); // could be non-empty-string
}
assertType('string', $s);
if (strstr($s, ':', true) !== false) {
assertType('non-empty-string', $s);
}
assertType('string', $s);
if (strstr($s, ':', true) === false) {
assertType('string', $s);
} else {
assertType('non-empty-string', $s);
}
assertType('string', $s);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -528,4 +528,11 @@ public function testSlevomatCsInArrayBug(): void
$this->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'], []);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace NonEmptyStringImpossibleType;

class Foo {
private function isPrefixedInterface(string $shortClassName): bool
{
if (strlen($shortClassName) <= 3) {
return false;
}

if (! \str_starts_with($shortClassName, 'I')) {
return false;
}

if (! ctype_upper($shortClassName[1])) {
return false;
}

return ctype_lower($shortClassName[2]);
}
}

0 comments on commit 49b8b26

Please sign in to comment.