From 8e279aee87f9f136f266c42f4d677bfee822d8c5 Mon Sep 17 00:00:00 2001 From: shakhobiddin Date: Fri, 28 Mar 2025 19:50:20 +0500 Subject: [PATCH 1/4] Add support for the optional chaining operator ?. --- .../Infix/OptionalChainExpressionParser.php | 107 +++++++++++ src/Node/Expression/GetAttrExpression.php | 169 +++++++++++++++++- tests/Extension/OptionalChainingTest.php | 14 ++ .../optional_chaining/array_access.test | 12 ++ .../extensions/optional_chaining/basic.test | 8 + .../optional_chaining/method_call.test | 20 +++ .../extensions/optional_chaining/mixed.test | 12 ++ .../optional_chaining/undefined.test | 7 + .../optional_chaining/with_values.test | 9 + 9 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 src/ExpressionParser/Infix/OptionalChainExpressionParser.php create mode 100644 tests/Extension/OptionalChainingTest.php create mode 100644 tests/Fixtures/extensions/optional_chaining/array_access.test create mode 100644 tests/Fixtures/extensions/optional_chaining/basic.test create mode 100644 tests/Fixtures/extensions/optional_chaining/method_call.test create mode 100644 tests/Fixtures/extensions/optional_chaining/mixed.test create mode 100644 tests/Fixtures/extensions/optional_chaining/undefined.test create mode 100644 tests/Fixtures/extensions/optional_chaining/with_values.test diff --git a/src/ExpressionParser/Infix/OptionalChainExpressionParser.php b/src/ExpressionParser/Infix/OptionalChainExpressionParser.php new file mode 100644 index 00000000000..0704e68ce7b --- /dev/null +++ b/src/ExpressionParser/Infix/OptionalChainExpressionParser.php @@ -0,0 +1,107 @@ +getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + $type = Template::ANY_CALL; + $isOptionalChain = true; + + // Проверка, является ли левое выражение переменной + $isVariable = $expr instanceof NameExpression; + + // Для обработки квадратных скобок + if ($stream->test(Token::OPERATOR_TYPE, '[')) { + $token = $stream->next(); + $attribute = $parser->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + $type = Template::ARRAY_CALL; + } elseif ($stream->nextIf(Token::OPERATOR_TYPE, '(')) { + $attribute = $parser->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + } else { + $token = $stream->next(); + if ( + $token->test(Token::NAME_TYPE) + || $token->test(Token::NUMBER_TYPE) + || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) + ) { + $attribute = new ConstantExpression($token->getValue(), $token->getLine()); + } else { + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); + } + } + + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $type = Template::METHOD_CALL; + $arguments = $this->parseCallableArguments($parser, $token->getLine()); + } + + if ( + $expr instanceof NameExpression + && ( + null !== $parser->getImportedSymbol('template', $expr->getAttribute('name')) + || '_self' === $expr->getAttribute('name') && $attribute instanceof ConstantExpression + ) + ) { + // Для макросов не используем optional chaining + return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine()); + } + + // Создаем специальный флаг для проверки существования переменной + if ($isVariable && $expr instanceof NameExpression) { + $expr->setAttribute('optional_chain', true); + } + + return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $isOptionalChain); + } + + public function getName(): string + { + return '?.'; + } + + public function getDescription(): string + { + return 'Optional chaining to safely access an attribute on a potentially null variable'; + } + + public function getPrecedence(): int + { + return 512; // Тот же приоритет, что и у оператора "." + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} \ No newline at end of file diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 781c8af3868..8b40f53ba5e 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -25,7 +25,7 @@ class GetAttrExpression extends AbstractExpression implements SupportDefinedTest /** * @param ArrayExpression|NameExpression|null $arguments */ - public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno) + public function __construct(AbstractExpression $node, AbstractExpression $attribute, ?AbstractExpression $arguments, string $type, int $lineno, bool $isOptionalChain = false) { $nodes = ['node' => $node, 'attribute' => $attribute]; if (null !== $arguments) { @@ -36,7 +36,7 @@ public function __construct(AbstractExpression $node, AbstractExpression $attrib trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); } - parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => true], $lineno); + parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => true, 'is_optional_chain' => $isOptionalChain], $lineno); } public function enableDefinedTest(): void @@ -50,6 +50,106 @@ public function compile(Compiler $compiler): void $env = $compiler->getEnvironment(); $arrayAccessSandbox = false; + // Если используется optional chaining + if ($this->getAttribute('is_optional_chain')) { + $var = '$'.$compiler->getVarName(); + + // Проверяем, является ли node NameExpression с флагом optional_chain + $isOptionalName = $this->getNode('node') instanceof NameExpression && + $this->getNode('node')->getAttribute('optional_chain', false); + + if ($isOptionalName) { + // Безопасный доступ к переменным контекста без выброса исключения + $compiler + ->raw('(array_key_exists(') + ->string($this->getNode('node')->getAttribute('name')) + ->raw(', $context) ? '); + + $compiler + ->raw('(null !== (') + ->raw($var) + ->raw(' = $context[') + ->string($this->getNode('node')->getAttribute('name')) + ->raw(']) ? '); + } else { + // Обычная проверка null для нормальных выражений + $compiler + ->raw('(null !== (') + ->raw($var) + ->raw(' = '); + + // Обращение к полю через ->subcompile может вызвать исключение + $this->getNode('node')->setAttribute('ignore_strict_check', true); + $compiler->subcompile($this->getNode('node')); + $compiler->raw(') ? '); + } + + // Генерируем код для доступа к атрибуту в зависимости от типа + if ($this->getAttribute('type') === Template::METHOD_CALL) { + // Вызов метода + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); + $compiler + ->raw($var) + ->raw(', ') + ->subcompile($this->getNode('attribute')); + + if ($this->hasNode('arguments')) { + $compiler->raw(', ')->subcompile($this->getNode('arguments')); + } else { + $compiler->raw(', []'); + } + + $compiler->raw(', ') + ->repr($this->getAttribute('type')) + ->raw(', ')->repr($this->definedTest ?? false) + ->raw(', ')->repr(true) // ignore_strict_check = true для optional chaining + ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) + ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) + ->raw(')'); + } elseif ($this->getAttribute('type') === Template::ARRAY_CALL) { + // Доступ к массиву + $compiler->raw('(is_array(') + ->raw($var) + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess ? (') + ->raw($var) + ->raw('[') + ->subcompile($this->getNode('attribute')) + ->raw('] ?? null) : null)'); + } else { + // Доступ к свойству + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); + $compiler + ->raw($var) + ->raw(', ') + ->subcompile($this->getNode('attribute')); + + if ($this->hasNode('arguments')) { + $compiler->raw(', ')->subcompile($this->getNode('arguments')); + } else { + $compiler->raw(', []'); + } + + $compiler->raw(', ') + ->repr($this->getAttribute('type')) + ->raw(', ')->repr($this->definedTest ?? false) + ->raw(', ')->repr(true) // ignore_strict_check = true для optional chaining + ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) + ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) + ->raw(')'); + } + + if ($isOptionalName) { + $compiler->raw(' : null) : null)'); + } else { + $compiler->raw(' : null)'); + } + + return; + } + + // Оригинальный код для обычного доступа к атрибутам // optimize array calls if ( $this->getAttribute('optimizable') @@ -113,7 +213,7 @@ public function compile(Compiler $compiler): void $compiler->raw(', ') ->repr($this->getAttribute('type')) - ->raw(', ')->repr($this->definedTest) + ->raw(', ')->repr($this->definedTest ?? false) ->raw(', ')->repr($this->getAttribute('ignore_strict_check')) ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) @@ -124,7 +224,6 @@ public function compile(Compiler $compiler): void $compiler->raw(')'); } } - private function changeIgnoreStrictCheck(GetAttrExpression $node): void { $node->setAttribute('optimizable', false); @@ -134,4 +233,66 @@ private function changeIgnoreStrictCheck(GetAttrExpression $node): void $this->changeIgnoreStrictCheck($node->getNode('node')); } } + private function compileGetAttr(Compiler $compiler, string $varName): void + { + $env = $compiler->getEnvironment(); + + if (\PHP_VERSION_ID >= 80000) { + $compiler->raw($varName); + if ($this->getAttribute('type') === Template::METHOD_CALL) { + $compiler->raw('?->'); + if ($this->getNode('attribute') instanceof ConstantExpression) { + $compiler->raw($this->getNode('attribute')->getAttribute('value')); + } else { + $compiler->raw('{'); + $compiler->subcompile($this->getNode('attribute')); + $compiler->raw('}'); + } + + $compiler->raw('('); + + if ($this->hasNode('arguments')) { + $first = true; + foreach ($this->getNode('arguments') as $argNode) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($argNode); + $first = false; + } + } + + $compiler->raw(')'); + } else { + $compiler->raw('?->'); + + if ($this->getNode('attribute') instanceof ConstantExpression) { + $compiler->raw($this->getNode('attribute')->getAttribute('value')); + } else { + $compiler->raw('{'); + $compiler->subcompile($this->getNode('attribute')); + $compiler->raw('}'); + } + } + } else { + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); + $compiler + ->raw($varName) + ->raw(', ') + ->subcompile($this->getNode('attribute')); + + if ($this->hasNode('arguments')) { + $compiler->raw(', ')->subcompile($this->getNode('arguments')); + } else { + $compiler->raw(', []'); + } + $compiler->raw(', ') + ->repr($this->getAttribute('type')) + ->raw(', ')->repr($this->definedTest ?? false) + ->raw(', ')->repr($this->getAttribute('ignore_strict_check')) + ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) + ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) + ->raw(')'); + } + } } diff --git a/tests/Extension/OptionalChainingTest.php b/tests/Extension/OptionalChainingTest.php new file mode 100644 index 00000000000..f6f14cba5a9 --- /dev/null +++ b/tests/Extension/OptionalChainingTest.php @@ -0,0 +1,14 @@ + ['bar' => 'value', 0 => 'zero'] +] +--EXPECT-- +value +zero \ No newline at end of file diff --git a/tests/Fixtures/extensions/optional_chaining/basic.test b/tests/Fixtures/extensions/optional_chaining/basic.test new file mode 100644 index 00000000000..ce545cde9a5 --- /dev/null +++ b/tests/Fixtures/extensions/optional_chaining/basic.test @@ -0,0 +1,8 @@ +--TEST-- +Basic optional chaining +--TEMPLATE-- +{{ foo?.bar }} +{{ foo?.bar?.baz }} +--DATA-- +return ['foo' => null] +--EXPECT-- \ No newline at end of file diff --git a/tests/Fixtures/extensions/optional_chaining/method_call.test b/tests/Fixtures/extensions/optional_chaining/method_call.test new file mode 100644 index 00000000000..06f5fc71a3f --- /dev/null +++ b/tests/Fixtures/extensions/optional_chaining/method_call.test @@ -0,0 +1,20 @@ +--TEST-- +Optional chaining with method calls +--TEMPLATE-- +{{ foo?.getBar() }} +{{ foo_with_nested?.bar?.getBaz() }} +--DATA-- +class TestObject { + public function getBar() { return 'bar_value'; } +} +class NestedObject { + public function getBaz() { return 'baz_value'; } +} +return [ + 'foo' => new TestObject(), + 'null_foo' => null, + 'foo_with_nested' => ['bar' => new NestedObject()] +] +--EXPECT-- +bar_value +baz_value \ No newline at end of file diff --git a/tests/Fixtures/extensions/optional_chaining/mixed.test b/tests/Fixtures/extensions/optional_chaining/mixed.test new file mode 100644 index 00000000000..fbb01906de1 --- /dev/null +++ b/tests/Fixtures/extensions/optional_chaining/mixed.test @@ -0,0 +1,12 @@ +--TEST-- +Mixed optional and normal chaining +--TEMPLATE-- +{{ foo?.bar.baz }} +{{ foo.bar?.baz }} +--DATA-- +return [ + 'foo' => ['bar' => ['baz' => 'value']], +] +--EXPECT-- +value +value \ No newline at end of file diff --git a/tests/Fixtures/extensions/optional_chaining/undefined.test b/tests/Fixtures/extensions/optional_chaining/undefined.test new file mode 100644 index 00000000000..af7ab0512f2 --- /dev/null +++ b/tests/Fixtures/extensions/optional_chaining/undefined.test @@ -0,0 +1,7 @@ +--TEST-- +Optional chaining with undefined variables +--TEMPLATE-- +{{ undefinedVar?.bar }} +--DATA-- +return [] +--EXPECT-- \ No newline at end of file diff --git a/tests/Fixtures/extensions/optional_chaining/with_values.test b/tests/Fixtures/extensions/optional_chaining/with_values.test new file mode 100644 index 00000000000..90fb0f3e92e --- /dev/null +++ b/tests/Fixtures/extensions/optional_chaining/with_values.test @@ -0,0 +1,9 @@ +--TEST-- +Optional chaining with values +--TEMPLATE-- +{{ foo?.bar }} +{{ foo?.bar?.baz }} +--DATA-- +return ['foo' => ['bar' => 'value', 'baz' => ['qux' => 'nested']]] +--EXPECT-- +value \ No newline at end of file From 1c94999fac2f2a63b8fb5ca1bf43b2417e726e00 Mon Sep 17 00:00:00 2001 From: shakhobiddin Date: Fri, 28 Mar 2025 19:51:52 +0500 Subject: [PATCH 2/4] Add support for the optional chaining operator ?. --- src/Extension/CoreExtension.php | 2 ++ src/Lexer.php | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index f7e4250ae50..808012dbe13 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -24,6 +24,7 @@ use Twig\ExpressionParser\Infix\FunctionExpressionParser; use Twig\ExpressionParser\Infix\IsExpressionParser; use Twig\ExpressionParser\Infix\IsNotExpressionParser; +use Twig\ExpressionParser\Infix\OptionalChainExpressionParser; use Twig\ExpressionParser\Infix\SquareBracketExpressionParser; use Twig\ExpressionParser\InfixAssociativity; use Twig\ExpressionParser\PrecedenceChange; @@ -391,6 +392,7 @@ public function getExpressionParsers(): array // all literals new LiteralExpressionParser(), + new OptionalChainExpressionParser(), ]; } diff --git a/src/Lexer.php b/src/Lexer.php index 027771accb9..2ab39afeef1 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -61,6 +61,9 @@ class Lexer public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; public const PUNCTUATION = '()[]{}?:.,|'; + const TOKEN_OPTIONAL_CHAIN = 17; + private const REGEX_OPTIONAL_CHAIN = '/\?\./A'; + private const SPECIAL_CHARS = [ 'f' => "\f", 'n' => "\n", From d01cc7ca47ca36f8264dc3abbf36443d20d3936a Mon Sep 17 00:00:00 2001 From: shakhobiddin Date: Fri, 28 Mar 2025 20:08:19 +0500 Subject: [PATCH 3/4] fix mini changes. remove comments and unused codes --- .../Infix/OptionalChainExpressionParser.php | 2 +- src/Lexer.php | 2 - src/Node/Expression/GetAttrExpression.php | 73 ------------------- 3 files changed, 1 insertion(+), 76 deletions(-) diff --git a/src/ExpressionParser/Infix/OptionalChainExpressionParser.php b/src/ExpressionParser/Infix/OptionalChainExpressionParser.php index 0704e68ce7b..e501ad2063c 100644 --- a/src/ExpressionParser/Infix/OptionalChainExpressionParser.php +++ b/src/ExpressionParser/Infix/OptionalChainExpressionParser.php @@ -97,7 +97,7 @@ public function getDescription(): string public function getPrecedence(): int { - return 512; // Тот же приоритет, что и у оператора "." + return 512; } public function getAssociativity(): InfixAssociativity diff --git a/src/Lexer.php b/src/Lexer.php index 2ab39afeef1..7438e2c3ace 100644 --- a/src/Lexer.php +++ b/src/Lexer.php @@ -61,8 +61,6 @@ class Lexer public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; public const PUNCTUATION = '()[]{}?:.,|'; - const TOKEN_OPTIONAL_CHAIN = 17; - private const REGEX_OPTIONAL_CHAIN = '/\?\./A'; private const SPECIAL_CHARS = [ 'f' => "\f", diff --git a/src/Node/Expression/GetAttrExpression.php b/src/Node/Expression/GetAttrExpression.php index 8b40f53ba5e..efffe400b90 100644 --- a/src/Node/Expression/GetAttrExpression.php +++ b/src/Node/Expression/GetAttrExpression.php @@ -50,16 +50,13 @@ public function compile(Compiler $compiler): void $env = $compiler->getEnvironment(); $arrayAccessSandbox = false; - // Если используется optional chaining if ($this->getAttribute('is_optional_chain')) { $var = '$'.$compiler->getVarName(); - // Проверяем, является ли node NameExpression с флагом optional_chain $isOptionalName = $this->getNode('node') instanceof NameExpression && $this->getNode('node')->getAttribute('optional_chain', false); if ($isOptionalName) { - // Безопасный доступ к переменным контекста без выброса исключения $compiler ->raw('(array_key_exists(') ->string($this->getNode('node')->getAttribute('name')) @@ -72,21 +69,17 @@ public function compile(Compiler $compiler): void ->string($this->getNode('node')->getAttribute('name')) ->raw(']) ? '); } else { - // Обычная проверка null для нормальных выражений $compiler ->raw('(null !== (') ->raw($var) ->raw(' = '); - // Обращение к полю через ->subcompile может вызвать исключение $this->getNode('node')->setAttribute('ignore_strict_check', true); $compiler->subcompile($this->getNode('node')); $compiler->raw(') ? '); } - // Генерируем код для доступа к атрибуту в зависимости от типа if ($this->getAttribute('type') === Template::METHOD_CALL) { - // Вызов метода $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); $compiler ->raw($var) @@ -107,7 +100,6 @@ public function compile(Compiler $compiler): void ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) ->raw(')'); } elseif ($this->getAttribute('type') === Template::ARRAY_CALL) { - // Доступ к массиву $compiler->raw('(is_array(') ->raw($var) ->raw(') || ') @@ -118,7 +110,6 @@ public function compile(Compiler $compiler): void ->subcompile($this->getNode('attribute')) ->raw('] ?? null) : null)'); } else { - // Доступ к свойству $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); $compiler ->raw($var) @@ -149,8 +140,6 @@ public function compile(Compiler $compiler): void return; } - // Оригинальный код для обычного доступа к атрибутам - // optimize array calls if ( $this->getAttribute('optimizable') && (!$env->isStrictVariables() || $this->getAttribute('ignore_strict_check')) @@ -233,66 +222,4 @@ private function changeIgnoreStrictCheck(GetAttrExpression $node): void $this->changeIgnoreStrictCheck($node->getNode('node')); } } - private function compileGetAttr(Compiler $compiler, string $varName): void - { - $env = $compiler->getEnvironment(); - - if (\PHP_VERSION_ID >= 80000) { - $compiler->raw($varName); - if ($this->getAttribute('type') === Template::METHOD_CALL) { - $compiler->raw('?->'); - if ($this->getNode('attribute') instanceof ConstantExpression) { - $compiler->raw($this->getNode('attribute')->getAttribute('value')); - } else { - $compiler->raw('{'); - $compiler->subcompile($this->getNode('attribute')); - $compiler->raw('}'); - } - - $compiler->raw('('); - - if ($this->hasNode('arguments')) { - $first = true; - foreach ($this->getNode('arguments') as $argNode) { - if (!$first) { - $compiler->raw(', '); - } - $compiler->subcompile($argNode); - $first = false; - } - } - - $compiler->raw(')'); - } else { - $compiler->raw('?->'); - - if ($this->getNode('attribute') instanceof ConstantExpression) { - $compiler->raw($this->getNode('attribute')->getAttribute('value')); - } else { - $compiler->raw('{'); - $compiler->subcompile($this->getNode('attribute')); - $compiler->raw('}'); - } - } - } else { - $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); - $compiler - ->raw($varName) - ->raw(', ') - ->subcompile($this->getNode('attribute')); - - if ($this->hasNode('arguments')) { - $compiler->raw(', ')->subcompile($this->getNode('arguments')); - } else { - $compiler->raw(', []'); - } - $compiler->raw(', ') - ->repr($this->getAttribute('type')) - ->raw(', ')->repr($this->definedTest ?? false) - ->raw(', ')->repr($this->getAttribute('ignore_strict_check')) - ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) - ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) - ->raw(')'); - } - } } From f415198d535cf779c24934704c8f19ea41bc6ffc Mon Sep 17 00:00:00 2001 From: Shakhobiddin <38453814+shokhaa@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:09:53 +0500 Subject: [PATCH 4/4] Update OptionalChainExpressionParser.php remove comments --- .../Infix/OptionalChainExpressionParser.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/ExpressionParser/Infix/OptionalChainExpressionParser.php b/src/ExpressionParser/Infix/OptionalChainExpressionParser.php index e501ad2063c..244833f5c2e 100644 --- a/src/ExpressionParser/Infix/OptionalChainExpressionParser.php +++ b/src/ExpressionParser/Infix/OptionalChainExpressionParser.php @@ -35,11 +35,7 @@ public function parse(Parser $parser, AbstractExpression $expr, Token $token): A $arguments = new ArrayExpression([], $lineno); $type = Template::ANY_CALL; $isOptionalChain = true; - - // Проверка, является ли левое выражение переменной $isVariable = $expr instanceof NameExpression; - - // Для обработки квадратных скобок if ($stream->test(Token::OPERATOR_TYPE, '[')) { $token = $stream->next(); $attribute = $parser->parseExpression(); @@ -73,11 +69,11 @@ public function parse(Parser $parser, AbstractExpression $expr, Token $token): A || '_self' === $expr->getAttribute('name') && $attribute instanceof ConstantExpression ) ) { - // Для макросов не используем optional chaining + return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine()); } - // Создаем специальный флаг для проверки существования переменной + if ($isVariable && $expr instanceof NameExpression) { $expr->setAttribute('optional_chain', true); } @@ -104,4 +100,4 @@ public function getAssociativity(): InfixAssociativity { return InfixAssociativity::Left; } -} \ No newline at end of file +}