Skip to content

Commit cdaff5f

Browse files
committed
pure-callable and pure-Closure
1 parent 242979a commit cdaff5f

22 files changed

+471
-18
lines changed

src/PhpDoc/TypeNodeResolver.php

+20-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
use PHPStan\Reflection\PassedByReference;
4444
use PHPStan\Reflection\ReflectionProvider;
4545
use PHPStan\ShouldNotHappenException;
46+
use PHPStan\TrinaryLogic;
4647
use PHPStan\Type\Accessory\AccessoryArrayListType;
4748
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
4849
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
@@ -352,9 +353,14 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
352353
return new IterableType(new MixedType(), new MixedType());
353354

354355
case 'callable':
355-
case 'pure-callable':
356356
return new CallableType();
357357

358+
case 'pure-callable':
359+
return new CallableType(null, null, true, null, null, [], TrinaryLogic::createYes());
360+
361+
case 'pure-closure':
362+
return new ClosureType();
363+
358364
case 'resource':
359365
$type = $this->tryResolvePseudoTypeClassType($typeNode, $nameScope);
360366

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

930936
if ($mainType instanceof CallableType) {
931-
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags);
937+
$pure = $mainType->isPure();
938+
if ($pure->yes() && $returnType->isVoid()->yes()) {
939+
return new ErrorType();
940+
}
941+
942+
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap, null, $templateTags, $pure);
932943

933944
} elseif (
934945
$mainType instanceof ObjectType
@@ -941,6 +952,13 @@ function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadi
941952
false,
942953
),
943954
]);
955+
} elseif ($mainType instanceof ClosureType) {
956+
$closure = new ClosureType($parameters, $returnType, $isVariadic, $templateTypeMap, null, null, $templateTags, [], $mainType->getImpurePoints());
957+
if ($closure->isPure()->yes() && $returnType->isVoid()->yes()) {
958+
return new ErrorType();
959+
}
960+
961+
return $closure;
944962
}
945963

946964
return new ErrorType();

src/Reflection/Callables/CallableParametersAcceptor.php

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Node\InvalidateExprNode;
66
use PHPStan\Reflection\ParametersAcceptor;
7+
use PHPStan\TrinaryLogic;
78

89
/**
910
* @api
@@ -16,6 +17,8 @@ interface CallableParametersAcceptor extends ParametersAcceptor
1617
*/
1718
public function getThrowPoints(): array;
1819

20+
public function isPure(): TrinaryLogic;
21+
1922
/**
2023
* @return SimpleImpurePoint[]
2124
*/

src/Reflection/Callables/FunctionCallableVariant.php

+21
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
use PHPStan\Reflection\FunctionReflection;
77
use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
88
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
9+
use PHPStan\TrinaryLogic;
910
use PHPStan\Type\Generic\TemplateTypeMap;
1011
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
1112
use PHPStan\Type\NeverType;
1213
use PHPStan\Type\ObjectType;
1314
use PHPStan\Type\Type;
1415
use Throwable;
1516
use function array_map;
17+
use function count;
1618
use function sprintf;
1719

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

120+
public function isPure(): TrinaryLogic
121+
{
122+
$impurePoints = $this->getImpurePoints();
123+
if (count($impurePoints) === 0) {
124+
return TrinaryLogic::createYes();
125+
}
126+
127+
$certainCount = 0;
128+
foreach ($impurePoints as $impurePoint) {
129+
if (!$impurePoint->isCertain()) {
130+
continue;
131+
}
132+
133+
$certainCount++;
134+
}
135+
136+
return $certainCount > 0 ? TrinaryLogic::createNo() : TrinaryLogic::createMaybe();
137+
}
138+
118139
public function getImpurePoints(): array
119140
{
120141
if ($this->impurePoints !== null) {

src/Reflection/InaccessibleMethod.php

+14-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PHPStan\Reflection;
44

55
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
6+
use PHPStan\Reflection\Callables\SimpleImpurePoint;
7+
use PHPStan\TrinaryLogic;
68
use PHPStan\Type\Generic\TemplateTypeMap;
79
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
810
use PHPStan\Type\MixedType;
@@ -58,9 +60,20 @@ public function getThrowPoints(): array
5860
return [];
5961
}
6062

63+
public function isPure(): TrinaryLogic
64+
{
65+
return TrinaryLogic::createMaybe();
66+
}
67+
6168
public function getImpurePoints(): array
6269
{
63-
return [];
70+
return [
71+
new SimpleImpurePoint(
72+
'methodCall',
73+
'call to unknown method',
74+
false,
75+
),
76+
];
6477
}
6578

6679
public function getInvalidateExpressions(): array

src/Reflection/TrivialParametersAcceptor.php

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
66
use PHPStan\Reflection\Callables\SimpleImpurePoint;
7+
use PHPStan\TrinaryLogic;
78
use PHPStan\Type\Generic\TemplateTypeMap;
89
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
910
use PHPStan\Type\MixedType;
@@ -64,6 +65,11 @@ public function getThrowPoints(): array
6465
return [];
6566
}
6667

