Skip to content

Commit

Permalink
pure-callable and pure-Closure
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Apr 7, 2024
1 parent 242979a commit cdaff5f
Show file tree
Hide file tree
Showing 22 changed files with 471 additions and 18 deletions.
22 changes: 20 additions & 2 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use PHPStan\Reflection\PassedByReference;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryArrayListType;
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
Expand Down Expand Up @@ -352,9 +353,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
return new IterableType(new MixedType(), new MixedType());

case 'callable':
case 'pure-callable':
return new CallableType();

case 'pure-callable':
return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes());

case 'pure-closure':
return new ClosureType();

case 'resource':
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);

Expand Down Expand Up @@ -928,7 +934,12 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
$returnType = $this->resolve($typeNode->returnType, $nameScope);

if ($mainType instanceof CallableType) {
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags);
$pure = $mainType->isPure();
if ($pure->yes() && $returnType->isVoid()->yes()) {
return new ErrorType();
}

return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure);

} elseif (
$mainType instanceof ObjectType
Expand All @@ -941,6 +952,13 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
false,
),
]);
} elseif ($mainType instanceof ClosureType) {
$closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints());
if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) {
return new ErrorType();
}

return $closure;
}

return new ErrorType();
Expand Down
3 changes: 3 additions & 0 deletions src/Reflection/Callables/CallableParametersAcceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Node\InvalidateExprNode;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\TrinaryLogic;

/**
* @api
Expand All @@ -16,6 +17,8 @@ interface CallableParametersAcceptor extends ParametersAcceptor
*/
public function getThrowPoints(): array;

public function isPure(): TrinaryLogic;

/**
* @return SimpleImpurePoint[]
*/
Expand Down
21 changes: 21 additions & 0 deletions src/Reflection/Callables/FunctionCallableVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Throwable;
use function array_map;
use function count;
use function sprintf;

class FunctionCallableVariant implements CallableParametersAcceptor, ParametersAcceptorWithPhpDocs
Expand Down Expand Up @@ -115,6 +117,25 @@ public function getThrowPoints(): array
return $this->throwPoints = $throwPoints;
}

public function isPure(): TrinaryLogic
{
$impurePoints = $this->getImpurePoints();
if (count($impurePoints) === 0) {
return TrinaryLogic::createYes();
}

$certainCount = 0;
foreach ($impurePoints as $impurePoint) {
if (!$impurePoint->isCertain()) {
continue;
}

$certainCount++;
}

return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe();
}

public function getImpurePoints(): array
{
if ($this->impurePoints !== null) {
Expand Down
15 changes: 14 additions & 1 deletion src/Reflection/InaccessibleMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\Reflection;

use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\MixedType;
Expand Down Expand Up @@ -58,9 +60,20 @@ public function getThrowPoints(): array
return [];
}

public function isPure(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}

public function getImpurePoints(): array
{
return [];
return [
new SimpleImpurePoint(
'methodCall',
'call to unknown method',
false,
),
];
}

public function getInvalidateExpressions(): array
Expand Down
6 changes: 6 additions & 0 deletions src/Reflection/TrivialParametersAcceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\Reflection\Callables\SimpleImpurePoint;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\MixedType;
Expand Down Expand Up @@ -64,6 +65,11 @@ public function getThrowPoints(): array
return [];
}

public function isPure(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}

