Skip to content

Commit f7bc280

Browse files
Niklanmglaman
andauthored
Add type specifier for Drupal\Component\Assertion\Inspector (mglaman#826)
* WIP * WIP * WIP * WIP * Fix CI errors * Try TypeCombinator instead of UnionType --------- Co-authored-by: Matt Glaman <[email protected]>
1 parent 9f27886 commit f7bc280

File tree

4 files changed

+521
-0
lines changed

4 files changed

+521
-0
lines changed

extension.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,3 +335,7 @@ services:
335335
class: mglaman\PHPStanDrupal\DeprecatedScope\DeprecationHelperScope
336336
tags:
337337
- phpstan.deprecations.deprecatedScopeResolver
338+
-
339+
class: mglaman\PHPStanDrupal\Type\InspectorTypeExtension
340+
tags:
341+
- phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension

src/Type/InspectorTypeExtension.php

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace mglaman\PHPStanDrupal\Type;
6+
7+
use Drupal\Component\Assertion\Inspector;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PHPStan\Analyser\Scope;
10+
use PHPStan\Analyser\SpecifiedTypes;
11+
use PHPStan\Analyser\TypeSpecifier;
12+
use PHPStan\Analyser\TypeSpecifierAwareExtension;
13+
use PHPStan\Analyser\TypeSpecifierContext;
14+
use PHPStan\Reflection\MethodReflection;
15+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
16+
use PHPStan\Type\Accessory\HasOffsetType;
17+
use PHPStan\Type\Accessory\NonEmptyArrayType;
18+
use PHPStan\Type\ArrayType;
19+
use PHPStan\Type\CallableType;
20+
use PHPStan\Type\ClosureType;
21+
use PHPStan\Type\Constant\ConstantStringType;
22+
use PHPStan\Type\FloatType;
23+
use PHPStan\Type\IntegerRangeType;
24+
use PHPStan\Type\IntegerType;
25+
use PHPStan\Type\IntersectionType;
26+
use PHPStan\Type\IterableType;
27+
use PHPStan\Type\MixedType;
28+
use PHPStan\Type\ObjectType;
29+
use PHPStan\Type\ResourceType;
30+
use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
31+
use PHPStan\Type\StringType;
32+
use PHPStan\Type\TypeCombinator;
33+
use PHPStan\Type\UnionType;
34+
use Stringable;
35+
use function class_exists;
36+
use function interface_exists;
37+
38+
final class InspectorTypeExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension
39+
{
40+
41+
private TypeSpecifier $typeSpecifier;
42+
43+
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
44+
{
45+
$this->typeSpecifier = $typeSpecifier;
46+
}
47+
48+
public function getClass(): string
49+
{
50+
return Inspector::class;
51+
}
52+
53+
public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool
54+
{
55+
$implemented_methods = [
56+
// 'assertTraverasble' is deprecated.
57+
'assertAll',
58+
'assertAllStrings',
59+
'assertAllStringable',
60+
'assertAllArrays',
61+
'assertStrictArray',
62+
'assertAllStrictArrays',
63+
'assertAllHaveKey',
64+
'assertAllIntegers',
65+
'assertAllFloat',
66+
'assertAllCallable',
67+
'assertAllNotEmpty',
68+
'assertAllNumeric',
69+
'assertAllMatch',
70+
'assertAllRegularExpressionMatch',
71+
'assertAllObjects',
72+
];
73+
74+
return in_array($staticMethodReflection->getName(), $implemented_methods, true);
75+
}
76+
77+
public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
78+
{
79+
return match ($staticMethodReflection->getName()) {
80+
default => new SpecifiedTypes(),
81+
'assertAll' => $this->specifyAssertAll($staticMethodReflection, $node, $scope, $context),
82+
'assertAllStrings' => $this->specifyAssertAllStrings($staticMethodReflection, $node, $scope, $context),
83+
'assertAllStringable' => $this->specifyAssertAllStringable($staticMethodReflection, $node, $scope, $context),
84+
'assertAllArrays' => $this->specifyAssertAllArrays($staticMethodReflection, $node, $scope, $context),
85+
'assertStrictArray' => $this->specifyAssertStrictArray($staticMethodReflection, $node, $scope, $context),
86+
'assertAllStrictArrays' => $this->specifyAssertAllStrictArrays($staticMethodReflection, $node, $scope, $context),
87+
'assertAllHaveKey' => $this->specifyAssertAllHaveKey($staticMethodReflection, $node, $scope, $context),
88+
'assertAllIntegers' => $this->specifyAssertAllIntegers($staticMethodReflection, $node, $scope, $context),
89+
'assertAllFloat' => $this->specifyAssertAllFloat($staticMethodReflection, $node, $scope, $context),
90+
'assertAllCallable' => $this->specifyAssertAllCallable($staticMethodReflection, $node, $scope, $context),
91+
'assertAllNotEmpty' => $this->specifyAssertAllNotEmpty($staticMethodReflection, $node, $scope, $context),
92+
'assertAllNumeric' => $this->specifyAssertAllNumeric($staticMethodReflection, $node, $scope, $context),
93+
'assertAllMatch' => $this->specifyAssertAllMatch($staticMethodReflection, $node, $scope, $context),
94+
'assertAllRegularExpressionMatch' => $this->specifyAssertAllRegularExpressionMatch($staticMethodReflection, $node, $scope, $context),
95+
'assertAllObjects' => $this->specifyAssertAllObjects($staticMethodReflection, $node, $scope, $context),
96+
};
97+
}
98+
99+
/**
100+
* @see Drupal\Component\Assertion\Inspector::assertAll()
101+
*/
102+
private function specifyAssertAll(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
103+
{
104+
$callable = $node->getArgs()[0]->value;
105+
$callableInfo = $scope->getType($callable);
106+
107+
if (!$callableInfo instanceof ClosureType) {
108+
return new SpecifiedTypes();
109+
}
110+
111+
return $this->typeSpecifier->create(
112+
$node->getArgs()[1]->value,
113+
new IterableType(new MixedType(true), $callableInfo->getReturnType()),
114+
TypeSpecifierContext::createTruthy(),
115+
$scope,
116+
);
117+
}
118+
119+
/**
120+
* @see Drupal\Component\Assertion\Inspector::assertAllStrings()
121+
*/
122+
private function specifyAssertAllStrings(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
123+
{
124+
return $this->typeSpecifier->create(
125+
$node->getArgs()[0]->value,
126+
new IterableType(new MixedType(true), new StringType()),
127+
TypeSpecifierContext::createTruthy(),
128+
$scope,
129+
);
130+
}
131+
132+
/**
133+
* @see Drupal\Component\Assertion\Inspector::assertAllStringable()
134+
*/
135+
private function specifyAssertAllStringable(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
136+
{
137+
// Drupal considers string as part of "stringable" as well.
138+
$stringable = TypeCombinator::union(new ObjectType(Stringable::class), new StringType());
139+
$newType = new IterableType(new MixedType(true), $stringable);
140+
141+
return $this->typeSpecifier->create(
142+
$node->getArgs()[0]->value,
143+
$newType,
144+
TypeSpecifierContext::createTruthy(),
145+
$scope,
146+
);
147+
}
148+
149+
/**
150+
* @see Drupal\Component\Assertion\Inspector::assertAllArrays()
151+
*/
152+
private function specifyAssertAllArrays(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
153+
{
154+
$arrayType = new ArrayType(new MixedType(true), new MixedType(true));
155+
$newType = new IterableType(new MixedType(true), $arrayType);
156+
157+
return $this->typeSpecifier->create(
158+
$node->getArgs()[0]->value,
159+
$newType,
160+
TypeSpecifierContext::createTruthy(),
161+
$scope,
162+
);
163+
}
164+
165+
/**
166+
* @see Drupal\Component\Assertion\Inspector::assertStrictArray()
167+
*/
168+
private function specifyAssertStrictArray(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
169+
{
170+
$newType = new ArrayType(
171+
// In Drupal, 'strict arrays' are defined as arrays whose indexes
172+
// consist of integers that are equal to or greater than 0.
173+
IntegerRangeType::createAllGreaterThanOrEqualTo(0),
174+
new MixedType(true),
175+
);
176+
177+
return $this->typeSpecifier->create(
178+
$node->getArgs()[0]->value,
179+
$newType,
180+
TypeSpecifierContext::createTruthy(),
181+
$scope,
182+
);
183+
}
184+
185+
/**
186+
* @see Drupal\Component\Assertion\Inspector::assertAllStrictArrays()
187+
*/
188+
private function specifyAssertAllStrictArrays(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
189+
{
190+
$newType = new IterableType(
191+
new MixedType(true),
192+
new ArrayType(
193+
IntegerRangeType::createAllGreaterThanOrEqualTo(0),
194+
new MixedType(true),
195+
),
196+
);
197+
198+
return $this->typeSpecifier->create(
199+
$node->getArgs()[0]->value,
200+
$newType,
201+
TypeSpecifierContext::createTruthy(),
202+
$scope,
203+
);
204+
}
205+
206+
/**
207+
* @see Drupal\Component\Assertion\Inspector::assertAllHaveKey()
208+
*/
209+
private function specifyAssertAllHaveKey(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
210+
{
211+
$args = $node->getArgs();
212+
213+
$traversableArg = $args[0]->value;
214+
$traversableType = $scope->getType($traversableArg);
215+
216+
if ($traversableType->isIterable()->no()) {
217+
return new SpecifiedTypes();
218+
}
219+
220+
$keys = [];
221+
foreach ($args as $delta => $arg) {
222+
if ($delta === 0) {
223+
continue;
224+
}
225+
226+
$argType = $scope->getType($arg->value);
227+
foreach ($argType->getConstantStrings() as $stringType) {
228+
$keys[] = $stringType->getValue();
229+
}
230+
}
231+
232+
$keyTypes = [];
233+
foreach ($keys as $key) {
234+
$keyTypes[] = new HasOffsetType(new ConstantStringType($key));
235+
}
236+
237+
$newArrayType = new ArrayType(
238+
new MixedType(true),
239+
new ArrayType(TypeCombinator::intersect(new MixedType(), ...$keyTypes), new MixedType(true)),
240+
);
241+
242+
return $this->typeSpecifier->create($traversableArg, $newArrayType, TypeSpecifierContext::createTruthy(), $scope);
243+
}
244+
245+
/**
246+
* @see Drupal\Component\Assertion\Inspector::assertAllIntegers()
247+
*/
248+
private function specifyAssertAllIntegers(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
249+
{
250+
return $this->typeSpecifier->create(
251+
$node->getArgs()[0]->value,
252+
new IterableType(new MixedType(true), new IntegerType()),
253+
TypeSpecifierContext::createTruthy(),
254+
$scope,
255+
);
256+
}
257+
258+
/**
259+
* @see Drupal\Component\Assertion\Inspector::assertAllFloat()
260+
*/
261+
private function specifyAssertAllFloat(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
262+
{
263+
return $this->typeSpecifier->create(
264+
$node->getArgs()[0]->value,
265+
new IterableType(new MixedType(true), new FloatType()),
266+
TypeSpecifierContext::createTruthy(),
267+
$scope,
268+
);
269+
}
270+
271+
/**
272+
* @see Drupal\Component\Assertion\Inspector::assertAllCallable()
273+
*/
274+
private function specifyAssertAllCallable(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
275+
{
276+
return $this->typeSpecifier->create(
277+
$node->getArgs()[0]->value,
278+
new IterableType(new MixedType(true), new CallableType()),
279+
TypeSpecifierContext::createTruthy(),
280+
$scope,
281+
);
282+
}
283+
284+
/**
285+
* @see Drupal\Component\Assertion\Inspector::assertAllNotEmpty()
286+
*/
287+
private function specifyAssertAllNotEmpty(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
288+
{
289+
$non_empty_types = [
290+
new NonEmptyArrayType(),
291+
new ObjectType('object'),
292+
new IntersectionType([new StringType(), new AccessoryNonEmptyStringType()]),
293+
IntegerRangeType::createAllGreaterThan(0),
294+
IntegerRangeType::createAllSmallerThan(0),
295+
new FloatType(),
296+
new ResourceType(),
297+
];
298+
$newType = new IterableType(new MixedType(true), new UnionType($non_empty_types));
299+
300+
return $this->typeSpecifier->create(
301+
$node->getArgs()[0]->value,
302+
$newType,
303+
TypeSpecifierContext::createTruthy(),
304+
$scope,
305+
);
306+
}
307+
308+
/**
309+
* @see Drupal\Component\Assertion\Inspector::assertAllNumeric()
310+
*/
311+
private function specifyAssertAllNumeric(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
312+
{
313+
return $this->typeSpecifier->create(
314+
$node->getArgs()[0]->value,
315+
new IterableType(new MixedType(true), new UnionType([new IntegerType(), new FloatType()])),
316+
TypeSpecifierContext::createTruthy(),
317+
$scope,
318+
);
319+
}
320+
321+
/**
322+
* @see Drupal\Component\Assertion\Inspector::assertAllMatch()
323+
*/
324+
private function specifyAssertAllMatch(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
325+
{
326+
return $this->typeSpecifier->create(
327+
$node->getArgs()[1]->value,
328+
new IterableType(new MixedType(true), new StringType()),
329+
TypeSpecifierContext::createTruthy(),
330+
$scope,
331+
);
332+
}
333+
334+
/**
335+
* @see Drupal\Component\Assertion\Inspector::assertAllRegularExpressionMatch()
336+
*/
337+
private function specifyAssertAllRegularExpressionMatch(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
338+
{
339+
return $this->typeSpecifier->create(
340+
$node->getArgs()[1]->value,
341+
// Drupal treats any non-string input in traversable as invalid
342+
// value, so it is possible to narrow type here.
343+
new IterableType(new MixedType(true), new StringType()),
344+
TypeSpecifierContext::createTruthy(),
345+
$scope,
346+
);
347+
}
348+
349+
/**
350+
* @see Drupal\Component\Assertion\Inspector::assertAllObjects()
351+
*/
352+
private function specifyAssertAllObjects(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
353+
{
354+
$args = $node->getArgs();
355+
$objectTypes = [];
356+
foreach ($args as $delta => $arg) {
357+
if ($delta === 0) {
358+
continue;
359+
}
360+
361+
$argType = $scope->getType($arg->value);
362+
foreach ($argType->getConstantStrings() as $stringType) {
363+
$classString = $stringType->getValue();
364+
// PHPStan does not recognize a string argument like '\\Stringable'
365+
// as a class string, so we need to explicitly check it.
366+
if (!class_exists($classString) && !interface_exists($classString)) {
367+
continue;
368+
}
369+
370+
$objectTypes[] = new ObjectType($classString);
371+
}
372+
}
373+
374+
return $this->typeSpecifier->create(
375+
$node->getArgs()[0]->value,
376+
new IterableType(new MixedType(true), TypeCombinator::union(...$objectTypes)),
377+
TypeSpecifierContext::createTruthy(),
378+
$scope,
379+
);
380+
}
381+
}

0 commit comments

Comments
 (0)