Fix phpstan/phpstan#14459: Null coalescing operator does not report redundant left operand when always defined#5452
Conversation
…edundant left operand for promoted readonly properties - Promoted readonly properties and promoted properties with hooks are always initialized and cannot be unset, so ??, isset(), and empty() with them is redundant when the type is not nullable - Updated IssetCheck::check() to not bail early for promoted readonly/hooked properties without default values - Mirrored the same fix in MutatingScope::issetCheck() as required by the codebase convention - Added regression test in tests/PHPStan/Rules/Variables/data/bug-14459.php covering promoted readonly, nullable, non-readonly, and non-promoted readonly cases
ondrejmirtes
left a comment
There was a problem hiding this comment.
- Hooks don't have a test case. It has to be a separate method with RequiresPhp
>= 8.4. - I don't love
&& !($nativeReflection->isPromoted() && ($nativeReflection->isReadOnly() || $nativeReflection->isHooked())). Rather, it should be&& (!$nativeReflection->isPromoted() || (!$nativeReflection->isReadOnly() && !$nativeReflection->isHooked()))
Apply De Morgan's law to improve readability: - `!($a && ($b || $c))` → `(!$a || (!$b && !$c))` Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a separate PHP 8.4 test method covering promoted hooked properties that are always initialized and cannot be unset. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Both commits have been pushed. Here's a summary of the changes made to address the review:
Both |
Summary
PHPStan did not report redundant usage of
??(andisset()/empty()) when the left-hand side was a promoted readonly property that is always defined and non-nullable. For example,$dto->policyholderId ?? $dto->payerIdwherepolicyholderIdispublic readonly \stdClasswas not flagged.Changes
src/Rules/IssetCheck.php: Added condition to not bail early when a property is promoted AND (readonly OR hooked) — these properties are always initialized and cannot be unset, so the isset/coalesce check should proceed to report the redundancy.src/Analyser/MutatingScope.php: Mirrored the same fix inissetCheck()as the codebase convention requires (comment: "mirrored in PHPStan\Rules\IssetCheck").tests/PHPStan/Rules/Variables/data/bug-14459.php: New test data file.tests/PHPStan/Rules/Variables/NullCoalesceRuleTest.php: New test methodtestBug14459.Root cause
In
IssetCheck::check(), when processing a property fetch that isn't$this->prop, the code checks if the property has an expression type in scope. If not, it checkshasDefaultValue()— promoted readonly properties returnfalsefor this (they get their value from the constructor parameter, not a default). This caused the method to returnnull(no error) even though the property is guaranteed to be initialized and cannot be unset.The fix adds an additional condition: if the property is promoted AND (readonly OR has hooks), skip the early return and let the error reporting proceed normally.
Test
The regression test covers four scenarios:
public readonly \stdClass $policyholderId) — expects error: "is not nullable"public readonly ?\stdClass $nullablePolicyholderId) — no error expected (nullable, so??is valid)public \stdClass $payerId) — no error expected (can beunset())public readonly \stdClass $payerIdwith manual constructor assignment) — no error expected (might be uninitialized)Fixes phpstan/phpstan#14459