Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x

AssertTypeSpecifyingExtensionHelper.php

Namespace

PHPStan\Type\PHPUnit\Assert

File

vendor/phpstan/phpstan-phpunit/src/Type/PHPUnit/Assert/AssertTypeSpecifyingExtensionHelper.php

View source
<?php

declare (strict_types=1);
namespace PHPStan\Type\PHPUnit\Assert;

use Closure;
use Countable;
use EmptyIterator;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BooleanNot;
use PhpParser\Node\Expr\ConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Instanceof_;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\LNumber;
use PhpParser\Node\Stmt;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierContext;
use ReflectionObject;
use function array_key_exists;
use function count;
use function in_array;
use function strlen;
use function strpos;
use function substr;
class AssertTypeSpecifyingExtensionHelper {
    
    /** @var Closure[] */
    private static $resolvers;
    
    /**
     * Those can specify types correctly, but would produce always-true issue
     * @var string[]
     */
    private static $resolversCausingAlwaysTrue = [
        'ContainsOnlyInstancesOf',
        'ContainsEquals',
        'Contains',
    ];
    
    /**
     * @param Arg[] $args
     */
    public static function isSupported(string $name, array $args) : bool {
        $trimmedName = self::trimName($name);
        $resolvers = self::getExpressionResolvers();
        if (!array_key_exists($trimmedName, $resolvers)) {
            return false;
        }
        $resolver = $resolvers[$trimmedName];
        $resolverReflection = new ReflectionObject($resolver);
        return count($args) >= count($resolverReflection->getMethod('__invoke')
            ->getParameters()) - 1;
    }
    private static function trimName(string $name) : string {
        if (strpos($name, 'assert') !== 0) {
            return $name;
        }
        $name = substr($name, strlen('assert'));
        if (strpos($name, 'Not') === 0) {
            return substr($name, 3);
        }
        if (strpos($name, 'IsNot') === 0) {
            return 'Is' . substr($name, 5);
        }
        return $name;
    }
    
