{"record":{"id":"69c02702a9234865","repo":"twigphp/Twig","slug":"the-variadic-argument-s-in-macro-s-must-be-the-last-one","errorCode":null,"errorMessage":"The variadic argument \"%s\" in macro \"%s\" must be the last one.","messagePattern":"The variadic argument \"(.+?)\" in macro \"(.+?)\" must be the last one\\.","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"src/TokenParser/MacroTokenParser.php","lineNumber":104,"sourceCode":"                $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma');\n\n                // if the comma above was a trailing comma, early exit the argument parse loop\n                if ($stream->test(Token::PUNCTUATION_TYPE, ')')) {\n                    break;\n                }\n            }\n\n            if ($stream->nextIf(Token::OPERATOR_TYPE, '...')) {\n                $token = $stream->expect(Token::NAME_TYPE, null, 'A variadic argument must be a name');\n                $variadicName = (new LocalVariable($token->getValue(), $token->getLine()))->getAttribute('name');\n                if (str_starts_with($variadicName, TempNameExpression::RESERVED_NAME_PREFIX)) {\n                    $variadicName = substr($variadicName, \\strlen(TempNameExpression::RESERVED_NAME_PREFIX));\n                }\n                if ($stream->test(Token::OPERATOR_TYPE, '=')) {\n                    throw new SyntaxError(\\sprintf('The variadic argument \"%s\" in macro \"%s\" cannot have a default value.', $variadicName, $macroName), $token->getLine(), $stream->getSourceContext());\n                }\n                if ($stream->nextIf(Token::PUNCTUATION_TYPE, ',') && !$stream->test(Token::PUNCTUATION_TYPE, ')')) {\n                    throw new SyntaxError(\\sprintf('The variadic argument \"%s\" in macro \"%s\" must be the last one.', $variadicName, $macroName), $token->getLine(), $stream->getSourceContext());\n                }\n\n                break;\n            }\n\n            $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name');\n            $name = new LocalVariable($token->getValue(), $token->getLine());\n            NodeDocumentation::add($name, $token);\n            if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) {\n                $default = $this->parser->parseExpression();\n            } else {\n                $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine());\n                $default->setAttribute('is_implicit', true);\n            }\n\n            if (!$this->checkConstantExpression($default)) {\n                throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext());\n            }","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/TokenParser/MacroTokenParser.php#L86-L122","documentation":"In a macro signature, the variadic argument must be the final parameter. Twig throws this when a comma follows the variadic argument and another argument comes after it, e.g. '{% macro foo(...rest, extra) %}'.","triggerScenarios":"Declaring '{% macro m(...args, name) %}'; in parseDefinition, after consuming the ',' the stream does not see ')' immediately, so the variadic is not last and a SyntaxError is raised.","commonSituations":"Adding a new parameter after an existing variadic when refactoring macros; porting signatures from languages that allow rest params anywhere.","solutions":["Move the variadic argument to the end of the parameter list","Rename and reorder parameters so all fixed arguments precede '...name'","Split the macro into two if you genuinely need parameters after the catch-all"],"exampleFix":"// before\n{% macro render(...attrs, class) %}\n// after\n{% macro render(class, ...attrs) %}","handlingStrategy":"validation","validationCode":"// Ensure '...' argument is last in every macro signature\nforeach ($macroSignatures as $sig) {\n    $variadicPos = null;\n    foreach ($sig as $i => $p) {\n        if (str_starts_with($p, '...')) { $variadicPos = $i; }\n    }\n    if ($variadicPos !== null && $variadicPos !== count($sig) - 1) {\n        throw new InvalidArgumentException('Variadic macro argument must be last.');\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    $twig->render($template, $context);\n} catch (SyntaxError $e) {\n    error_log($e->getMessage() . ' at line ' . $e->getTemplateLine());\n}","preventionTips":["Always order macro parameters: required, optional (with defaults), then the single variadic","When adding parameters to a macro with '...args', insert them before the variadic","Code-review macro signature changes"],"tags":["twig","macro","syntax-error","template"],"backgroundTag":"invalid-argument-value","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"}