Skip to content

Treat Expr\UnaryMinus with scalar operand as scalar literal in isExprSafeToProjectThroughVariable and comparison type specifying#5557

Merged
ondrejmirtes merged 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-j9k5ef5
Apr 28, 2026
Merged

Treat Expr\UnaryMinus with scalar operand as scalar literal in isExprSafeToProjectThroughVariable and comparison type specifying#5557
ondrejmirtes merged 2 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-j9k5ef5

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

Fixes a crash (TypeError: Argument #1 ($exprString) must be of type string, int given) when analysing code with comparisons against negative integer literals in ternary assignments, such as $x = array_search($a, $ids) > -1 ? $index : PHP_INT_MAX.

Changes

  • src/Analyser/ExprHandler/AssignHandler.php:
    • Added Expr\UnaryMinus && $expr->expr instanceof Node\Scalar to the isExprSafeToProjectThroughVariable filter, preventing negated scalar literals (which are not valid narrowing targets) from entering the conditional expressions map.
    • Added (string) casts in processSureTypesForConditionalExpressionsAfterAssign, processSureNotTypesForConditionalExpressionsAfterAssign, and the consuming foreach loop as defense-in-depth against PHP's numeric-string array-key autocasting.
  • src/Analyser/TypeSpecifier.php:
    • Extended the Smaller/SmallerOrEqual comparison handling to skip creating sure types for UnaryMinus(Scalar) expressions, matching the existing Node\Scalar filter.
  • phpstan-baseline.neon:
    • Added baseline entry for the 3 defensive (string) casts in AssignHandler.php (matching existing baseline for same pattern in MutatingScope.php).
  • tests/PHPStan/Analyser/AnalyserIntegrationTest.php + tests/PHPStan/Analyser/data/bug-14542.php:
    • Added regression test reproducing the crash.

Root cause

In PHP Parser v5, negative integer literals like -1 are represented as Expr\UnaryMinus(Scalar\Int_(1)), not Scalar\Int_(-1). The isExprSafeToProjectThroughVariable method checked for instanceof Node\Scalar to skip scalar literals, but UnaryMinus is not a Scalar node, so -1 bypassed the filter. Its expression string "-1" is a numeric string, which PHP's array key semantics auto-cast to int(-1). When the code later iterated over the array and passed the key to addConditionalExpressions(string $exprString, ...), the integer caused a TypeError.

MutatingScope::filterBySpecifiedTypes already handled this case correctly (filtering Expr\UnaryMinus && $expr->expr instanceof Node\Scalar and casting to (string)), but the parallel code in AssignHandler did not.

Analogous cases probed:

  • UnaryPlus(Scalar): Not a practical concern — PHP's is_numeric() treats "+1" as numeric, but PHP Parser doesn't commonly produce UnaryPlus for positive literals, and MutatingScope::filterBySpecifiedTypes doesn't check for it either.
  • Other sure type consumers in TypeSpecifier (e.g., processBooleanSureConditionalTypes): These filter to Expr\Variable only, so numeric expression strings never reach them.
  • SpecifiedTypes::intersectWith/unionWith/normalize: These propagate keys between arrays without typed consumption, so int keys don't cause TypeErrors (they're just passed through).

Test

  • testBug14542 in AnalyserIntegrationTest: reproduces the original crash with array_search() result compared against -1 in a ternary assignment. Without the fix, throws TypeError; with the fix, analyses without errors.

Fixes phpstan/phpstan#14542

…xprSafeToProjectThroughVariable` and comparison type specifying

- In PHP Parser v5, negative literals like `-1` are represented as
  `Expr\UnaryMinus(Scalar\Int_(1))`, not `Scalar\Int_(-1)`. The
  `isExprSafeToProjectThroughVariable` filter in AssignHandler only
  checked for `instanceof Node\Scalar`, missing the `UnaryMinus` wrapper.
  This allowed a sure type with expression string `"-1"` (a numeric
  string) to enter the conditional expressions array. PHP's array key
  autocasting then turned it into an integer key, causing a TypeError
  when passed to `addConditionalExpressions(string $exprString, ...)`.
- Added `Expr\UnaryMinus && $expr->expr instanceof Node\Scalar` to the
  filter in `isExprSafeToProjectThroughVariable`, matching what
  `MutatingScope::filterBySpecifiedTypes` already does.
- Added the same `UnaryMinus(Scalar)` check to TypeSpecifier's
  `Smaller`/`SmallerOrEqual` handling, which skips creating sure types
  for scalar literals but was missing the negated-scalar case.
- Added defensive `(string)` casts in three locations in AssignHandler
  where expression string keys from `getSureTypes()`/`getSureNotTypes()`
  are consumed, matching the pattern already used in
  `MutatingScope::filterBySpecifiedTypes`.
Copy link
Copy Markdown
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

And regenerate the baseline with make phpstan-generate-baseline and commit that

$this->assertNoErrors($errors);
}

public function testBug14542(): void
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add RequiresPhp >= 8.0.0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Because of the mixed type

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. Both review comments are addressed:

  1. Added #[RequiresPhp('>= 8.0.0')] to testBug14542 because the test data file uses the mixed type which requires PHP 8.0+.
  2. Regenerated the baseline with make phpstan-generate-baseline — the AssignHandler.php entry moved to its correct alphabetical position.

Tests (11936 passing) and static analysis (no errors) both verified clean.

The test uses `mixed` type which requires PHP 8.0+. Baseline
regenerated with `make phpstan-generate-baseline` to fix entry ordering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ondrejmirtes ondrejmirtes merged commit e3ceaf2 into phpstan:2.1.x Apr 28, 2026
658 of 659 checks passed
@ondrejmirtes ondrejmirtes deleted the create-pull-request/patch-j9k5ef5 branch April 28, 2026 15:41
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.

2 participants