rectorphp/rector · error · LogicException

Trying to replace expression (%s) with statement (%s)

Error message

Trying to replace expression (%s) with statement (%s)

What it means

The mirror case of the statement/expression guard in ensureReplacementReasonable(): when the old node is an Expr (lives inside a statement, argument, or property) and the replacement is a Stmt, no valid parent slot exists, so Rector throws LogicException printing both concrete types.

Source

Thrown at src/PhpParser/NodeTraverser/RectorNodeTraverser.php:257

            }
        }
        if ($doNodes !== []) {
            while ([$i, $replace] = array_pop($doNodes)) {
                array_splice($nodes, $i, 1, $replace);
            }
        }
        return $nodes;
    }
    private function ensureReplacementReasonable(Node $old, Node $new): void
    {
        if ($old instanceof Stmt) {
            if ($new instanceof Expr) {
                throw new LogicException(sprintf('Trying to replace statement (%s) ', $old->getType()) . sprintf('with expression (%s). Are you missing a ', $new->getType()) . 'Stmt_Expression wrapper?');
            }
            return;
        }
        if ($new instanceof Stmt) {
            throw new LogicException(sprintf('Trying to replace expression (%s) ', $old->getType()) . sprintf('with statement (%s)', $new->getType()));
        }
    }
    /**
     * This must happen after $this->configuration is set after ProcessCommand::execute() is run, otherwise we get default false positives.
     *
     * This should be removed after https://github.com/rectorphp/rector/issues/5584 is resolved
     */
    private function prepareNodeVisitors(): void
    {
        if ($this->areNodeVisitorsPrepared) {
            return;
        }
        // filter out by PHP version
        $this->visitors = $this->phpVersionedFilter->filter($this->rectors);
        // filter out by composer package constraint
        $this->visitors = $this->composerPackageConstraintFilter->filter($this->visitors);
        // filter by configuration
        $this->visitors = $this->configurationRuleFilter->filter($this->visitors);

View on GitHub (pinned to 408fcb0ff1)

Solutions

  1. Return an Expr when the matched node is an Expr; restructure the rule to match the wrapping statement (e.g. Expression parent) and return the statement from there
  2. Use Rector's helpers like NodeReplacer/exprToStatement patterns or PhpParserBuilder to emit the statement at the parent level
  3. Check the message's pair of types to confirm which level you accidentally crossed

Example fix

// before: rule matches Expr (MethodCall) but returns a statement
public function refactor(Node $node): ?Node
{
    return new Echo_([$node]); // Stmt inside Expr slot -> throws
}

// after: match the wrapping statement instead
public function refactor(Node $node): ?Node
{
    if (! $node instanceof Expression) {
        return null;
    }
    return new Echo_([$node->expr]);
}
Defensive patterns

Strategy: type-guard

Type guard

/** True when a replacement Node can legally occupy the old node's slot. */
function isSameStmtExprLevel(\PhpParser\Node $old, \PhpParser\Node $new): bool
{
    $oldIsStmt = $old instanceof \PhpParser\Node\Stmt;
    $newIsStmt = $new instanceof \PhpParser\Node\Stmt;
    return $oldIsStmt === $newIsStmt;
}

Prevention

When it happens

Trigger: refactor() on an Expr node returns a statement object such as new Expression(...), new Echo_(...), or a ClassMethod — e.g. a rule that tries to hoist an expression into its own statement from inside the expression match.

Common situations: Rules that wrap an expression in a statement (extract-to-statement refactorings) implemented at the wrong match level; reusing code between an Expression-level and Expr-level rule.

Related errors


AI-assisted analysis of rectorphp/rector@408fcb0ff1 (2026-08-21). Data as JSON: /api/errors/ecbdbd4aa6545e79. Report an issue: GitHub.