{"record":{"id":"f4384ee6eb0a38ad","repo":"twigphp/Twig","slug":"argument-s-is-defined-twice-for-s-s-f4384e","errorCode":null,"errorMessage":"Argument \"%s\" is defined twice for %s \"%s\".","messagePattern":"Argument \"(.+?)\" is defined twice for (.+?) \"(.+?)\"\\.","errorType":"exception","errorClass":"SyntaxError","httpStatus":null,"severity":"error","filePath":"src/Util/CallableArgumentsExtractor.php","lineNumber":96,"sourceCode":"        $missingArguments = [];\n        $optionalArguments = [];\n        $pos = 0;\n        foreach ($callableParameters as $callableParameter) {\n            $callableParameterName = $callableParameter->name;\n            if (\\PHP_VERSION_ID >= 80000 && 'range' === $callable) {\n                if ('start' === $callableParameterName) {\n                    $callableParameterName = 'low';\n                } elseif ('end' === $callableParameterName) {\n                    $callableParameterName = 'high';\n                }\n            }\n\n            $callableParameterNames[] = $callableParameterName;\n            $normalizedCallableParameterName = $this->normalizeName($callableParameterName);\n\n            if (\\array_key_exists($normalizedCallableParameterName, $extractedArguments)) {\n                if (\\array_key_exists($pos, $extractedArguments)) {\n                    throw new SyntaxError(\\sprintf('Argument \"%s\" is defined twice for %s \"%s\".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext());\n                }\n\n                if (\\count($missingArguments)) {\n                    throw new SyntaxError(\\sprintf(\n                        'Argument \"%s\" could not be assigned for %s \"%s(%s)\" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s \"%s\".',\n                        $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \\count($missingArguments) > 1 ? 's' : '', implode('\", \"', $missingArguments)\n                    ), $this->node->getTemplateLine(), $this->node->getSourceContext());\n                }\n\n                $arguments = array_merge($arguments, $optionalArguments);\n                $arguments[] = $extractedArguments[$normalizedCallableParameterName];\n                unset($extractedArguments[$normalizedCallableParameterName]);\n                $optionalArguments = [];\n            } elseif (\\array_key_exists($pos, $extractedArguments)) {\n                $arguments = array_merge($arguments, $optionalArguments);\n                $arguments[] = $extractedArguments[$pos];\n                unset($extractedArguments[$pos]);\n                $optionalArguments = [];","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/Util/CallableArgumentsExtractor.php#L78-L114","documentation":"While mapping extracted template arguments onto the callable's parameters, extractArguments() detects when a parameter has already received a value — both positionally and by name. It throws a SyntaxError naming the doubly-defined argument, the callable type, and its name, preventing silent overwrites during compilation.","triggerScenarios":"A template call supplies argument N positionally and the same parameter also by name (normalized names collide), e.g. {{ replace('a', 'b', from='a') }} where the first parameter is $from.","commonSituations":"Migrating calls from positional to named style and leaving one value in both forms; dynamic argument building in extensions producing collisions; case/underscore naming differences that normalize to the same parameter.","solutions":["Remove the duplicate — keep either the positional or the named value, not both.","Use named arguments exclusively for the affected call.","In extension code, guard against programmatic argument arrays that can collide with positional entries."],"exampleFix":"{# before #}\n{{ filter.colorize('text', color='red', 'red') }}\n{# after #}\n{{ filter.colorize('text', color='red') }}","handlingStrategy":"validation","validationCode":"// Normalize names the way Twig does and reject collisions before rendering\nfunction hasDuplicateCallableArgs(array $supplied, array $paramNames): bool {\n    $norm = fn (string $s) => strtolower(preg_replace('/[_A-Z]/', '_', lcfirst($s)));\n    $used = [];\n    foreach ($supplied as $k => $_) {\n        $key = is_int($k) ? (isset($paramNames[$k]) ? $norm($paramNames[$k]) : '#'.$k) : $norm($k);\n        if (isset($used[$key])) { return true; }\n        $used[$key] = true;\n    }\n    return false;\n}","typeGuard":null,"tryCatchPattern":"try {\n    $out = $twig->render($template, $ctx);\n} catch (Twig\\Error\\SyntaxError $e) {\n    if (str_contains($e->getMessage(), 'is defined twice for')) {\n        error_log($e->getMessage().' at '.$e->getTemplateLine());\n    }\n    throw $e;\n}","preventionTips":["When switching a call to named arguments, remove the positional duplicate in the same change.","Avoid mixing positional and named arguments for the same callable.","Run template lints in CI to surface normalization collisions (case/underscore variants)."],"tags":["twig","duplicate-argument","compile-time","syntax-error"],"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"}