Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Do not report non-falsy-string == 0 as always false in AccessoryNonFalsyStringType::looseCompare()#5654

Merged
staabm merged 4 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-qgnbucy
May 13, 2026
Merged

Do not report non-falsy-string == 0 as always false in AccessoryNonFalsyStringType::looseCompare()#5654
staabm merged 4 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-qgnbucy

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

PHPStan incorrectly reported that non-falsy-string == 0 is always false. This is wrong because non-falsy numeric strings like '0.0' can loosely equal 0'0.0' == 0 evaluates to true in all PHP versions since '0.0' is a numeric string that gets compared numerically.

Changes

  • src/Type/Accessory/AccessoryNonFalsyStringType.php: Replaced the blanket StaticTypeFactory::falsey()->isSuperTypeOf($type)->yes() check with specific checks for null, false, and empty string ''. These are the only falsey types where comparison with a non-falsy-string is guaranteed to be false. Integer 0, float 0.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 with 0, 0.0, '0' (no error expected), and with false, null, '' (always-false errors still expected).

  • tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php: Added testBug14606() test method.

  • tests/PHPStan/Analyser/nsrt/loose-comparisons.php: Updated two assertions from 'false' to 'bool' for $nonFalsyString == $zero and $nonFalsyString == $zeroStr — these previously asserted the buggy behavior.

Root cause

AccessoryNonFalsyStringType::looseCompare() used StaticTypeFactory::falsey() which includes 0, 0.0, '0', null, false, '', and [] — then concluded that loose comparison with any of these is always false. 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 equals 0 because the comparison happens numerically.

Analogous cases probed

  • AccessoryNonEmptyStringType: Already correct — only claims false for null and empty string, not for 0/0.0/false/'0'.
  • AccessoryNumericStringType: Already correct — only claims false for null and 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

  • Rule test testBug14606 with 6 scenarios: 3 that should produce no error (== 0, == 0.0, == '0') and 3 that should still report always-false (== false, == null, == '').
  • Updated 2 existing NSRT assertions from 'false' to 'bool'.

Fixes phpstan/phpstan#14606

…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
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Pushed. Here's what was done to address the review:

Problem: The original fix used individual isNull()->yes(), isFalse()->yes(), and isNonEmptyString()->no() checks, which had two issues:

  1. Union types like null|false wouldn't match either check individually, so $nonFalsyString == $nullOrFalse would incorrectly return bool instead of false
  2. Empty array [] comparison was missing entirely, regressing $nonFalsyString == []

Fix: Replaced the individual checks with a single isSuperTypeOf() call on a union of the falsey types where comparison with non-falsy-string is guaranteed false: null, false, '', and []. This correctly handles union types and restores the empty array case, while still excluding 0, 0.0, and '0' (the original bug fix).

Tests added: Two new test cases for == [] (always false) and == $nullOrFalse (always false for null|false union).

All 12,061 tests pass and make phpstan reports 0 errors.

assertType('false', $nonFalsyString == $false);
assertType('bool', $nonFalsyString == $one);
assertType('false', $nonFalsyString == $zero);
assertType('bool', $nonFalsyString == $zero);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment to explain when it can be true

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added explanatory comments to the two changed assertions in loose-comparisons.php:

  • $nonFalsyString == $zero: explains that e.g. '0.0' == 0 is 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment to explain when it can be true

@VincentLanglet VincentLanglet self-assigned this May 13, 2026
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a separate type assertion for the constant type '0.0' == 0 comparison.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a separate type assertion for the constant type '0.0' == '0' comparison.

@staabm staabm merged commit 0c6d698 into phpstan:2.1.x May 13, 2026
653 of 661 checks passed
@staabm staabm deleted the create-pull-request/patch-qgnbucy branch May 13, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants