🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Add UnaryOperatorTypeSpecifyingExtension for BcMath\Number so unary +/- infer BcMath\Number - #5994

Merged
staabm merged 5 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-het2rx0
Jul 5, 2026
Merged

Add UnaryOperatorTypeSpecifyingExtension for BcMath\Number so unary +/- infer BcMath\Number#5994
staabm merged 5 commits into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-het2rx0

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Unary operations on BcMath\Number were incorrectly flagged. $x++/$x-- were already
fixed earlier, but unary -$x and +$x still resolved to *ERROR* and produced
Unary operation "-"/"+" on BcMath\Number results in an error., even though PHP 8.4's
BcMath\Number overloads these operators and returns a BcMath\Number.

This PR adds a UnaryOperatorTypeSpecifyingExtension for BcMath\Number so that -$x
and +$x infer BcMath\Number and no longer produce a false positive.

Changes

  • Added src/Type/Php/BcMathNumberUnaryOperatorTypeSpecifyingExtension.php, an autowired
    UnaryOperatorTypeSpecifyingExtension gated on
    PhpVersion::supportsBcMathNumberOperatorOverloading(). It supports unary - and +
    on BcMath\Number operands, returning BcMath\Number. ~ is deliberately not handled
    (see Root cause).
  • Added type-inference assertions for -$a, +$a, ~$a in
    tests/PHPStan/Analyser/nsrt/bcmath-number.php.
  • Added tests/PHPStan/Rules/Operators/data/unary-bcmath-number.php and a
    testBcMathNumber case in InvalidUnaryOperationRuleTest asserting that -/+ are
    accepted and only ~ reports unaryOp.invalid.

Root cause

Unary +/-/~ type resolution in InitializerExprTypeResolver::getUnaryPlusType(),
getUnaryMinusType() and getBitwiseNotType() first consults the
UnaryOperatorTypeSpecifyingExtension registry and only then falls back to
Type::toNumber() / Type::toBitwiseNotType(). ObjectType::toNumber() returns
ErrorType for BcMath\Number — correctly, because a (int)/(float) cast of a
BcMath\Number is a runtime error — so without a dedicated unary extension the fallback
produced *ERROR*.

GMP already had GmpUnaryOperatorTypeSpecifyingExtension; BcMath\Number had a binary
BcMathNumberOperatorTypeSpecifyingExtension but no unary counterpart. The fix mirrors
the GMP extension. Unlike GMP, BcMath\Number does not overload bitwise not, so ~ is
left unsupported and continues to be reported as an error.

I probed the parallel construct on this axis: the only two operator-overloading classes
with a binary OperatorTypeSpecifyingExtension are GMP and BcMath\Number; GMP's unary
extension already existed, so with this change the unary family is complete. I also
considered fixing ObjectType::toNumber()/toInt()/toFloat() instead (as suggested in
the issue), but rejected it because those also drive (int)/(float) casts, which are
invalid on BcMath\Number.

Test

  • tests/PHPStan/Analyser/nsrt/bcmath-number.php now asserts -$a and +$a are
    BcMath\Number and ~$a is *ERROR*.
  • InvalidUnaryOperationRuleTest::testBcMathNumber asserts no error for -/+ and a
    single Unary operation "~" on BcMath\Number results in an error. for ~.

Both tests fail without the new extension and pass with it.

Fixes phpstan/phpstan#13965

@VincentLanglet VincentLanglet self-assigned this Jul 4, 2026
@VincentLanglet
VincentLanglet requested a review from staabm July 4, 2026 18:15
return false;
}

$bcMathNumberType = new ObjectType('BcMath\Number');

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.

might make sense to put the object type on a property of the extension, so we don't need to create these objects over and over (and the ObjectType internals also has some per instance caching, which might benefit from it).

I have no proof though whether it would have a perf impact.

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. I addressed staabm's review by caching the BcMath\Number ObjectType on a private property so it's constructed once per extension instance instead of being recreated on every isOperatorSupported() / specifyType() call.

Changes in src/Type/Php/BcMathNumberUnaryOperatorTypeSpecifyingExtension.php:

  • Added private ObjectType $bcMathNumberType; initialized in the constructor.
  • isOperatorSupported() and specifyType() now reuse $this->bcMathNumberType.

Verification:

  • InvalidUnaryOperationRuleTest — 4 tests pass.
  • NodeScopeResolverTest --filter bcmath — 2 tests pass.
  • make phpstan — no errors.

Committed and pushed to create-pull-request/patch-het2rx0 (e8784877f).

Comment thread tests/PHPStan/Rules/Operators/data/unary-bcmath-number.php

public function __construct(private PhpVersion $phpVersion)
{
$this->bcMathNumberType = new ObjectType('BcMath\Number');

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.

Lazy init the object so we don't eagerly trigger reflection

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. I addressed staabm's review by lazily initializing the BcMath\Number ObjectType instead of eagerly constructing it in the constructor.

Changes

  • src/Type/Php/BcMathNumberUnaryOperatorTypeSpecifyingExtension.php — Changed $bcMathNumberType to a nullable property (?ObjectType = null), removed the constructor assignment, and added a private getBcMathNumberType() helper that lazily creates the type on first use (??=). isOperatorSupported() and specifyType() now call the helper. This keeps the per-instance caching benefit while avoiding eager reflection when the extension is constructed but never exercised (e.g. on older PHP versions where supportsBcMathNumberOperatorOverloading() short-circuits first).

Verification

  • InvalidUnaryOperationRuleTest — 4 tests pass.
  • NodeScopeResolverTest --filter bcmath — 2 tests pass.
  • make phpstan — no errors.

Committed and pushed to create-pull-request/patch-het2rx0.

VincentLanglet and others added 4 commits July 5, 2026 10:16
…ry `+`/`-` infer `BcMath\Number`

- Add `BcMathNumberUnaryOperatorTypeSpecifyingExtension` mirroring the existing
  `GmpUnaryOperatorTypeSpecifyingExtension`, gated on
  `PhpVersion::supportsBcMathNumberOperatorOverloading()`.
- The extension handles unary `-` and `+` (both return `BcMath\Number`); `~` is
  intentionally left unsupported because `BcMath\Number` does not overload bitwise
  not, so `~$x` still resolves to `*ERROR*` and is reported by
  `InvalidUnaryOperationRule`.
- Not touching `ObjectType::toNumber()`/`toInt()`/`toFloat()` on purpose: those feed
  `(int)`/`(float)` casts which are invalid on `BcMath\Number` (they emit a runtime
  warning), so the operator-extension route is the correct fix.
- Probed the parallel construct on the same axis: `GmpUnaryOperatorTypeSpecifyingExtension`
  already covers GMP; these two are the only operator-overloading classes with a binary
  `OperatorTypeSpecifyingExtension`, so the unary family is now complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@staabm
staabm force-pushed the create-pull-request/patch-het2rx0 branch from 4c3e1e1 to 5ab8c40 Compare July 5, 2026 08:16
@staabm
staabm merged commit e40f7fe into phpstan:2.2.x Jul 5, 2026
114 of 116 checks passed
@staabm
staabm deleted the create-pull-request/patch-het2rx0 branch July 5, 2026 08:28
@VincentLanglet

Copy link
Copy Markdown
Contributor

I don't think it's worth caching the objectType @staabm

But if we do it should be consistent with BcMathNumberOperatorTypeSpecifyingExtension, GmpOperatorTypeSpecifyingExtension and GmpUnaryOperatorTypeSpecifyingExtension no ?

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.

BcMath\Number unary operations result in error

3 participants