Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

+ builder: emit implicit hash passed to a method call as kwargs #769

Merged
merged 2 commits into from
Nov 29, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^hash^kwargs

# (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 @@ -1164,6 +1222,10 @@ def keyword_cmd(type, keyword_t, lparen_t=nil, args=[], rparen_t=nil)
if last_arg.type == :block_pass
diagnostic :error, :block_given_to_yield, nil, loc(keyword_t), [last_arg.loc.expression]
end

if self.class.emit_kwargs
rewrite_hash_args_to_kwargs(args)
end
end

n(type, args,
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