{"record":{"id":"a8ba8ae810a2406e","repo":"twigphp/Twig","slug":"operators-with-more-than-2-operands-are-not-supported-yet","errorCode":null,"errorMessage":"Operators with more than 2 operands are not supported yet, got %d.","messagePattern":"Operators with more than 2 operands are not supported yet, got (.+?)\\.","errorType":"exception","errorClass":"LogicException","httpStatus":null,"severity":"error","filePath":"src/NodeVisitor/SafeAnalysisNodeVisitor.php","lineNumber":108,"sourceCode":"        return $node;\n    }\n\n    public function leaveNode(Node $node, Environment $env): ?Node\n    {\n        if ($node instanceof ConstantExpression) {\n            // constants are marked safe for all\n            $this->setSafe($node, ['all']);\n        } elseif ($node instanceof BlockReferenceExpression) {\n            // blocks are safe by definition\n            $this->setSafe($node, ['all']);\n        } elseif ($node instanceof ParentExpression) {\n            // parent block is safe by definition\n            $this->setSafe($node, ['all']);\n        } elseif ($node instanceof OperatorEscapeInterface) {\n            // intersect safeness of operands\n            $operands = $node->getOperandNamesToEscape();\n            if (2 < \\count($operands)) {\n                throw new \\LogicException(\\sprintf('Operators with more than 2 operands are not supported yet, got %d.', \\count($operands)));\n            } elseif (2 === \\count($operands)) {\n                $safe = $this->intersectSafe($this->getSafe($node->getNode($operands[0])), $this->getSafe($node->getNode($operands[1])));\n                $this->setSafe($node, $safe);\n            }\n        } elseif ($node instanceof FilterExpression) {\n            // filter expression is safe when the filter is safe\n            if ($node->hasAttribute('twig_callable')) {\n                $filter = $node->getAttribute('twig_callable');\n            } else {\n                // legacy\n                $filter = $env->getFilter($node->getAttribute('name'));\n            }\n\n            if ($filter) {\n                $safe = $filter->getSafe($node->getNode('arguments'));\n                if (null === $safe) {\n                    trigger_deprecation('twig/twig', '3.16', 'The \"%s::getSafe()\" method should not return \"null\" anymore, return \"[]\" instead.', $filter::class);\n                    $safe = [];","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/NodeVisitor/SafeAnalysisNodeVisitor.php#L90-L126","documentation":"During safe-analysis, when a node implements OperatorEscapeInterface the visitor intersects the safeness of its operands; the current implementation only supports one or two operands. An operator declaring more than 2 operands to escape hits this LogicException, signaling an unsupported operator definition in the optimizer pipeline.","triggerScenarios":"A custom Twig operator/expression node implementing OperatorEscapeInterface::getOperandNamesToEscape() returning 3+ child names; adding an escape-analysis-aware operator extension with multiple operands processed by SafeAnalysisNodeVisitor with the optimizer enabled.","commonSituations":"Writing a custom ternary-style or variadic operator expression class in a Twig extension; a third-party extension that defines an OperatorEscapeInterface node designed for newer/older Twig internals; forked core modifications adding operands to an existing operator.","solutions":["Change getOperandNamesToEscape() to return at most two operand names; escape the extra operands by different means (e.g. mark them safe manually via setSafe or handle them in your own visitor).","Split the multi-operand operation into nested binary nodes so each has at most 2 escapable operands.","Implement a custom NodeVisitor that performs safe analysis for your operator and registers it, bypassing the core limitation.","Keep the operands <= 2 when designing any node implementing OperatorEscapeInterface."],"exampleFix":"// before\npublic function getOperandNamesToEscape(): array\n{\n    return ['left', 'middle', 'right'];\n}\n// after\npublic function getOperandNamesToEscape(): array\n{\n    return ['left', 'right']; // handle 'middle' elsewhere or nest nodes\n}","handlingStrategy":"validation","validationCode":"$operands = $node->getOperandNamesToEscape();\nif (count($operands) > 2) {\n    throw new \\LogicException('Operator ' . get_class($node) . ' declares ' . count($operands) . ' escapable operands; max supported is 2.');\n}","typeGuard":null,"tryCatchPattern":"try {\n    $env->compile($source);\n} catch (\\LogicException $e) {\n    if (str_contains($e->getMessage(), 'more than 2 operands')) {\n        // disable optimizer or fix the custom operator definition\n    } else {\n        throw $e;\n    }\n}","preventionTips":["Design OperatorEscapeInterface nodes with at most 2 escapable operands.","Split multi-operand operations into nested binary nodes.","Test custom operators with the optimizer enabled (safe analysis) before release."],"tags":["twig","optimizer","safe-analysis","operator","logic-exception"],"backgroundTag":"unsupported-operation","analyzedSha":"a414c3a491defb5a60f2fc88ef79ff37c90010cd","analyzedAt":"2026-09-13T15:10:46.849Z","contentChangedAt":"2026-09-13T15:10:46.849Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}