AssertTypeSpecifyingExtensionHelper.php
Namespace
PHPStan\Type\PHPUnit\AssertFile
-
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 |