Skip to content

Commit 23c9ddf

Browse files
authored
Fixes for InspectorTypeExtension (mglaman#855)
* Fixes for InspectorTypeExtension (mglaman#853) * test for mglaman#852 * Update InspectorTypeExtension - assertAll accepts any callable and does not use the callable's return type but uses it like array_filter - assertAllHaveKey does not specify an array, just iterable - never type caused by intersect vs union but we lose offset exists * fix usage of HasOffsetType HasOffsetType is a compound type which represents an accessible object that has a key, not the constant string key offset itself * add exact test for project_browser * do not mark mixed as explicitly mixed (cherry picked from commit 9a31538) * fix calls to TypeSpecifier::create * Revert "fix calls to TypeSpecifier::create" This reverts commit dc8c110. * fix error properly * fix tests
1 parent 7683852 commit 23c9ddf

File tree

4 files changed

+72
-41
lines changed

4 files changed

+72
-41
lines changed

src/Type/InspectorTypeExtension.php

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use PHPStan\Type\Accessory\NonEmptyArrayType;
1818
use PHPStan\Type\ArrayType;
1919
use PHPStan\Type\CallableType;
20-
use PHPStan\Type\ClosureType;
2120
use PHPStan\Type\Constant\ConstantStringType;
2221
use PHPStan\Type\FloatType;
2322
use PHPStan\Type\IntegerRangeType;
@@ -104,13 +103,13 @@ private function specifyAssertAll(MethodReflection $staticMethodReflection, Stat
104103
$callable = $node->getArgs()[0]->value;
105104
$callableInfo = $scope->getType($callable);
106105

107-
if (!$callableInfo instanceof ClosureType) {
106+
if (!$callableInfo->isCallable()->yes()) {
108107
return new SpecifiedTypes();
109108
}
110109

111110
return $this->typeSpecifier->create(
112111
$node->getArgs()[1]->value,
113-
new IterableType(new MixedType(true), $callableInfo->getReturnType()),
112+
new IterableType(new MixedType(), new MixedType()),
114113
TypeSpecifierContext::createTruthy(),
115114
false,
116115
$scope,
@@ -124,7 +123,7 @@ private function specifyAssertAllStrings(MethodReflection $staticMethodReflectio
124123
{
125124
return $this->typeSpecifier->create(
126125
$node->getArgs()[0]->value,
127-
new IterableType(new MixedType(true), new StringType()),
126+
new IterableType(new MixedType(), new StringType()),
128127
TypeSpecifierContext::createTruthy(),
129128
false,
130129
$scope,
@@ -138,7 +137,7 @@ private function specifyAssertAllStringable(MethodReflection $staticMethodReflec
138137
{
139138
// Drupal considers string as part of "stringable" as well.
140139
$stringable = TypeCombinator::union(new ObjectType(Stringable::class), new StringType());
141-
$newType = new IterableType(new MixedType(true), $stringable);
140+
$newType = new IterableType(new MixedType(), $stringable);
142141

143142
return $this->typeSpecifier->create(
144143
$node->getArgs()[0]->value,
@@ -154,8 +153,8 @@ private function specifyAssertAllStringable(MethodReflection $staticMethodReflec
154153
*/
155154
private function specifyAssertAllArrays(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
156155
{
157-
$arrayType = new ArrayType(new MixedType(true), new MixedType(true));
158-
$newType = new IterableType(new MixedType(true), $arrayType);
156+
$arrayType = new ArrayType(new MixedType(), new MixedType());
157+
$newType = new IterableType(new MixedType(), $arrayType);
159158

160159
return $this->typeSpecifier->create(
161160
$node->getArgs()[0]->value,
@@ -175,7 +174,7 @@ private function specifyAssertStrictArray(MethodReflection $staticMethodReflecti
175174
// In Drupal, 'strict arrays' are defined as arrays whose indexes
176175
// consist of integers that are equal to or greater than 0.
177176
IntegerRangeType::createAllGreaterThanOrEqualTo(0),
178-
new MixedType(true),
177+
new MixedType(),
179178
);
180179

181180
return $this->typeSpecifier->create(
@@ -193,10 +192,10 @@ private function specifyAssertStrictArray(MethodReflection $staticMethodReflecti
193192
private function specifyAssertAllStrictArrays(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes
194193
{
195194
$newType = new IterableType(
196-
new MixedType(true),
195+
new MixedType(),
197196
new ArrayType(
198197
IntegerRangeType::createAllGreaterThanOrEqualTo(0),
199-
new MixedType(true),
198+
new MixedType(),
200199
),
201200
);
202201

@@ -235,17 +234,20 @@ private function specifyAssertAllHaveKey(MethodReflection $staticMethodReflectio
235234
}
236235
}
237236

238-
$keyTypes = [];
237+
// @see ArrayKeyExistsFunctionTypeSpecifyingExtension.
238+
$possibleTypes = [
239+
new ArrayType(new MixedType(), new MixedType())
240+
];
239241
foreach ($keys as $key) {
240-
$keyTypes[] = new HasOffsetType(new ConstantStringType($key));
242+
$possibleTypes[] = new HasOffsetType(new ConstantStringType($key));
241243
}
242244

243-
$newArrayType = new ArrayType(
244-
new MixedType(true),
245-
new ArrayType(TypeCombinator::intersect(new MixedType(), ...$keyTypes), new MixedType(true)),
245+
$newType = new IterableType(
246+
new MixedType(),
247+
TypeCombinator::intersect(...$possibleTypes),
246248
);
247249

248-
return $this->typeSpecifier->create($traversableArg, $newArrayType, TypeSpecifierContext::createTruthy(), false, $scope);
250+
return $this->typeSpecifier->create($traversableArg, $newType, TypeSpecifierContext::createTruthy(), false, $scope);
249251
}
250252

251253
/**
@@ -255,7 +257,7 @@ private function specifyAssertAllIntegers(MethodReflection $staticMethodReflecti
255257
{
256258
return $this->typeSpecifier->create(
257259
$node->getArgs()[0]->value,
258-
new IterableType(new MixedType(true), new IntegerType()),
260+
new IterableType(new MixedType(), new IntegerType()),
259261
TypeSpecifierContext::createTruthy(),
260262
false,
261263
$scope,
@@ -269,7 +271,7 @@ private function specifyAssertAllFloat(MethodReflection $staticMethodReflection,
269271
{
270272
return $this->typeSpecifier->create(
271273
$node->getArgs()[0]->value,
272-
new IterableType(new MixedType(true), new FloatType()),
274+
new IterableType(new MixedType(), new FloatType()),
273275
TypeSpecifierContext::createTruthy(),
274276
false,
275277
$scope,
@@ -283,7 +285,7 @@ private function specifyAssertAllCallable(MethodReflection $staticMethodReflecti
283285
{
284286
return $this->typeSpecifier->create(
285287
$node->getArgs()[0]->value,
286-
new IterableType(new MixedType(true), new CallableType()),
288+
new IterableType(new MixedType(), new CallableType()),
287289
TypeSpecifierContext::createTruthy(),
288290
false,
289291
$scope,
@@ -304,7 +306,7 @@ private function specifyAssertAllNotEmpty(MethodReflection $staticMethodReflecti
304306
new FloatType(),
305307
new ResourceType(),
306308
];
307-
$newType = new IterableType(new MixedType(true), new UnionType($non_empty_types));
309+
$newType = new IterableType(new MixedType(), new UnionType($non_empty_types));
308310

309311
return $this->typeSpecifier->create(
310312
$node->getArgs()[0]->value,
@@ -322,7 +324,7 @@ private function specifyAssertAllNumeric(MethodReflection $staticMethodReflectio
322324
{
323325
return $this->typeSpecifier->create(
324326
$node->getArgs()[0]->value,
325-
new IterableType(new MixedType(true), new UnionType([new IntegerType(), new FloatType()])),
327+
new IterableType(new MixedType(), new UnionType([new IntegerType(), new FloatType()])),
326328
TypeSpecifierContext::createTruthy(),
327329
false,
328330
$scope,
@@ -336,7 +338,7 @@ private function specifyAssertAllMatch(MethodReflection $staticMethodReflection,
336338
{
337339
return $this->typeSpecifier->create(
338340
$node->getArgs()[1]->value,
339-
new IterableType(new MixedType(true), new StringType()),
341+
new IterableType(new MixedType(), new StringType()),
340342
TypeSpecifierContext::createTruthy(),
341343
false,
342344
$scope,
@@ -352,7 +354,7 @@ private function specifyAssertAllRegularExpressionMatch(MethodReflection $static
352354
$node->getArgs()[1]->value,
353355
// Drupal treats any non-string input in traversable as invalid
354356
// value, so it is possible to narrow type here.
355-
new IterableType(new MixedType(true), new StringType()),
357+
new IterableType(new MixedType(), new StringType()),
356358
TypeSpecifierContext::createTruthy(),
357359
false,
358360
$scope,
@@ -386,7 +388,7 @@ private function specifyAssertAllObjects(MethodReflection $staticMethodReflectio
386388

387389
return $this->typeSpecifier->create(
388390
$node->getArgs()[0]->value,
389-
new IterableType(new MixedType(true), TypeCombinator::union(...$objectTypes)),
391+
new IterableType(new MixedType(), TypeCombinator::union(...$objectTypes)),
390392
TypeSpecifierContext::createTruthy(),
391393
false,
392394
$scope,

tests/src/Type/InspectorTypeExtensionTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ final class InspectorTypeExtensionTest extends TypeInferenceTestCase {
1313

1414
public static function dataFileAsserts(): iterable {
1515
yield from self::gatherAssertTypes(__DIR__ . '/data/inspector.php');
16+
yield from self::gatherAssertTypes(__DIR__ . '/data/bug-852.php');
1617
}
1718

1819
/**
@@ -28,5 +29,5 @@ public function testFileAsserts(
2829
): void {
2930
$this->assertFileAsserts($assertType, $file, ...$args);
3031
}
31-
32+
3233
}

tests/src/Type/data/bug-852.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
4+
use Drupal\Component\Assertion\Inspector;
5+
use Drupal\Core\Url;
6+
use function PHPStan\Testing\assertType;
7+
8+
function foo(array $baz): void {
9+
assert(Inspector::assertAllArrays($baz));
10+
assertType('array<array>', $baz);
11+
12+
assert(Inspector::assertAll(fn (array $i): bool => $i['file'] instanceof Url, $baz));
13+
assertType('array<array>', $baz);
14+
15+
assert(Inspector::assertAllHaveKey($baz, 'alt'));
16+
assertType("array<array&hasOffset('alt')>", $baz);
17+
}
18+
19+
function bar(array $zed): void {
20+
assert(Inspector::assertAll(fn (string $value) => $value === 'foo', $zed));
21+
assertType('array', $zed);
22+
23+
}
24+
25+
/**
26+
* @param array $images
27+
* Images of the project. Each item needs to be an array with two elements:
28+
* `file`, which is a \Drupal\Core\Url object pointing to the image, and
29+
* `alt`, which is the alt text.
30+
*/
31+
function project_browser_example(array $images) {
32+
assert(
33+
Inspector::assertAllArrays($images) &&
34+
Inspector::assertAllHaveKey($images, 'file') &&
35+
Inspector::assertAll(fn (array $i): bool => $i['file'] instanceof Url, $images) &&
36+
Inspector::assertAllHaveKey($images, 'alt')
37+
) or throw new \InvalidArgumentException('The project images must be arrays with `file` and `alt` elements.');
38+
assertType("array<array&hasOffset('alt')&hasOffset('file')>", $images);
39+
}

tests/src/Type/data/inspector.php

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,16 @@ function mixed_function(): mixed {
1010
}
1111

1212
// Inspector::assertAll()
13-
$callable = fn (): string => 'foo';
13+
$callable = fn (string $value): bool => $value === 'foo';
1414
$input = mixed_function();
1515
assert(Inspector::assertAll($callable, $input));
16-
assertType("iterable<'foo'>", $input);
16+
assertType("iterable", $input);
1717

1818
$input = mixed_function();
19-
$callable = static function (): int {
20-
return rand(0, 1000);
21-
};
19+
$callable = is_string(...);
2220
assert(Inspector::assertAll($callable, $input));
23-
assertType('iterable<int<0, 1000>>', $input);
21+
assertType('iterable', $input);
2422

25-
$input = mixed_function();
26-
$callable = Closure::fromCallable('is_string');
27-
assert(Inspector::assertAll($callable, $input));
28-
assertType('iterable<bool>', $input);
29-
30-
$callable = fn(mixed $arg): string => (string) $arg;
31-
$input = mixed_function();
32-
assert(Inspector::assertAll($callable, $input));
33-
assertType('iterable<string>', $input);
3423

3524
// Inspector::assertAllStrings()
3625
$input = mixed_function();
@@ -60,7 +49,7 @@ function mixed_function(): mixed {
6049
// Inspector::assertAllHaveKey()
6150
$input = mixed_function();
6251
assert(Inspector::assertAllHaveKey($input, 'foo', 'baz'));
63-
assertType("array<array<hasOffset('baz')&hasOffset('foo'), mixed>>", $input);
52+
assertType("iterable<array&hasOffset('baz')&hasOffset('foo')>", $input);
6453

6554
// Inspector::assertAllIntegers()
6655
$input = mixed_function();

0 commit comments

Comments
 (0)