From 9411e3678963169f3591a0c792f7a3b937f6a8a8 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:33:55 +0000 Subject: [PATCH] Pick the ternary's final scope from the native condition type when `treatPhpDocTypesAsCertain` is off - TernaryHandler decided whether to keep only the truthy or only the falsey branch scope from the PHPDoc condition type. With a PHPDoc-only `@return Expr`, `$x === null` is PHPDoc-false, so the truthy branch (where the native type is `null`) was dropped and the native type stayed `mixed~null` after the ternary, making a repeated `$x === null` "always false" even with treatPhpDocTypesAsCertain: false. - Inject the `treatPhpDocTypesAsCertain` parameter and read the native condition type when it is off, like IfHandler, ForHandler, WhileHandler, DoWhileHandler and MatchHandler already do. - Port the change to the turbo-ext C++ twin (TernaryHandler.cpp + regenerated declarations header). - Drop the stale line-20 expectation in ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest: it was produced by the same scope drop after the line-19 ternary. - Probed if/elseif, switch, match, &&, ||, ??, while and short ternary; only the ternary (long and short) was affected. --- src/Analyser/ExprHandler/TernaryHandler.php | 5 +- ...anceOfRuleWithUncertainPhpDocTypesTest.php | 4 - ...tTypesRuleWithUncertainPhpDocTypesTest.php | 47 ++++++++ .../Rules/Comparison/data/bug-15403.php | 47 ++++++++ turbo-ext/src/TernaryHandler.cpp | 11 +- turbo-ext/src/generated/TernaryHandler.h | 102 +++++++++--------- 6 files changed, 158 insertions(+), 58 deletions(-) create mode 100644 tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithUncertainPhpDocTypesTest.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-15403.php diff --git a/src/Analyser/ExprHandler/TernaryHandler.php b/src/Analyser/ExprHandler/TernaryHandler.php index 0e9880711fb..6238de7298a 100644 --- a/src/Analyser/ExprHandler/TernaryHandler.php +++ b/src/Analyser/ExprHandler/TernaryHandler.php @@ -21,6 +21,7 @@ use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Analyser\VariableFlow; use PHPStan\Dependency\Dependencies; +use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\BooleanType; @@ -59,6 +60,8 @@ public function __construct( private ExpressionResultFactory $expressionResultFactory, private DefaultNarrowingHelper $defaultNarrowingHelper, private BooleanNarrowingHelper $booleanNarrowingHelper, + #[AutowiredParameter] + private bool $treatPhpDocTypesAsCertain, ) { } @@ -122,7 +125,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex $this->capturedResults[spl_object_id($expr)] = [$expr, $ternaryCondResult, $ifResult, $elseResult]; } - $condType = $ternaryCondResult->getType(); + $condType = $this->treatPhpDocTypesAsCertain ? $ternaryCondResult->getType() : $ternaryCondResult->getNativeType(); if ($condType->isTrue()->yes()) { $finalScope = $ifTrueScope; } elseif ($condType->isFalse()->yes()) { diff --git a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php index ff7da0ad122..5ab221bdeb8 100644 --- a/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php +++ b/tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleWithUncertainPhpDocTypesTest.php @@ -117,10 +117,6 @@ public function testTernaryElseDoNotReportPhpDoc(): void 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', 17, ], - [ - 'Instanceof between UnreachableTernaryElseBranchNotPhpDoc\Foo and UnreachableTernaryElseBranchNotPhpDoc\Foo will always evaluate to true.', - 20, - ], ]); } diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithUncertainPhpDocTypesTest.php new file mode 100644 index 00000000000..56cec92a94a --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleWithUncertainPhpDocTypesTest.php @@ -0,0 +1,47 @@ + + */ +class StrictComparisonOfDifferentTypesRuleWithUncertainPhpDocTypesTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new StrictComparisonOfDifferentTypesRule( + self::getContainer()->getByType(RicherScopeGetTypeHelper::class), + self::getContainer()->getByType(ConstantConditionInTraitHelper::class), + false, + false, + true, + ); + } + + public static function getAdditionalConfigFiles(): array + { + return array_merge( + parent::getAdditionalConfigFiles(), + [ + __DIR__ . '/../uncertain-phpdoc-types.neon', + ], + ); + } + + protected function shouldTreatPhpDocTypesAsCertain(): bool + { + return false; + } + + public function testBug15403(): void + { + $this->analyse([__DIR__ . '/data/bug-15403.php'], []); + } + +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-15403.php b/tests/PHPStan/Rules/Comparison/data/bug-15403.php new file mode 100644 index 00000000000..e34850714bc --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-15403.php @@ -0,0 +1,47 @@ +getCondition(); + return implode("\0", [ + $condition === null ? '' : (string) $condition->getTitle(), + $condition === null ? '' : (string) $condition->getExpression(), + ]); +} + +function assigned(Binding $b): void +{ + $condition = $b->getCondition(); + $x = $condition === null ? '' : (string) $condition->getTitle(); + $y = $condition === null ? '' : (string) $condition->getExpression(); +} + +function shortTernary(Binding $b): void +{ + $condition = $b->getCondition(); + $x = $condition === null ?: 'a'; + $y = $condition === null ?: 'b'; +} + +function negated(Binding $b): void +{ + $condition = $b->getCondition(); + $x = $condition !== null ? 'a' : 'b'; + $y = $condition !== null ? 'a' : 'b'; +} diff --git a/turbo-ext/src/TernaryHandler.cpp b/turbo-ext/src/TernaryHandler.cpp index ef749c1d597..dcac7e921c9 100644 --- a/turbo-ext/src/TernaryHandler.cpp +++ b/turbo-ext/src/TernaryHandler.cpp @@ -57,11 +57,12 @@ class TernaryHandler explicit TernaryHandler(zend_object *self) : self(self) {} /* the constructor body: the promoted properties */ - void construct(zval *expressionResultFactory, zval *defaultNarrowingHelper, zval *booleanNarrowingHelper) const + void construct(zval *expressionResultFactory, zval *defaultNarrowingHelper, zval *booleanNarrowingHelper, bool treatPhpDocTypesAsCertain) const { pt_write_slot(self, slots::expressionResultFactory, expressionResultFactory); pt_write_slot(self, slots::defaultNarrowingHelper, defaultNarrowingHelper); pt_write_slot(self, slots::booleanNarrowingHelper, booleanNarrowingHelper); + zv::ObjRef(self).propAtWrite(slots::treatPhpDocTypesAsCertain, zv::Val::boolean(treatPhpDocTypesAsCertain)); } /* Mirrors resetFileAnalysisState(). */ @@ -167,7 +168,8 @@ class TernaryHandler zend_hash_index_update(Z_ARRVAL_P(capturedResults), Z_OBJ_HANDLE_P(expr), &entryValue); } - zv::Val condType = pt_expression_result_get_type(ternaryCondResult.raw()); + bool treatPhpDocTypesAsCertain = Z_TYPE_P(OBJ_PROP_NUM(self, slots::treatPhpDocTypesAsCertain)) == IS_TRUE; + zv::Val condType = treatPhpDocTypesAsCertain ? pt_expression_result_get_type(ternaryCondResult.raw()) : pt_expression_result_get_native_type(ternaryCondResult.raw()); if (UNEXPECTED(condType.isUndef())) return zv::Val(); zval *finalScope; zv::Val mergedScope; @@ -952,8 +954,9 @@ PT_MINIT_REGISTRATION(pt_register_ternary_handler) * service by reflecting the constructor */ cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { zval *expressionResultFactory, *defaultNarrowingHelper, *booleanNarrowingHelper; - if (!zp::parse(execute_data, expressionResultFactory, defaultNarrowingHelper, booleanNarrowingHelper)) RETURN_THROWS(); - TernaryHandler(Z_OBJ_P(ZEND_THIS)).construct(expressionResultFactory, defaultNarrowingHelper, booleanNarrowingHelper); + bool treatPhpDocTypesAsCertain; + if (!zp::parse(execute_data, expressionResultFactory, defaultNarrowingHelper, booleanNarrowingHelper, treatPhpDocTypesAsCertain)) RETURN_THROWS(); + TernaryHandler(Z_OBJ_P(ZEND_THIS)).construct(expressionResultFactory, defaultNarrowingHelper, booleanNarrowingHelper, treatPhpDocTypesAsCertain); }); cls.method(sigs::supports, [](INTERNAL_FUNCTION_PARAMETERS) { diff --git a/turbo-ext/src/generated/TernaryHandler.h b/turbo-ext/src/generated/TernaryHandler.h index ca776fc0f20..7a492cc7635 100644 --- a/turbo-ext/src/generated/TernaryHandler.h +++ b/turbo-ext/src/generated/TernaryHandler.h @@ -14,6 +14,7 @@ inline constexpr uint32_t capturedResults = 0; inline constexpr uint32_t expressionResultFactory = 1; inline constexpr uint32_t defaultNarrowingHelper = 2; inline constexpr uint32_t booleanNarrowingHelper = 3; +inline constexpr uint32_t treatPhpDocTypesAsCertain = 4; } // namespace slot inline void declareClass(reg::Class &cls) @@ -29,6 +30,7 @@ inline void declareProperties(reg::Class &cls) cls.property("expressionResultFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExpressionResultFactory"); cls.property("defaultNarrowingHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper"); cls.property("booleanNarrowingHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\BooleanNarrowingHelper"); + cls.property("treatPhpDocTypesAsCertain", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, MAY_BE_BOOL); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -42,57 +44,59 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper\0" /* 112 */ "booleanNarrowingHelper\0" /* 171 */ "PHPStan\\Analyser\\ExprHandler\\Helper\\BooleanNarrowingHelper\0" /* 194 */ - "__construct\0" /* 253 */ - "expr\0" /* 265 */ - "PhpParser\\Node\\Expr\0" /* 270 */ - "supports\0" /* 290 */ - "PhpParser\\Node\\Expr\\Ternary\0" /* 299 */ - "getCapturedResults\0" /* 327 */ - "nodeScopeResolver\0" /* 346 */ - "PHPStan\\Analyser\\NodeScopeResolver\0" /* 364 */ - "stmt\0" /* 399 */ - "PhpParser\\Node\\Stmt\0" /* 404 */ - "scope\0" /* 424 */ - "PHPStan\\Analyser\\MutatingScope\0" /* 430 */ - "storage\0" /* 461 */ - "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 469 */ - "nodeCallback\0" /* 510 */ - "context\0" /* 523 */ - "PHPStan\\Analyser\\ExpressionContext\0" /* 531 */ - "PHPStan\\Analyser\\ExpressionResult\0" /* 566 */ - "processExpr\0" /* 600 */ - "isDisjunction\0" /* 612 */ - "armExpr\0" /* 626 */ - "armResult\0" /* 634 */ - "processingScope\0" /* 644 */ - "evaluatedScope\0" /* 660 */ - "armContext\0" /* 675 */ - "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 686 */ - "createArmOperand"; /* 724 */ + "treatPhpDocTypesAsCertain\0" /* 253 */ + "__construct\0" /* 279 */ + "expr\0" /* 291 */ + "PhpParser\\Node\\Expr\0" /* 296 */ + "supports\0" /* 316 */ + "PhpParser\\Node\\Expr\\Ternary\0" /* 325 */ + "getCapturedResults\0" /* 353 */ + "nodeScopeResolver\0" /* 372 */ + "PHPStan\\Analyser\\NodeScopeResolver\0" /* 390 */ + "stmt\0" /* 425 */ + "PhpParser\\Node\\Stmt\0" /* 430 */ + "scope\0" /* 450 */ + "PHPStan\\Analyser\\MutatingScope\0" /* 456 */ + "storage\0" /* 487 */ + "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 495 */ + "nodeCallback\0" /* 536 */ + "context\0" /* 549 */ + "PHPStan\\Analyser\\ExpressionContext\0" /* 557 */ + "PHPStan\\Analyser\\ExpressionResult\0" /* 592 */ + "processExpr\0" /* 626 */ + "isDisjunction\0" /* 638 */ + "armExpr\0" /* 652 */ + "armResult\0" /* 660 */ + "processingScope\0" /* 670 */ + "evaluatedScope\0" /* 686 */ + "armContext\0" /* 701 */ + "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 712 */ + "createArmOperand"; /* 750 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, MAY_BE_VOID), /* resetFileAnalysisState return */ reg::packed(24, 0, 48), /* __construct $expressionResultFactory */ reg::packed(89, 0, 112), /* __construct $defaultNarrowingHelper */ reg::packed(171, 0, 194), /* __construct $booleanNarrowingHelper */ - reg::packed(265, 0, 270), /* supports $expr */ + reg::packed(253, MAY_BE_BOOL), /* __construct $treatPhpDocTypesAsCertain */ + reg::packed(291, 0, 296), /* supports $expr */ reg::packed(0, MAY_BE_BOOL), /* supports return */ - reg::packed(265, 0, 299), /* getCapturedResults $expr */ + reg::packed(291, 0, 325), /* getCapturedResults $expr */ reg::packed(0, MAY_BE_NULL | MAY_BE_ARRAY), /* getCapturedResults return */ - reg::packed(346, 0, 364), /* processExpr $nodeScopeResolver */ - reg::packed(399, 0, 404), /* processExpr $stmt */ - reg::packed(265, 0, 270), /* processExpr $expr */ - reg::packed(424, 0, 430), /* processExpr $scope */ - reg::packed(461, 0, 469), /* processExpr $storage */ - reg::packed(510, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ - reg::packed(523, 0, 531), /* processExpr $context */ - reg::packed(0, 0, 566), /* processExpr return */ - reg::packed(265, 0, 270), /* isDisjunction $expr */ + reg::packed(372, 0, 390), /* processExpr $nodeScopeResolver */ + reg::packed(425, 0, 430), /* processExpr $stmt */ + reg::packed(291, 0, 296), /* processExpr $expr */ + reg::packed(450, 0, 456), /* processExpr $scope */ + reg::packed(487, 0, 495), /* processExpr $storage */ + reg::packed(536, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ + reg::packed(549, 0, 557), /* processExpr $context */ + reg::packed(0, 0, 592), /* processExpr return */ + reg::packed(291, 0, 296), /* isDisjunction $expr */ reg::packed(0, MAY_BE_BOOL), /* isDisjunction return */ - reg::packed(626, 0, 270), /* createArmOperand $armExpr */ - reg::packed(634, 0, 566), /* createArmOperand $armResult */ - reg::packed(644, 0, 430), /* createArmOperand $processingScope */ - reg::packed(660, 0, 430), /* createArmOperand $evaluatedScope */ - reg::packed(675, MAY_BE_NULL, 686), /* createArmOperand $armContext */ + reg::packed(652, 0, 296), /* createArmOperand $armExpr */ + reg::packed(660, 0, 592), /* createArmOperand $armResult */ + reg::packed(670, 0, 456), /* createArmOperand $processingScope */ + reg::packed(686, 0, 456), /* createArmOperand $evaluatedScope */ + reg::packed(701, MAY_BE_NULL, 712), /* createArmOperand $armContext */ reg::packed(0, MAY_BE_ARRAY), /* createArmOperand return */ }; using Sig = reg::Sig; @@ -101,12 +105,12 @@ using Sig = reg::Sig; /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { inline constexpr sigtab::Sig resetFileAnalysisState = { { 1 /* resetFileAnalysisState */, 0, 0, 0, 0, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig __construct = { { 253 /* __construct */, 3, 1, 3, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig supports = { { 290 /* supports */, 1, 4, 1, 5, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getCapturedResults = { { 327 /* getCapturedResults */, 1, 6, 1, 7, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processExpr = { { 600 /* processExpr */, 7, 8, 7, 15, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig isDisjunction = { { 612 /* isDisjunction */, 1, 16, 1, 17, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; -inline constexpr sigtab::Sig createArmOperand = { { 724 /* createArmOperand */, 5, 18, 5, 23, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig __construct = { { 279 /* __construct */, 4, 1, 4, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig supports = { { 316 /* supports */, 1, 5, 1, 6, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getCapturedResults = { { 353 /* getCapturedResults */, 1, 7, 1, 8, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processExpr = { { 626 /* processExpr */, 7, 9, 7, 16, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig isDisjunction = { { 638 /* isDisjunction */, 1, 17, 1, 18, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } }; +inline constexpr sigtab::Sig createArmOperand = { { 750 /* createArmOperand */, 5, 19, 5, 24, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::TernaryHandler