Skip to content

Commit

Permalink
+ builder: emit implicit hash passed to a method call as kwargs (#769)
Browse files Browse the repository at this point in the history
  • Loading branch information
iliabylich authored Nov 29, 2020
1 parent d366f34 commit c7a6b3f
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 20 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ below for explanation of `emit_*` calls):
Parser::Builders::Default.emit_index = true
Parser::Builders::Default.emit_arg_inside_procarg0 = true
Parser::Builders::Default.emit_forward_arg = true
Parser::Builders::Default.emit_kwargs = true

Parse a chunk of code:

Expand Down
17 changes: 17 additions & 0 deletions doc/AST_FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,23 @@ Format:
~~~~~~~~~~~~~~~~ expression
~~~

### Kwargs

Starting from Ruby 2.7 only implicit hash literals (that are not wrapped into `{ .. }`) are passed as keyword arguments.
Explicit hash literals are passed as positional arguments.
This is reflected in AST as `kwargs` node that is emitted only for implicit
hash literals and only if `emit_kwargs` compatibility flag is enabled.

Note that it can be a part of `send`, `csend`, `index` and `yield` nodes.

Format:

~~~
(kwargs (pair (int 1) (int 2)) (kwsplat (lvar :bar)) (pair (sym :baz) (int 3)))
"foo(1 => 2, **bar, baz: 3)"
~~~~~~~~~~~~~~~~~~~~~ expression
~~~

#### Keyword splat (2.0)

Can also be used in argument lists: `foo(bar, **baz)`
Expand Down
1 change: 1 addition & 0 deletions lib/parser/ast/processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def process_regular_node(node)
alias on_array process_regular_node
alias on_pair process_regular_node
alias on_hash process_regular_node
alias on_kwargs process_regular_node
alias on_irange process_regular_node
alias on_erange process_regular_node

Expand Down
76 changes: 76 additions & 0 deletions lib/parser/builders/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,54 @@ class << self

@emit_forward_arg = false

class << self
##
# AST compatibility attribute; Starting from Ruby 2.7 keyword arguments
# of method calls that are passed explicitly as a hash (i.e. with curly braces)
# are treated as positional arguments and Ruby 2.7 emits a warning on such method
# call. Ruby 3.0 given an ArgumentError.
#
# If set to false (the default) the last hash argument is emitted as `hash`:
#
# ```
# (send nil :foo
# (hash
# (pair
# (sym :bar)
# (int 42))))
# ```
#
# If set to true it is emitted as `kwargs`:
#
# ```
# (send nil :foo
# (kwargs
# (pair
# (sym :bar)
# (int 42))))
# ```
#
# Note that `kwargs` node is just a replacement for `hash` argument,
# so if there's are multiple arguments (or a `kwsplat`) all of them
# are wrapped into `kwargs` instead of `hash`:
#
# ```
# (send nil :foo
# (hash
# (pair
# (sym :a)
# (int 42))
# (kwsplat
# (send nil :b))
# (pair
# (sym :c)
# (int 10))))
# ```
attr_accessor :emit_kwargs
end

@emit_kwargs = false

class << self
##
# @api private
Expand All @@ -138,6 +186,7 @@ def modernize
@emit_index = true
@emit_arg_inside_procarg0 = true
@emit_forward_arg = true
@emit_kwargs = true
end
end

Expand Down Expand Up @@ -927,6 +976,11 @@ def forwarded_args(dots_t)
def call_method(receiver, dot_t, selector_t,
lparen_t=nil, args=[], rparen_t=nil)
type = call_type_for_dot(dot_t)

if self.class.emit_kwargs
rewrite_hash_args_to_kwargs(args)
end

if selector_t.nil?
n(type, [ receiver, :call, *args ],
send_map(receiver, dot_t, nil, lparen_t, args, rparen_t))
Expand Down Expand Up @@ -1004,6 +1058,10 @@ def attr_asgn(receiver, dot_t, selector_t)
end

def index(receiver, lbrack_t, indexes, rbrack_t)
if self.class.emit_kwargs
rewrite_hash_args_to_kwargs(indexes)
end

if self.class.emit_index
n(:index, [ receiver, *indexes ],
index_map(receiver, lbrack_t, rbrack_t))
Expand Down Expand Up @@ -1166,6 +1224,10 @@ def keyword_cmd(type, keyword_t, lparen_t=nil, args=[], rparen_t=nil)
end
end

if %i[yield super].include?(type) && self.class.emit_kwargs
rewrite_hash_args_to_kwargs(args)
end

n(type, args,
keyword_map(keyword_t, lparen_t, args, rparen_t))
end
Expand Down Expand Up @@ -2098,6 +2160,20 @@ def validate_definee(definee)
true
end
end

def rewrite_hash_args_to_kwargs(args)
if args.any? && kwargs?(args.last)
# foo(..., bar: baz)
args[args.length - 1] = args[args.length - 1].updated(:kwargs)
elsif args.length > 1 && args.last.type == :block_pass && kwargs?(args[args.length - 2])
# foo(..., bar: baz, &blk)
args[args.length - 2] = args[args.length - 2].updated(:kwargs)
end
end

def kwargs?(node)
node.type == :hash && node.loc.begin.nil? && node.loc.end.nil?
end
end

end
2 changes: 1 addition & 1 deletion lib/parser/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ module class sclass def defs undef alias args
match_var pin match_alt match_as match_rest
array_pattern match_with_trailing_comma array_pattern_with_tail
hash_pattern const_pattern if_guard unless_guard match_nil_pattern
empty_else find_pattern
empty_else find_pattern kwargs
).to_set.freeze

end # Meta
Expand Down
3 changes: 3 additions & 0 deletions lib/parser/ruby18.y
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,9 @@ rule
arg_value: arg

aref_args: none
{
result = []
}
| command opt_nl
{
result = [ val[0] ]
Expand Down
2 changes: 1 addition & 1 deletion lib/parser/runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def execute(options)

private

LEGACY_MODES = %i[lambda procarg0 encoding index arg_inside_procarg0 forward_arg].freeze
LEGACY_MODES = %i[lambda procarg0 encoding index arg_inside_procarg0 forward_arg kwargs].freeze

def runner_name
raise NotImplementedError, "implement #{self.class}##{__callee__}"
Expand Down
Loading

0 comments on commit c7a6b3f

Please sign in to comment.