Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions extra/intl-extra/Tests/Fixtures/script_names.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"script_names" function
--TEMPLATE--
{{ script_names('UNKNOWN')|length }}
{{ script_names()|length }}
{{ script_names('fr')|length }}
{{ script_names()|length > 200 ? 'more than 200' : 'less than 200' }}
{{ script_names('fr')|length > 200 ? 'more than 200' : 'less than 200' }}
{{ script_names()['Marc'] }}
{{ script_names('fr')['Marc'] }}
--DATA--
return [];
--EXPECT--
0
208
208
more than 200
more than 200
Marchen
Marchen
10 changes: 8 additions & 2 deletions src/ExpressionParser/Infix/DotExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ final class DotExpressionParser extends AbstractExpressionParser implements Infi

public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression
{
$nullSafe = '?.' === $token->getValue();
$stream = $parser->getStream();
$token = $stream->getCurrent();
$lineno = $token->getLine();
Expand All @@ -55,7 +56,7 @@ public function parse(Parser $parser, AbstractExpression $expr, Token $token): A
) {
$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());
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type "%s".', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext());
}
}

Expand All @@ -74,14 +75,19 @@ public function parse(Parser $parser, AbstractExpression $expr, Token $token): A
return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $expr->getTemplateLine());
}

return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno);
return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe);
}

public function getName(): string
{
return '.';
}

public function getAliases(): array
{
return ['?.'];
}

public function getDescription(): string
{
return 'Get an attribute on a variable';
Expand Down
30 changes: 24 additions & 6 deletions src/Node/Expression/GetAttrExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* This file is part of Twig.
*
* (c) Fabien Potencier
* (c) Armin Ronacher
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
Expand All @@ -25,7 +24,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 $nullSafe = false)
{
$nodes = ['node' => $node, 'attribute' => $attribute];
if (null !== $arguments) {
Expand All @@ -36,7 +35,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' => !$nullSafe, 'null_safe' => $nullSafe], $lineno);
}

public function enableDefinedTest(): void
Expand All @@ -49,6 +48,8 @@ public function compile(Compiler $compiler): void
{
$env = $compiler->getEnvironment();
$arrayAccessSandbox = false;
$nullSafe = $this->getAttribute('null_safe');
$objectVar = null;

// optimize array calls
if (
Expand Down Expand Up @@ -93,14 +94,27 @@ public function compile(Compiler $compiler): void
;
}

$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');

if ($this->getAttribute('ignore_strict_check')) {
$this->getNode('node')->setAttribute('ignore_strict_check', true);
}

if ($nullSafe) {
$objectVar = '$'.$compiler->getVarName();
$compiler
->raw('((null === ('.$objectVar.' = ')
->subcompile($this->getNode('node'))
->raw(')) ? null : ');
}

$compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ');

if ($nullSafe) {
$compiler->raw($objectVar);
} else {
$compiler->subcompile($this->getNode('node'));
}

$compiler
->subcompile($this->getNode('node'))
->raw(', ')
->subcompile($this->getNode('attribute'))
;
Expand All @@ -123,6 +137,10 @@ public function compile(Compiler $compiler): void
if ($arrayAccessSandbox) {
$compiler->raw(')');
}

if ($nullSafe) {
$compiler->raw(')');
}
}

private function changeIgnoreStrictCheck(self $node): void
Expand Down
56 changes: 56 additions & 0 deletions tests/ExpressionParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,62 @@ public static function getTestsForString()
];
}

/**
* @dataProvider getTestsForNullSafeOperator
*/
public function testNullSafeOperator($template, $data, $expected)
{
$env = new Environment(new ArrayLoader(['template' => $template]));

$this->assertSame($expected, $env->render('template', $data));
}

public static function getTestsForNullSafeOperator()
{
return [
[
'{{ foo?.bar }}',
['foo' => (object) ['bar' => 'baz']],
'baz',
],
[
'{{ foo?.bar }}',
['foo' => null],
'',
],
[
'{{ foo?.bar?.baz }}',
['foo' => (object) ['bar' => (object) ['baz' => 'qux']]],
'qux',
],
[
'{{ foo?.bar?.baz }}',
['foo' => (object) ['bar' => null]],
'',
],
[
'{{ foo?.bar?.baz }}',
['foo' => null],
'',
],
[
'{{ foo?.bar?.baz ?? "qux" }}',
['foo' => null],
'qux',
],
[
'{{ foo?.bar ?? "qux" }}',
['foo' => (object) ['bar' => 0]],
'0',
],
[
'{{ foo?.bar ?? "qux" }}',
['foo' => (object) ['bar' => false]],
'',
],
];
}

public function testMacroDefinitionDoesNotSupportNonNameVariableName()
{
$env = new Environment(new ArrayLoader(), ['cache' => false, 'autoescape' => false]);
Expand Down
2 changes: 1 addition & 1 deletion tests/Fixtures/expressions/dot_as_concatenation.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ Twig does not support using . for concatenation
--DATA--
return []
--EXCEPTION--
Twig\Error\SyntaxError: Expected name or number, got value "b" of type string in "index.twig" at line 2.
Twig\Error\SyntaxError: Expected name or number, got value "b" of type "string" in "index.twig" at line 2.
5 changes: 5 additions & 0 deletions tests/Node/Expression/GetAttrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public function testConstructor()
$this->assertEquals($attr, $node->getNode('attribute'));
$this->assertEquals($args, $node->getNode('arguments'));
$this->assertEquals(Template::ARRAY_CALL, $node->getAttribute('type'));
$this->assertFalse($node->getAttribute('null_safe'));
}

public static function provideTests(): iterable
Expand All @@ -51,9 +52,13 @@ public static function provideTests(): iterable
$expr = new ContextVariable('foo', 1);
$attr = new ConstantExpression('bar', 1);
$args = new ArrayExpression([], 1);

$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1);
$tests[] = [$node, \sprintf('%s%s, "bar", [], "any", false, false, false, 1)', self::createAttributeGetter(), self::createVariableGetter('foo', 1))];

$node = new GetAttrExpression($expr, $attr, $args, Template::ANY_CALL, 1, true);
$tests[] = [$node, '((null === ($_v%s = // line 1'."\n".'($context["foo"] ?? null))) ? null : '.self::createAttributeGetter().'$_v%s, "bar", [], "any", false, false, false, 1))', null, true];

$node = new GetAttrExpression($expr, $attr, $args, Template::ARRAY_CALL, 1);
$tests[] = [$node, '(($_v%s = // line 1'."\n".
'($context["foo"] ?? null)) && is_array($_v%s) || $_v%s instanceof ArrayAccess ? ($_v%s["bar"] ?? null) : null)', null, true, ];
Expand Down