Skip to content

Commit d27106a

Browse files
authored
Keep a by-ref writeback's native type only when the written-back type fits it (#6564)
1 parent 6a09253 commit d27106a

4 files changed

Lines changed: 73 additions & 5 deletions

File tree

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2368,11 +2368,19 @@ public function processArgs(
23682368
}
23692369

23702370
// what the call writes back is described by PHPDoc (@param, @param-out,
2371-
// a parameter-out extension) - natively only the parameter's own
2372-
// type declaration is guaranteed
2373-
$byRefNativeType = $currentParameter instanceof ExtendedParameterReflection
2374-
? $currentParameter->getNativeType()
2375-
: $byRefType;
2371+
// a parameter-out extension) - natively only the parameter's own type
2372+
// can describe it: its declaration, or for a builtin its signature map
2373+
// entry. PHP checks a declaration only on the way in and a signature
2374+
// map entry not at all, so it holds only when the written-back type
2375+
// fits inside it. preg_match() with PREG_OFFSET_CAPTURE writes arrays
2376+
// into a slot the signature map declares as string[].
2377+
$byRefNativeType = $byRefType;
2378+
if ($currentParameter instanceof ExtendedParameterReflection) {
2379+
$byRefNativeType = $currentParameter->getNativeType();
2380+
if (!$byRefNativeType->isSuperTypeOf($byRefType)->yes()) {
2381+
$byRefNativeType = new MixedType();
2382+
}
2383+
}
23762384

23772385
$scope = $this->processVirtualAssign(
23782386
$scope,
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ByRefWritebackNativeType;
4+
5+
use function PHPStan\Testing\assertNativeType;
6+
use function PHPStan\Testing\assertType;
7+
8+
function offsetCapture(string $s): void
9+
{
10+
if (! preg_match('/(a)(b)?/', $s, $m, PREG_OFFSET_CAPTURE)) {
11+
return;
12+
}
13+
14+
assertType("array{0: array{non-falsy-string, int<-1, max>}, 1: array{'a', int<-1, max>}, 2?: array{'b', int<-1, max>}}", $m);
15+
assertNativeType("array{0: array{non-falsy-string, int<-1, max>}, 1: array{'a', int<-1, max>}, 2?: array{'b', int<-1, max>}}", $m);
16+
}
17+
18+
function noFlags(string $s): void
19+
{
20+
if (! preg_match('/(a)(b)?/', $s, $m)) {
21+
return;
22+
}
23+
24+
assertType("array{0: non-falsy-string, 1: 'a', 2?: 'b'}", $m);
25+
assertNativeType("array{0: non-falsy-string, 1: 'a', 2?: 'b'}", $m);
26+
}
27+
28+
/** @param-out string $v */
29+
function paramOutContradictsDeclaration(int &$v): void
30+
{
31+
$v = 'now a string';
32+
}
33+
34+
function userlandParamOut(): void
35+
{
36+
$x = 1;
37+
paramOutContradictsDeclaration($x);
38+
39+
assertType('string', $x);
40+
assertNativeType('mixed', $x);
41+
}

‎tests/PHPStan/Rules/Variables/IssetRuleTest.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,4 +671,11 @@ public function testBug14416(): void
671671
$this->analyse([__DIR__ . '/data/bug-14416.php'], []);
672672
}
673673

674+
public function testPregMatchOffsetCaptureWithoutTreatPhpDocTypesAsCertain(): void
675+
{
676+
$this->treatPhpDocTypesAsCertain = false;
677+
678+
$this->analyse([__DIR__ . '/data/preg-match-offset-capture-isset.php'], []);
679+
}
680+
674681
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PregMatchOffsetCaptureIsset;
4+
5+
function offsetCapture(string $s): ?string
6+
{
7+
if (! preg_match('/(a)(b)?/', $s, $m, PREG_OFFSET_CAPTURE)) {
8+
return null;
9+
}
10+
11+
return isset($m[2]) ? $m[2][0] : null;
12+
}

0 commit comments

Comments
 (0)