Repository: twigphp/Twig (php)

Issue:
Macro arguments using leading/tailing underscore (BC introduced in v3.15)
Hi. I discovered a side effect after upgrading to the latest version, related to [this line in PR 4391](https://github.com/twigphp/Twig/pull/4391/files#diff-60c82d266554727246bf978c6075764c0538da108886e554bb8b10fa3b21c751R88).
For me, it's a BC, which may affect others as well.
The usage of macro arguments having leading/tailing underscores worked well until 3.15.

Example:
```twig
{% macro testing(_message) %}
The message: '{{ _message }}'.
{% endmacro %}
{{ _self.testing('Hello World!') }}
```

Output:
3.14: `The message: 'Hello World!'.`
3.15: `The message: ''.`

Compiled template:
In the referred line of the PR, underscores are trimmed off from arguments as they are assigned onto `$context`. `_message` becomes `message`, but the original `_message` is requested in the `escape`-method later. That's an error.
```php
[...]
class __TwigTemplate_[...] extends Template
[...]
    // line 1
    public function macro_testing($_message = null, ...$varargs): string|Markup
    {
        $macros = $this->macros;
        $context = [
            "message" => $_message,
            "varargs" => $varargs,
        ] + $this->env->getGlobals();

        $blocks = [];

        return ('' === $tmp = \Twig\Extension\CoreExtension::captureOutput((function () use (&$context, $macros, $blocks) {
            // line 2
            yield "The message: '";
            yield $this->env->getRuntime('Twig\Runtime\EscaperRuntime')->escape(($context["_message"] ?? null), "html", null, true);
            yield "'.
";
            yield from [];
        })())) ? '' : new Markup($tmp, $this->env->getCharset());
    }
```

I'm curious how I should see the code change. If this is the intention, maybe a `trigger_deprecation` should be called while evaluating the macro arguments.
I just wanted to let you know - the affected templates will be fixed on my side.

Pull request description:
Fix having macro variables starting with an underscore
Closes #4452


Patch:
diff --git a/CHANGELOG b/CHANGELOG
index e76aceca..ef40b205 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,6 @@
 # 3.16.0 (2024-XX-XX)
 
+ * Fix having macro variables starting with an underscore
  * Deprecate not passing a `Source` instance to `TokenStream`
  * Deprecate returning `null` from `TwigFilter::getSafe()` and `TwigFunction::getSafe()`, return `[]` instead
 
diff --git a/src/Node/Expression/TempNameExpression.php b/src/Node/Expression/TempNameExpression.php
index b6ce30c8..925b0e7b 100644
--- a/src/Node/Expression/TempNameExpression.php
+++ b/src/Node/Expression/TempNameExpression.php
@@ -32,7 +32,7 @@ class TempNameExpression extends AbstractExpression
         if (null !== $name && (is_int($name) || ctype_digit($name))) {
             $name = (int) $name;
         } elseif (in_array($name, self::RESERVED_NAMES)) {
-            $name = '_'.$name.'_';
+            $name = "\u{035C}".$name;
         }
 
         parent::__construct([], ['name' => $name], $lineno);
diff --git a/src/Node/MacroNode.php b/src/Node/MacroNode.php
index 86abee75..e0961d1b 100644
--- a/src/Node/MacroNode.php
+++ b/src/Node/MacroNode.php
@@ -14,6 +14,7 @@ namespace Twig\Node;
 use Twig\Attribute\YieldReady;
 use Twig\Compiler;
 use Twig\Error\SyntaxError;
+use Twig\Markup;
 use Twig\Node\Expression\ArrayExpression;
 use Twig\Node\Expression\Variable\LocalVariable;
 
@@ -48,7 +49,7 @@ class MacroNode extends Node
         }
 
         foreach ($arguments->getKeyValuePairs() as $pair) {
-            if ('_'.self::VARARGS_NAME.'_' === $pair['key']->getAttribute('name')) {
+            if ("\u{035C}".self::VARARGS_NAME === $pair['key']->getAttribute('name')) {
                 throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext());
             }
         }
@@ -88,9 +89,13 @@ class MacroNode extends Node
 
         foreach ($arguments->getKeyValuePairs() as $pair) {
             $name = $pair['key'];
+            $var = $name->getAttribute('name');
+            if (str_starts_with($var, "\u{035C}")) {
+                $var = substr($var, \strlen("\u{035C}"));
+            }
             $compiler
                 ->write('')
-                ->string(trim($name->getAttribute('name'), '_'))
+                ->string($var)
                 ->raw(' => ')
                 ->subcompile($name)
                 ->raw(",\n")


Test patch:
diff --git a/tests/Node/MacroTest.php b/tests/Node/MacroTest.php
index fcd5db40..11bdc2f1 100644
--- a/tests/Node/MacroTest.php
+++ b/tests/Node/MacroTest.php
@@ -42,6 +42,8 @@ class MacroTest extends NodeTestCase
             new ConstantExpression(null, 1),
             new LocalVariable('bar', 1),
             new ConstantExpression('Foo', 1),
+            new LocalVariable('_underscore', 1),
+            new ConstantExpression(null, 1),
         ], 1);
 
         $body = new BodyNode([new TextNode('foo', 1)]);
@@ -49,12 +51,13 @@ class MacroTest extends NodeTestCase
 
         yield 'with use_yield = true' => [$node, <<<EOF
 // line 1
-public function macro_foo(\$foo = null, \$bar = "Foo", ...\$varargs): string|Markup
+public function macro_foo(\$foo = null, \$bar = "Foo", \$_underscore = null, ...\$varargs): string|Markup
 {
     \$macros = \$this->macros;
     \$context = [
         "foo" => \$foo,
         "bar" => \$bar,
+        "_underscore" => \$_underscore,
         "varargs" => \$varargs,
     ] + \$this->env->getGlobals();
 
@@ -71,12 +74,13 @@ EOF
 
         yield 'with use_yield = false' => [$node, <<<EOF
 // line 1
-public function macro_foo(\$foo = null, \$bar = "Foo", ...\$varargs): string|Markup
+public function macro_foo(\$foo = null, \$bar = "Foo", \$_underscore = null, ...\$varargs): string|Markup
 {
     \$macros = \$this->macros;
     \$context = [
         "foo" => \$foo,
         "bar" => \$bar,
+        "_underscore" => \$_underscore,
         "varargs" => \$varargs,
     ] + \$this->env->getGlobals();