Skip to content

Commit

Permalink
Implement TypeSpecifierComparisonContext
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Sep 3, 2024
1 parent 4f33a8e commit cbca9a3
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 23 deletions.
72 changes: 63 additions & 9 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BooleanType;
use PHPStan\Type\ComparisonAwareTypeSpecifyingExtension;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand Down Expand Up @@ -1983,6 +1984,47 @@ private function getTypeSpecifyingExtensionsForType(array $extensions, string $c
return array_merge(...$extensionsForClass);
}

private function specifyWithComparisonAwareTypeSpecifyingExtensions(
Expr\BinaryOp $binaryOp,
Expr $callExpr,
Expr\CallLike $callLike,
Type $comparisonType,
Scope $scope,
TypeSpecifierContext $context,
?Expr $rootExpr,
): ?SpecifiedTypes
{
if ($callLike instanceof FuncCall && $callLike->name instanceof Name) {
if (!$this->reflectionProvider->hasFunction($callLike->name, $scope)) {
return null;
}
$functionReflection = $this->reflectionProvider->getFunction($callLike->name, $scope);

$comparisonContext = TypeSpecifierContext::createComparison(
new TypeSpecifierComparisonContext(
$binaryOp,
$callExpr,
$comparisonType,
$context,
$rootExpr,
),
);
foreach ($this->getFunctionTypeSpecifyingExtensions() as $extension) {
if (!$extension instanceof ComparisonAwareTypeSpecifyingExtension) {
continue;
}

if (!$extension->isFunctionSupported($functionReflection, $callLike, $comparisonContext)) {
continue;
}

return $extension->specifyTypes($functionReflection, $callLike, $scope, $comparisonContext);
}
}

return null;
}

