Do not report non-falsy-string == 0 as always false in AccessoryNonFalsyStringType::looseCompare()#5654
Conversation
…nFalsyStringType::looseCompare()` - Replace blanket `StaticTypeFactory::falsey()` check with specific checks for null, false, and empty string in `AccessoryNonFalsyStringType::looseCompare()` - Non-falsy numeric strings like '0.0' can loosely equal 0, 0.0, and '0' (via numeric string comparison), so these must not be reported as always-false - Update existing NSRT assertions in loose-comparisons.php that asserted the buggy behavior ($nonFalsyString == $zero and $nonFalsyString == $zeroStr) - Verified analogous accessory string types (NonEmptyString, NumericString, Lowercase, Uppercase) are not affected — they don't use the blanket falsey check
VincentLanglet
left a comment
There was a problem hiding this comment.
I'm afraid this will believe that $nonFalsyString == $nullOrFalse is bool when it should be false.
This also regress $nonFalsyString == $emptyArray.
Why not just changing the falseyTypes used for isSuperTypeOf check ?
…pe checks Address review: use a union of null, false, '', and [] with isSuperTypeOf() instead of individual isNull()/isFalse()/isNonEmptyString() checks. This correctly handles union types like null|false and restores the empty array case that was regressed. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
Pushed. Here's what was done to address the review: Problem: The original fix used individual
Fix: Replaced the individual checks with a single Tests added: Two new test cases for All 12,061 tests pass and |
| assertType('false', $nonFalsyString == $false); | ||
| assertType('bool', $nonFalsyString == $one); | ||
| assertType('false', $nonFalsyString == $zero); | ||
| assertType('bool', $nonFalsyString == $zero); |
There was a problem hiding this comment.
Add a comment to explain when it can be true
There was a problem hiding this comment.
Done. Added explanatory comments to the two changed assertions in loose-comparisons.php:
$nonFalsyString == $zero: explains that e.g.'0.0' == 0is true (non-falsy numeric string compared numerically)$nonFalsyString == $zeroStr: explains that e.g.'0.0' == '0'is true (numeric strings compared numerically)
Tests and make phpstan both pass.
| assertType('bool', $nonFalsyString == $minusOne); | ||
| assertType('bool', $nonFalsyString == $oneStr); | ||
| assertType('false', $nonFalsyString == $zeroStr); | ||
| assertType('bool', $nonFalsyString == $zeroStr); |
There was a problem hiding this comment.
Add a comment to explain when it can be true
…ero can be true Co-Authored-By: Claude Opus 4.6 <[email protected]>
| assertType('false', $nonFalsyString == $false); | ||
| assertType('bool', $nonFalsyString == $one); | ||
| assertType('false', $nonFalsyString == $zero); | ||
| assertType('bool', $nonFalsyString == $zero); // e.g. '0.0' == 0 is true (non-falsy numeric string compared numerically) |
There was a problem hiding this comment.
add a separate type assertion for the constant type '0.0' == 0 comparison.
There was a problem hiding this comment.
Pushed. Added assertType('true', '0.0' == 0) and assertType('true', '0.0' == '0') right after the respective non-falsy-string assertions to prove with constant types that the comments are correct. All tests pass and make phpstan reports 0 errors.
| assertType('bool', $nonFalsyString == $minusOne); | ||
| assertType('bool', $nonFalsyString == $oneStr); | ||
| assertType('false', $nonFalsyString == $zeroStr); | ||
| assertType('bool', $nonFalsyString == $zeroStr); // e.g. '0.0' == '0' is true (numeric strings compared numerically) |
There was a problem hiding this comment.
add a separate type assertion for the constant type '0.0' == '0' comparison.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Summary
PHPStan incorrectly reported that
non-falsy-string == 0is always false. This is wrong because non-falsy numeric strings like'0.0'can loosely equal0—'0.0' == 0evaluates totruein all PHP versions since'0.0'is a numeric string that gets compared numerically.Changes
src/Type/Accessory/AccessoryNonFalsyStringType.php: Replaced the blanketStaticTypeFactory::falsey()->isSuperTypeOf($type)->yes()check with specific checks fornull,false, and empty string''. These are the only falsey types where comparison with a non-falsy-string is guaranteed to be false. Integer0, float0.0, and string'0'are no longer claimed as always-false because non-falsy numeric strings (e.g.,'0.0') can loosely equal them.tests/PHPStan/Rules/Comparison/data/bug-14606.php: New test data exercising loose comparisons of non-falsy-string with0,0.0,'0'(no error expected), and withfalse,null,''(always-false errors still expected).tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php: AddedtestBug14606()test method.tests/PHPStan/Analyser/nsrt/loose-comparisons.php: Updated two assertions from'false'to'bool'for$nonFalsyString == $zeroand$nonFalsyString == $zeroStr— these previously asserted the buggy behavior.Root cause
AccessoryNonFalsyStringType::looseCompare()usedStaticTypeFactory::falsey()which includes0,0.0,'0',null,false,'', and[]— then concluded that loose comparison with any of these is alwaysfalse. This assumed that "truthy value == falsey value" is always false, but PHP's loose comparison uses type coercion, not truthiness: a non-falsy numeric string like'0.0'is truthy yet loosely equals0because the comparison happens numerically.Analogous cases probed
AccessoryNonEmptyStringType: Already correct — only claims false fornulland empty string, not for0/0.0/false/'0'.AccessoryNumericStringType: Already correct — only claims false fornulland non-numeric strings.AccessoryLowercaseStringType/AccessoryUppercaseStringType: Already correct — only claim false for specific string-vs-string comparisons, not for numeric types.StringType: Already correct — only claims false for array comparisons.Test
testBug14606with 6 scenarios: 3 that should produce no error (== 0, == 0.0, == '0') and 3 that should still report always-false (== false, == null, == '').'false'to'bool'.Fixes phpstan/phpstan#14606