    /**
     * @param Arg[] $args $args
     */
    public static function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, string $name, array $args) : SpecifiedTypes {
        $expression = self::createExpression($scope, $name, $args);
        if ($expression === null) {
            return new SpecifiedTypes([], []);
        }
        $bypassAlwaysTrueIssue = in_array(self::trimName($name), self::$resolversCausingAlwaysTrue, true);
        return $typeSpecifier->specifyTypesInCondition($scope, $expression, TypeSpecifierContext::createTruthy(), $bypassAlwaysTrueIssue ? new Expr\BinaryOp\BooleanAnd($expression, new Expr\Variable('nonsense')) : null);
    }
    
    /**
     * @param Arg[] $args
     */
    private static function createExpression(Scope $scope, string $name, array $args) : ?Expr {
        $trimmedName = self::trimName($name);
        $resolvers = self::getExpressionResolvers();
        $resolver = $resolvers[$trimmedName];
        $expression = $resolver($scope, ...$args);
        if ($expression === null) {
            return null;
        }
        if (strpos($name, 'Not') !== false) {
            $expression = new BooleanNot($expression);
        }
        return $expression;
    }
    
    /**
     * @return Closure[]
     */
    private static function getExpressionResolvers() : array {
        if (self::$resolvers === null) {
            self::$resolvers = [
                'Count' => static function (Scope $scope, Arg $expected, Arg $actual) : Identical {
                    return new Identical($expected->value, new FuncCall(new Name('count'), [
                        $actual,
                    ]));
                },
                'NotCount' => static function (Scope $scope, Arg $expected, Arg $actual) : BooleanNot {
                    return new BooleanNot(new Identical($expected->value, new FuncCall(new Name('count'), [
                        $actual,
                    ])));
                },
                'InstanceOf' => static function (Scope $scope, Arg $class, Arg $object) : Instanceof_ {
                    return new Instanceof_($object->value, $class->value);
                },
                'Same' => static function (Scope $scope, Arg $expected, Arg $actual) : Identical {
                    return new Identical($expected->value, $actual->value);
                },
                'True' => static function (Scope $scope, Arg $actual) : Identical {
                    return new Identical($actual->value, new ConstFetch(new Name('true')));
                },
                'False' => static function (Scope $scope, Arg $actual) : Identical {
                    return new Identical($actual->value, new ConstFetch(new Name('false')));
                },
                'Null' => static function (Scope $scope, Arg $actual) : Identical {
                    return new Identical($actual->value, new ConstFetch(new Name('null')));
                },
                'Empty' => static function (Scope $scope, Arg $actual) : Expr\BinaryOp\BooleanOr {
                    return new Expr\BinaryOp\BooleanOr(new Instanceof_($actual->value, new Name(EmptyIterator::class)), new Expr\BinaryOp\BooleanOr(new Expr\BinaryOp\BooleanAnd(new Instanceof_($actual->value, new Name(Countable::class)), new Identical(new FuncCall(new Name('count'), [
                        new Arg($actual->value),
                    ]), new LNumber(0))), new Expr\Empty_($actual->value)));
                },
                'IsArray' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_array'), [
                        $actual,
                    ]);
                },
                'IsBool' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_bool'), [
                        $actual,
                    ]);
                },
                'IsCallable' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_callable'), [
                        $actual,
                    ]);
                },
                'IsFloat' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_float'), [
                        $actual,
                    ]);
                },
                'IsInt' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_int'), [
                        $actual,
                    ]);
                },
                'IsIterable' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_iterable'), [
                        $actual,
                    ]);
                },
                'IsNumeric' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_numeric'), [
                        $actual,
                    ]);
                },
                'IsObject' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_object'), [
                        $actual,
                    ]);
                },
                'IsResource' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_resource'), [
                        $actual,
                    ]);
                },
                'IsString' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_string'), [
                        $actual,
                    ]);
                },
                'IsScalar' => static function (Scope $scope, Arg $actual) : FuncCall {
                    return new FuncCall(new Name('is_scalar'), [
                        $actual,
                    ]);
                },
                'InternalType' => static function (Scope $scope, Arg $type, Arg $value) : ?FuncCall {
                    $typeNames = $scope->getType($type->value)
                        ->getConstantStrings();
                    if (count($typeNames) !== 1) {
                        return null;
                    }
                    switch ($typeNames[0]->getValue()) {
                        case 'numeric':
                            $functionName = 'is_numeric';
                            break;
                        case 'integer':
                        case 'int':
                            $functionName = 'is_int';
                            break;
                        case 'double':
                        case 'float':
                        case 'real':
                            $functionName = 'is_float';
                            break;
                        case 'string':
                            $functionName = 'is_string';
                            break;
                        case 'boolean':
                        case 'bool':
                            $functionName = 'is_bool';
                            break;
                        case 'scalar':
                            $functionName = 'is_scalar';
                            break;
                        case 'null':
                            $functionName = 'is_null';
                            break;
                        case 'array':
                            $functionName = 'is_array';
                            break;
                        case 'object':
                            $functionName = 'is_object';
                            break;
                        case 'resource':
                            $functionName = 'is_resource';
                            break;
                        case 'callable':
                            $functionName = 'is_callable';
                            break;
                        default:
                            return null;
                    }
                    return new FuncCall(new Name($functionName), [
                        $value,
                    ]);
                },
                'ArrayHasKey' => static function (Scope $scope, Arg $key, Arg $array) : Expr {
                    return new Expr\BinaryOp\BooleanOr(new Expr\BinaryOp\BooleanAnd(new Expr\Instanceof_($array->value, new Name('ArrayAccess')), new Expr\MethodCall($array->value, 'offsetExists', [
                        $key,
                    ])), new FuncCall(new Name('array_key_exists'), [
                        $key,
                        $array,
                    ]));
                },
                'ObjectHasAttribute' => static function (Scope $scope, Arg $property, Arg $object) : FuncCall {
                    return new FuncCall(new Name('property_exists'), [
                        $object,
                        $property,
                    ]);
                },
                'ObjectHasProperty' => static function (Scope $scope, Arg $property, Arg $object) : FuncCall {
                    return new FuncCall(new Name('property_exists'), [
                        $object,
                        $property,
                    ]);
                },
                'Contains' => static function (Scope $scope, Arg $needle, Arg $haystack) : Expr {
                    return new Expr\BinaryOp\BooleanOr(new Expr\Instanceof_($haystack->value, new Name('Traversable')), new FuncCall(new Name('in_array'), [
                        $needle,
                        $haystack,
                        new Arg(new ConstFetch(new Name('true'))),
                    ]));
                },
                'ContainsEquals' => static function (Scope $scope, Arg $needle, Arg $haystack) : Expr {
                    return new Expr\BinaryOp\BooleanOr(new Expr\Instanceof_($haystack->value, new Name('Traversable')), new Expr\BinaryOp\BooleanAnd(new Expr\BooleanNot(new Expr\Empty_($haystack->value)), new FuncCall(new Name('in_array'), [
                        $needle,
                        $haystack,
                        new Arg(new ConstFetch(new Name('false'))),
                    ])));
                },
                'ContainsOnlyInstancesOf' => static function (Scope $scope, Arg $className, Arg $haystack) : Expr {
                    return new Expr\BinaryOp\BooleanOr(new Expr\Instanceof_($haystack->value, new Name('Traversable')), new Identical($haystack->value, new FuncCall(new Name('array_filter'), [
                        $haystack,
                        new Arg(new Expr\Closure([
                            'static' => true,
                            'params' => [
                                new Param(new Expr\Variable('_')),
                            ],
                            'stmts' => [
                                new Stmt\Return_(new FuncCall(new Name('is_a'), [
                                    new Arg(new Expr\Variable('_')),
                                    $className,
                                ])),
                            ],
                        ])),
                    ])));
                },
            ];
        }
        return self::$resolvers;
    }

}

Classes

Title Deprecated Summary
AssertTypeSpecifyingExtensionHelper
RSS feed
Powered by Drupal