68+
public function isPure(): TrinaryLogic
69+
{
70+
return TrinaryLogic::createMaybe();
71+
}
72+
6773
public function getImpurePoints(): array
6874
{
6975
return [

src/Rules/MissingTypehintCheck.php

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\ShouldNotHappenException;
1010
use PHPStan\Type\Accessory\AccessoryType;
1111
use PHPStan\Type\CallableType;
12+
use PHPStan\Type\ClosureType;
1213
use PHPStan\Type\ConditionalType;
1314
use PHPStan\Type\ConditionalTypeForParameter;
1415
use PHPStan\Type\Generic\GenericObjectType;
@@ -162,8 +163,10 @@ public function getCallablesWithMissingSignature(Type $type): array
162163
$result = [];
163164
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type {
164165
if (
165-
($type instanceof CallableType && $type->isCommonCallable()) ||
166-
($type instanceof ObjectType && $type->getClassName() === Closure::class)) {
166+
($type instanceof CallableType && $type->isCommonCallable())
167+
|| ($type instanceof ClosureType && $type->isCommonCallable())
168+
|| ($type instanceof ObjectType && $type->getClassName() === Closure::class)
169+
) {
167170
$result[] = $type;
168171
}
169172
return $traverse($type);

src/Rules/RuleLevelHelper.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,25 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
9595
$acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
9696
if ($acceptedType instanceof CallableType) {
9797
if ($acceptedType->isCommonCallable()) {
98-
return new CallableType(null, null, $acceptedType->isVariadic());
98+
return $acceptedType;
9999
}
100100

101101
return new CallableType(
102102
$acceptedType->getParameters(),
103103
$traverse($this->transformCommonType($acceptedType->getReturnType())),
104104
$acceptedType->isVariadic(),
105+
$acceptedType->getTemplateTypeMap(),
106+
$acceptedType->getResolvedTemplateTypeMap(),
107+
$acceptedType->getTemplateTags(),
108+
$acceptedType->isPure(),
105109
);
106110
}
107111

108112
if ($acceptedType instanceof ClosureType) {
113+
if ($acceptedType->isCommonCallable()) {
114+
return $acceptedType;
115+
}
116+
109117
return new ClosureType(
110118
$acceptedType->getParameters(),
111119
$traverse($this->transformCommonType($acceptedType->getReturnType())),

src/Type/CallableType.php

+38-4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ class CallableType implements CompoundType, CallableParametersAcceptor
6363

6464
private TemplateTypeMap $resolvedTemplateTypeMap;
6565

66+
private TrinaryLogic $isPure;
67+
6668
/**
6769
* @api
6870
* @param array<int, ParameterReflection>|null $parameters
@@ -75,13 +77,15 @@ public function __construct(
7577
?TemplateTypeMap $templateTypeMap = null,
7678
?TemplateTypeMap $resolvedTemplateTypeMap = null,
7779
private array $templateTags = [],
80+
?TrinaryLogic $isPure = null,
7881
)
7982
{
8083
$this->parameters = $parameters ?? [];
8184
$this->returnType = $returnType ?? new MixedType();
8285
$this->isCommonCallable = $parameters === null && $returnType === null;
8386
$this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
8487
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
88+
$this->isPure = $isPure ?? TrinaryLogic::createMaybe();
8589
}
8690

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

99+
public function isPure(): TrinaryLogic
100+
{
101+
return $this->isPure;
102+
}
103+
95104
/**
96105
* @return string[]
97106
*/
@@ -146,7 +155,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic
146155
private function isSuperTypeOfInternal(Type $type, bool $treatMixedAsAny): AcceptsResult
147156
{
148157
$isCallable = new AcceptsResult($type->isCallable(), []);
149-
if ($isCallable->no() || $this->isCommonCallable) {
158+
if ($isCallable->no()) {
150159
return $isCallable;
151160
}
152161

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

167+
if ($this->isCommonCallable) {
168+
if ($this->isPure()->yes()) {
169+
$typePure = TrinaryLogic::createYes();
170+
foreach ($type->getCallableParametersAcceptors($scope) as $variant) {
171+
$typePure = $typePure->and($variant->isPure());
172+
}
173+
174+
return $isCallable->and(new AcceptsResult($typePure, []));
175+
}
176+
177+
return $isCallable;
178+
}
179+
158180
$variantsResult = null;
159181
foreach ($type->getCallableParametersAcceptors($scope) as $variant) {
160182
$isSuperType = CallableTypeHelper::isParametersAcceptorSuperTypeOf($this, $variant, $treatMixedAsAny);
@@ -221,6 +243,7 @@ function (): string {
221243
$this->templateTypeMap,
222244
$this->resolvedTemplateTypeMap,
223245
$this->templateTags,
246+
$this->isPure,
224247
);
225248

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

248271
public function getImpurePoints(): array
249272
{
273+
$pure = $this->isPure();
274+
if ($pure->yes()) {
275+
return [];
276+
}
277+
250278
return [
251279
new SimpleImpurePoint(
252280
'functionCall',
253281
'call to a callable',
254-
false,
282+
$pure->no(),
255283
),
256284
];
257285
}
@@ -414,6 +442,7 @@ public function traverse(callable $cb): Type
414442
$this->templateTypeMap,
415443
$this->resolvedTemplateTypeMap,
416444
$this->templateTags,
445+
$this->isPure,
417446
);
418447
}
419448

@@ -463,6 +492,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type
463492
$this->templateTypeMap,
464493
$this->resolvedTemplateTypeMap,
465494
$this->templateTags,
495+
$this->isPure,
466496
);
467497
}
468498

@@ -599,7 +629,7 @@ public function getFiniteTypes(): array
599629
public function toPhpDocNode(): TypeNode
600630
{
601631
if ($this->isCommonCallable) {
602-
return new IdentifierTypeNode('callable');
632+
return new IdentifierTypeNode($this->isPure()->yes() ? 'pure-callable' : 'callable');
603633
}
604634

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

625655
return new CallableTypeNode(
626-
new IdentifierTypeNode('callable'),
656+
new IdentifierTypeNode($this->isPure->yes() ? 'pure-callable' : 'callable'),
627657
$parameters,
628658
$this->returnType->toPhpDocNode(),
629659
$templateTags,
@@ -639,6 +669,10 @@ public static function __set_state(array $properties): Type
639669
(bool) $properties['isCommonCallable'] ? null : $properties['parameters'],
640670
(bool) $properties['isCommonCallable'] ? null : $properties['returnType'],
641671
$properties['variadic'],
672+
$properties['templateTypeMap'],
673+
$properties['resolvedTemplateTypeMap'],
674+
$properties['templateTags'],
675+
$properties['isPure'],
642676
);
643677
}
644678

src/Type/CallableTypeHelper.php

+7-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace PHPStan\Type;
44

5-
use PHPStan\Reflection\ParametersAcceptor;
5+
use PHPStan\Reflection\Callables\CallableParametersAcceptor;
66
use PHPStan\TrinaryLogic;
77
use function array_key_exists;
88
use function array_merge;
@@ -13,8 +13,8 @@ class CallableTypeHelper
1313
{
1414

1515
public static function isParametersAcceptorSuperTypeOf(
16-
ParametersAcceptor $ours,
17-
ParametersAcceptor $theirs,
16+
CallableParametersAcceptor $ours,
17+
CallableParametersAcceptor $theirs,
1818
bool $treatMixedAsAny,
1919
): AcceptsResult
2020
{
@@ -103,6 +103,10 @@ public static function isParametersAcceptorSuperTypeOf(
103103
$isReturnTypeSuperType = new AcceptsResult($ours->getReturnType()->isSuperTypeOf($theirReturnType), []);
104104
}
105105

106+
if ($ours->isPure()->yes()) {
107+
$result = $result->and(new AcceptsResult($theirs->isPure(), []));
108+
}
109+
106110
return $result->and($isReturnTypeSuperType);
107111
}
108112

0 commit comments

Comments
 (0)