Skip to content

Commit 0a73fb2

Browse files
authored
1.x: Fix negation types for Inspector (#861)
* Fix negation types for Inspector * Fix CI * Improve type for negated '::assertAll()'
1 parent 23c9ddf commit 0a73fb2

File tree

2 files changed

+124
-47
lines changed

2 files changed

+124
-47
lines changed

src/Type/InspectorTypeExtension.php

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,23 @@ private function specifyAssertAll(MethodReflection $staticMethodReflection, Stat
107107
return new SpecifiedTypes();
108108
}
109109

110+
// In a negation context, we cannot precisely narrow types because we do
111+
// not know the exact logic of the callable function. This means we
112+
// cannot safely return 'mixed~iterable' since the value might still be
113+
// a valid iterable.
114+
//
115+
// For example, a negation context with an 'is_string(...)' callable
116+
// does not necessarily mean that the value cannot be an
117+
// 'iterable<int>'. In such cases, it is safer to skip type narrowing
118+
// altogether to prevent introducing new bugs into the code.
119+
if ($context->false()) {
120+
return new SpecifiedTypes();
121+
}
122+
110123
return $this->typeSpecifier->create(
111124
$node->getArgs()[1]->value,
112125
new IterableType(new MixedType(), new MixedType()),
113-
TypeSpecifierContext::createTruthy(),
126+
$context,
114127
false,
115128
$scope,
116129
);
@@ -124,7 +137,7 @@ private function specifyAssertAllStrings(MethodReflection $staticMethodReflectio
124137
return $this->typeSpecifier->create(
125138
$node->getArgs()[0]->value,
126139
new IterableType(new MixedType(), new StringType()),
127-
TypeSpecifierContext::createTruthy(),
140+
$context,
128141
false,
129142
$scope,
130143
);
@@ -142,7 +155,7 @@ private function specifyAssertAllStringable(MethodReflection $staticMethodReflec
142155
return $this->typeSpecifier->create(
143156
$node->getArgs()[0]->value,
144157
$newType,
145-
TypeSpecifierContext::createTruthy(),
158+
$context,
146159
false,
147160
$scope,
148161
);
@@ -159,7 +172,7 @@ private function specifyAssertAllArrays(MethodReflection $staticMethodReflection
159172
return $this->typeSpecifier->create(
160173
$node->getArgs()[0]->value,
161174
$newType,
162-
TypeSpecifierContext::createTruthy(),
175+
$context,
163176
false,
164177
$scope,
165178
);
@@ -180,7 +193,7 @@ private function specifyAssertStrictArray(MethodReflection $staticMethodReflecti
180193
return $this->typeSpecifier->create(
181194
$node->getArgs()[0]->value,
182195
$newType,
183-
TypeSpecifierContext::createTruthy(),
196+
$context,
184197
false,
185198
$scope,
186199
);
@@ -202,7 +215,7 @@ private function specifyAssertAllStrictArrays(MethodReflection $staticMethodRefl
202215
return $this->typeSpecifier->create(
203216
$node->getArgs()[0]->value,
204217
$newType,
205-
TypeSpecifierContext::createTruthy(),
218+
$context,
206219
false,
207220
$scope,
208221
);
@@ -247,7 +260,7 @@ private function specifyAssertAllHaveKey(MethodReflection $staticMethodReflectio
247260
TypeCombinator::intersect(...$possibleTypes),
248261
);
249262

250-
return $this->typeSpecifier->create($traversableArg, $newType, TypeSpecifierContext::createTruthy(), false, $scope);
263+
return $this->typeSpecifier->create($traversableArg, $newType, $context, false, $scope);
251264
}
252265

253266
/**
@@ -258,7 +271,7 @@ private function specifyAssertAllIntegers(MethodReflection $staticMethodReflecti
258271
return $this->typeSpecifier->create(
259272
$node->getArgs()[0]->value,
260273
new IterableType(new MixedType(), new IntegerType()),
261-
TypeSpecifierContext::createTruthy(),
274+
$context,
262275
false,
263276
$scope,
264277
);
@@ -272,7 +285,7 @@ private function specifyAssertAllFloat(MethodReflection $staticMethodReflection,
272285
return $this->typeSpecifier->create(
273286
$node->getArgs()[0]->value,
274287
new IterableType(new MixedType(), new FloatType()),
275-
TypeSpecifierContext::createTruthy(),
288+
$context,
276289
false,
277290
$scope,
278291
);
@@ -286,7 +299,7 @@ private function specifyAssertAllCallable(MethodReflection $staticMethodReflecti
286299
return $this->typeSpecifier->create(
287300
$node->getArgs()[0]->value,
288301
new IterableType(new MixedType(), new CallableType()),
289-
TypeSpecifierContext::createTruthy(),
302+
$context,
290303
false,
291304
$scope,
292305
);
@@ -311,7 +324,7 @@ private function specifyAssertAllNotEmpty(MethodReflection $staticMethodReflecti
311324
return $this->typeSpecifier->create(
312325
$node->getArgs()[0]->value,
313326
$newType,
314-
TypeSpecifierContext::createTruthy(),
327+
$context,
315328
false,
316329
$scope,
317330
);
@@ -325,7 +338,7 @@ private function specifyAssertAllNumeric(MethodReflection $staticMethodReflectio
325338
return $this->typeSpecifier->create(
326339
$node->getArgs()[0]->value,
327340
new IterableType(new MixedType(), new UnionType([new IntegerType(), new FloatType()])),
328-
TypeSpecifierContext::createTruthy(),
341+
$context,
329342
false,
330343
$scope,
331344
);
@@ -339,7 +352,7 @@ private function specifyAssertAllMatch(MethodReflection $staticMethodReflection,
339352
return $this->typeSpecifier->create(
340353
$node->getArgs()[1]->value,
341354
new IterableType(new MixedType(), new StringType()),
342-
TypeSpecifierContext::createTruthy(),
355+
$context,
343356
false,
344357
$scope,
345358
);
@@ -355,7 +368,7 @@ private function specifyAssertAllRegularExpressionMatch(MethodReflection $static
355368
// Drupal treats any non-string input in traversable as invalid
356369
// value, so it is possible to narrow type here.
357370
new IterableType(new MixedType(), new StringType()),
358-
TypeSpecifierContext::createTruthy(),
371+
$context,
359372
false,
360373
$scope,
361374
);
@@ -389,7 +402,7 @@ private function specifyAssertAllObjects(MethodReflection $staticMethodReflectio
389402
return $this->typeSpecifier->create(
390403
$node->getArgs()[0]->value,
391404
new IterableType(new MixedType(), TypeCombinator::union(...$objectTypes)),
392-
TypeSpecifierContext::createTruthy(),
405+
$context,
393406
false,
394407
$scope,
395408
);

tests/src/Type/data/inspector.php

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,82 +12,146 @@ function mixed_function(): mixed {
1212
// Inspector::assertAll()
1313
$callable = fn (string $value): bool => $value === 'foo';
1414
$input = mixed_function();
15-
assert(Inspector::assertAll($callable, $input));
16-
assertType("iterable", $input);
15+
if (Inspector::assertAll($callable, $input)) {
16+
assertType("iterable", $input);
17+
}
18+
else {
19+
assertType('mixed', $input);
20+
}
1721

1822
$input = mixed_function();
1923
$callable = is_string(...);
20-
assert(Inspector::assertAll($callable, $input));
21-
assertType('iterable', $input);
24+
if (Inspector::assertAll($callable, $input)) {
25+
assertType('iterable', $input);
26+
}
27+
else {
28+
assertType('mixed', $input);
29+
}
2230

2331

2432
// Inspector::assertAllStrings()
2533
$input = mixed_function();
26-
assert(Inspector::assertAllStrings($input));
27-
assertType('iterable<string>', $input);
34+
if (Inspector::assertAllStrings($input)) {
35+
assertType('iterable<string>', $input);
36+
}
37+
else {
38+
assertType('mixed~iterable<string>', $input);
39+
}
2840

2941
// Inspector::assertAllStringable()
3042
$input = mixed_function();
31-
assert(Inspector::assertAllStringable($input));
32-
assertType('iterable<string|Stringable>', $input);
43+
if (Inspector::assertAllStringable($input)) {
44+
assertType('iterable<string|Stringable>', $input);
45+
}
46+
else {
47+
assertType('mixed~iterable<string|Stringable>', $input);
48+
}
3349

3450
// Inspector::assertAllArrays()
3551
$input = mixed_function();
36-
\assert(Inspector::assertAllArrays($input));
37-
assertType('iterable<array>', $input);
52+
if (Inspector::assertAllArrays($input)) {
53+
assertType('iterable<array>', $input);
54+
}
55+
else {
56+
assertType('mixed~iterable<array>', $input);
57+
}
3858

3959
// Inspector::assertStrictArray()
4060
$input = mixed_function();
41-
assert(Inspector::assertStrictArray($input));
42-
assertType('array<int<0, max>, mixed>', $input);
61+
if (Inspector::assertStrictArray($input)) {
62+
assertType('array<int<0, max>, mixed>', $input);
63+
}
64+
else {
65+
assertType('mixed~array<int<0, max>, mixed>', $input);
66+
}
4367

4468
// Inspector::assertAllStrictArrays()
4569
$input = mixed_function();
46-
assert(Inspector::assertAllStrictArrays($input));
47-
assertType('iterable<array<int<0, max>, mixed>>', $input);
70+
if (Inspector::assertAllStrictArrays($input)) {
71+
assertType('iterable<array<int<0, max>, mixed>>', $input);
72+
}
73+
else {
74+
assertType('mixed~iterable<array<int<0, max>, mixed>>', $input);
75+
}
4876

4977
// Inspector::assertAllHaveKey()
5078
$input = mixed_function();
51-
assert(Inspector::assertAllHaveKey($input, 'foo', 'baz'));
52-
assertType("iterable<array&hasOffset('baz')&hasOffset('foo')>", $input);
79+
if (Inspector::assertAllHaveKey($input, 'foo', 'baz')) {
80+
assertType("iterable<array&hasOffset('baz')&hasOffset('foo')>", $input);
81+
}
82+
else {
83+
assertType("mixed~iterable<array&hasOffset('baz')&hasOffset('foo')>", $input);
84+
}
5385

5486
// Inspector::assertAllIntegers()
5587
$input = mixed_function();
56-
assert(Inspector::assertAllIntegers($input));
57-
assertType('iterable<int>', $input);
88+
if (Inspector::assertAllIntegers($input)) {
89+
assertType('iterable<int>', $input);
90+
}
91+
else {
92+
assertType('mixed~iterable<int>', $input);
93+
}
5894

5995
// Inspector::assertAllFloat()
6096
$input = mixed_function();
61-
assert(Inspector::assertAllFloat($input));
62-
assertType('iterable<float>', $input);
97+
if (Inspector::assertAllFloat($input)) {
98+
assertType('iterable<float>', $input);
99+
}
100+
else {
101+
assertType('mixed~iterable<float>', $input);
102+
}
63103

64104
// Inspector::assertAllCallable()
65105
$input = mixed_function();
66-
assert(Inspector::assertAllCallable($input));
67-
assertType('iterable<callable(): mixed>', $input);
106+
if (Inspector::assertAllCallable($input)) {
107+
assertType('iterable<callable(): mixed>', $input);
108+
}
109+
else {
110+
assertType('mixed~iterable<callable(): mixed>', $input);
111+
}
68112

69113
// Inspector::assertAllNotEmpty()
70114
$input = mixed_function();
71-
assert(Inspector::assertAllNotEmpty($input));
72-
assertType('iterable<float|int<min, -1>|int<1, max>|object|resource|non-empty-string|non-empty-array>', $input);
115+
if (Inspector::assertAllNotEmpty($input)) {
116+
assertType('iterable<float|int<min, -1>|int<1, max>|object|resource|non-empty-string|non-empty-array>', $input);
117+
}
118+
else {
119+
assertType('mixed~iterable<float|int<min, -1>|int<1, max>|object|resource|non-empty-string|non-empty-array>', $input);
120+
}
73121

74122
// Inspector::assertAllNumeric()
75123
$input = mixed_function();
76-
assert(Inspector::assertAllNumeric($input));
77-
assertType('iterable<float|int>', $input);
124+
if (Inspector::assertAllNumeric($input)) {
125+
assertType('iterable<float|int>', $input);
126+
}
127+
else {
128+
assertType('mixed~iterable<float|int>', $input);
129+
}
78130

79131
// Inspector::assertAllMatch()
80132
$pattern = 'foo';
81133
$input = mixed_function();
82-
assert(Inspector::assertAllMatch($pattern, $input, false));
83-
assertType('iterable<string>', $input);
134+
if (Inspector::assertAllMatch($pattern, $input, false)) {
135+
assertType('iterable<string>', $input);
136+
}
137+
else {
138+
assertType('mixed~iterable<string>', $input);
139+
}
84140

85141
// Inspector::assertAllRegularExpressionMatch()
86142
$input = mixed_function();
87-
assert(Inspector::assertAllRegularExpressionMatch($pattern, $input));
88-
assertType('iterable<string>', $input);
143+
if (Inspector::assertAllRegularExpressionMatch($pattern, $input)) {
144+
assertType('iterable<string>', $input);
145+
}
146+
else {
147+
assertType('mixed~iterable<string>', $input);
148+
}
89149

90150
// Inspector::assertAllObjects()
91151
$input = mixed_function();
92-
assert(Inspector::assertAllObjects($input, TranslatableMarkup::class, '\\Stringable', '\\Drupal\\jsonapi\\JsonApiResource\\ResourceIdentifier'));
93-
assertType('iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|\Stringable>', $input);
152+
if (Inspector::assertAllObjects($input, TranslatableMarkup::class, '\\Stringable', '\\Drupal\\jsonapi\\JsonApiResource\\ResourceIdentifier')) {
153+
assertType('iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|\Stringable>', $input);
154+
}
155+
else {
156+
assertType('mixed~iterable<\Drupal\jsonapi\JsonApiResource\ResourceIdentifier|\Stringable>', $input);
157+
}

0 commit comments

Comments
 (0)