From 83d0eaf665713481bb49156604565f6d9a48513d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Jul 2026 08:01:44 +0200 Subject: [PATCH 1/4] Fix "ob_get_*() === false will always evaluate to false" false positive --- .../OutputBufferingDynamicReturnTypeExtension.php | 3 ++- .../StrictComparisonOfDifferentTypesRuleTest.php | 5 +++++ tests/PHPStan/Rules/Comparison/data/bug-14985.php | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14985.php diff --git a/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php b/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php index 1f424205b22..878db354aa0 100644 --- a/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php +++ b/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php @@ -13,6 +13,7 @@ use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use PHPStan\Type\TypeUtils; use function in_array; /** @@ -48,7 +49,7 @@ public function getTypeFromFunctionCall( $outputBufferLevelType = $scope->getType(new FuncCall(new Name('ob_get_level'), [])); if (IntegerRangeType::createAllGreaterThanOrEqualTo(1)->isSuperTypeOf($outputBufferLevelType)->yes()) { - return TypeCombinator::remove($defaultReturnType, new ConstantBooleanType(false)); + return TypeUtils::toBenevolentUnion($defaultReturnType); } return $defaultReturnType; diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 0d476571c8a..de35e7711eb 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1261,6 +1261,11 @@ public function testBug14878(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14878.php'], []); } + public function testBug14985(): void + { + $this->analyse([__DIR__ . '/data/bug-14985.php'], []); + } + public function testBug14847(): void { $this->analyse([__DIR__ . '/data/bug-14847.php'], [ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14985.php b/tests/PHPStan/Rules/Comparison/data/bug-14985.php new file mode 100644 index 00000000000..ee44d515ad5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14985.php @@ -0,0 +1,15 @@ + Date: Fri, 24 Jul 2026 08:02:35 +0200 Subject: [PATCH 2/4] cs --- src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php b/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php index 878db354aa0..4a80a433dee 100644 --- a/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php +++ b/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php @@ -8,11 +8,9 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\IntegerRangeType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; use function in_array; From 0f4bf6641e3815a304fa3fd22d08f73e7ab2c832 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Jul 2026 08:03:52 +0200 Subject: [PATCH 3/4] Update output-buffering.php --- .../Analyser/nsrt/output-buffering.php | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/output-buffering.php b/tests/PHPStan/Analyser/nsrt/output-buffering.php index df4f5f5f180..91a101f68b5 100644 --- a/tests/PHPStan/Analyser/nsrt/output-buffering.php +++ b/tests/PHPStan/Analyser/nsrt/output-buffering.php @@ -17,8 +17,8 @@ function activeBuffer(): void { ob_start(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_contents()); - assertType('int', ob_get_length()); + assertType('(string|false)', ob_get_contents()); + assertType('(int|false)', ob_get_length()); } function obCleanAndFlushKeepBuffer(): void @@ -27,17 +27,17 @@ function obCleanAndFlushKeepBuffer(): void assertType('int<1, max>', ob_get_level()); ob_clean(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); ob_flush(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); } function getCleanClosesBuffer(): void { ob_start(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); assertType('int<0, max>', ob_get_level()); assertType('string|false', ob_get_contents()); } @@ -46,7 +46,7 @@ function getFlushClosesBuffer(): void { ob_start(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_flush()); + assertType('(string|false)', ob_get_flush()); assertType('int<0, max>', ob_get_level()); assertType('string|false', ob_get_contents()); } @@ -55,7 +55,7 @@ function endCleanClosesBuffer(): void { ob_start(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); ob_end_clean(); assertType('int<0, max>', ob_get_level()); assertType('string|false', ob_get_contents()); @@ -65,7 +65,7 @@ function endFlushClosesBuffer(): void { ob_start(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); ob_end_flush(); assertType('int<0, max>', ob_get_level()); assertType('string|false', ob_get_contents()); @@ -77,10 +77,10 @@ function nested(): void assertType('int<1, max>', ob_get_level()); ob_start(); assertType('int<2, max>', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); ob_end_clean(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); ob_end_clean(); assertType('int<0, max>', ob_get_level()); assertType('string|false', ob_get_contents()); @@ -99,15 +99,15 @@ function fullyQualified(): void { \ob_start(); assertType('int<1, max>', ob_get_level()); - assertType('string', \ob_get_contents()); - assertType('string', ob_get_contents()); + assertType('(string|false)', \ob_get_contents()); + assertType('(string|false)', ob_get_contents()); } function levelNarrowedToConstInt(): void { if (ob_get_level() === 2) { assertType('2', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); // closing call decrements the const-int level, keeping it exact assertType('1', ob_get_level()); } @@ -123,7 +123,7 @@ function levelNarrowedToIntRange(): void { if (ob_get_level() >= 1) { assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } } @@ -131,10 +131,10 @@ function levelNarrowedToUnionInt(): void { if (ob_get_level() === 1 || ob_get_level() === 3) { assertType('1|3', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); ob_start(); assertType('2|4', ob_get_level()); - assertType('string', ob_get_contents()); + assertType('(string|false)', ob_get_contents()); } } @@ -150,7 +150,7 @@ function levelNarrowedToBoundedIntRange(): void { if (ob_get_level() >= 2 && ob_get_level() <= 5) { assertType('int<2, 5>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); // closing call shifts the whole range down, preserving the upper bound assertType('int<1, 4>', ob_get_level()); } @@ -180,7 +180,7 @@ function pureCallableKeepsLevel(callable $cb): void ob_start(); $cb(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } function impureFunctionForgetsLevel(): void @@ -206,7 +206,7 @@ function pureFunctionKeepsLevel(): void ob_start(); $x=pureFunction(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } class Service @@ -245,7 +245,7 @@ function pureMethodKeepsLevel(Service $service): void ob_start(); $x=$service->pureMethod(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } function impureStaticMethodForgetsLevel(): void @@ -269,7 +269,7 @@ function arrayMapPureCallbackKeepsLevel(array $a): void ob_start(); array_map('strtoupper', $a); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } function laterInvokedCallableKeepsLevel(callable $cb): void @@ -277,7 +277,7 @@ function laterInvokedCallableKeepsLevel(callable $cb): void ob_start(); register_shutdown_function($cb); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } function builtinKeepsLevel(): void @@ -285,7 +285,7 @@ function builtinKeepsLevel(): void ob_start(); printf('hello'); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } class WithImpureConstructor @@ -326,7 +326,7 @@ function pureConstructorKeepsLevel(): void ob_start(); new WithPureConstructor(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } function noConstructorKeepsLevel(): void @@ -334,7 +334,7 @@ function noConstructorKeepsLevel(): void ob_start(); new WithoutConstructor(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } /** @param class-string $className */ @@ -351,7 +351,7 @@ function builtinConstructorKeepsLevel(): void ob_start(); new \ArrayObject(); assertType('int<1, max>', ob_get_level()); - assertType('string', ob_get_clean()); + assertType('(string|false)', ob_get_clean()); } function withRequire(): void From 7a6b35b3f59e3fdcd42b0a07c73d6d23ff720176 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 24 Jul 2026 10:07:44 +0200 Subject: [PATCH 4/4] Update OutputBufferingDynamicReturnTypeExtension.php --- src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php b/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php index 4a80a433dee..9808049052b 100644 --- a/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php +++ b/src/Type/Php/OutputBufferingDynamicReturnTypeExtension.php @@ -47,6 +47,9 @@ public function getTypeFromFunctionCall( $outputBufferLevelType = $scope->getType(new FuncCall(new Name('ob_get_level'), [])); if (IntegerRangeType::createAllGreaterThanOrEqualTo(1)->isSuperTypeOf($outputBufferLevelType)->yes()) { + // checking error state return values of ob_* functions is essentially useless + // as this usually means that your system is out of memory and your process is going to die anyway. + // that's why error states oftentimes are not checked. return TypeUtils::toBenevolentUnion($defaultReturnType); }