Skip to content

Commit 18b34d4

Browse files
phpstan-botclaude
andcommitted
Add isFirstClassCallable() guards in resolveNormalizedIdentical and expand tests
Add missing guards for strlen/mb_strlen, array_key_first/array_key_last, get_class/get_debug_type, and substr/strstr/etc patterns in resolveNormalizedIdentical that were not covered by the initial fix. Expand tests to cover all affected functions: strlen, mb_strlen, count, sizeof, preg_match, gettype, get_class, get_debug_type, get_parent_class, trim, ltrim, rtrim, array_keys (foreach), and count (for-loop). Tests exercise both assignment and comparison/equality code paths. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9445f33 commit 18b34d4

2 files changed

Lines changed: 68 additions & 22 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2996,9 +2996,10 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
29962996
if (
29972997
!$context->null()
29982998
&& $unwrappedLeftExpr instanceof FuncCall
2999-
&& count($unwrappedLeftExpr->getArgs()) === 1
30002999
&& $unwrappedLeftExpr->name instanceof Name
3000+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30013001
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true)
3002+
&& count($unwrappedLeftExpr->getArgs()) === 1
30023003
&& $rightType->isInteger()->yes()
30033004
) {
30043005
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
@@ -3034,6 +3035,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
30343035
if (
30353036
$unwrappedLeftExpr instanceof FuncCall
30363037
&& $unwrappedLeftExpr->name instanceof Name
3038+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30373039
&& in_array($unwrappedLeftExpr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
30383040
&& isset($unwrappedLeftExpr->getArgs()[0])
30393041
&& $rightType->isNull()->yes()
@@ -3065,6 +3067,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
30653067
$context->true()
30663068
&& $unwrappedLeftExpr instanceof FuncCall
30673069
&& $unwrappedLeftExpr->name instanceof Name
3070+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30683071
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
30693072
&& isset($unwrappedLeftExpr->getArgs()[0])
30703073
) {
@@ -3090,6 +3093,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
30903093
$context->truthy()
30913094
&& $unwrappedLeftExpr instanceof FuncCall
30923095
&& $unwrappedLeftExpr->name instanceof Name
3096+
&& !$unwrappedLeftExpr->isFirstClassCallable()
30933097
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), [
30943098
'substr', 'strstr', 'stristr', 'strchr', 'strrchr', 'strtolower', 'strtoupper', 'ucfirst', 'lcfirst',
30953099
'mb_substr', 'mb_strstr', 'mb_stristr', 'mb_strchr', 'mb_strrchr', 'mb_strtolower', 'mb_strtoupper', 'mb_ucfirst', 'mb_lcfirst',

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

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -42,80 +42,122 @@ function crashArraySearch(array $list, string $s): void
4242
assertType('Closure(mixed, array, bool=): (int|string|false)', $fn);
4343
}
4444

45-
function crashCount(): void
45+
function testStrlen(): void
4646
{
47-
$fn = count(...);
48-
assertType('Closure(array|Countable, 0|1=): int<0, max>', $fn);
47+
$fn = strlen(...);
48+
assertType('Closure(string): int<0, max>', $fn);
49+
50+
if (strlen(...) < 1) {}
51+
if (0 < strlen(...)) {}
4952
}
5053

51-
function crashSizeof(): void
54+
function testMbStrlen(): void
5255
{
53-
$fn = sizeof(...);
54-
assertType('Closure(array|Countable, int=): int', $fn);
56+
$fn = mb_strlen(...);
57+
assertType('Closure(string, string|null=): int<0, max>', $fn);
58+
59+
if (mb_strlen(...) < 1) {}
60+
if (0 < mb_strlen(...)) {}
5561
}
5662

57-
function crashStrlen(): void
63+
function testCount(): void
5864
{
59-
$fn = strlen(...);
60-
assertType('Closure(string): int<0, max>', $fn);
65+
$fn = count(...);
66+
assertType('Closure(array|Countable, 0|1=): int<0, max>', $fn);
67+
68+
if (count(...) < 1) {}
69+
if (0 < count(...)) {}
70+
if (count(...) === 0) {}
6171
}
6272

63-
function crashMbStrlen(): void
73+
function testSizeof(): void
6474
{
65-
$fn = mb_strlen(...);
66-
assertType('Closure(string, string|null=): int<0, max>', $fn);
75+
$fn = sizeof(...);
76+
assertType('Closure(array|Countable, int=): int', $fn);
6777
}
6878

69-
function crashPregMatch(): void
79+
function testPregMatch(): void
7080
{
7181
$fn = preg_match(...);
7282
assertType('Closure(string, string, array<string>|null=, TFlags=, int=): (0|1|false)', $fn);
83+
84+
if (preg_match(...) < 1) {}
85+
if (1 <= preg_match(...)) {}
7386
}
7487

75-
function crashGettype(): void
88+
function testGettype(): void
7689
{
7790
$fn = gettype(...);
7891
assertType('Closure(mixed): string', $fn);
92+
93+
if (gettype(...) === 'string') {}
94+
if (gettype(...) == 'string') {}
7995
}
8096

81-
function crashGetClass(): void
97+
function testGetClass(): void
8298
{
8399
$fn = get_class(...);
84100
assertType('Closure(object=): class-string', $fn);
101+
102+
if (get_class(...) == 'stdClass') {}
85103
}
86104

87-
function crashGetDebugType(): void
105+
function testGetDebugType(): void
88106
{
89107
$fn = get_debug_type(...);
90108
assertType('Closure(mixed): string', $fn);
109+
110+
if (get_debug_type(...) == 'string') {}
91111
}
92112

93-
function crashGetParentClass(): void
113+
function testGetParentClass(): void
94114
{
95115
$fn = get_parent_class(...);
96116
assertType('Closure(object|string=): (class-string|false)', $fn);
117+
118+
if (get_parent_class(...) === 'stdClass') {}
97119
}
98120

99-
function crashTrim(): void
121+
function testTrim(): void
100122
{
101123
$fn = trim(...);
102124
assertType('Closure(string, string=): string', $fn);
125+
126+
if (trim(...) !== '') {}
103127
}
104128

105-
function crashLtrim(): void
129+
function testLtrim(): void
106130
{
107131
$fn = ltrim(...);
108132
assertType('Closure(string, string=): string', $fn);
133+
134+
if (ltrim(...) !== '') {}
109135
}
110136

111-
function crashRtrim(): void
137+
function testRtrim(): void
112138
{
113139
$fn = rtrim(...);
114140
assertType('Closure(string, string=): string', $fn);
141+
142+
if (rtrim(...) !== '') {}
115143
}
116144

117-
function crashArrayKeys(): void
145+
/**
146+
* @param list<string> $list
147+
*/
148+
function testArrayKeysInForeach(array $list): void
118149
{
150+
foreach (array_keys(...) as $key) {
151+
}
119152
$fn = array_keys(...);
120153
assertType('Closure(array, mixed=, bool=): list<int|string>', $fn);
121154
}
155+
156+
/**
157+
* @param list<string> $list
158+
*/
159+
function testCountInForLoop(array $list): void
160+
{
161+
for ($i = 0; $i < count(...); $i++) {
162+
}
163+
}

0 commit comments

Comments
 (0)