Skip to content

Commit 6940bca

Browse files
ondrejmirtesclaude
andcommitted
Narrow the left side of ?? to null in a non-true context only when its other values cannot satisfy it
`!($b ?? false)`, `($b ?? false) === false` and `($b ?? false) !== true` narrowed a `?bool $b` to `null`, although a `false` left side produces the same value. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuUxEfBoMtVfZmJsUxA85s
1 parent 86ee61c commit 6940bca

4 files changed

Lines changed: 138 additions & 1 deletion

File tree

‎src/Analyser/ExprHandler/CoalesceHandler.php‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use PHPStan\Type\NeverType;
2626
use PHPStan\Type\NullType;
2727
use PHPStan\Type\Type;
28+
use PHPStan\Type\TypeCombinator;
2829
use function array_merge;
2930

3031
/**
@@ -131,6 +132,14 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
131132

132133
$s = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope;
133134
if (!$context->true()) {
135+
// the left side was null only when none of its other values
136+
// could have produced the value - `?bool ?? false` is falsey
137+
// for a `false` left side too
138+
$leftType = TypeCombinator::removeNull($nativeTypesPromoted ? $condResult->getNativeType() : $condResult->getType());
139+
if (self::canNonNullLeftSatisfyContext($leftType, $context)) {
140+
return (new SpecifiedTypes([], []))->setRootExpr($expr);
141+
}
142+
134143
return $this->coalesceCompositionHelper->getFalseySpecifiedTypes($s, $s, $expr->left, $condResult, $expr, $context);
135144
}
136145

@@ -171,4 +180,24 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
171180
);
172181
}
173182

183+
/**
184+
* For the contexts without the `true` bit: falsey, `=== false` and `!== true`.
185+
*/
186+
private static function canNonNullLeftSatisfyContext(Type $leftType, TypeSpecifierContext $context): bool
187+
{
188+
if ($leftType instanceof NeverType) {
189+
return false;
190+
}
191+
192+
if ($context === TypeSpecifierContext::createFalsey()) {
193+
return !$leftType->toBoolean()->isTrue()->yes();
194+
}
195+
196+
if (!$context->truthy()) {
197+
return !$leftType->isFalse()->no();
198+
}
199+
200+
return !$leftType->isTrue()->yes();
201+
}
202+
174203
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php // lint >= 8.2
2+
3+
declare(strict_types = 1);
4+
5+
namespace CoalesceNonNullLeftInFalseyContext;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @param int|true|null $m
11+
*/
12+
function doFoo(?bool $b, ?int $i, ?int $j, $m, ?true $t, ?int $n): void
13+
{
14+
if (!($b ?? false)) {
15+
assertType('bool|null', $b);
16+
}
17+
18+
if (($b ?? false) === false) {
19+
assertType('bool|null', $b);
20+
}
21+
22+
if (($b ?? false) !== true) {
23+
assertType('bool|null', $b);
24+
}
25+
26+
if (($i ?? 0) !== true) {
27+
assertType('int|null', $i);
28+
}
29+
30+
if (!($j ?? 1)) {
31+
assertType('int|null', $j);
32+
}
33+
34+
if (($m ?? false) !== true) {
35+
assertType('int|true|null', $m);
36+
}
37+
38+
if (!($t ?? false)) {
39+
assertType('null', $t);
40+
}
41+
42+
if (($t ?? 0) !== true) {
43+
assertType('null', $t);
44+
}
45+
46+
if (($n ?? false) === false) {
47+
assertType('null', $n);
48+
}
49+
}

‎turbo-ext/src/CoalesceHandler.cpp‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,46 @@ class CoalesceHandler
309309
specifiedTypes.intoReturnValue(return_value);
310310
}
311311

