From 148de159049017d66cb14a50d48d292951d1e0f3 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 13 Jun 2025 08:50:17 +0200 Subject: [PATCH 1/2] Resolve substr based on configured PHP version --- build/baseline-8.0.neon | 5 --- .../Php/SubstrDynamicReturnTypeExtension.php | 37 ++++++++++++++++++- ...Test.php => NodeScopeResolverPhp7Test.php} | 6 ++- ...Test.php => NodeScopeResolverPhp8Test.php} | 6 ++- .../PHPStan/Analyser/data/bug-13129-php7.php | 9 +++++ .../PHPStan/Analyser/data/bug-13129-php8.php | 9 +++++ ...onPhp7.neon => nodeScopeResolverPhp7.neon} | 0 ...onPhp8.neon => nodeScopeResolverPhp8.neon} | 0 8 files changed, 62 insertions(+), 10 deletions(-) rename tests/PHPStan/Analyser/{LooseConstComparisonPhp7Test.php => NodeScopeResolverPhp7Test.php} (81%) rename tests/PHPStan/Analyser/{LooseConstComparisonPhp8Test.php => NodeScopeResolverPhp8Test.php} (81%) create mode 100644 tests/PHPStan/Analyser/data/bug-13129-php7.php create mode 100644 tests/PHPStan/Analyser/data/bug-13129-php8.php rename tests/PHPStan/Analyser/{looseConstComparisonPhp7.neon => nodeScopeResolverPhp7.neon} (100%) rename tests/PHPStan/Analyser/{looseConstComparisonPhp8.neon => nodeScopeResolverPhp8.neon} (100%) diff --git a/build/baseline-8.0.neon b/build/baseline-8.0.neon index 95dfa6cf8a..1618897e6e 100644 --- a/build/baseline-8.0.neon +++ b/build/baseline-8.0.neon @@ -29,8 +29,3 @@ parameters: message: "#^Strict comparison using \\=\\=\\= between list and false will always evaluate to false\\.$#" count: 1 path: ../src/Type/Php/StrSplitFunctionReturnTypeExtension.php - - - - message: "#^Call to function is_bool\\(\\) with string will always evaluate to false\\.$#" - count: 1 - path: ../src/Type/Php/SubstrDynamicReturnTypeExtension.php diff --git a/src/Type/Php/SubstrDynamicReturnTypeExtension.php b/src/Type/Php/SubstrDynamicReturnTypeExtension.php index 00bfb3f33b..002340465b 100644 --- a/src/Type/Php/SubstrDynamicReturnTypeExtension.php +++ b/src/Type/Php/SubstrDynamicReturnTypeExtension.php @@ -24,6 +24,7 @@ use function in_array; use function is_bool; use function mb_substr; +use function strlen; use function substr; #[AutowiredService] @@ -76,19 +77,29 @@ public function getTypeFromFunctionCall( if ($length !== null) { if ($functionReflection->getName() === 'mb_substr') { $substr = mb_substr($constantString->getValue(), $offset->getValue(), $length->getValue()); + } elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + $substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue(), $length->getValue()); } else { $substr = substr($constantString->getValue(), $offset->getValue(), $length->getValue()); } } else { if ($functionReflection->getName() === 'mb_substr') { $substr = mb_substr($constantString->getValue(), $offset->getValue()); + } elseif ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + // Simulate substr call on an older PHP version if the runtime one is too new. + $substr = $this->substrOrFalse($constantString->getValue(), $offset->getValue()); } else { $substr = substr($constantString->getValue(), $offset->getValue()); } } if (is_bool($substr)) { - $results[] = new ConstantBooleanType($substr); + if ($this->phpVersion->substrReturnFalseInsteadOfEmptyString()) { + $results[] = new ConstantBooleanType($substr); + } else { + // Simulate substr call on a recent PHP version if the runtime one is too old. + $results[] = new ConstantStringType(''); + } } else { $results[] = new ConstantStringType($substr); } @@ -129,4 +140,28 @@ public function getTypeFromFunctionCall( return null; } + private function substrOrFalse(string $string, int $offset, ?int $length = null): false|string + { + $strlen = strlen($string); + + if ($offset > $strlen) { + return false; + } + + if ($length !== null && $length < 0) { + if ($offset < 0 && -$length > $strlen) { + return false; + } + if ($offset >= 0 && -$length > $strlen - $offset) { + return false; + } + } + + if ($length === null) { + return substr($string, $offset); + } + + return substr($string, $offset, $length); + } + } diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php b/tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php similarity index 81% rename from tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php rename to tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php index 582997ffa3..8b6e494dca 100644 --- a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php @@ -5,7 +5,7 @@ use PHPStan\Testing\TypeInferenceTestCase; use PHPUnit\Framework\Attributes\DataProvider; -class LooseConstComparisonPhp7Test extends TypeInferenceTestCase +class NodeScopeResolverPhp7Test extends TypeInferenceTestCase { /** @@ -16,6 +16,8 @@ public static function dataFileAsserts(): iterable // compares constants according to the php-version phpstan configuration, // _NOT_ the current php runtime version yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); + + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php7.php'); } /** @@ -34,7 +36,7 @@ public function testFileAsserts( public static function getAdditionalConfigFiles(): array { return [ - __DIR__ . '/looseConstComparisonPhp7.neon', + __DIR__ . '/nodeScopeResolverPhp7.neon', ]; } diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php b/tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php similarity index 81% rename from tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php rename to tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php index 549a1d2de4..bd6f3aa54b 100644 --- a/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php @@ -5,7 +5,7 @@ use PHPStan\Testing\TypeInferenceTestCase; use PHPUnit\Framework\Attributes\DataProvider; -class LooseConstComparisonPhp8Test extends TypeInferenceTestCase +class NodeScopeResolverPhp8Test extends TypeInferenceTestCase { /** @@ -16,6 +16,8 @@ public static function dataFileAsserts(): iterable // compares constants according to the php-version phpstan configuration, // _NOT_ the current php runtime version yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); + + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php8.php'); } /** @@ -34,7 +36,7 @@ public function testFileAsserts( public static function getAdditionalConfigFiles(): array { return [ - __DIR__ . '/looseConstComparisonPhp8.neon', + __DIR__ . '/nodeScopeResolverPhp8.neon', ]; } diff --git a/tests/PHPStan/Analyser/data/bug-13129-php7.php b/tests/PHPStan/Analyser/data/bug-13129-php7.php new file mode 100644 index 0000000000..c12e1db1a6 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-13129-php7.php @@ -0,0 +1,9 @@ + Date: Thu, 17 Jul 2025 16:45:58 +0200 Subject: [PATCH 2/2] Rename --- ...t.php => LooseConstComparisonPhp7Test.php} | 4 +- ...t.php => LooseConstComparisonPhp8Test.php} | 4 +- tests/PHPStan/Analyser/SubstrPhp7Test.php | 39 +++++++++++++++++++ tests/PHPStan/Analyser/SubstrPhp8Test.php | 39 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) rename tests/PHPStan/Analyser/{NodeScopeResolverPhp7Test.php => LooseConstComparisonPhp7Test.php} (85%) rename tests/PHPStan/Analyser/{NodeScopeResolverPhp8Test.php => LooseConstComparisonPhp8Test.php} (85%) create mode 100644 tests/PHPStan/Analyser/SubstrPhp7Test.php create mode 100644 tests/PHPStan/Analyser/SubstrPhp8Test.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php similarity index 85% rename from tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php rename to tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php index 8b6e494dca..3b4f48d07a 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverPhp7Test.php +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php @@ -5,7 +5,7 @@ use PHPStan\Testing\TypeInferenceTestCase; use PHPUnit\Framework\Attributes\DataProvider; -class NodeScopeResolverPhp7Test extends TypeInferenceTestCase +class LooseConstComparisonPhp7Test extends TypeInferenceTestCase { /** @@ -16,8 +16,6 @@ public static function dataFileAsserts(): iterable // compares constants according to the php-version phpstan configuration, // _NOT_ the current php runtime version yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); - - yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php7.php'); } /** diff --git a/tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php similarity index 85% rename from tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php rename to tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php index bd6f3aa54b..fae702cec2 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverPhp8Test.php +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp8Test.php @@ -5,7 +5,7 @@ use PHPStan\Testing\TypeInferenceTestCase; use PHPUnit\Framework\Attributes\DataProvider; -class NodeScopeResolverPhp8Test extends TypeInferenceTestCase +class LooseConstComparisonPhp8Test extends TypeInferenceTestCase { /** @@ -16,8 +16,6 @@ public static function dataFileAsserts(): iterable // compares constants according to the php-version phpstan configuration, // _NOT_ the current php runtime version yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php8.php'); - - yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php8.php'); } /** diff --git a/tests/PHPStan/Analyser/SubstrPhp7Test.php b/tests/PHPStan/Analyser/SubstrPhp7Test.php new file mode 100644 index 0000000000..561b33500c --- /dev/null +++ b/tests/PHPStan/Analyser/SubstrPhp7Test.php @@ -0,0 +1,39 @@ +> + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php7.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp7.neon', + ]; + } + +} diff --git a/tests/PHPStan/Analyser/SubstrPhp8Test.php b/tests/PHPStan/Analyser/SubstrPhp8Test.php new file mode 100644 index 0000000000..934a7ac78c --- /dev/null +++ b/tests/PHPStan/Analyser/SubstrPhp8Test.php @@ -0,0 +1,39 @@ +> + */ + public static function dataFileAsserts(): iterable + { + yield from self::gatherAssertTypes(__DIR__ . '/data/bug-13129-php8.php'); + } + + /** + * @param mixed ...$args + */ + #[DataProvider('dataFileAsserts')] + public function testFileAsserts( + string $assertType, + string $file, + ...$args, + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/nodeScopeResolverPhp8.neon', + ]; + } + +}