{"record":{"id":"b1bab82e9dcf4b96","repo":"rectorphp/rector","slug":"array-of-nodes-cannot-be-empty-ensure-s-refact","errorCode":null,"errorMessage":"Array of nodes cannot be empty. Ensure \"%s->refactor()\" returns non-empty array for Nodes.\n\nA) Direct return null for no change:\n\n    return null;\n\nB) Remove the Node:\n\n    return \\PhpParser\\NodeVisitor::REMOVE_NODE;","messagePattern":"Array of nodes cannot be empty\\. Ensure \"(.+?)->refactor\\(\\)\" returns non-empty array for Nodes\\.\n\nA\\) Direct return null for no change:\n\n    return null;\n\nB\\) Remove the Node:\n\n    return \\\\PhpParser\\\\NodeVisitor::REMOVE_NODE;","errorType":"exception","errorClass":"ShouldNotHappenException","httpStatus":null,"severity":"error","filePath":"src/Rector/AbstractRector.php","lineNumber":136,"sourceCode":"        // whether it would actually have changed anything. Only a skip that prevents a real change\n        // counts as used; the original node is left untouched, so the file stays skipped either way.\n        $skipMatch = $this->skipper->matchSkip($this, $filePath);\n        if ($skipMatch instanceof SkipMatch) {\n            if ($this->refactor($this->cloneNode($node)) !== null) {\n                $this->skipper->markSkipUsed($skipMatch);\n            }\n            return null;\n        }\n        // ensure origNode pulled before refactor to avoid changed during refactor, ref https://3v4l.org/YMEGN\n        $originalNode = $node->getAttribute(AttributeKey::ORIGINAL_NODE) ?? $node;\n        $refactoredNodeOrState = $this->refactor($node);\n        // nothing to change → continue\n        if ($refactoredNodeOrState === null) {\n            return null;\n        }\n        if ($refactoredNodeOrState === []) {\n            $errorMessage = sprintf(self::EMPTY_NODE_ARRAY_MESSAGE, static::class);\n            throw new ShouldNotHappenException($errorMessage);\n        }\n        $isState = is_int($refactoredNodeOrState);\n        if ($isState) {\n            $this->createdByRuleDecorator->decorate($node, $originalNode, static::class);\n            // only remove node is supported\n            if ($refactoredNodeOrState !== NodeVisitor::REMOVE_NODE) {\n                // @todo warn about unsupported state in the future\n                return null;\n            }\n            // notify this rule changed code\n            $rectorWithLineChange = new RectorWithLineChange(static::class, $originalNode->getStartLine());\n            $this->file->addRectorClassWithLine($rectorWithLineChange);\n            return $refactoredNodeOrState;\n        }\n        return $this->postRefactorProcess($originalNode, $node, $refactoredNodeOrState, $filePath);\n    }\n    /**\n     * @return mixed[]|int|\\PhpParser\\Node|null","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/rectorphp/rector/blob/408fcb0ff1833e3d26cbc9b04c23a77565814a16/src/Rector/AbstractRector.php#L118-L154","documentation":"AbstractRector::refactorNode() interprets refactor()'s return: null means no change, an int is a visitor state (REMOVE_NODE), a Node replaces, and a non-empty array of nodes splices in. An empty array is neither 'no change' nor a valid splice, so it throws ShouldNotHappenException with EMPTY_NODE_ARRAY_MESSAGE, naming your rule class and spelling out the two legal alternatives (return null, or return REMOVE_NODE).","triggerScenarios":"A custom rule's refactor() builds $statements = [] and returns it when a loop/condition matched nothing but an early guard was missed — e.g. return $statements; after a foreach that added zero nodes.","commonSituations":"First draft of a rule where the collection path exists but is never populated; refactoring an if/else so a fall-through path returns [] instead of null; copying a multi-node rule template and deleting the append lines.","solutions":["Return null on every nothing-changed path","Return NodeVisitor::REMOVE_NODE (the int constant) when the intent was to delete the node","Guard before returning: if ($statements === []) { return null; } return $statements;"],"exampleFix":"// before\n$statements = [];\nforeach ($args as $arg) {\n    // ...\n}\nreturn $statements; // [] when loop body never appends\n\n// after\nif ($statements === []) {\n    return null;\n}\nreturn $statements;","handlingStrategy":"validation","validationCode":"// normalize refactor() results through one exit point\npublic function refactor(Node $node): null|Node|array|int\n{\n    $statements = $this->buildStatements($node);\n\n    return $statements === [] ? null : $statements;\n}","typeGuard":"/** @param mixed $return */\nfunction isLegalRefactorReturn($return): bool\n{\n    if ($return === null || $return instanceof \\PhpParser\\Node || is_int($return)) {\n        return true;\n    }\n    if (! is_array($return)) {\n        return false;\n    }\n    return $return !== []; // non-empty node arrays only\n}","tryCatchPattern":null,"preventionTips":["Adopt the rule: empty means null — never return [] from refactor()","Return NodeVisitor::REMOVE_NODE when deletion is the intent","Run the custom rule over an empty-ish fixture in CI to exercise the nothing-matched path"],"tags":["php","rector","custom-rule","refactor-contract","empty-array"],"backgroundTag":"invalid-refactor-return","analyzedSha":"408fcb0ff1833e3d26cbc9b04c23a77565814a16","analyzedAt":"2026-08-21T05:11:02.643Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}