312+
/* Mirrors canNonNullLeftSatisfyContext() — for the contexts without the
313+
* `true` bit: falsey, `=== false` and `!== true`; false = pending
314+
* exception */
315+
[[nodiscard]] static bool canNonNullLeftSatisfyContext(zval *leftType, zval *context, bool &out)
316+
{
317+
if (UNEXPECTED(Z_TYPE_P(leftType) != IS_OBJECT)) {
318+
zend_throw_error(NULL, "Call to a member function isTrue() on %s", zend_zval_value_name(leftType));
319+
return false;
320+
}
321+
if (instanceof_function(Z_OBJCE_P(leftType), pt_ce_never_type)) {
322+
out = false;
323+
return true;
324+
}
325+
326+
zend_object *falsey = pt_type_specifier_context_create_falsey();
327+
if (UNEXPECTED(falsey == NULL)) return false;
328+
if (Z_OBJ_P(context) == falsey) {
329+
ptoh::BooleanOf boolean;
330+
if (UNEXPECTED(!boolean.init(leftType))) return false;
331+
int isTrue = boolean.isTrue();
332+
if (UNEXPECTED(isTrue < 0)) return false;
333+
out = !isTrue;
334+
return true;
335+
}
336+
337+
bool truthy;
338+
if (UNEXPECTED(!pt_type_specifier_context_truthy(Z_OBJ_P(context), truthy))) return false;
339+
if (!truthy) {
340+
zend_long isFalse = pt_type_call_trinary(Z_OBJ_P(leftType), PT_LC("isfalse"), 0, NULL);
341+
if (UNEXPECTED(isFalse < 0)) return false;
342+
out = isFalse != PT_TRI_NO;
343+
return true;
344+
}
345+
346+
zend_long isTrue = pt_type_call_trinary(Z_OBJ_P(leftType), PT_LC("istrue"), 0, NULL);
347+
if (UNEXPECTED(isTrue < 0)) return false;
348+
out = isTrue != PT_TRI_YES;
349+
return true;
350+
}
351+
312352
static zv::Val specifyTypes(zval *captures, zval *context, bool nativeTypesPromoted)
313353
{
314354
zend_object *handler = Z_OBJ(captures[0]);
@@ -326,6 +366,17 @@ class CoalesceHandler
326366
bool contextTrue;
327367
if (UNEXPECTED(!pt_type_specifier_context_true(Z_OBJ_P(context), contextTrue))) return zv::Val();
328368
if (!contextTrue) {
369+
// the left side was null only when none of its other values
370+
// could have produced the value - `?bool ?? false` is falsey
371+
// for a `false` left side too
372+
zv::Val condType = nativeTypesPromoted ? pt_expression_result_get_native_type(condResult) : pt_expression_result_get_type(condResult);
373+
if (UNEXPECTED(condType.isUndef())) return zv::Val();
374+
zv::Val leftType = pt_type_combinator_remove_null(condType.raw());
375+
if (UNEXPECTED(leftType.isUndef())) return zv::Val();
376+
bool canSatisfy;
377+
if (UNEXPECTED(!canNonNullLeftSatisfyContext(leftType.raw(), context, canSatisfy))) return zv::Val();
378+
if (canSatisfy) return pt_specified_types_new_with_root_expr(NULL, NULL, expr);
379+
329380
zval *left = ptoh::binaryOpLeft(expr);
330381
if (UNEXPECTED(left == NULL)) return zv::Val();
331382
return pt_coalesce_composition_helper_get_falsey_specified_types(OBJ_PROP_NUM(handler, slots::coalesceCompositionHelper), s.raw(), s.raw(), left, condResult, expr, context);

‎turbo-ext/src/generated/CoalesceHandler.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ inline constexpr char strings[] =
5959
"context\0" /* 537 */
6060
"PHPStan\\Analyser\\ExpressionContext\0" /* 545 */
6161
"PHPStan\\Analyser\\ExpressionResult\0" /* 580 */
62-
"processExpr"; /* 614 */
62+
"processExpr\0" /* 614 */
63+
"leftType\0" /* 626 */
64+
"PHPStan\\Type\\Type\0" /* 635 */
65+
"PHPStan\\Analyser\\TypeSpecifierContext\0" /* 653 */
66+
"canNonNullLeftSatisfyContext"; /* 691 */
6367
inline constexpr reg::PackedArg args[] = {
6468
reg::packed(0, 0, 21), /* __construct $nonNullabilityHelper */
6569
reg::packed(78, 0, 102), /* __construct $expressionResultFactory */
@@ -75,6 +79,9 @@ inline constexpr reg::PackedArg args[] = {
7579
reg::packed(524, MAY_BE_CALLABLE), /* processExpr $nodeCallback */
7680
reg::packed(537, 0, 545), /* processExpr $context */
7781
reg::packed(350, 0, 580), /* processExpr return */
82+
reg::packed(626, 0, 635), /* canNonNullLeftSatisfyContext $leftType */
83+
reg::packed(537, 0, 653), /* canNonNullLeftSatisfyContext $context */
84+
reg::packed(350, MAY_BE_BOOL), /* canNonNullLeftSatisfyContext return */
7885
};
7986
using Sig = reg::Sig<strings, args>;
8087
} // namespace sigtab
@@ -84,6 +91,7 @@ namespace sig {
8491
inline constexpr sigtab::Sig __construct = { { 313 /* __construct */, 4, 0, 4, reg::NoArg, ZEND_ACC_PUBLIC } };
8592
inline constexpr sigtab::Sig supports = { { 351 /* supports */, 1, 4, 1, 5, ZEND_ACC_PUBLIC } };
8693
inline constexpr sigtab::Sig processExpr = { { 614 /* processExpr */, 7, 6, 7, 13, ZEND_ACC_PUBLIC } };
94+
inline constexpr sigtab::Sig canNonNullLeftSatisfyContext = { { 691 /* canNonNullLeftSatisfyContext */, 2, 14, 2, 16, ZEND_ACC_PRIVATE | ZEND_ACC_STATIC } };
8795
} // namespace sig
8896

8997
} // namespace ptdecl::CoalesceHandler

0 commit comments

Comments
 (0)