{"record":{"id":"0368a6084aab2097","repo":"twigphp/Twig","slug":"argument-s-is-defined-twice-for-macro-s-twigmacro","errorCode":null,"errorMessage":"Argument \"%s\" is defined twice for macro \"%s\".","messagePattern":"Argument \"(.+?)\" is defined twice for macro \"(.+?)\"\\.","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"src/TwigMacro.php","lineNumber":132,"sourceCode":"                $sawNamed = true;\n                if (null === $i = $this->argumentIndexes[$key] ?? null) {\n                    $hasUnknownNamed = true;\n                } else {\n                    if (null === $duplicate && $i < $positionalCount) {\n                        $duplicate = $key;\n                    }\n                    if (isset($this->requiredNames[$key])) {\n                        ++$namedRequired;\n                    }\n                }\n            }\n        }\n\n        if ($misordered) {\n            throw new RuntimeError(\\sprintf('Positional arguments cannot be used after named arguments for macro \"%s\".', $this->name), $lineno, $source);\n        }\n        if (null !== $duplicate) {\n            throw new RuntimeError(\\sprintf('Argument \"%s\" is defined twice for macro \"%s\".', $duplicate, $this->name), $lineno, $source);\n        }\n\n        // For a fully named call, the coverage of the required arguments is exact; a\n        // mixed call falls back to the precise (and slower) per-argument check.\n        $mayMissRequired = 0 === $positionalCount\n            ? $namedRequired < \\count($this->requiredNames)\n            : $positionalCount < $this->requiredCount;\n\n        if ($mayMissRequired || (!$this->variadic && ($hasUnknownNamed || $positionalCount > \\count($this->arguments)))) {\n            $this->triggerLegacyDeprecations($arguments, $positionalCount, $source, $lineno);\n        }\n\n        foreach ($this->renamedArguments as $name => $parameterName) {\n            if (\\array_key_exists($name, $arguments)) {\n                $arguments[$parameterName] = $arguments[$name];\n                unset($arguments[$name]);\n            }\n        }","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/TwigMacro.php#L114-L150","documentation":"Thrown by TwigMacro::callLegacy() when the same macro argument receives two values — once positionally and once by name (the named argument's index falls below the positional count). Twig detects this before invoking the macro body and raises a RuntimeError anchored to the calling template's line, instead of letting the value be silently overwritten.","triggerScenarios":"Calling a macro with a value for argument N positionally and then also passing the same argument by name, e.g. {{ m('x', name='y') }} for a macro whose first parameter is $name.","commonSituations":"Refactoring a call to use named arguments but forgetting to remove the old positional value; dynamically assembled argument arrays where a name collides with an already-supplied positional slot; stricter Twig 4-style checks surfacing bugs that Twig 3 silently accepted.","solutions":["Remove either the positional or the named value so each macro argument is supplied exactly once.","Convert the whole call to named arguments to make duplicates obvious.","If building arguments programmatically, deduplicate by normalized argument name before calling the macro."],"exampleFix":"{# before #}\n{{ macro.link('Homepage', text='Homepage') }}\n{# after #}\n{{ macro.link('Homepage') }}","handlingStrategy":"validation","validationCode":"// PHP: ensure no named arg collides with an occupied positional slot\nfunction hasDuplicateMacroArgs(array $args, array $argumentIndexes): bool {\n    $positionalCount = count(array_filter(array_keys($args), 'is_int'));\n    foreach ($args as $k => $_) {\n        if (is_string($k) && isset($argumentIndexes[$k]) && $argumentIndexes[$k] < $positionCount()) {}\n    }\n    return false; // implement using $positionalCount comparison per TwigMacro\n}","typeGuard":"function findDuplicateMacroArg(array $args, array $argumentIndexes): ?string {\n    $positionalCount = 0;\n    foreach ($args as $k => $_) { if (is_int($k)) { $positionalCount++; } }\n    foreach ($args as $k => $_) {\n        if (is_string($k) && in_array($argumentIndexes[$k] ?? null, range(0, $positionalCount - 1), true)) {\n            return $k;\n        }\n    }\n    return null;\n}","tryCatchPattern":"try {\n    $out = $macro->callLegacy($args, $source, $lineno);\n} catch (Twig\\Error\\RuntimeError $e) {\n    if (str_contains($e->getMessage(), 'is defined twice for macro')) {\n        error_log('Duplicate macro argument at '.$e->getTemplateLine().': '.$e->getMessage());\n    }\n    throw $e;\n}","preventionTips":["When migrating a call to named arguments, delete the original positional value in the same edit.","Use one style (all positional or all named) per macro call.","Deduplicate programmatic argument arrays by normalized name before invocation."],"tags":["twig","macro","duplicate-argument","template"],"backgroundTag":"duplicate-argument","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"}