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
48 changes: 48 additions & 0 deletions lib/twig/expression_parser/infix/assignment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module Twig
module ExpressionParser
module Infix
class Assignment < InfixExpressionParser
def parse(parser, left, token)
right = parser.parse_expression(precedence)

case left
when Node::Expression::Array
return Node::Expression::Binary::SequenceDestructuringSetBinary.new(left, right, token.lineno)
when Node::Expression::Hash
return Node::Expression::Binary::ObjectDestructuringSetBinary.new(left, right, token.lineno)
when Node::Expression::Variable::Context
return Node::Expression::Binary::SetBinary.new(left, right, token.lineno)
end

raise Error::Syntax.new(
"Cannot assign to \"#{left.class}\", only variables can be assigned.",
token.lineno,
parser.stream.source
)
end

def name
'='
end

def description
'Assignment operator'
end

def precedence
0
end

def associativity
RIGHT
end

def aliases
[]
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/twig/expression_parser/infix/dot.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def parse(parser, left, _token)
attribute = Node::Expression::Constant.new(token.value, token.lineno)
else
raise Error::Syntax.new(
"Expected name or number, got value \"#{token.value}\" of type #{token.type}.",
"Expected name or number, got value \"#{token.value}\" of type \"#{token.to_english}\".",
token.lineno,
stream.source
)
Expand Down
7 changes: 5 additions & 2 deletions lib/twig/expression_parser/infix/parses_arguments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ def parse_named_arguments(parser, parse_open_parenthesis: true)
end

name = nil
if (token = stream.next_if(Token::OPERATOR_TYPE, '=')) ||
(token = stream.next_if(Token::PUNCTUATION_TYPE, ':'))
if value.is_a?(Node::Expression::Binary::SetBinary)
name = value.nodes[:left].attributes[:name]
value = value.nodes[:right]
elsif (token = stream.next_if(Token::OPERATOR_TYPE, '=')) ||
(token = stream.next_if(Token::PUNCTUATION_TYPE, ':'))
# Allow quoted kwargs - form_with("data-turbo-stream": true)
if value.is_a?(Node::Expression::Constant) && value.attributes[:value].is_a?(String)
name = value.attributes[:value]
Expand Down
12 changes: 3 additions & 9 deletions lib/twig/expression_parser/prefix/literal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ def parse(parser, token)
return Node::Expression::Variable::Context.new(token.value, token.lineno)
end

if token.value == '=' && %w[== !=].include?(parser.stream.look(-1).value)
raise Error::Syntax.new(
"Unexpected operator of value \"#{token.value}\". Did you try to use \"===\" or \"!==\" for " \
'strict comparison? Use "is same as(value)" instead.',
token.lineno,
parser.stream.source
)
end
end

raise Error::Syntax.new(
Expand Down Expand Up @@ -155,7 +147,9 @@ def parse_sequence_expression(parser)

first = false

if stream.next_if(Token::OPERATOR_TYPE, '...')
if stream.test(Token::PUNCTUATION_TYPE, ',') || stream.test(Token::PUNCTUATION_TYPE, ']')
node.add_element(Node::Expression::EmptySlot.new(stream.current.lineno))
elsif stream.next_if(Token::OPERATOR_TYPE, '...')
expr = parser.parse_expression
node.add_element(Node::Expression::Unary::ArraySpread.new(expr, expr.lineno))
else
Expand Down
5 changes: 3 additions & 2 deletions lib/twig/expression_parser/prefix/unary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ module Prefix
class Unary < PrefixExpressionParser
attr_reader :aliases, :precedence, :name

def initialize(node_class, name, precedence, description: nil, aliases: [])
def initialize(node_class, name, precedence, description: nil, aliases: [], operand_precedence: nil)
super()

@node_class = node_class
@name = name
@precedence = precedence
@operand_precedence = operand_precedence
@description = description
@aliases = aliases
end

def parse(parser, token)
@node_class.new(parser.parse_expression(precedence), token.lineno)
@node_class.new(parser.parse_expression(@operand_precedence || precedence), token.lineno)
end

def description
Expand Down
7 changes: 6 additions & 1 deletion lib/twig/extension/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def expression_parsers
[
# Unary operators
unary.new(Node::Expression::Unary::Not, 'not', 70),
unary.new(Node::Expression::Unary::Spread, '...', 512, description: 'Spread Operator'),
unary.new(Node::Expression::Unary::Spread, '...', 512, description: 'Spread Operator', operand_precedence: 0),
unary.new(Node::Expression::Unary::Neg, '-', 500),
unary.new(Node::Expression::Unary::Pos, '+', 500),

Expand All @@ -46,6 +46,8 @@ def expression_parsers
binary.new(Node::Expression::Binary::BitwiseAnd, 'b-and', 16),
binary.new(Node::Expression::Binary::Equal, '==', 20),
binary.new(Node::Expression::Binary::NotEqual, '!=', 20),
binary.new(Node::Expression::Binary::SameAs, '===', 20),
binary.new(Node::Expression::Binary::NotSameAs, '!==', 20),
binary.new(Node::Expression::Binary::Spaceship, '<=>', 20),
binary.new(Node::Expression::Binary::Less, '<', 20),
binary.new(Node::Expression::Binary::Greater, '>', 20),
Expand Down Expand Up @@ -87,6 +89,9 @@ def expression_parsers
# Arrow function
ExpressionParser::Infix::Arrow.new,

# Assignment operator
ExpressionParser::Infix::Assignment.new,

# All literals
ExpressionParser::Prefix::Literal.new,
]
Expand Down
2 changes: 1 addition & 1 deletion lib/twig/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ def lex_raw_data_regex
def operator_regex
return @operator_regex if defined?(@operator_regex)

expression_parsers = ['=']
expression_parsers = []

environment.expression_parsers.each do |expression_parser|
expression_parsers.concat([expression_parser.name], expression_parser.aliases)
Expand Down
8 changes: 8 additions & 0 deletions lib/twig/node/expression/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ def compile(compiler)
return compiler.repr(true)
end

# Empty expressions are only valid in destructuring contexts
values.each do |value|
if value.is_a?(Expression::EmptySlot)
raise Error::Syntax.new('Empty array elements are only allowed in destructuring assignments.',
value.lineno)
end
end

compiler.
raw('[').
indent
Expand Down
2 changes: 2 additions & 0 deletions lib/twig/node/expression/binary/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def operator(compiler)
Div: '/',
Mod: '%',
Power: '**',
SameAs: '==',
NotSameAs: '!=',
}.freeze

