Skip to content

Commit 526b403

Browse files
committed
Support for AssertionChain
1 parent e3f2ef8 commit 526b403

12 files changed

+811
-291
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ This extension specifies types of values passed to:
3030
* `Assertion::notSame`
3131
* `nullOr*` and `all*` variants of the above methods
3232

33+
`Assert::that`, `Assert::thatNullOr` and `Assert::thatAll` chaining methods are also supported.
34+
3335
## Usage
3436

3537
To use this extension, require it in [Composer](https://getcomposer.org/):

extension.neon

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
services:
2+
-
3+
class: PHPStan\Type\BeberleiAssert\AssertionChainDynamicReturnTypeExtension
4+
tags:
5+
- phpstan.broker.dynamicMethodReturnTypeExtension
6+
7+
-
8+
class: PHPStan\Type\BeberleiAssert\AssertionChainTypeSpecifyingExtension
9+
tags:
10+
- phpstan.typeSpecifier.methodTypeSpecifyingExtension
11+
12+
-
13+
class: PHPStan\Type\BeberleiAssert\AssertThatDynamicMethodReturnTypeExtension
14+
tags:
15+
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
16+
217
-
318
class: PHPStan\Type\BeberleiAssert\AssertTypeSpecifyingExtension
419
tags:
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\BeberleiAssert;
4+
5+
use PhpParser\Node\Arg;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Analyser\SpecifiedTypes;
8+
use PHPStan\Analyser\TypeSpecifier;
9+
use PHPStan\Analyser\TypeSpecifierContext;
10+
use PHPStan\Type\ArrayType;
11+
use PHPStan\Type\Constant\ConstantStringType;
12+
use PHPStan\Type\IterableType;
13+
use PHPStan\Type\MixedType;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
18+
class AssertHelper
19+
{
20+
21+
/** @var \Closure[] */
22+
private static $resolvers;
23+
24+
/**
25+
* @param string $assertName
26+
* @param \PhpParser\Node\Arg[] $args
27+
* @return bool
28+
*/
29+
public static function isSupported(
30+
string $assertName,
31+
array $args
32+
): bool
33+
{
34+
$resolvers = self::getExpressionResolvers();
35+
36+
if (!array_key_exists($assertName, $resolvers)) {
37+
return false;
38+
}
39+
40+
$resolver = $resolvers[$assertName];
41+
$resolverReflection = new \ReflectionObject($resolver);
42+
43+
return count($args) >= (count($resolverReflection->getMethod('__invoke')->getParameters()) - 1);
44+
}
45+
46+
/**
47+
* @param TypeSpecifier $typeSpecifier
48+
* @param Scope $scope
49+
* @param string $assertName
50+
* @param \PhpParser\Node\Arg[] $args
51+
* @param bool $nullOr
52+
* @return SpecifiedTypes
53+
*/
54+
public static function specifyTypes(
55+
TypeSpecifier $typeSpecifier,
56+
Scope $scope,
57+
string $assertName,
58+
array $args,
59+
bool $nullOr
60+
): SpecifiedTypes
61+
{
62+
$expression = self::createExpression($scope, $assertName, $args);
63+
if ($expression === null) {
64+
return new SpecifiedTypes([], []);
65+
}
66+
67+
if ($nullOr) {
68+
$expression = new \PhpParser\Node\Expr\BinaryOp\BooleanOr(
69+
$expression,
70+
new \PhpParser\Node\Expr\BinaryOp\Identical(
71+
$args[0]->value,
72+
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null'))
73+
)
74+
);
75+
}
76+
77+
return $typeSpecifier->specifyTypesInCondition(
78+
$scope,
79+
$expression,
80+
TypeSpecifierContext::createTruthy()
81+
);
82+
}
83+
84+
public static function handleAll(
85+
TypeSpecifier $typeSpecifier,
86+
Scope $scope,
87+
SpecifiedTypes $specifiedTypes
88+
): SpecifiedTypes
89+
{
90+
if (count($specifiedTypes->getSureTypes()) > 0) {
91+
$sureTypes = $specifiedTypes->getSureTypes();
92+
reset($sureTypes);
93+
$exprString = key($sureTypes);
94+
$sureType = $sureTypes[$exprString];
95+
return self::arrayOrIterable($typeSpecifier, $scope, $sureType[0], $sureType[1]);
96+
}
97+
if (count($specifiedTypes->getSureNotTypes()) > 0) {
98+
throw new \PHPStan\ShouldNotHappenException();
99+
}
100+
101+
return $specifiedTypes;
102+
}
103+
104+
/**
105+
* @param TypeSpecifier $typeSpecifier
106+
* @param Scope $scope
107+
* @param string $assertName
108+
* @param \PhpParser\Node\Arg[] $args
109+
* @return SpecifiedTypes
110+
*/
111+
public static function handleAllNot(
112+
TypeSpecifier $typeSpecifier,
113+
Scope $scope,
114+
string $assertName,
115+
array $args
116+
): SpecifiedTypes
117+
{
118+
if ($assertName === 'notNull') {
119+
$expr = $args[0]->value;
120+
$currentType = $scope->getType($expr);
121+
return self::arrayOrIterable(
122+
$typeSpecifier,
123+
$scope,
124+
$expr,
125+
TypeCombinator::removeNull($currentType->getIterableValueType())
126+
);
127+
} elseif ($assertName === 'notIsInstanceOf') {
128+
$classType = $scope->getType($args[1]->value);
129+
if (!$classType instanceof ConstantStringType) {
130+
return new SpecifiedTypes([], []);
131+
}
132+
133+
$expr = $args[0]->value;
134+
$currentType = $scope->getType($expr);
135+
return self::arrayOrIterable(
136+
$typeSpecifier,
137+
$scope,
138+
$expr,
139+
TypeCombinator::remove(
140+
$currentType->getIterableValueType(),
141+
new ObjectType($classType->getValue())
142+
)
143+
);
144+
} elseif ($assertName === 'notSame') {
145+
$expr = $args[0]->value;
146+
$currentType = $scope->getType($expr);
147+
return self::arrayOrIterable(
148+
$typeSpecifier,
149+
$scope,
150+
$expr,
151+
TypeCombinator::remove(
152+
$currentType->getIterableValueType(),
153+
$scope->getType($args[1]->value)
154+
)
155+
);
156+
}
157+
158+
throw new \PHPStan\ShouldNotHappenException();
159+
}
160+
161+
private static function arrayOrIterable(
162+
TypeSpecifier $typeSpecifier,
163+
Scope $scope,
164+
\PhpParser\Node\Expr $expr,
165+
Type $type
166+
): SpecifiedTypes
167+
{
168+
$currentType = $scope->getType($expr);
169+
if ((new ArrayType(new MixedType(), new MixedType()))->isSuperTypeOf($currentType)->yes()) {
170+
$specifiedType = new ArrayType($currentType->getIterableKeyType(), $type);
171+
} elseif ((new IterableType(new MixedType(), new MixedType()))->isSuperTypeOf($currentType)->yes()) {
172+
$specifiedType = new IterableType($currentType->getIterableKeyType(), $type);
173+
} else {
174+
return new SpecifiedTypes([], []);
175+
}
176+
177+
return $typeSpecifier->create(
178+
$expr,
179+
$specifiedType,
180+
TypeSpecifierContext::createTruthy()
181+
);
182+
}
183+
184+
/**
185+
* @param Scope $scope
186+
* @param string $assertName
187+
* @param \PhpParser\Node\Arg[] $args
188+
* @return \PhpParser\Node\Expr|null
189+
*/
190+
private static function createExpression(
191+
Scope $scope,
192+
string $assertName,
193+
array $args
194+
): ?\PhpParser\Node\Expr
195+
{
196+
$resolvers = self::getExpressionResolvers();
197+
$resolver = $resolvers[$assertName];
198+
199+
return $resolver($scope, ...$args);
200+
}
201+
202+
/**
203+
* @return \Closure[]
204+
*/
205+
private static function getExpressionResolvers(): array
206+
{
207+
if (self::$resolvers === null) {
208+
self::$resolvers = [
209+
'integer' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
210+
return new \PhpParser\Node\Expr\FuncCall(
211+
new \PhpParser\Node\Name('is_int'),
212+
[$value]
213+
);
214+
},
215+
'string' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
216+
return new \PhpParser\Node\Expr\FuncCall(
217+
new \PhpParser\Node\Name('is_string'),
218+
[$value]
219+
);
220+
},
221+
'float' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
222+
return new \PhpParser\Node\Expr\FuncCall(
223+
new \PhpParser\Node\Name('is_float'),
224+
[$value]
225+
);
226+
},
227+
'numeric' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
228+
return new \PhpParser\Node\Expr\FuncCall(
229+
new \PhpParser\Node\Name('is_numeric'),
230+
[$value]
231+
);
232+
},
233+
'boolean' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
234+
return new \PhpParser\Node\Expr\FuncCall(
235+
new \PhpParser\Node\Name('is_bool'),
236+
[$value]
237+
);
238+
},
239+
'scalar' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
240+
return new \PhpParser\Node\Expr\FuncCall(
241+
new \PhpParser\Node\Name('is_scalar'),
242+
[$value]
243+
);
244+
},
245+
'objectOrClass' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
246+
$valueType = $scope->getType($value->value);
247+
if ((new \PHPStan\Type\StringType())->isSuperTypeOf($valueType)->yes()) {
248+
return null;
249+
}
250+
251+
return new \PhpParser\Node\Expr\FuncCall(
252+
new \PhpParser\Node\Name('is_object'),
253+
[$value]
254+
);
255+
},
256+
'isResource' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
257+
return new \PhpParser\Node\Expr\FuncCall(
258+
new \PhpParser\Node\Name('is_resource'),
259+
[$value]
260+
);
261+
},
262+
'isCallable' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
263+
return new \PhpParser\Node\Expr\FuncCall(
264+
new \PhpParser\Node\Name('is_callable'),
265+
[$value]
266+
);
267+
},
268+
'isArray' => function (Scope $scope, Arg $value): ?\PhpParser\Node\Expr {
269+
return new \PhpParser\Node\Expr\FuncCall(
270+
new \PhpParser\Node\Name('is_array'),
271+
[$value]
272+
);
273+
},
274+
'isInstanceOf' => function (Scope $scope, Arg $expr, Arg $class): ?\PhpParser\Node\Expr {
275+
$classType = $scope->getType($class->value);
276+
if (!$classType instanceof ConstantStringType) {
277+
return null;
278+
}
279+
280+
return new \PhpParser\Node\Expr\Instanceof_(
281+
$expr->value,
282+
new \PhpParser\Node\Name($classType->getValue())
283+
);
284+
},
285+
'notIsInstanceOf' => function (Scope $scope, Arg $expr, Arg $class): ?\PhpParser\Node\Expr {
286+
$classType = $scope->getType($class->value);
287+
if (!$classType instanceof ConstantStringType) {
288+
return null;
289+
}
290+
291+
return new \PhpParser\Node\Expr\BooleanNot(
292+
new \PhpParser\Node\Expr\Instanceof_(
293+
$expr->value,
294+
new \PhpParser\Node\Name($classType->getValue())
295+
)
296+
);
297+
},
298+
'true' => function (Scope $scope, Arg $expr): ?\PhpParser\Node\Expr {
299+
return new \PhpParser\Node\Expr\BinaryOp\Identical(
300+
$expr->value,
301+
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('true'))
302+
);
303+
},
304+
'false' => function (Scope $scope, Arg $expr): ?\PhpParser\Node\Expr {
305+
return new \PhpParser\Node\Expr\BinaryOp\Identical(
306+
$expr->value,
307+
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('false'))
308+
);
309+
},
310+
'null' => function (Scope $scope, Arg $expr): ?\PhpParser\Node\Expr {
311+
return new \PhpParser\Node\Expr\BinaryOp\Identical(
312+
$expr->value,
313+
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null'))
314+
);
315+
},
316+
'notNull' => function (Scope $scope, Arg $expr): ?\PhpParser\Node\Expr {
317+
return new \PhpParser\Node\Expr\BinaryOp\NotIdentical(
318+
$expr->value,
319+
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('null'))
320+
);
321+
},
322+
'same' => function (Scope $scope, Arg $value1, Arg $value2): ?\PhpParser\Node\Expr {
323+
return new \PhpParser\Node\Expr\BinaryOp\Identical(
324+
$value1->value,
325+
$value2->value
326+
);
327+
},
328+
'notSame' => function (Scope $scope, Arg $value1, Arg $value2): ?\PhpParser\Node\Expr {
329+
return new \PhpParser\Node\Expr\BinaryOp\NotIdentical(
330+
$value1->value,
331+
$value2->value
332+
);
333+
},
334+
'subclassOf' => function (Scope $scope, Arg $expr, Arg $class): ?\PhpParser\Node\Expr {
335+
return new \PhpParser\Node\Expr\FuncCall(
336+
new \PhpParser\Node\Name('is_subclass_of'),
337+
[
338+
new Arg($expr->value),
339+
$class,
340+
]
341+
);
342+
},
343+
];
344+
}
345+
346+
return self::$resolvers;
347+
}
348+
349+
}

0 commit comments

Comments
 (0)