Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 20 additions & 40 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,46 +333,6 @@ public function specifyTypesInCondition(
}
}

if (
!$context->null()
&& $expr->right instanceof FuncCall
&& count($expr->right->getArgs()) >= 3
&& $expr->right->name instanceof Name
&& in_array(strtolower((string) $expr->right->name), ['preg_match'], true)
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()
) {
return $this->specifyTypesInCondition(
$scope,
new Expr\BinaryOp\NotIdentical($expr->right, new ConstFetch(new Name('false'))),
$context,
)->setRootExpr($expr);
}

if (
!$context->null()
&& $expr->right instanceof FuncCall
&& count($expr->right->getArgs()) === 1
&& $expr->right->name instanceof Name
&& in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true)
&& $leftType->isInteger()->yes()
) {
if (
$context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes())
|| ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
) {
$argType = $scope->getType($expr->right->getArgs()[0]->value);
if ($argType->isString()->yes()) {
$accessory = new AccessoryNonEmptyStringType();

if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) {
$accessory = new AccessoryNonFalsyStringType();
}

$result = $result->unionWith($this->create($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr));
}
}
}

if ($leftType instanceof ConstantIntegerType) {
if ($expr->right instanceof Expr\PostInc) {
$result = $result->unionWith($this->createRangeTypes(
Expand Down Expand Up @@ -468,6 +428,26 @@ public function specifyTypesInCondition(
}
}

if (
!$context->null()
&& $expr->right instanceof Expr\FuncCall
&& $expr->right->name instanceof Name
&& in_array(strtolower((string) $expr->right->name), ['preg_match', 'strlen', 'mb_strlen'], true)
) {
if (!$scope instanceof MutatingScope) {
throw new ShouldNotHappenException();
}
$newScope = $scope->filterBySpecifiedTypes($result);
$callType = $newScope->getType($expr->right);
$newContext = $context->newWithReturnType($callType);

$result = $result->unionWith($this->specifyTypesInCondition(
$scope,
$expr->right,
$newContext,
)->setRootExpr($expr));
}

return $result;

} elseif ($expr instanceof Node\Expr\BinaryOp\Greater) {
Expand Down
19 changes: 15 additions & 4 deletions src/Analyser/TypeSpecifierContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Analyser;

use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Type;

/**
* @api
Expand All @@ -18,17 +19,15 @@ final class TypeSpecifierContext
public const CONTEXT_FALSEY = self::CONTEXT_FALSE | self::CONTEXT_FALSEY_BUT_NOT_FALSE;
public const CONTEXT_BITMASK = 0b1111;

/** @var self[] */
private static array $registry;
private ?Type $returnType = null;

private function __construct(private ?int $value)
{
}

private static function create(?int $value): self
{
self::$registry[$value] ??= new self($value);
return self::$registry[$value];
return new self($value);
}

public static function createTrue(): self
Expand Down Expand Up @@ -89,4 +88,16 @@ public function null(): bool
return $this->value === null;
}

public function newWithReturnType(Type $type): self
{
$new = self::create($this->value);
$new->returnType = $type;
return $new;
}

public function getReturnType(): ?Type
{
return $this->returnType;
}

}
74 changes: 74 additions & 0 deletions src/Type/Php/StrlenTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\NeverType;
use function count;
use function in_array;
use function strtolower;

#[AutowiredService]
final class StrlenTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{

private TypeSpecifier $typeSpecifier;

public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
{
return in_array(strtolower($functionReflection->getName()), ['strlen', 'mb_strlen'], true) && !$context->null();
}

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$args = $node->getArgs();
if (
count($args) < 1
|| $context->getReturnType() === null
) {
return new SpecifiedTypes();
}

$argType = $scope->getType($args[0]->value);
if (!$argType->isString()->yes()) {
return new SpecifiedTypes();
}

$returnType = $context->getReturnType();
if ($returnType instanceof NeverType) {
return $this->typeSpecifier->create($args[0]->value, $returnType, $context->negate(), $scope);
}

if (
$context->true() && IntegerRangeType::createAllGreaterThanOrEqualTo(1)->isSuperTypeOf($returnType)->yes()
|| ($context->false() && (new ConstantIntegerType(0))->isSuperTypeOf($returnType)->yes())
) {
$accessory = new AccessoryNonEmptyStringType();
if (IntegerRangeType::createAllGreaterThanOrEqualTo(2)->isSuperTypeOf($returnType)->yes()) {
$accessory = new AccessoryNonFalsyStringType();
}

return $this->typeSpecifier->create($args[0]->value, $accessory, $context, $scope)->setRootExpr($node);
}

return new SpecifiedTypes();
}

}
50 changes: 50 additions & 0 deletions tests/PHPStan/Analyser/nsrt/strlen-never.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace StrlenNever;

use function PHPStan\Testing\assertType;

/**
* @param non-empty-string $nonES
* @param non-falsy-string $nonFalsy
* @param numeric-string $numericString
* @param lowercase-string $lower
* @param uppercase-string $upper
*/
function doFoo(string $s, $nonES, $nonFalsy, $numericString, $lower, $upper) {
if (strlen($s) <= 0) {
assertType("''", $s);
}
if (strlen($nonES) <= 0) {
assertType('*NEVER*', $nonES);
}
if (strlen($nonFalsy) <= 0) {
assertType('*NEVER*', $nonFalsy);
}
if (strlen($numericString) <= 0) {
assertType("*NEVER*", $numericString);
}
if (strlen($lower) <= 0) {
assertType("''", $lower);
}
if (strlen($upper) <= 0) {
assertType("''", $upper);
}

if (strlen($nonES) >= 0) {
assertType('non-empty-string', $nonES);
} else {
assertType('*NEVER*', $nonES);
}
}


function doBar(string $m): void
{
if (strlen($m) >= 1) {
if (strlen($m) <= 0) {
assertType('*NEVER*', $m);
}
}
}

Loading