public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecifierContext $context, ?Expr $rootExpr): SpecifiedTypes
{
$expressions = $this->findTypeExpressionsFromBinaryOperation($scope, $expr);
Expand Down Expand Up @@ -2019,12 +2061,20 @@ public function resolveEqual(Expr\BinaryOp\Equal $expr, Scope $scope, TypeSpecif

if (
$context->true()
&& $exprNode instanceof FuncCall
&& $exprNode->name instanceof Name
&& $exprNode->name->toLowerString() === 'preg_match'
&& (new ConstantIntegerType(1))->isSuperTypeOf($constantType)->yes()
&& $exprNode instanceof Expr\CallLike
) {
return $this->specifyTypesInCondition($scope, new Expr\BinaryOp\Identical($expr->left, $expr->right), $context, $rootExpr);
$specifiedTypes = $this->specifyWithComparisonAwareTypeSpecifyingExtensions(
$expr,
$exprNode,
$exprNode,
$constantType,
$scope,
$context,
$rootExpr,
);
if ($specifiedTypes !== null) {
return $specifiedTypes;
}
}
}

Expand Down Expand Up @@ -2119,15 +2169,19 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
$context->true()
&& $unwrappedLeftExpr instanceof FuncCall
&& $unwrappedLeftExpr->name instanceof Name
&& $unwrappedLeftExpr->name->toLowerString() === 'preg_match'
&& (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes()
) {
return $this->specifyTypesInCondition(
$scope,
$specifiedTypes = $this->specifyWithComparisonAwareTypeSpecifyingExtensions(
$expr,
$leftExpr,
$unwrappedLeftExpr,
$rightType,
$scope,
$context,
$rootExpr,
);
if ($specifiedTypes !== null) {
return $specifiedTypes;
}
}

if (
Expand Down
48 changes: 48 additions & 0 deletions src/Analyser/TypeSpecifierComparisonContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp;
use PHPStan\Type\Type;

/** @api */
final class TypeSpecifierComparisonContext
{

public function __construct(
private BinaryOp $binaryOp,
private Expr $callExpr,
private Type $comparisonType,
private TypeSpecifierContext $context,
private ?Expr $rootExpr,
)
{
}

public function getBinaryOp(): BinaryOp
{
return $this->binaryOp;
}

public function getCallExpr(): Expr
{
return $this->callExpr;
}

public function getComparisonType(): Type
{
return $this->comparisonType;
}

public function getTypeSpecifierContext(): TypeSpecifierContext
{
return $this->context;
}

public function getRootExpr(): ?Expr
{
return $this->rootExpr;
}

}
22 changes: 18 additions & 4 deletions src/Analyser/TypeSpecifierContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,22 @@ class TypeSpecifierContext
public const CONTEXT_FALSE = 0b0100;
public const CONTEXT_FALSEY_BUT_NOT_FALSE = 0b1000;
public const CONTEXT_FALSEY = self::CONTEXT_FALSE | self::CONTEXT_FALSEY_BUT_NOT_FALSE;
public const CONTEXT_BITMASK = 0b1111;
public const CONTEXT_COMPARISON = 0b10000;
public const CONTEXT_BITMASK = 0b01111;

/** @var self[] */
private static array $registry;

private function __construct(private ?int $value)
private function __construct(
private ?int $value,
private ?TypeSpecifierComparisonContext $comparisonContext,
)
{
}

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

Expand All @@ -49,6 +53,11 @@ public static function createFalsey(): self
return self::create(self::CONTEXT_FALSEY);
}

public static function createComparison(TypeSpecifierComparisonContext $comparisonContext): self
{
return self::create(self::CONTEXT_COMPARISON, $comparisonContext);
}

public static function createNull(): self
{
return self::create(null);
Expand Down Expand Up @@ -87,4 +96,9 @@ public function null(): bool
return $this->value === null;
}

public function comparison(): ?TypeSpecifierComparisonContext
{
return $this->comparisonContext;
}

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

namespace PHPStan\Type;

/**
* This is the marker interface *TypeSpecifyingExtension might implement to specify types for comparisons.
*
* Use it in your already registered type specifying extension.
*
* Learn more: https://phpstan.org/developing-extensions/type-specifying-extensions
*
* @api
*
*/
interface ComparisonAwareTypeSpecifyingExtension
{

}
25 changes: 24 additions & 1 deletion src/Type/Php/PregMatchTypeSpecifyingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\BinaryOp\Equal;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
Expand All @@ -10,11 +12,13 @@
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ComparisonAwareTypeSpecifyingExtension;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
use function in_array;
use function strtolower;

final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
final class PregMatchTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension, ComparisonAwareTypeSpecifyingExtension
{

private TypeSpecifier $typeSpecifier;
Expand All @@ -37,6 +41,25 @@ public function isFunctionSupported(FunctionReflection $functionReflection, Func

public function specifyTypes(FunctionReflection $functionReflection, FuncCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
{
$comparisonContext = $context->comparison();
if ($comparisonContext !== null) {
$binaryOp = $comparisonContext->getBinaryOp();
if (
($binaryOp instanceof Equal || $binaryOp instanceof Identical)
&& $comparisonContext->getTypeSpecifierContext()->true()
&& (new ConstantIntegerType(1))->isSuperTypeOf($comparisonContext->getComparisonType())->yes()
) {
return $this->typeSpecifier->specifyTypesInCondition(
$scope,
$comparisonContext->getCallExpr(),
$comparisonContext->getTypeSpecifierContext(),
$comparisonContext->getRootExpr(),
);
}

return new SpecifiedTypes();
}

$args = $node->getArgs();
$patternArg = $args[0] ?? null;
$matchesArg = $args[2] ?? null;
Expand Down
58 changes: 49 additions & 9 deletions tests/PHPStan/Analyser/TypeSpecifierContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

namespace PHPStan\Analyser;

use PhpParser\Node\Expr\BinaryOp\Equal;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Scalar\String_;
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\NullType;

class TypeSpecifierContextTest extends PHPStanTestCase
{
Expand All @@ -13,23 +17,27 @@ public function dataContext(): array
return [
[
TypeSpecifierContext::createTrue(),
[true, true, false, false, false],
[true, true, false, false, false, false],
],
[
TypeSpecifierContext::createTruthy(),
[true, true, false, false, false],
[true, true, false, false, false, false],
],
[
TypeSpecifierContext::createFalse(),
[false, false, true, true, false],
[false, false, true, true, false, false],
],
[
TypeSpecifierContext::createFalsey(),
[false, false, true, true, false],
[false, false, true, true, false, false],
],
[
TypeSpecifierContext::createNull(),
[false, false, false, false, true],
[false, false, false, false, true, false],
],
[
$this->createComparisonContext(),
[false, false, false, false, false, true],
],
];
}
Expand All @@ -45,27 +53,40 @@ public function testContext(TypeSpecifierContext $context, array $results): void
$this->assertSame($results[2], $context->false());
$this->assertSame($results[3], $context->falsey());
$this->assertSame($results[4], $context->null());

if ($results[5]) {
$this->assertNotNull($context->comparison());
} else {
$this->assertNull($context->comparison());
}
}

public function dataNegate(): array
{
return [
[
TypeSpecifierContext::createTrue()->negate(),
[false, true, true, true, false],
[false, true, true, true, false, false],
],
[
TypeSpecifierContext::createTruthy()->negate(),
[false, false, true, true, false],
[false, false, true, true, false, false],
],
[
TypeSpecifierContext::createFalse()->negate(),
[true, true, false, true, false],
[true, true, false, true, false, false],
],
[
TypeSpecifierContext::createFalsey()->negate(),
[true, true, false, false, false],
[true, true, false, false, false, false],
],
/*
// XXX should a comparison context be negatable?
[
$this->createComparisonContext()->negate(),
[false, false, false, false, false, true],
],
*/
];
}

Expand All @@ -80,6 +101,12 @@ public function testNegate(TypeSpecifierContext $context, array $results): void
$this->assertSame($results[2], $context->false());
$this->assertSame($results[3], $context->falsey());
$this->assertSame($results[4], $context->null());

if ($results[5]) {
$this->assertNotNull($context->comparison());
} else {
$this->assertNull($context->comparison());
}
}

public function testNegateNull(): void
Expand All @@ -88,4 +115,17 @@ public function testNegateNull(): void
TypeSpecifierContext::createNull()->negate();
}

private function createComparisonContext(): TypeSpecifierContext
{
return TypeSpecifierContext::createComparison(
new TypeSpecifierComparisonContext(
new Equal(new String_('dummy'), new String_('dummy2')),
new FuncCall('dummyFunc'),
new NullType(),
TypeSpecifierContext::createNull(),
null,
),
);
}

}

0 comments on commit cbca9a3

Please sign in to comment.