# Lots of simple operator classes can just be generated dynamically
Expand Down
61 changes: 61 additions & 0 deletions lib/twig/node/expression/binary/object_destructuring_set_binary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module Twig
module Node
module Expression
module Binary
class ObjectDestructuringSetBinary < Binary::Base
def initialize(left, right, lineno)
@mappings = []

left.key_value_pairs.each do |key, value|
unless value.is_a?(Variable::Context)
raise Error::Syntax.new(
"Cannot assign to \"#{value.class}\", only variables can be assigned in " \
'object/mapping destructuring.',
lineno
)
end

@mappings << {
property: key.attributes[:value],
variable: value.attributes[:name],
}
end

super
end

def compile(compiler)
compiler.add_debug_info(self)

@mappings.each_with_index do |mapping, i|
compiler.raw(', ') if i.positive?
compiler.raw('context[').string(mapping[:variable]).raw(']')
end

compiler.raw(' = ')

@mappings.each_with_index do |mapping, i|
compiler.raw(', ') if i.positive?
compiler.
raw('::Twig::Extension::Core.get_attribute(env, source_context, ').
subcompile(nodes[:right]).
raw(', ').
repr(mapping[:property]).
raw(', ').
repr(Template::ANY_CALL).
raw(', lineno: ').
repr(nodes[:right].lineno).
raw(')')
end
end

def operator(compiler)
compiler.raw('=')
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

module Twig
module Node
module Expression
module Binary
class SequenceDestructuringSetBinary < Binary::Base
def initialize(left, right, lineno)
@variables = []

left.each_value do |value|
if value.is_a?(Expression::EmptySlot)
@variables << nil
elsif value.is_a?(Variable::Context)
@variables << value.attributes[:name]
else
raise Error::Syntax.new(
"Cannot assign to \"#{value.class}\", only variables can be assigned in destructuring.",
lineno
)
end
end

super
end

def compile(compiler)
compiler.add_debug_info(self)

@variables.each_with_index do |name, i|
compiler.raw(', ') if i.positive?
if name
compiler.raw('context[').string(name).raw(']')
else
compiler.raw('_')
end
end

compiler.raw(' = *(').subcompile(nodes[:right]).raw(' + ::Array.new(')
compiler.repr(@variables.length).raw('))')
end

def operator(compiler)
compiler.raw('=')
end
end
end
end
end
end
22 changes: 22 additions & 0 deletions lib/twig/node/expression/binary/set_binary.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Twig
module Node
module Expression
module Binary
class SetBinary < Binary::Base
def initialize(left, right, lineno)
name = left.attributes[:name]
left = Variable::AssignContext.new(name, left.lineno)

super
end

def operator(compiler)
compiler.raw('=')
end
end
end
end
end
end
17 changes: 17 additions & 0 deletions lib/twig/node/expression/empty.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

module Twig
module Node
module Expression
class EmptySlot < Expression::Base
def initialize(lineno)
super({}, {}, lineno)
end

def compile(compiler)
# Empty slot compiles to nothing
end
end
end
end
end
5 changes: 5 additions & 0 deletions lib/twig/node_visitor/safe_analysis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ def safe(node)

if bucket[:value].include?(:html_attr)
bucket[:value] << :html
bucket[:value] << :html_attr_relaxed
end

if bucket[:value].include?(:html_attr_relaxed)
bucket[:value] << :html
end

return bucket[:value]
Expand Down
Loading