Skip to content

Report ConsoleExecuteReturnIntRector when it rewrites a return - #1064

Open
dchaudhari7177 wants to merge 1 commit into
rectorphp:mainfrom
dchaudhari7177:fix/9897-report-console-execute-mutations
Open

Report ConsoleExecuteReturnIntRector when it rewrites a return#1064
dchaudhari7177 wants to merge 1 commit into
rectorphp:mainfrom
dchaudhari7177:fix/9897-report-console-execute-mutations

Conversation

@dchaudhari7177

Copy link
Copy Markdown

The other half of rectorphp/rector#9897. The unreachable-return itself is rectorphp/rector-src#8476 (TerminatedNodeAnalyzer); this is the "no rule listed under Applied rules:" half, which lives here.

The bug

refactor() returns the node only when $this->hasChanged is set, and only refactorReturnTypeDeclaration() ever sets it. Everything else mutates the AST in place and says nothing:

what it does declared?
refactorReturnTypeDeclaration() — adds : int
setReturnTo0InsteadOfNull()return nullreturn 0
return falsereturn 1
… the null side of a ?? or of a ternary
… wraps a non-int expression in (int)
processReturn0ToMethod() — appends return 0;

So for a command whose execute() already carries : int, the body is rewritten and refactor() returns null. That is the empty Applied rules: in #9897 — a printed diff with no rule attributed to it.

It is not only a reporting problem. What happens to an undeclared in-place mutation is the caller's choice, and it has changed: the reporter says the same input produced no diff at all on 2.4.5 and a diff on 2.6.6. A rule that does not declare its own changes is at the mercy of that.

The fix

setReturnTo0InsteadOfNull() now returns whether it rewrote anything, the caller raises hasChanged from it, and the append raises it too.

if ($this->setReturnTo0InsteadOfNull($node)) {
    $this->hasChanged = true;
}

No mutation is added or removed — only the declaration of the ones already being made. The one restructuring is the ternary branch, which was already computing exactly this boolean and throwing it away:

-        if ($return->expr instanceof Ternary) {
-            $hasChanged = $this->isSuccessfulRefactorTernaryReturn($return->expr);
-            if ($hasChanged) {
-                return;
-            }
-        }
+        if ($return->expr instanceof Ternary && $this->isSuccessfulRefactorTernaryReturn($return->expr)) {
+            return true;
+        }

Fixture, and its honest limit

return_null_with_int_return_type.php.inc: a command already returning int whose return null still needs rewriting. Every existing fixture with a null return also lacks the return type, so refactorReturnTypeDeclaration() set hasChanged for them and this path had no coverage at all.

To be straight about what it does and does not prove: a .php.inc fixture asserts the printed output, not which rule was attributed to it. On rector 2.6.x the mutation is printed either way, so this fixture very likely passes with and without the production change. It is here to pin the shape — and it is the case that would start failing on any version that goes back to dropping undeclared mutations, which is what 2.4.5 did.

Verification gap

I could not run the suite. composer install here resolves phpunit/phpunit ^13.2, which needs PHP ≥ 8.4.1, and this machine has 8.3.6 (tomasvotruba/type-coverage wants ^8.4 too). So the fixture and the change are for CI to confirm. What I did check locally is that both changed files pass php -l, and I traced every return; in setReturnTo0InsteadOfNull() to make sure each one that mutates now returns true and the single non-mutating fall-through returns false.

Note

This PR was drafted with AI assistance. I understand the change and can defend or revise it.

The rule tracks $this->hasChanged, and refactor() returns the node only when
that flag is set -- but only refactorReturnTypeDeclaration() ever set it.
Every other mutation was made in place and not declared:

  setReturnTo0InsteadOfNull() rewrites `return null` to `return 0`,
  `return false` to `return 1`, the null side of a `??` or of a ternary, and
  wraps a non-int expression in `(int)`;

  processReturn0ToMethod() appends `return 0;` to the method.

So a command whose execute() already carries `: int` gets its body rewritten
while refactor() returns null. That is the empty `Applied rules:` in
rectorphp/rector#9897: the diff is printed with no rule attributed to it, and
which of the two happens is left to how the caller treats an undeclared
in-place mutation -- on 2.4.5 it was dropped, on 2.6.6 it is printed.

setReturnTo0InsteadOfNull() now reports whether it rewrote anything, the
caller raises hasChanged from it, and the append raises it too. No mutation is
added or removed; only the declaration of the ones already being made.

Fixture: a command already returning int whose `return null` still needs
rewriting -- the shape that had no coverage, since every existing fixture with
a null return also lacks the return type and so was carried by
refactorReturnTypeDeclaration().
@TomasVotruba

Copy link
Copy Markdown
Member

Thanks! Looks great, just one issue in PHPStan
Better method name might help

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants