[前][次][番号順一覧][スレッド一覧]

ruby-changes:59141

From: Jeremy <ko1@a...>
Date: Tue, 10 Dec 2019 01:26:24 +0900 (JST)
Subject: [ruby-changes:59141] f45c0dc239 (master): Add Proc#ruby2_keywords

https://git.ruby-lang.org/ruby.git/commit/?id=f45c0dc239

From f45c0dc23980d7fd8167d290ea7c354cf2cdb500 Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@j...>
Date: Thu, 5 Dec 2019 11:01:20 -0800
Subject: Add Proc#ruby2_keywords

This allows passing keywords through a normal argument splat in a
Proc.  While needing ruby2_keywords support for methods is more
common, there is code that delegates keywords through normal
argument splats in procs, including code in Rails.  For that
reason, it makes sense to expose this for procs as well.

Internally, ruby2_keywords is not tied to methods, but iseqs,
so this just allows for setting the ruby2_keywords for the iseq
related to the proc.

diff --git a/proc.c b/proc.c
index 4d9998b..b1b6735 100644
--- a/proc.c
+++ b/proc.c
@@ -3465,6 +3465,70 @@ rb_method_compose_to_right(VALUE self, VALUE g) https://github.com/ruby/ruby/blob/trunk/proc.c#L3465
 }
 
 /*
+ *  call-seq:
+ *     proc.ruby2_keywords -> proc
+ *
+ *  Marks the proc as passing keywords through a normal argument splat.
+ *  This should only be called on procs that accept an argument splat
+ *  (<tt>*args</tt>) but not explicit keywords or a keyword splat.  It
+ *  marks the proc such that if the proc is called with keyword arguments,
+ *  the final hash argument is marked with a special flag such that if it
+ *  is the final element of a normal argument splat to another method call,
+ *  and that method call does not include explicit keywords or a keyword
+ *  splat, the final element is interpreted as keywords.  In other words,
+ *  keywords will be passed through the proc to other methods.
+ *
+ *  This should only be used for procs that delegate keywords to another
+ *  method, and only for backwards compatibility with Ruby versions before
+ *  2.7.
+ *
+ *  This method will probably be removed at some point, as it exists only
+ *  for backwards compatibility. As it does not exist in Ruby versions
+ *  before 2.7, check that the proc responds to this method before calling
+ *  it. Also, be aware that if this method is removed, the behavior of the
+ *  proc will change so that it does not pass through keywords.
+ *
+ *    module Mod
+ *      foo = ->(meth, *args, &block) do
+ *        send(:"do_#{meth}", *args, &block)
+ *      end
+ *      foo.ruby2_keywords if foo.respond_to?(:ruby2_keywords)
+ *    end
+ */
+
+static VALUE
+proc_ruby2_keywords(VALUE procval)
+{
+    rb_proc_t *proc;
+    GetProcPtr(procval, proc);
+
+    rb_check_frozen(procval);
+
+    if (proc->is_from_method) {
+            rb_warn("Skipping set of ruby2_keywords flag for proc (proc created from method)");
+            return procval;
+    }
+
+    switch (proc->block.type) {
+      case block_type_iseq:
+        if (proc->block.as.captured.code.iseq->body->param.flags.has_rest &&
+                !proc->block.as.captured.code.iseq->body->param.flags.has_kw &&
+                !proc->block.as.captured.code.iseq->body->param.flags.has_kwrest) {
+            proc->block.as.captured.code.iseq->body->param.flags.ruby2_keywords = 1;
+        }
+        else {
+            rb_warn("Skipping set of ruby2_keywords flag for proc (proc accepts keywords or proc does not accept argument splat)");
+        }
+        break;
+      default:
+        rb_warn("Skipping set of ruby2_keywords flag for proc (proc not defined in Ruby)");
+        break;
+    }
+
+    return procval;
+}
+
+/*
  *  Document-class: LocalJumpError
  *
  *  Raised when Ruby can't yield as requested.
@@ -3789,6 +3853,7 @@ Init_Proc(void) https://github.com/ruby/ruby/blob/trunk/proc.c#L3853
     rb_define_method(rb_cProc, ">>", proc_compose_to_right, 1);
     rb_define_method(rb_cProc, "source_location", rb_proc_location, 0);
     rb_define_method(rb_cProc, "parameters", rb_proc_parameters, 0);
+    rb_define_method(rb_cProc, "ruby2_keywords", proc_ruby2_keywords, 0);
 
     /* Exceptions */
     rb_eLocalJumpError = rb_define_class("LocalJumpError", rb_eStandardError);
diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb
index 295b499..b0199e0 100644
--- a/test/ruby/test_keyword.rb
+++ b/test/ruby/test_keyword.rb
@@ -2684,6 +2684,45 @@ class TestKeywordArguments < Test::Unit::TestCase https://github.com/ruby/ruby/blob/trunk/test/ruby/test_keyword.rb#L2684
     assert_raise(ArgumentError) { m.call(42, a: 1, **h2) }
   end
 
+  def test_proc_ruby2_keywords
+    h1 = {:a=>1}
+    foo = ->(*args, &block){block.call(*args)}
+    assert_same(foo, foo.ruby2_keywords)
+
+    assert_equal([[1], h1], foo.call(1, :a=>1, &->(*args, **kw){[args, kw]}))
+    assert_equal([1, h1], foo.call(1, :a=>1, &->(*args){args}))
+    assert_warn(/The last argument is used as the keyword parameter/) do
+      assert_equal([[1], h1], foo.call(1, {:a=>1}, &->(*args, **kw){[args, kw]}))
+    end
+    assert_equal([1, h1], foo.call(1, {:a=>1}, &->(*args){args}))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([h1, {}], foo.call(:a=>1, &->(arg, **kw){[arg, kw]}))
+    end
+    assert_equal(h1, foo.call(:a=>1, &->(arg){arg}))
+
+    [->(){}, ->(arg){}, ->(*args, **kw){}, ->(*args, k: 1){}, ->(*args, k: ){}].each do |pr|
+      assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc accepts keywords or proc does not accept argument splat\)/) do
+        pr.ruby2_keywords
+      end
+    end
+
+    o = Object.new
+    def o.foo(*args)
+      yield *args
+    end
+    foo = o.method(:foo).to_proc
+    assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc created from method\)/) do
+      foo.ruby2_keywords
+    end
+
+    foo = :foo.to_proc
+    assert_warn(/Skipping set of ruby2_keywords flag for proc \(proc not defined in Ruby\)/) do
+      foo.ruby2_keywords
+    end
+
+    assert_raise(FrozenError) { ->(*args){}.freeze.ruby2_keywords }
+  end
+
   def test_ruby2_keywords
     c = Class.new do
       ruby2_keywords def foo(meth, *args)
-- 
cgit v0.10.2


--
ML: ruby-changes@q...
Info: http://www.atdot.net/~ko1/quickml/

[前][次][番号順一覧][スレッド一覧]