Skip to content

Commit 6a33de9

Browse files
committed
Fix inferring template type from non-empty-string
1 parent 35a66a2 commit 6a33de9

File tree

7 files changed

+114
-6
lines changed

7 files changed

+114
-6
lines changed

src/Type/Constant/ConstantStringType.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,11 @@ public function append(self $otherString): self
298298
public function generalize(?GeneralizePrecision $precision = null): Type
299299
{
300300
if ($this->isClassString) {
301-
return new ClassStringType();
301+
if ($precision !== null && $precision->isMoreSpecific()) {
302+
return new ClassStringType();
303+
}
304+
305+
return new StringType();
302306
}
303307

304308
if ($this->getValue() !== '' && $precision !== null && $precision->isMoreSpecific()) {

src/Type/Generic/TemplateTypeHelper.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Type\ConstantType;
77
use PHPStan\Type\ErrorType;
88
use PHPStan\Type\GeneralizePrecision;
9+
use PHPStan\Type\StringType;
910
use PHPStan\Type\Type;
1011
use PHPStan\Type\TypeTraverser;
1112

@@ -70,6 +71,10 @@ public static function generalizeType(Type $type): Type
7071
return $type->generalize(GeneralizePrecision::lessSpecific());
7172
}
7273

74+
if ($type->isNonEmptyString()->yes()) {
75+
return new StringType();
76+
}
77+
7378
return $traverse($type);
7479
});
7580
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ public function dataFileAsserts(): iterable
447447
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4970.php');
448448
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5322.php');
449449
yield from $this->gatherAssertTypes(__DIR__ . '/data/splfixedarray-iterator-types.php');
450+
yield from $this->gatherAssertTypes(__DIR__ . '/../Rules/Methods/data/bug-5372.php');
450451
}
451452

452453
/**

tests/PHPStan/Analyser/data/generics.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,15 @@ function f($a, $b)
147147
*/
148148
function testF($arrayOfInt, $callableOrNull)
149149
{
150-
assertType('array<string&numeric>', f($arrayOfInt, function (int $a): string {
150+
assertType('Closure(int): string&numeric', function (int $a): string {
151+
return (string)$a;
152+
});
153+
assertType('array<string>', f($arrayOfInt, function (int $a): string {
151154
return (string)$a;
152155
}));
156+
assertType('Closure(mixed): string', function ($a): string {
157+
return (string)$a;
158+
});
153159
assertType('array<string>', f($arrayOfInt, function ($a): string {
154160
return (string)$a;
155161
}));

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -563,11 +563,11 @@ public function testArrayReduceCallback(): void
563563
5,
564564
],
565565
[
566-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.',
566+
'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.',
567567
13,
568568
],
569569
[
570-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.',
570+
'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.',
571571
22,
572572
],
573573
]);
@@ -584,11 +584,11 @@ public function testArrayReduceArrowFunctionCallback(): void
584584
5,
585585
],
586586
[
587-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.',
587+
'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.',
588588
11,
589589
],
590590
[
591-
'Parameter #2 $callback of function array_reduce expects callable(non-empty-string|null, int): non-empty-string|null, Closure(string, int): non-empty-string given.',
591+
'Parameter #2 $callback of function array_reduce expects callable(string|null, int): string|null, Closure(string, int): non-empty-string given.',
592592
18,
593593
],
594594
]);

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2029,4 +2029,21 @@ public function testNonEmptyStringVerbosity(): void
20292029
]);
20302030
}
20312031

2032+
public function testBug5372(): void
2033+
{
2034+
if (!self::$useStaticReflectionProvider && PHP_VERSION_ID < 70400) {
2035+
$this->markTestSkipped('Test requires PHP 7.4.');
2036+
}
2037+
2038+
$this->checkThisOnly = false;
2039+
$this->checkNullables = true;
2040+
$this->checkUnionTypes = true;
2041+
$this->analyse([__DIR__ . '/data/bug-5372.php'], [
2042+
[
2043+
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, class-string> given.',
2044+
72,
2045+
],
2046+
]);
2047+
}
2048+
20322049
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php // lint >= 7.4
2+
3+
namespace Bug5372;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template T
10+
*/
11+
class Collection
12+
{
13+
14+
/** @var array<TKey, T> */
15+
private $values;
16+
17+
/**
18+
* @param array<TKey, T> $values
19+
*/
20+
public function __construct(array $values)
21+
{
22+
$this->values = $values;
23+
}
24+
25+
/**
26+
* @template V
27+
*
28+
* @param callable(T): V $callback
29+
*
30+
* @return self<TKey, V>
31+
*/
32+
public function map(callable $callback): self {
33+
return new self(array_map($callback, $this->values));
34+
}
35+
36+
/**
37+
* @template V of string
38+
*
39+
* @param callable(T): V $callback
40+
*
41+
* @return self<TKey, V>
42+
*/
43+
public function map2(callable $callback): self {
44+
return new self(array_map($callback, $this->values));
45+
}
46+
}
47+
48+
class Foo
49+
{
50+
51+
/** @param Collection<int, string> $list */
52+
function takesStrings(Collection $list): void {
53+
echo serialize($list);
54+
}
55+
56+
/** @param class-string $classString */
57+
public function doFoo(string $classString)
58+
{
59+
$col = new Collection(['foo', 'bar']);
60+
assertType('Bug5372\Collection<int, string>', $col);
61+
62+
$newCol = $col->map(static fn(string $var): string => $var . 'bar');
63+
assertType('Bug5372\Collection<int, string>', $newCol);
64+
$this->takesStrings($newCol);
65+
66+
$newCol = $col->map(static fn(string $var): string => $classString);
67+
assertType('Bug5372\Collection<int, string>', $newCol);
68+
$this->takesStrings($newCol);
69+
70+
$newCol = $col->map2(static fn(string $var): string => $classString);
71+
assertType('Bug5372\Collection<int, class-string>', $newCol);
72+
$this->takesStrings($newCol);
73+
}
74+
75+
}

0 commit comments

Comments
 (0)