Skip to content

Commit dff735b

Browse files
phpstan-botclaude
andcommitted
Rewrite tests to exercise actual guarded code paths
Most tests were simple `$fn = someFunc(...); assertType(...)` patterns that never triggered the assertion because standalone assignments only exercise the TypeSpecifier assign patterns (array_key_first, array_key_last, array_rand, count-1). Functions like strlen, count, gettype, trim etc. need their first-class callables inside if-conditions to exercise the comparison, identical, and string-equality guards. Each test now uses the specific expression pattern that would crash without the corresponding isFirstClassCallable() guard: - Comparisons: `if (count(...) < 1)`, `if (0 < strlen(...))` - Identicals: `if (count(...) === 0)`, `if (strlen(...) === 0)` - Null checks: `if (array_key_first(...) !== null)` - String equality: `if (gettype(...) === 'string')`, `if (trim(...) !== '')` - Conditions: `if ($key = array_search(...))` - Foreach: `foreach (array_keys(...) as $key)` - For loops: `for ($i = 0; $i < count(...); $i++)` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a45a0fd commit dff735b

1 file changed

Lines changed: 96 additions & 62 deletions

File tree

tests/PHPStan/Analyser/nsrt/bug-14550.php

Lines changed: 96 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,121 +6,155 @@
66

77
use function PHPStan\Testing\assertType;
88

9-
/**
10-
* @param list<string> $list
11-
*/
12-
function crashArrayKeyFirst(array $list): void
9+
// Standalone assignments trigger TypeSpecifier via NodeScopeResolver null-context call
10+
function testArrayKeyFirstAssign(): void
1311
{
1412
$fn = array_key_first(...);
1513
assertType('Closure(array): (int|string|null)', $fn);
1614
}
1715

18-
/**
19-
* @param list<string> $list
20-
*/
21-
function crashArrayKeyLast(array $list): void
16+
function testArrayKeyLastAssign(): void
2217
{
2318
$fn = array_key_last(...);
2419
assertType('Closure(array): (int|string|null)', $fn);
2520
}
2621

27-
/**
28-
* @param list<string> $list
29-
*/
30-
function crashArrayRand(array $list): void
22+
function testArrayRandAssign(): void
3123
{
3224
$fn = array_rand(...);
3325
assertType('(Closure(non-empty-array): (int|string))|(Closure(non-empty-array, int<1, max>): (array<int, int|string>|int|string))', $fn);
3426
}
3527

36-
/**
37-
* @param list<string> $list
38-
*/
39-
function crashArraySearch(array $list, string $s): void
28+
function testCountMinusOneAssign(): void
4029
{
41-
$fn = array_search(...);
42-
assertType('Closure(mixed, array, bool=): (int|string|false)', $fn);
30+
$idx = count(...) - 1;
31+
assertType('Closure(array|Countable, 0|1=): int<0, max>', count(...));
4332
}
4433

45-
function testStrlen(): void
34+
// array_search guard needs true context, so it must be in a condition
35+
function testArraySearchInCondition(): void
4636
{
47-
$fn = strlen(...);
48-
assertType('Closure(string): int<0, max>', $fn);
37+
if ($key = array_search(...)) {
38+
assertType('Closure(mixed, array, bool=): (int|string|false)', $key);
39+
}
4940
}
5041

51-
function testMbStrlen(): void
42+
// Comparison guards in TypeSpecifier (Smaller/SmallerOrEqual)
43+
function testCountInComparisons(): void
5244
{
53-
$fn = mb_strlen(...);
54-
assertType('Closure(string, string|null=): int<0, max>', $fn);
45+
if (count(...) < 1) {}
46+
if (0 < count(...)) {}
47+
assertType('Closure(array|Countable, 0|1=): int<0, max>', count(...));
5548
}
5649

57-
function testCount(): void
50+
function testSizeofInComparisons(): void
5851
{
59-
$fn = count(...);
60-
assertType('Closure(array|Countable, 0|1=): int<0, max>', $fn);
52+
if (sizeof(...) < 1) {}
53+
if (0 < sizeof(...)) {}
54+
assertType('Closure(array|Countable, int=): int', sizeof(...));
6155
}
6256

63-
function testSizeof(): void
57+
function testCountMinusOneInComparison(): void
6458
{
65-
$fn = sizeof(...);
66-
assertType('Closure(array|Countable, int=): int', $fn);
59+
$i = 0;
60+
if ($i < count(...) - 1) {}
61+
assertType('Closure(array|Countable, 0|1=): int<0, max>', count(...));
6762
}
6863

69-
function testPregMatch(): void
64+
function testStrlenInComparisons(): void
7065
{
71-
$fn = preg_match(...);
72-
assertType('Closure(string, string, array<string>|null=, TFlags=, int=): (0|1|false)', $fn);
66+
if (strlen(...) < 1) {}
67+
if (0 < strlen(...)) {}
68+
assertType('Closure(string): int<0, max>', strlen(...));
7369
}
7470