public function getImpurePoints(): array
{
return [
Expand Down
7 changes: 5 additions & 2 deletions src/Rules/MissingTypehintCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\CallableType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\ConditionalType;
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Generic\GenericObjectType;
Expand Down Expand Up @@ -162,8 +163,10 @@ public function getCallablesWithMissingSignature(Type $type): array
$result = [];
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type {
if (
($type instanceof CallableType && $type->isCommonCallable()) ||
($type instanceof ObjectType && $type->getClassName() === Closure::class)) {
($type instanceof CallableType && $type->isCommonCallable())
|| ($type instanceof ClosureType && $type->isCommonCallable())
|| ($type instanceof ObjectType && $type->getClassName() === Closure::class)
) {
$result[] = $type;
}
return $traverse($type);
Expand Down
10 changes: 9 additions & 1 deletion src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,25 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
$acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
if ($acceptedType instanceof CallableType) {
if ($acceptedType->isCommonCallable()) {
return new CallableType(null, null, $acceptedType->isVariadic());
return $acceptedType;
}

return new CallableType(
$acceptedType->getParameters(),
$traverse($this->transformCommonType($acceptedType->getReturnType())),
$acceptedType->isVariadic(),
$acceptedType->getTemplateTypeMap(),
$acceptedType->getResolvedTemplateTypeMap(),
$acceptedType->getTemplateTags(),
$acceptedType->isPure(),
);
}

if ($acceptedType instanceof ClosureType) {
if ($acceptedType->isCommonCallable()) {
return $acceptedType;
}

return new ClosureType(
$acceptedType->getParameters(),
$traverse($this->transformCommonType($acceptedType->getReturnType())),
Expand Down
42 changes: 38 additions & 4 deletions src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ class CallableType implements CompoundType, CallableParametersAcceptor

private TemplateTypeMap $resolvedTemplateTypeMap;

private TrinaryLogic $isPure;

/**
* @api
* @param array<int, ParameterReflection>|null $parameters
Expand All @@ -75,13 +77,15 @@ public function __construct(
?TemplateTypeMap $templateTypeMap = null,
?TemplateTypeMap $resolvedTemplateTypeMap = null,
private array $templateTags = [],
?TrinaryLogic $isPure = null,
)
{
$this->parameters = $parameters ?? [];
$this->returnType = $returnType ?? new MixedType();
$this->isCommonCallable = $parameters === null && $returnType === null;
$this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
$this->isPure = $isPure ?? TrinaryLogic::createMaybe();
}

/**
Expand All @@ -92,6 +96,11 @@ public function getTemplateTags(): array
return $this->templateTags;
}

public function isPure(): TrinaryLogic
{
return $this->isPure;
}

/**
* @return string[]
*/
Expand Down Expand Up @@ -146,7 +155,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic
private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult
{
$isCallable = new AcceptsResult($type->isCallable(), []);
if ($isCallable->no() || $this->isCommonCallable) {
if ($isCallable->no()) {
return $isCallable;
}

Expand All @@ -155,6 +164,19 @@ private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): Accep
$scope = new OutOfClassScope();
}

if ($this->isCommonCallable) {
if ($this->isPure()->yes()) {
$typePure = TrinaryLogic::createYes();
foreach ($type->getCallableParametersAcceptors($scope) as $variant) {
$typePure = $typePure->and($variant->isPure());
}

return $isCallable->and(new AcceptsResult($typePure, []));
}

return $isCallable;
}

$variantsResult = null;
foreach ($type->getCallableParametersAcceptors($scope) as $variant) {
$isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny);
Expand Down Expand Up @@ -221,6 +243,7 @@ function (): string {
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
$this->templateTags,
$this->isPure,
);

return $printer->print($selfWithoutParameterNames->toPhpDocNode());
Expand All @@ -247,11 +270,16 @@ public function getThrowPoints(): array

public function getImpurePoints(): array
{
$pure = $this->isPure();
if ($pure->yes()) {
return [];
}

return [
new SimpleImpurePoint(
'functionCall',
'call to a callable',
false,
$pure->no(),
),
];
}
Expand Down Expand Up @@ -414,6 +442,7 @@ public function traverse(callable $cb): Type
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
$this->templateTags,
$this->isPure,
);
}

Expand Down Expand Up @@ -463,6 +492,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
$this->templateTypeMap,
$this->resolvedTemplateTypeMap,
$this->templateTags,
$this->isPure,
);
}

Expand Down Expand Up @@ -599,7 +629,7 @@ public function getFiniteTypes(): array
public function toPhpDocNode(): TypeNode
{
if ($this->isCommonCallable) {
return new IdentifierTypeNode('callable');
return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable');
}

$parameters = [];
Expand All @@ -623,7 +653,7 @@ public function toPhpDocNode(): TypeNode
}

return new CallableTypeNode(
new IdentifierTypeNode('callable'),
new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'),
$parameters,
$this->returnType->toPhpDocNode(),
$templateTags,
Expand All @@ -639,6 +669,10 @@ public static function __set_state(array $properties): Type
(bool) $properties['isCommonCallable'] ? null : $properties['parameters'],
(bool) $properties['isCommonCallable'] ? null : $properties['returnType'],
$properties['variadic'],
$properties['templateTypeMap'],
$properties['resolvedTemplateTypeMap'],
$properties['templateTags'],
$properties['isPure'],
);
}

Expand Down
10 changes: 7 additions & 3 deletions src/Type/CallableTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace PHPStan\Type;

use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
use PHPStan\TrinaryLogic;
use function array_key_exists;
use function array_merge;
Expand All @@ -13,8 +13,8 @@ class CallableTypeHelper
{

public static function isParametersAcceptorSuperTypeOf(
ParametersAcceptor $ours,
ParametersAcceptor $theirs,
CallableParametersAcceptor $ours,
CallableParametersAcceptor $theirs,
bool $treatMixedAsAny,
): AcceptsResult
{
Expand Down Expand Up @@ -103,6 +103,10 @@ public static function isParametersAcceptorSuperTypeOf(
$isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []);
}

if ($ours->isPure()->yes()) {
$result = $result->and(new AcceptsResult($theirs->isPure(), []));
}

return $result->and($isReturnTypeSuperType);
}

Expand Down
Loading

0 comments on commit cdaff5f

Please sign in to comment.