Skip to content

Add regression tests for null !== $x = expr() assignment-in-condition pattern with &&#5684

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-tjg82md
Closed

Add regression tests for null !== $x = expr() assignment-in-condition pattern with &&#5684
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-tjg82md

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

The issue reports that null !== $programStartDate = $foo->getProgramStartDate() && $now > $programStartDate produces "Undefined variable: $programStartDate". After thorough analysis, this is correct behavior — the code has a genuine PHP bug due to operator precedence.

The && operator has higher precedence than = (assignment), so the expression parses as:

null !== ($programStartDate = ($foo->getProgramStartDate() && ($now > $programStartDate)))

The variable $programStartDate is used inside the right side of &&, which is part of the assignment's RHS — meaning $programStartDate is genuinely undefined at that point in PHP execution.

Changes

  • Added NSRT regression test (tests/PHPStan/Analyser/nsrt/bug-4662.php) verifying that the correctly parenthesized patterns all work:
    • (null !== $x = expr()) && use($x) — type narrowing works
    • ($x = expr()) !== null && use($x) — reversed comparison
    • null !== $x = expr() and use($x)and has lower precedence than =
    • Chained assignments, false !== pattern, || with === null, while loops, nested assignments
  • Added DefinedVariableRule test (tests/PHPStan/Rules/Variables/data/bug-4662.php) documenting:
    • Unparenthesized version correctly reports "Undefined variable" (line 27)
    • Parenthesized version produces no error
    • and keyword version produces no error

Root cause

Not a bug. PHP operator precedence causes && to bind tighter than =, so:

  • null !== $x = expr() && use($x)null !== ($x = (expr() && use($x)))$x is undefined in use($x)
  • (null !== $x = expr()) && use($x) → correctly defines $x first, then uses it

PHPStan correctly analyzes both forms according to PHP's actual parsing rules.

Workarounds for users

  1. Add parentheses: (null !== $x = expr()) && use($x)
  2. Use and keyword (lower precedence): null !== $x = expr() and use($x)
  3. Separate the assignment: $x = expr(); if (null !== $x && use($x)) { ... }

Test

  • NSRT test covers 10 variations of the correctly-parenthesized pattern, verifying type narrowing and variable availability
  • DefinedVariableRule test verifies the unparenthesized version correctly reports the error while the correct versions produce no error

Fixes phpstan/phpstan#4662

…on pattern with `&&`

- Add NSRT test verifying type narrowing works correctly for the
  parenthesized pattern: (null !== $x = expr()) && use($x)
- Add DefinedVariableRule test documenting that the unparenthesized
  version correctly reports "Undefined variable" (due to PHP operator
  precedence, && binds tighter than =)
- Cover variations: logical 'and'/'or' (lower precedence), chained
  assignments, false !== pattern, while loops, || with === null

The reported code `null !== $x = expr() && use($x)` parses as
`null !== ($x = (expr() && use($x)))` due to && having higher
precedence than =. The variable $x is genuinely undefined inside the
BooleanAnd's right side. PHPStan correctly reports this error.

The correctly parenthesized version `(null !== $x = expr()) && use($x)`
already works correctly in PHPStan - variable is defined and narrowed
to non-null in the right side of &&.

Closes phpstan/phpstan#4662
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.

The following code works fine in PHP

class Foo
{
	/**
	 * @var \DateTimeImmutable|null
	 */
	private $programStartDate;
	
	public function getProgramStartDate(): ?\DateTimeImmutable
    {
        return $this->programStartDate;
    }
}

function test(Foo $foo): void
{
	$now = new \DateTimeImmutable('now');

	if (
		null !== $programStartDate = $foo->getProgramStartDate()
		&& $now > $programStartDate
	) {
		echo 'ok';
	}
}


test(new Foo());

Do you have a proof that $programStartDate is not defined ?

@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The token doesn't have permission to comment on the PR. No code changes are needed here — the PR is correct and the tests pass. Let me summarize the findings.


Summary of the review response:

The reviewer's question "Do you have a proof that $programStartDate is not defined?" has a clear answer: yes.

  1. PHP runtime proof: When getProgramStartDate() returns a non-null value, PHP emits Warning: Undefined variable $programStartDate. The reviewer's example only avoids this because their Foo has null as the default, causing && to short-circuit.

  2. AST proof: php-parser shows the expression parses as null !== ($programStartDate = (getProgramStartDate() && ($now > $programStartDate))) — the variable is used inside the BooleanAnd RHS which is part of the assignment's value expression.

  3. Behavioral proof: Even in the "working" null case, $programStartDate ends up as bool(false) (not null or DateTimeImmutable), and the if block is entered when it shouldn't be — proving the code is semantically buggy.

No code changes are needed. The PR correctly documents PHPStan's behavior. Tests pass and make phpstan is clean. I was unable to post a comment to the PR due to token permission restrictions — the response above should be posted manually or through the separate step mentioned in the instructions.

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