75-
function testGettype(): void
71+
function testMbStrlenInComparisons(): void
7672
{
77-
$fn = gettype(...);
78-
assertType('Closure(mixed): string', $fn);
73+
if (mb_strlen(...) < 1) {}
74+
if (0 < mb_strlen(...)) {}
75+
assertType('Closure(string, string|null=): int<0, max>', mb_strlen(...));
7976
}
8077

81-
function testGetClass(): void
78+
function testPregMatchInComparisons(): void
8279
{
83-
$fn = get_class(...);
84-
assertType('Closure(object=): class-string', $fn);
80+
if (preg_match(...) < 1) {}
81+
if (0 < preg_match(...)) {}
82+
assertType('Closure(string, string, array<string>|null=, TFlags=, int=): (0|1|false)', preg_match(...));
8583
}
8684

87-
function testGetDebugType(): void
85+
// Identical/NotIdentical guards in resolveNormalizedIdentical
86+
function testCountIdentical(): void
8887
{
89-
$fn = get_debug_type(...);
90-
assertType('Closure(mixed): string', $fn);
88+
if (count(...) === 0) {}
89+
assertType('Closure(array|Countable, 0|1=): int<0, max>', count(...));
9190
}
9291

93-
function testGetParentClass(): void
92+
function testStrlenIdentical(): void
9493
{
95-
$fn = get_parent_class(...);
96-
assertType('Closure(object|string=): (class-string|false)', $fn);
94+
if (strlen(...) === 0) {}
95+
if (mb_strlen(...) === 0) {}
96+
assertType('Closure(string): int<0, max>', strlen(...));
9797
}
9898

99-
function testTrim(): void
99+
function testArrayKeyFirstNullComparison(): void
100100
{
101-
$fn = trim(...);
102-
assertType('Closure(string, string=): string', $fn);
101+
if (array_key_first(...) !== null) {}
102+
if (array_key_last(...) !== null) {}
103+
assertType('Closure(array): (int|string|null)', array_key_first(...));
103104
}
104105

105-
function testLtrim(): void
106+
function testGetClassIdentical(): void
106107
{
107-
$fn = ltrim(...);
108-
assertType('Closure(string, string=): string', $fn);
108+
if (get_class(...) === 'stdClass') {}
109+
if (get_debug_type(...) === 'string') {}
110+
assertType('Closure(object=): class-string', get_class(...));
109111
}
110112

111-
function testRtrim(): void
113+
function testStringFuncIdentical(): void
112114
{
113-
$fn = rtrim(...);
114-
assertType('Closure(string, string=): string', $fn);
115+
if (strtolower(...) === 'test') {}
116+
assertType('Closure(string): lowercase-string', strtolower(...));
115117
}
116118

117-
/**
118-
* @param list<string> $list
119-
*/
120-
function testArrayKeysInForeach(array $list): void
119+
// String equality guards in specifyTypesForConstantStringBinaryExpression
120+
function testGettypeEquality(): void
121121
{
122-
foreach (array_keys(...) as $key) {
123-
}
124-
$fn = array_keys(...);
125-
assertType('Closure(array, mixed=, bool=): list<int|string>', $fn);
122+
if (gettype(...) === 'string') {}
123+
if (gettype(...) == 'string') {}
124+
assertType('Closure(mixed): string', gettype(...));
125+
}
126+
127+
function testGetClassEquality(): void
128+
{
129+
if (get_class(...) == 'stdClass') {}
130+
if (get_debug_type(...) == 'string') {}
131+
assertType('Closure(object=): class-string', get_class(...));
132+
}
133+
134+
function testGetParentClassEquality(): void
135+
{
136+
if (get_parent_class(...) === 'stdClass') {}
137+
assertType('Closure(object|string=): (class-string|false)', get_parent_class(...));
138+
}
139+
140+
function testTrimEquality(): void
141+
{
142+
if (trim(...) !== '') {}
143+
if (ltrim(...) !== '') {}
144+
if (rtrim(...) !== '') {}
145+
assertType('Closure(string, string=): string', trim(...));
146+
}
147+
148+
// NodeScopeResolver guards
149+
function testArrayKeysInForeach(): void
150+
{
151+
foreach (array_keys(...) as $key) {}
152+
assertType('Closure(array, mixed=, bool=): list<int|string>', array_keys(...));
153+
}
154+
155+
function testCountInForLoop(): void
156+
{
157+
for ($i = 0; $i < count(...); $i++) {}
158+
for ($i = 0; count(...) > $i; $i++) {}
159+
assertType('Closure(array|Countable, 0|1=): int<0, max>', count(...));
126160
}

0 commit comments

Comments
 (0)