Skip to content

Commit b06df46

Browse files
committed
Use tentative return types of built-in methods as native return types
Amp-Thread-ID: https://ampcode.com/threads/T-01a179f2-995f-7745-8d8c-4c174ba5c0ce
1 parent f3d2f71 commit b06df46

4 files changed

Lines changed: 117 additions & 5 deletions

File tree

‎src/Reflection/SignatureMap/Php8SignatureMapProvider.php‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,64 @@ public function hasFunctionSignature(string $name): bool
195195
}
196196

197197
public function getMethodSignatures(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array
198+
{
199+
return $this->applyTentativeReturnType(
200+
$className,
201+
$reflectionMethod,
202+
$this->getMethodSignaturesWithoutTentativeReturnType($className, $methodName, $reflectionMethod),
203+
);
204+
}
205+
206+
/**
207+
* PHP checks a tentative return type of a built-in method like a real one,
208+
* and overriding it with an incompatible type is deprecated,
209+
* so it is used as the native return type when the signature does not have one.
210+
*
211+
* @param array{positional: array<int, FunctionSignature>, named: ?array<int, FunctionSignature>} $signatures
212+
* @return array{positional: array<int, FunctionSignature>, named: ?array<int, FunctionSignature>}
213+
*/
214+
private function applyTentativeReturnType(string $className, ?ReflectionMethod $reflectionMethod, array $signatures): array
215+
{
216+
if ($reflectionMethod === null || !$this->phpVersion->hasTentativeReturnTypes()) {
217+
return $signatures;
218+
}
219+
220+
$tentativeReturnType = $reflectionMethod->getTentativeReturnType();
221+
if ($tentativeReturnType === null) {
222+
return $signatures;
223+
}
224+
225+
$nativeReturnType = TypehintHelper::decideTypeFromReflection(
226+
$tentativeReturnType,
227+
selfClass: $this->reflectionProviderProvider->getReflectionProvider()->getClass($className),
228+
);
229+
foreach ($signatures as $signatureType => $typeSignatures) {
230+
if ($typeSignatures === null) {
231+
continue;
232+
}
233+
234+
foreach ($typeSignatures as $i => $signature) {
235+
$signatureNativeReturnType = $signature->getNativeReturnType();
236+
if (!$signatureNativeReturnType instanceof MixedType || $signatureNativeReturnType->isExplicitMixed()) {
237+
continue;
238+
}
239+
240+
$signatures[$signatureType][$i] = new FunctionSignature(
241+
$signature->getParameters(),
242+
$signature->getReturnType(),
243+
$nativeReturnType,
244+
$signature->isVariadic(),
245+
);
246+
}
247+
}
248+
249+
return $signatures;
250+
}
251+
252+
/**
253+
* @return array{positional: array<int, FunctionSignature>, named: ?array<int, FunctionSignature>}
254+
*/
255+
private function getMethodSignaturesWithoutTentativeReturnType(string $className, string $methodName, ?ReflectionMethod $reflectionMethod): array
198256
{
199257
$lowerClassName = strtolower($className);
200258
if (!array_key_exists($lowerClassName, $this->map->classes)) {

‎src/Rules/Methods/OverridingMethodRule.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,9 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE
274274
$reportReturnType = !$realPrototype instanceof MethodPrototypeReflection
275275
|| $realPrototype->getTentativeReturnType() === null
276276
|| (is_bool($prototype->isBuiltin()) ? !$prototype->isBuiltin() : $prototype->isBuiltin()->no());
277+
} elseif ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->getTentativeReturnType() !== null) {
278+
// return types that are only tentative are not checked before PHP 8.1
279+
$reportReturnType = false;
277280
} else {
278281
if ($realPrototype instanceof MethodPrototypeReflection && $realPrototype->isInternal()) {
279282
if (
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php // lint >= 8.1
2+
3+
namespace TentativeReturnTypeNative;
4+
5+
use function PHPStan\Testing\assertNativeType;
6+
use function PHPStan\Testing\assertType;
7+
8+
/**
9+
* @extends \ArrayObject<int, string>
10+
*/
11+
class MyArrayObject extends \ArrayObject
12+
{
13+
14+
}
15+
16+
/**
17+
* @implements \IteratorAggregate<int, string>
18+
*/
19+
class MyAggregate implements \IteratorAggregate
20+
{
21+
22+
#[\ReturnTypeWillChange]
23+
public function getIterator()
24+
{
25+
return new \ArrayIterator([]);
26+
}
27+
28+
}
29+
30+
class Foo
31+
{
32+
33+
/**
34+
* @param \ArrayObject<int, string> $arrayObject
35+
*/
36+
public function doFoo(\ArrayObject $arrayObject, MyArrayObject $myArrayObject, \DateTimeImmutable $date, \SplObjectStorage $storage, MyAggregate $aggregate): void
37+
{
38+
assertType('int<0, max>', $arrayObject->count());
39+
assertNativeType('int', $arrayObject->count());
40+
assertNativeType('int', $myArrayObject->count());
41+
assertNativeType('int', $date->getTimestamp());
42+
assertNativeType('string', $storage->getHash(new \stdClass()));
43+
assertNativeType('mixed', $aggregate->getIterator());
44+
}
45+
46+
}

‎tests/PHPStan/Rules/Comparison/IfConstantConditionRuleTest.php‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use PHPStan\Testing\CompositeRule;
77
use PHPStan\Testing\RuleTestCase;
88
use PHPUnit\Framework\Attributes\RequiresPhp;
9+
use const PHP_VERSION_ID;
910

1011
/**
1112
* @extends RuleTestCase<CompositeRule>
@@ -151,12 +152,16 @@ public function testReportPhpDoc(): void
151152
public function testBug4043(): void
152153
{
153154
$this->treatPhpDocTypesAsCertain = true;
155+
$assertError = [
156+
'Call to function assert() with true will always evaluate to true.',
157+
13,
158+
];
159+
if (PHP_VERSION_ID < 80100) {
160+
// tentative return types are native only on PHP 8.1+
161+
$assertError[] = 'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
162+
}
154163
$this->analyse([__DIR__ . '/data/bug-4043.php'], [
155-
[
156-
'Call to function assert() with true will always evaluate to true.',
157-
13,
158-
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
159-
],
164+
$assertError,
160165
[
161166
'If condition is always false.',
162167
43,

0 commit comments

Comments
 (0)