Skip to content

Commit ecbb692

Browse files
Resolve substr based on configured PHP version
1 parent 6e87a98 commit ecbb692

File tree

7 files changed

+57
-5
lines changed

7 files changed

+57
-5
lines changed

src/Type/Php/SubstrDynamicReturnTypeExtension.php

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,29 @@ public function getTypeFromFunctionCall(
7474
if ($length !== null) {
7575
if ($functionReflection->getName() === 'mb_substr') {
7676
$substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue());
77+
} elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) {
78+
$substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue());
7779
} else {
7880
$substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue());
7981
}
8082
} else {
8183
if ($functionReflection->getName() === 'mb_substr') {
8284
$substr = mb_substr($constantString->getValue(), $offset->getValue());
85+
} elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) {
86+
// Simulate substr call on an older PHP version if the runtime one is too new.
87+
$substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue());
8388
} else {
8489
$substr = substr($constantString->getValue(), $offset->getValue());
8590
}
8691
}
8792

8893
if (is_bool($substr)) {
89-
$results[] = new ConstantBooleanType($substr);
94+
if ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) {
95+
$results[] = new ConstantBooleanType($substr);
96+
} else {
97+
// Simulate substr call on a recent PHP version if the runtime one is too old.
98+
$results[] = new ConstantStringType('');
99+
}
90100
} else {
91101
$results[] = new ConstantStringType($substr);
92102
}
@@ -127,4 +137,24 @@ public function getTypeFromFunctionCall(
127137
return null;
128138
}
129139

140+
private function substrOrFalse(string $string, int $offset, ?int $length = null): false|string
141+
{
142+
$strlen = strlen($string);
143+
144+
if ($offset > $strlen) {
145+
return false;
146+
}
147+
148+
if ($length !== null && $length < 0) {
149+
if ($offset < 0 && -$length > $strlen) {
150+
return false;
151+
}
152+
if ($offset >= 0 && -$length > $strlen - $offset) {
153+
return false;
154+
}
155+
}
156+
157+
return substr($string, $offset, $length);
158+
}
159+
130160
}

tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php renamed to tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

7-
class LooseConstComparisonPhp7Test extends TypeInferenceTestCase
7+
class NodeScopeResolverPhp7Test extends TypeInferenceTestCase
88
{
99

1010
/**
@@ -15,6 +15,8 @@ public function dataFileAsserts(): iterable
1515
// compares constants according to the php-version phpstan configuration,
1616
// _NOT_ the current php runtime version
1717
yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php');
18+
19+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-13129-php7.php');
1820
}
1921

2022
/**
@@ -33,7 +35,7 @@ public function testFileAsserts(
3335
public static function getAdditionalConfigFiles(): array
3436
{
3537
return [
36-
__DIR__ . '/looseConstComparisonPhp7.neon',
38+
__DIR__ . '/nodeScopeResolverPhp7.neon',
3739
];
3840
}
3941

tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php renamed to tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use PHPStan\Testing\TypeInferenceTestCase;
66

7-
class LooseConstComparisonPhp8Test extends TypeInferenceTestCase
7+
class NodeScopeResolverPhp8Test extends TypeInferenceTestCase
88
{
99

1010
/**
@@ -15,6 +15,8 @@ public function dataFileAsserts(): iterable
1515
// compares constants according to the php-version phpstan configuration,
1616
// _NOT_ the current php runtime version
1717
yield from $this->gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php');
18+
19+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-13129-php8.php');
1820
}
1921

2022
/**
@@ -33,7 +35,7 @@ public function testFileAsserts(
3335
public static function getAdditionalConfigFiles(): array
3436
{
3537
return [
36-
__DIR__ . '/looseConstComparisonPhp8.neon',
38+
__DIR__ . '/nodeScopeResolverPhp8.neon',
3739
];
3840
}
3941

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Bug13129PHP7;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
assertType("false", substr("test", 5));
8+
assertType("false", substr("test", -1, -5));
9+
assertType("false", substr("test", 1, -4));
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Bug13129PHP8;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
assertType("''", substr("test", 5));
8+
assertType("''", substr("test", -1, -5));
9+
assertType("''", substr("test", 1, -4));

0 commit comments

Comments
 (0)