Skip to content

Report null coalesce errors on the last line of the left operand - #6636

Open
Cayan wants to merge 2 commits into
phpstan:2.3.xfrom
Cayan:coalesce-error-operator-line
Open

Cayan wants to merge 2 commits into
phpstan:2.3.xfrom
Cayan:coalesce-error-operator-line

Conversation

@Cayan

@Cayan Cayan commented Sep 30, 2026

Copy link
Copy Markdown

Refs phpstan/phpstan#10714

?? and ??= errors used the start line of the whole expression, which is the first line of the left operand. When the left operand spans several lines, as in the issue, the error lands above the operator. The errors now use the last line of the left operand.

The AST does not keep the ?? token, so the exact operator line is not available to rules. Of the lines the AST has, the last line of the left operand is the one that never moves a correct report:

Layout Before After
Left operand on several lines (the issue) above the operator operator line
$a ?? then $b on the next line operator line operator line
Single line operator line operator line
$a then ?? $b on the next line one line above one line above (unchanged)

Since the operator is always on that line or below it, every error that was already on the right line stays there, so existing @phpstan-ignore comments keep working. Using the start of the right operand instead would fix the last row but move the second one down a line, which testBug14213 catches.

The remaining case, ?? at the start of a line, needs the token position. As far as I can tell that means a new rich parser visitor, which per CLAUDE.md also needs its turbo-ext mirror, so I kept it out of this PR. I left the details on the issue.

The new test covers the issue's layout, the operator at the end and at the start of a line, a single line, and ??= on the same layouts. isset() and empty() are unchanged.

- `??` and `??=` errors used the start line of the whole expression, which is the first line of
  the left operand. When the left operand spans several lines, the error landed above the
  operator.
- The AST does not keep the operator token, so the error now uses the last line of the left
  operand: the operator is on that line or below it, so a line that was already correct stays.
- When `??` starts its own line, the error is still one line above the operator.
Comment on lines +71 to +74
$error = RuleErrorBuilder::message($error->getMessage())
->identifier($error->getIdentifier())
->line($this->getOperatorLine($node))
->build();

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.

Feels wrong to me to rebuild an error manually from another.

Can't issetCheck and/or checkUnnecessaryNullCoalesce use the right operatorLine directly ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 2902a04: IssetCheck::check() now takes an optional line and every error it builds uses it, and checkUnnecessaryNullCoalesce() sets it too, so nothing is rebuilt. isset() and empty() pass no line and report where they did before. I also added cases for an unnecessary ?? null and a property fetch spanning lines.

- IssetCheck::check() takes an optional line and passes it to every error it builds, so
  NullCoalesceRule no longer copies an error to change its line and nothing else is lost.
  isset() and empty() pass no line and keep reporting where they did.
- The unnecessary `?? null` error gets the same line.
- Tests cover an unnecessary `?? null` and a property fetch whose left side spans lines.
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