From aa95774902bcd1405f75bf18029229133e9be984 Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@jeremyevans.net>
Date: Sat, 14 Sep 2019 13:56:43 -0700
Subject: [PATCH] Make rb_scan_args handle keywords more similar to Ruby
 methods

Cfuncs that use rb_scan_args with the : entry suffer similar keyword
argument separation issues that Ruby methods suffer if the cfuncs
accept optional or variable arguments.

This makes the following changes to : handling.

* If optional or variable arguments are also requested, treat
  the option handling similar to Ruby keyword option handling
  (keyword mode).  Otherwise, if only mandatory arguments are
  requested other than this option hash, treat it similarly to
  an optional argument (hash mode).

* In keyword mode, do not look for an option hash if empty
  keywords are provided.  For backwards compatibility, in both modes,
  treat an empty keyword splat as a empty mandatory positional hash
  argument, but emit a a warning, as this behavior should be removed
  in Ruby 3.  The argument number check needs to be moved lower so it
  can correctly handle an empty positional argument being added.

* In both modes, if the last argument is nil and it is necessary to
  treat it as an option hash in order to make sure all arguments are
  processed, continue to treat the last argument as the option hash.
  Emit a warning in this case, as this behavior should be removed
  in Ruby 3.

* In both modes, if splitting the option hash into two hashes, issue
  a warning, as we will not be splitting hashes in Ruby 3.

* In keyword mode, if keyword arguments are not provided and the
  last argument is a hash, emit a warning, as in Ruby 3 it will be
  treated as a positional argument instead of the option hash.

* In keyword mode, if the keyword argument is required to fill a
  mandatory positional argument, continue to do so, but emit a
  warning as this behavior will be going away in Ruby 3.

* In keyword mode, if keyword arguments are provided an the last
  argument is not a hash, that indicates something wrong. This can
  happen if a cfunc is calling rb_scan_args multiple times, and
  providing arguments that were not passed to it from Ruby.  Such
  functions will need to be updated, and there is a new feature
  to handle this.  Instead of calling rb_keyword_given_p() and
  rb_empty_keyword_given_p(), allow an 'k', 'e', or 'n' prefix
  to the rb_scan_args string:

  * k: Treat as if the keyword given flag is set (last argument
    should be a hash.

  * e: Treat as if the empty keyword given flag is set (in
    keyword mode, do not look for a last positional hash).

  * n: In keyword mode, assume the call did not set the keyword
    or empty keyword flags, but do not issue a warning if the
    last argument is a hash that is treated as keywords.

  For external Cfuncs, you can be backwards compatible with Ruby
  <2.7 by using a macro. On 2.7+, the macro can be defined to
  "k", "e", or "n", and on <2.7, it can be defined to "";

This commit fixes all warnings caused by the changes above.

It switches some function calls to *_kw versions with appropriate
kw_splat flags. If delegating arguments, RB_PASS_CALLED_KEYWORDS
is used.  If creating new arguments, RB_PASS_KEYWORDS is used if
the last argument is a hash to be treated as keywords.

In open_key_args in io.c, use the 'n' prefix to rb_scan_args.
In this case, the arguments provided come from another C
function, not Ruby.  The last argument may or may not be a hash,
so we can't set keyword argument mode.  However, if it is a
hash, we don't want to warn when treating it as keywords.

In Ruby files, make sure to appropriately use keyword splats
or literal keywords when calling Cfuncs that now issue keyword
argument separation warnings through rb_scan_args.  Also, make
sure not to pass nil in place of an option hash.

Work around Kernel#warn warnings due to problems in the Rubygems
override of the method.  There is an open pull request to fix
these issues in Rubygems, but part of the Rubygems tests for
their override fail on ruby-head due to rb_scan_args not
recognizing empty keyword splats, which this commit fixes.
---
 class.c                              | 119 ++++++++++++++----
 error.c                              |   4 +-
 ext/-test-/scan_args/scan_args.c     |  31 ++++-
 ext/pathname/pathname.c              |  10 +-
 ext/stringio/stringio.c              |   4 +-
 include/ruby/ruby.h                  | 177 ++++++++++++++++++++++-----
 internal.h                           |   3 -
 io.c                                 |   8 +-
 lib/csv.rb                           |   2 +-
 lib/open-uri.rb                      |   6 +-
 lib/pstore.rb                        |   6 +-
 lib/rubygems/core_ext/kernel_warn.rb |   9 +-
 lib/tempfile.rb                      |   6 +-
 rational.c                           |   2 +-
 test/-ext-/test_scan_args.rb         | 135 ++++++++++++++++----
 test/pathname/test_pathname.rb       |   2 +-
 test/reline/helper.rb                |   2 +-
 test/ruby/test_dir_m17n.rb           |  32 ++---
 test/ruby/test_econv.rb              |   7 +-
 test/ruby/test_exception.rb          |   8 ++
 test/ruby/test_io.rb                 |  12 +-
 test/ruby/test_io_m17n.rb            |   3 +-
 test/ruby/test_literal.rb            |   4 +-
 test/ruby/test_numeric.rb            |   7 +-
 test/ruby/test_string.rb             |   4 +-
 25 files changed, 468 insertions(+), 135 deletions(-)

diff --git a/class.c b/class.c
index 6b3b6623f1..645d78ae8e 100644
--- a/class.c
+++ b/class.c
@@ -1954,7 +1954,30 @@ rb_scan_args(int argc, const VALUE *argv, const char *fmt, ...)
     int f_var = 0, f_hash = 0, f_block = 0;
     int n_lead = 0, n_opt = 0, n_trail = 0, n_mand;
     int argi = 0, last_idx = -1;
+    int f_kw = 0;
+    int keyword_given = 0;
+    int empty_keyword_given = 0;
+    int allow_hash_to_keyword = 0;
     VALUE hash = Qnil, last_hash = 0;
+    VALUE tmp_buffer = 0;
+
+    if (*p == 'k') {
+        keyword_given = 1;
+        p++;
+    }
+    else if (*p == 'e') {
+        empty_keyword_given = 1;
+        p++;
+    }
+    else if (*p == 'n') {
+        allow_hash_to_keyword = 1;
+        p++;
+    }
+    else {
+        if(!(keyword_given = rb_keyword_given_p())) {
+            empty_keyword_given = rb_empty_keyword_given_p();
+        }
+    }
 
     if (ISDIGIT(*p)) {
 	n_lead = *p - '0';
@@ -1974,6 +1997,9 @@ rb_scan_args(int argc, const VALUE *argv, const char *fmt, ...)
     }
     if (*p == ':') {
 	f_hash = 1;
+        /* Optional or variable arguments turn on keyword argument mode */
+        /* If all arguments are mandatory, operate in regular argument mode */
+        f_kw = f_var || n_opt;
 	p++;
     }
     if (*p == '&') {
@@ -1985,32 +2011,79 @@ rb_scan_args(int argc, const VALUE *argv, const char *fmt, ...)
     }
     n_mand = n_lead + n_trail;
 
-    if (argc < n_mand)
-	goto argc_error;
-
     va_start(vargs, fmt);
 
     /* capture an option hash - phase 1: pop */
-    if (f_hash && n_mand < argc) {
-	VALUE last = argv[argc - 1];
-
-	if (NIL_P(last)) {
-	    /* nil is taken as an empty option hash only if it is not
-	       ambiguous; i.e. '*' is not specified and arguments are
-	       given more than sufficient */
-	    if (!f_var && n_mand + n_opt < argc)
-		argc--;
-	}
-	else {
-	    hash = rb_check_hash_type(last);
-	    if (!NIL_P(hash)) {
-		VALUE opts = rb_extract_keywords(&hash);
-		if (!(last_hash = hash)) argc--;
-		else last_idx = argc - 1;
-		hash = opts ? opts : Qnil;
-	    }
-	}
+    /* In keyword arugment mode, ignore final positional hash if empty keywords given */
+    if (argc > 0 && !(f_kw && empty_keyword_given)) {
+        VALUE last = argv[argc - 1];
+
+        /* Ruby 3: if (f_hash && (f_kw && keyword_given) || n_mand < argc)) { */
+        if (f_hash && n_mand < argc) {
+            if (f_kw && keyword_given) {
+                if (!RB_TYPE_P(last, T_HASH)) {
+                    rb_warn("Keyword flag set when calling rb_scan_args, but last entry is not a hash");
+                }
+                else {
+                    hash = last;
+                }
+            }
+            else if (NIL_P(last)) {
+                /* For backwards compatibility, nil is taken as an empty
+                   option hash only if it is not ambiguous; i.e. '*' is
+                   not specified and arguments are given more than sufficient.
+                   This will be removed in Ruby 3. */
+                if (!f_var && n_mand + n_opt < argc) {
+                    rb_warn("The last argument is nil, treating as empty keywords");
+                    argc--;
+                }
+            }
+            else {
+                hash = rb_check_hash_type(last);
+            }
+
+            /* Ruby 3: Remove if branch, as it will not attempt to split hashes */
+            if (!NIL_P(hash)) {
+                VALUE opts = rb_extract_keywords(&hash);
+
+                if (!(last_hash = hash)) {
+                    if (f_kw && !keyword_given && !allow_hash_to_keyword) {
+                        /* Warn in keyword argument mode if treating positional
+                           as keyword, as in Ruby 3, this will be an error */
+                        rb_warn("The last argument is used as the keyword parameter");
+                    }
+                    argc--;
+                }
+                else {
+                    /* Warn in keyword argument mode if splitting either positional hash
+                       to keywords or keywords to positional hash, as in Ruby 3,
+                       no splitting will be done */
+                    rb_warn("The last argument is split into positional and keyword parameters");
+                    last_idx = argc - 1;
+                }
+                hash = opts ? opts : Qnil;
+            }
+        }
+        else if (f_kw && keyword_given && n_mand == argc) {
+            /* Warn in keyword argument mode if treating keywords as positional,
+               as in Ruby 3, this will be an error */
+            rb_warn("The keyword argument is passed as the last hash parameter");
+        }
     }
+    if (f_hash && n_mand == argc+1 && empty_keyword_given) {
+        VALUE *ptr = rb_alloc_tmp_buffer2(&tmp_buffer, argc+1, sizeof(VALUE));
+        memcpy(ptr, argv, sizeof(VALUE)*argc);
+        ptr[argc] = rb_hash_new();
+        argc++;
+        *(&argv) = ptr;
+        rb_warn("The keyword argument is passed as the last hash parameter");
+    }
+
+    if (argc < n_mand) {
+        va_end(vargs);
+        goto argc_error;
+    }
+
     /* capture leading mandatory arguments */
     for (i = n_lead; i-- > 0; ) {
 	var = va_arg(vargs, VALUE *);
@@ -2070,9 +2143,11 @@ rb_scan_args(int argc, const VALUE *argv, const char *fmt, ...)
 
     if (argi < argc) {
       argc_error:
+        if(tmp_buffer) rb_free_tmp_buffer(&tmp_buffer);
 	rb_error_arity(argc, n_mand, f_var ? UNLIMITED_ARGUMENTS : n_mand + n_opt);
     }
 
+    if(tmp_buffer) rb_free_tmp_buffer(&tmp_buffer);
     return argc;
 }
 
diff --git a/error.c b/error.c
index 87357a4ebe..bef5090011 100644
--- a/error.c
+++ b/error.c
@@ -341,7 +341,7 @@ rb_warn_m(int argc, VALUE *argv, VALUE exc)
     VALUE opts, location = Qnil;
 
     if (!NIL_P(ruby_verbose) && argc > 0 &&
-	    (argc = rb_scan_args(argc, argv, "*:", NULL, &opts)) > 0) {
+            (argc = rb_scan_args(argc, argv, "*:", NULL, &opts)) > 0) {
 	VALUE str = argv[0], uplevel = Qnil;
 	if (!NIL_P(opts)) {
 	    static ID kwds[1];
@@ -1595,7 +1595,7 @@ nometh_err_initialize(int argc, VALUE *argv, VALUE self)
     priv = (argc > 3) && (--argc, RTEST(argv[argc]));
     args = (argc > 2) ? argv[--argc] : Qnil;
     if (!NIL_P(options)) argv[argc++] = options;
-    rb_call_super(argc, argv);
+    rb_call_super_kw(argc, argv, RB_PASS_CALLED_KEYWORDS);
     return nometh_err_init_attr(self, args, priv);
 }
 
diff --git a/ext/-test-/scan_args/scan_args.c b/ext/-test-/scan_args/scan_args.c
index dca353f643..7593e57b71 100644
--- a/ext/-test-/scan_args/scan_args.c
+++ b/ext/-test-/scan_args/scan_args.c
@@ -250,6 +250,33 @@ scan_args_lead_opt_var_trail_hash(int argc, VALUE *argv, VALUE self)
     return rb_ary_new_from_values(numberof(args), args);
 }
 
+static VALUE
+scan_args_k_lead_opt_hash(int argc, VALUE *argv, VALUE self)
+{
+    VALUE args[4];
+    int n = rb_scan_args(argc, argv, "k11:", args+1, args+2, args+3);
+    args[0] = INT2NUM(n);
+    return rb_ary_new_from_values(numberof(args), args);
+}
+
+static VALUE
+scan_args_e_lead_opt_hash(int argc, VALUE *argv, VALUE self)
+{
+    VALUE args[4];
+    int n = rb_scan_args(argc, argv, "e11:", args+1, args+2, args+3);
+    args[0] = INT2NUM(n);
+    return rb_ary_new_from_values(numberof(args), args);
+}
+
+static VALUE
+scan_args_n_lead_opt_hash(int argc, VALUE *argv, VALUE self)
+{
+    VALUE args[4];
+    int n = rb_scan_args(argc, argv, "n11:", args+1, args+2, args+3);
+    args[0] = INT2NUM(n);
+    return rb_ary_new_from_values(numberof(args), args);
+}
+
 void
 Init_scan_args(void)
 {
@@ -282,5 +309,7 @@ Init_scan_args(void)
     rb_define_singleton_method(module, "lead_var_trail_hash", scan_args_lead_var_trail_hash, -1);
     rb_define_singleton_method(module, "opt_var_trail_hash", scan_args_opt_var_trail_hash, -1);
     rb_define_singleton_method(module, "lead_opt_var_trail_hash", scan_args_lead_opt_var_trail_hash, -1);
+    rb_define_singleton_method(module, "k_lead_opt_hash", scan_args_k_lead_opt_hash, -1);
+    rb_define_singleton_method(module, "e_lead_opt_hash", scan_args_e_lead_opt_hash, -1);
+    rb_define_singleton_method(module, "n_lead_opt_hash", scan_args_n_lead_opt_hash, -1);
 }
-
diff --git a/ext/pathname/pathname.c b/ext/pathname/pathname.c
index 70f82583a1..52ddda1f8f 100644
--- a/ext/pathname/pathname.c
+++ b/ext/pathname/pathname.c
@@ -393,7 +393,7 @@ path_read(int argc, VALUE *argv, VALUE self)
 
     args[0] = get_strpath(self);
     n = rb_scan_args(argc, argv, "03", &args[1], &args[2], &args[3]);
-    return rb_funcallv(rb_cFile, id_read, 1+n, args);
+    return rb_funcallv_kw(rb_cFile, id_read, 1+n, args, RB_PASS_CALLED_KEYWORDS);
 }
 
 /*
@@ -1097,12 +1097,12 @@ path_s_glob(int argc, VALUE *argv, VALUE klass)
 
     n = rb_scan_args(argc, argv, "12", &args[0], &args[1], &args[2]);
     if (rb_block_given_p()) {
-        return rb_block_call(rb_cDir, id_glob, n, args, s_glob_i, klass);
+        return rb_block_call_kw(rb_cDir, id_glob, n, args, s_glob_i, klass, RB_PASS_CALLED_KEYWORDS);
     }
     else {
         VALUE ary;
         long i;
-        ary = rb_funcallv(rb_cDir, id_glob, n, args);
+        ary = rb_funcallv_kw(rb_cDir, id_glob, n, args, RB_PASS_CALLED_KEYWORDS);
         ary = rb_convert_type(ary, T_ARRAY, "Array", "to_ary");
         for (i = 0; i < RARRAY_LEN(ary); i++) {
             VALUE elt = RARRAY_AREF(ary, i);
@@ -1145,12 +1145,12 @@ path_glob(int argc, VALUE *argv, VALUE self)
     n = 3;
 
     if (rb_block_given_p()) {
-        return rb_block_call(rb_cDir, id_glob, n, args, glob_i, self);
+        return rb_block_call_kw(rb_cDir, id_glob, n, args, glob_i, self, RB_PASS_KEYWORDS);
     }
     else {
         VALUE ary;
         long i;
-        ary = rb_funcallv(rb_cDir, id_glob, n, args);
+        ary = rb_funcallv_kw(rb_cDir, id_glob, n, args, RB_PASS_KEYWORDS);
         ary = rb_convert_type(ary, T_ARRAY, "Array", "to_ary");
         for (i = 0; i < RARRAY_LEN(ary); i++) {
             VALUE elt = RARRAY_AREF(ary, i);
diff --git a/ext/stringio/stringio.c b/ext/stringio/stringio.c
index 4a81e84183..ab533a03fb 100644
--- a/ext/stringio/stringio.c
+++ b/ext/stringio/stringio.c
@@ -385,7 +385,7 @@ strio_finalize(VALUE self)
 static VALUE
 strio_s_open(int argc, VALUE *argv, VALUE klass)
 {
-    VALUE obj = rb_class_new_instance(argc, argv, klass);
+    VALUE obj = rb_class_new_instance_kw(argc, argv, klass, RB_PASS_CALLED_KEYWORDS);
     if (!rb_block_given_p()) return obj;
     return rb_ensure(rb_yield, obj, strio_finalize, obj);
 }
@@ -400,7 +400,7 @@ strio_s_new(int argc, VALUE *argv, VALUE klass)
 	rb_warn("%"PRIsVALUE"::new() does not take block; use %"PRIsVALUE"::open() instead",
 		cname, cname);
     }
-    return rb_class_new_instance(argc, argv, klass);
+    return rb_class_new_instance_kw(argc, argv, klass, RB_PASS_CALLED_KEYWORDS);
 }
 
 /*
diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h
index c438b0aea7..9252563146 100644
--- a/include/ruby/ruby.h
+++ b/include/ruby/ruby.h
@@ -2325,6 +2325,9 @@ unsigned long ruby_strtoul(const char *str, char **endptr, int base);
 PRINTF_ARGS(int ruby_snprintf(char *str, size_t n, char const *fmt, ...), 3, 4);
 int ruby_vsnprintf(char *str, size_t n, char const *fmt, va_list ap);
 
+/* -- Remove In 3.0, Only public for rb_scan_args optimized version -- */
+int rb_empty_keyword_given_p(void);
+
 #if defined(HAVE_BUILTIN___BUILTIN_CHOOSE_EXPR_CONSTANT_P) && defined(HAVE_VA_ARGS_MACRO) && defined(__OPTIMIZE__)
 # define rb_scan_args(argc,argvp,fmt,...) \
     __builtin_choose_expr(__builtin_constant_p(fmt), \
@@ -2374,12 +2377,17 @@ ERRORFUNC(("variable argument length doesn't match"), void rb_scan_args_length_m
      rb_scan_args_count_var(fmt, ofs, vari) : \
      rb_scan_args_count_var(fmt, ofs+1, vari+fmt[ofs]-'0'))
 
+# define rb_scan_kw_p(c) ((c) == 'k' || (c) == 'e' || (c) == 'n')
+
 # define rb_scan_args_count_lead(fmt, ofs, vari) \
     (!rb_scan_args_isdigit(fmt[ofs]) ? \
       rb_scan_args_count_var(fmt, ofs, vari) : \
       rb_scan_args_count_opt(fmt, ofs+1, vari+fmt[ofs]-'0'))
 
-# define rb_scan_args_count(fmt) rb_scan_args_count_lead(fmt, 0, 0)
+# define rb_scan_args_count(fmt) \
+    (rb_scan_kw_p(fmt[0]) ? \
+     rb_scan_args_count_lead((fmt+1), 0, 0) : \
+     rb_scan_args_count_lead((fmt), 0, 0))
 
 # if defined(__has_attribute) && __has_attribute(diagnose_if)
 #  define rb_scan_args_verify(fmt, varc) (void)0
@@ -2392,39 +2400,75 @@ ERRORFUNC(("variable argument length doesn't match"), void rb_scan_args_length_m
      (void)0)
 # endif
 
+ALWAYS_INLINE(static int rb_scan_args_kw_p(const char *fmt));
+static inline int
+rb_scan_args_kw_p(const char *fmt)
+{
+    switch (fmt[0]) {
+      case 'k':
+      case 'e':
+      case 'n':
+        return 1;
+    }
+    return 0;
+}
+
+ALWAYS_INLINE(static int rb_scan_args_kw(const char *fmt));
+static inline int
+rb_scan_args_kw(const char *fmt)
+{
+    switch (fmt[0]) {
+      case 'k':
+        return 1;
+      case 'e':
+        return 2;
+      case 'n':
+        return 3;
+    }
+    return 0;
+}
+
+ALWAYS_INLINE(static int rb_scan_args_n_lead_idx(const char *fmt));
+static inline int
+rb_scan_args_n_lead_idx(const char *fmt)
+{
+    return rb_scan_args_kw_p(fmt);
+}
+
 ALWAYS_INLINE(static int rb_scan_args_lead_p(const char *fmt));
 static inline int
 rb_scan_args_lead_p(const char *fmt)
 {
-    return rb_scan_args_isdigit(fmt[0]);
+    return rb_scan_args_isdigit(fmt[rb_scan_args_n_lead_idx(fmt)]);
 }
 
 ALWAYS_INLINE(static int rb_scan_args_n_lead(const char *fmt));
 static inline int
 rb_scan_args_n_lead(const char *fmt)
 {
-    return (rb_scan_args_lead_p(fmt) ? fmt[0]-'0' : 0);
+    return (rb_scan_args_lead_p(fmt) ? fmt[rb_scan_args_n_lead_idx(fmt)]-'0' : 0);
 }
 
 ALWAYS_INLINE(static int rb_scan_args_opt_p(const char *fmt));
 static inline int
 rb_scan_args_opt_p(const char *fmt)
 {
-    return (rb_scan_args_lead_p(fmt) && rb_scan_args_isdigit(fmt[1]));
+    return (rb_scan_args_lead_p(fmt) && rb_scan_args_isdigit(fmt[rb_scan_args_n_lead_idx(fmt)+1]));
 }
 
 ALWAYS_INLINE(static int rb_scan_args_n_opt(const char *fmt));
 static inline int
 rb_scan_args_n_opt(const char *fmt)
 {
-    return (rb_scan_args_opt_p(fmt) ? fmt[1]-'0' : 0);
+    return (rb_scan_args_opt_p(fmt) ? fmt[rb_scan_args_n_lead_idx(fmt)+1]-'0' : 0);
 }
 
 ALWAYS_INLINE(static int rb_scan_args_var_idx(const char *fmt));
 static inline int
 rb_scan_args_var_idx(const char *fmt)
 {
-    return (!rb_scan_args_lead_p(fmt) ? 0 : !rb_scan_args_isdigit(fmt[1]) ? 1 : 2);
+    const int idx  = rb_scan_args_n_lead_idx(fmt);
+    return idx + (!rb_scan_args_lead_p(fmt) ? 0 : !rb_scan_args_isdigit(fmt[idx+1]) ? 1 : 2);
 }
 
 ALWAYS_INLINE(static int rb_scan_args_f_var(const char *fmt));
@@ -2494,6 +2538,7 @@ rb_scan_args_end_idx(const char *fmt)
 /* https://bugs.llvm.org/show_bug.cgi?id=38095 */
 # define rb_scan_args0(argc, argv, fmt, varc, vars) \
     rb_scan_args_set(argc, argv, \
+                     rb_scan_args_kw(fmt), \
 		     rb_scan_args_n_lead(fmt), \
 		     rb_scan_args_n_opt(fmt), \
 		     rb_scan_args_n_trail(fmt), \
@@ -2502,13 +2547,13 @@ rb_scan_args_end_idx(const char *fmt)
 		     rb_scan_args_f_block(fmt), \
 		     (rb_scan_args_verify(fmt, varc), vars), (char *)fmt, varc)
 ALWAYS_INLINE(static int
-rb_scan_args_set(int argc, const VALUE *argv,
+rb_scan_args_set(int argc, const VALUE *argv, int kw_flag,
 		 int n_lead, int n_opt, int n_trail,
 		 int f_var, int f_hash, int f_block,
 		 VALUE *vars[], char *fmt, int varc));
 
 inline int
-rb_scan_args_set(int argc, const VALUE *argv,
+rb_scan_args_set(int argc, const VALUE *argv, int kw_flag,
 		 int n_lead, int n_opt, int n_trail,
 		 int f_var, int f_hash, int f_block,
 		 VALUE *vars[], RB_UNUSED_VAR(char *fmt), RB_UNUSED_VAR(int varc))
@@ -2520,30 +2565,99 @@ rb_scan_args_set(int argc, const VALUE *argv,
     int i, argi = 0, vari = 0, last_idx = -1;
     VALUE *var, hash = Qnil, last_hash = 0;
     const int n_mand = n_lead + n_trail;
+    const int f_kw = (f_hash && (f_var || n_opt));
+    int keyword_given = 0;
+    int empty_keyword_given = 0;
+    int allow_hash_to_keyword = 0;
+    VALUE tmp_buffer = 0;
+
+    switch (kw_flag) {
+      case 0:
+        if (!(keyword_given =  rb_keyword_given_p())) {
+            empty_keyword_given = rb_empty_keyword_given_p();
+        }
+        break;
+      case 1:
+        keyword_given = 1;
+        break;
+      case 2:
+        empty_keyword_given = 1;
+        break;
+      case 3:
+        allow_hash_to_keyword = 1;
+        break;
+    }
 
     /* capture an option hash - phase 1: pop */
-    if (f_hash && n_mand < argc) {
-	VALUE last = argv[argc - 1];
-
-	if (RB_NIL_P(last)) {
-	    /* nil is taken as an empty option hash only if it is not
-	       ambiguous; i.e. '*' is not specified and arguments are
-	       given more than sufficient */
-	    if (!f_var && n_mand + n_opt < argc)
-		argc--;
-	}
-	else {
-	    hash = rb_check_hash_type(last);
-	    if (!RB_NIL_P(hash)) {
-		VALUE opts = rb_extract_keywords(&hash);
-		if (!(last_hash = hash)) argc--;
-		else last_idx = argc - 1;
-		hash = opts ? opts : Qnil;
-	    }
-	}
+    /* In keyword arugment mode, ignore final positional hash if empty keywords given */
+    if (argc > 0 && !(f_kw && empty_keyword_given)) {
+        VALUE last = argv[argc - 1];
+
+        /* Ruby 3: if (f_hash && (f_kw && keyword_given) || n_mand < argc)) { */
+        if (f_hash && n_mand < argc) {
+            if (f_kw && keyword_given) {
+                if (!RB_TYPE_P(last, T_HASH)) {
+                    rb_warn("Keyword flag set when calling rb_scan_args, but last entry is not a hash");
+                }
+                else {
+                    hash = last;
+                }
+            }
+            else if (NIL_P(last)) {
+                /* For backwards compatibility, nil is taken as an empty
+                   option hash only if it is not ambiguous; i.e. '*' is
+                   not specified and arguments are given more than sufficient.
+                   This will be removed in Ruby 3. */
+                if (!f_var && n_mand + n_opt < argc) {
+                    rb_warn("The last argument is nil, treating as empty keywords");
+                    argc--;
+                }
+            }
+            else {
+                hash = rb_check_hash_type(last);
+            }
+
+            /* Ruby 3: Remove if branch, as it will not attempt to split hashes */
+            if (!NIL_P(hash)) {
+                VALUE opts = rb_extract_keywords(&hash);
+
+                if (!(last_hash = hash)) {
+                    if (f_kw && !keyword_given && !allow_hash_to_keyword) {
+                        /* Warn in keyword argument mode if treating positional
+                           as keyword, as in Ruby 3, this will be an error */
+                        rb_warn("The last argument is used as the keyword parameter");
+                    }
+                    argc--;
+                }
+                else {
+                    /* Warn in keyword argument mode if splitting either positional hash
+                       to keywords or keywords to positional hash, as in Ruby 3,
+                       no splitting will be done */
+                    rb_warn("The last argument is split into positional and keyword parameters");
+                    last_idx = argc - 1;
+                }
+                hash = opts ? opts : Qnil;
+            }
+        }
+        else if (f_kw && keyword_given && n_mand == argc) {
+            /* Warn in keyword argument mode if treating keywords as positional,
+               as in Ruby 3, this will be an error */
+            rb_warn("The keyword argument is passed as the last hash parameter");
+        }
+    }
+    if (f_hash && n_mand == argc+1 && empty_keyword_given) {
+        VALUE *ptr = (VALUE *)rb_alloc_tmp_buffer2(&tmp_buffer, argc+1, sizeof(VALUE));
+        memcpy(ptr, argv, sizeof(VALUE)*argc);
+        ptr[argc] = rb_hash_new();
+        argc++;
+        *(&argv) = ptr;
+        rb_warn("The keyword argument is passed as the last hash parameter");
     }
 
-    rb_check_arity(argc, n_mand, f_var ? UNLIMITED_ARGUMENTS : n_mand + n_opt);
+
+    if (argc < n_mand) {
+        goto argc_error;
+    }
 
     /* capture leading mandatory arguments */
     for (i = n_lead; i-- > 0; ) {
@@ -2601,6 +2715,13 @@ rb_scan_args_set(int argc, const VALUE *argv,
 	}
     }
 
+    if (argi < argc) {
+      argc_error:
+        if(tmp_buffer) rb_free_tmp_buffer(&tmp_buffer);
+        rb_error_arity(argc, n_mand, f_var ? UNLIMITED_ARGUMENTS : n_mand + n_opt);
+    }
+
+    if(tmp_buffer) rb_free_tmp_buffer(&tmp_buffer);
     return argc;
 }
 #endif
diff --git a/internal.h b/internal.h
index 3dbe9ad38e..76cf49c5d1 100644
--- a/internal.h
+++ b/internal.h
@@ -1552,9 +1552,6 @@ void rb_class_modify_check(VALUE);
 #define id_status ruby_static_id_status
 NORETURN(VALUE rb_f_raise(int argc, VALUE *argv));
 
-/* -- Remove In 3.0 -- */
-int rb_empty_keyword_given_p(void);
-
 /* eval_error.c */
 VALUE rb_get_backtrace(VALUE info);
 
diff --git a/io.c b/io.c
index 2520725814..ffa484f0b8 100644
--- a/io.c
+++ b/io.c
@@ -7022,7 +7022,7 @@ rb_open_file(int argc, const VALUE *argv, VALUE io)
 static VALUE
 rb_io_s_open(int argc, VALUE *argv, VALUE klass)
 {
-    VALUE io = rb_class_new_instance(argc, argv, klass);
+    VALUE io = rb_class_new_instance_kw(argc, argv, klass, RB_PASS_CALLED_KEYWORDS);
 
     if (rb_block_given_p()) {
 	return rb_ensure(rb_yield, io, io_close, io);
@@ -7209,7 +7209,7 @@ rb_f_open(int argc, VALUE *argv, VALUE _)
 	}
     }
     if (redirect) {
-	VALUE io = rb_funcallv(argv[0], to_open, argc-1, argv+1);
+        VALUE io = rb_funcallv_kw(argv[0], to_open, argc-1, argv+1, RB_PASS_CALLED_KEYWORDS);
 
 	if (rb_block_given_p()) {
 	    return rb_ensure(rb_yield, io, io_close, io);
@@ -8390,7 +8390,7 @@ rb_io_s_new(int argc, VALUE *argv, VALUE klass)
 	rb_warn("%"PRIsVALUE"::new() does not take block; use %"PRIsVALUE"::open() instead",
 		cname, cname);
     }
-    return rb_class_new_instance(argc, argv, klass);
+    return rb_class_new_instance_kw(argc, argv, klass, RB_PASS_CALLED_KEYWORDS);
 }
 
 
@@ -10373,7 +10373,7 @@ open_key_args(VALUE klass, int argc, VALUE *argv, VALUE opt, struct foreach_arg
 	v = rb_to_array_type(v);
 	n = RARRAY_LENINT(v);
 	rb_check_arity(n, 0, 3); /* rb_io_open */
-	rb_scan_args(n, RARRAY_CONST_PTR(v), "02:", &vmode, &vperm, &opt);
+        rb_scan_args(n, RARRAY_CONST_PTR(v), "n02:", &vmode, &vperm, &opt);
     }
     arg->io = rb_io_open(klass, path, vmode, vperm, opt);
 }
diff --git a/lib/csv.rb b/lib/csv.rb
index 61702137d0..60dbbcc230 100644
--- a/lib/csv.rb
+++ b/lib/csv.rb
@@ -637,7 +637,7 @@ def self.open(filename, mode="r", **options)
     file_opts = {universal_newline: false}.merge(options)
 
     begin
-      f = File.open(filename, mode, file_opts)
+      f = File.open(filename, mode, **file_opts)
     rescue ArgumentError => e
       raise unless /needs binmode/.match?(e.message) and mode == "r"
       mode = "rb"
diff --git a/lib/open-uri.rb b/lib/open-uri.rb
index d9517e4b9e..f4cb536006 100644
--- a/lib/open-uri.rb
+++ b/lib/open-uri.rb
@@ -10,15 +10,15 @@ class << self
     alias open_uri_original_open open # :nodoc:
   end
 
-  def open(name, *rest, &block) # :nodoc:
+  def open(name, *rest, **kw, &block) # :nodoc:
     if (name.respond_to?(:open) && !name.respond_to?(:to_path)) ||
        (name.respond_to?(:to_str) &&
         %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
         (uri = URI.parse(name)).respond_to?(:open))
       warn('calling URI.open via Kernel#open is deprecated, call URI.open directly', uplevel: 1)
-      URI.open(name, *rest, &block)
+      URI.open(name, *rest, **kw, &block)
     else
-      open_uri_original_open(name, *rest, &block)
+      open_uri_original_open(name, *rest, **kw, &block)
     end
   end
   module_function :open
diff --git a/lib/pstore.rb b/lib/pstore.rb
index 4daa2e003f..1180fd50a0 100644
--- a/lib/pstore.rb
+++ b/lib/pstore.rb
@@ -374,7 +374,7 @@ def transaction(read_only = false)  # :yields:  pstore
   def open_and_lock_file(filename, read_only)
     if read_only
       begin
-        file = File.new(filename, RD_ACCESS)
+        file = File.new(filename, **RD_ACCESS)
         begin
           file.flock(File::LOCK_SH)
           return file
@@ -386,7 +386,7 @@ def open_and_lock_file(filename, read_only)
         return nil
       end
     else
-      file = File.new(filename, RDWR_ACCESS)
+      file = File.new(filename, **RDWR_ACCESS)
       file.flock(File::LOCK_EX)
       return file
     end
@@ -449,7 +449,7 @@ def save_data(original_checksum, original_file_size, file)
 
   def save_data_with_atomic_file_rename_strategy(data, file)
     temp_filename = "#{@filename}.tmp.#{Process.pid}.#{rand 1000000}"
-    temp_file = File.new(temp_filename, WR_ACCESS)
+    temp_file = File.new(temp_filename, **WR_ACCESS)
     begin
       temp_file.flock(File::LOCK_EX)
       temp_file.write(data)
diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb
index 3e531441ed..6c5b387e88 100755
--- a/lib/rubygems/core_ext/kernel_warn.rb
+++ b/lib/rubygems/core_ext/kernel_warn.rb
@@ -12,9 +12,9 @@ module Kernel
 
     original_warn = method(:warn)
 
-    module_function define_method(:warn) {|*messages, uplevel: nil|
-      unless uplevel
-        return original_warn.call(*messages)
+    module_function define_method(:warn) {|*messages, **kw|
+      unless uplevel = kw[:uplevel]
+        return original_warn.call(*messages, **kw)
       end
 
       # Ensure `uplevel` fits a `long`
@@ -39,7 +39,8 @@ module Kernel
         end
         uplevel = start
       end
-      original_warn.call(*messages, uplevel: uplevel)
+      kw[:uplevel] = uplevel
+      original_warn.call(*messages, **kw)
     }
   end
 end
diff --git a/lib/tempfile.rb b/lib/tempfile.rb
index 2bd124abea..f7caf65f0c 100644
--- a/lib/tempfile.rb
+++ b/lib/tempfile.rb
@@ -130,7 +130,7 @@ def initialize(basename="", tmpdir=nil, mode: 0, **options)
     @mode = mode|File::RDWR|File::CREAT|File::EXCL
     ::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
       opts[:perm] = 0600
-      @tmpfile = File.open(tmpname, @mode, opts)
+      @tmpfile = File.open(tmpname, @mode, **opts)
       @opts = opts.freeze
     end
     ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
@@ -142,7 +142,7 @@ def initialize(basename="", tmpdir=nil, mode: 0, **options)
   def open
     _close
     mode = @mode & ~(File::CREAT|File::EXCL)
-    @tmpfile = File.open(@tmpfile.path, mode, @opts)
+    @tmpfile = File.open(@tmpfile.path, mode, **@opts)
     __setobj__(@tmpfile)
   end
 
@@ -329,7 +329,7 @@ def Tempfile.create(basename="", tmpdir=nil, mode: 0, **options)
   Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
     mode |= File::RDWR|File::CREAT|File::EXCL
     opts[:perm] = 0600
-    tmpfile = File.open(tmpname, mode, opts)
+    tmpfile = File.open(tmpname, mode, **opts)
   end
   if block_given?
     begin
diff --git a/rational.c b/rational.c
index 0dfc8abe12..bd71c215ee 100644
--- a/rational.c
+++ b/rational.c
@@ -1543,7 +1543,7 @@ nurat_round_n(int argc, VALUE *argv, VALUE self)
 {
     VALUE opt;
     enum ruby_num_rounding_mode mode = (
-	argc = rb_scan_args(argc, argv, "*:", NULL, &opt),
+        argc = rb_scan_args(argc, argv, "*:", NULL, &opt),
 	rb_num_get_rounding_option(opt));
     VALUE (*round_func)(VALUE) = ROUND_FUNC(mode, nurat_round);
     return f_round_common(argc, argv, self, round_func);
diff --git a/test/-ext-/test_scan_args.rb b/test/-ext-/test_scan_args.rb
index cb2dab5760..7150ce25b7 100644
--- a/test/-ext-/test_scan_args.rb
+++ b/test/-ext-/test_scan_args.rb
@@ -94,6 +94,13 @@ def test_lead_hash
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_hash("a", "b")}
     assert_equal([1, "a", {b: 1}], Bug::ScanArgs.lead_hash("a", b: 1))
     assert_equal([1, {b: 1}, nil], Bug::ScanArgs.lead_hash(b: 1))
+    assert_equal([1, {"a"=>0, b: 1}, nil], Bug::ScanArgs.lead_hash({"a"=>0, b: 1}, **{}))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_raise(ArgumentError) {Bug::ScanArgs.lead_hash(1, {"a"=>0, b: 1}, **{})}
+    end
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, {}, nil], Bug::ScanArgs.lead_hash(**{}))
+    end
   end
 
   def test_opt_hash
@@ -102,7 +109,10 @@ def test_opt_hash
     assert_equal([0, nil, {b: 1}], Bug::ScanArgs.opt_hash(b: 1))
     assert_equal([1, "a", {b: 1}], Bug::ScanArgs.opt_hash("a", b: 1))
     assert_raise(ArgumentError) {Bug::ScanArgs.opt_hash("a", "b")}
-    assert_equal([1, {"a"=>0}, {b: 1}], Bug::ScanArgs.opt_hash("a"=>0, b: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([1, {"a"=>0}, {b: 1}], Bug::ScanArgs.opt_hash("a"=>0, b: 1))
+    end
+    assert_equal([1, {"a"=>0, b: 1}, nil], Bug::ScanArgs.opt_hash({"a"=>0, b: 1}, **{}))
   end
 
   def test_lead_opt_hash
@@ -110,9 +120,13 @@ def test_lead_opt_hash
     assert_equal([2, "a", "b", nil], Bug::ScanArgs.lead_opt_hash("a", "b"))
     assert_equal([1, "a", nil, {c: 1}], Bug::ScanArgs.lead_opt_hash("a", c: 1))
     assert_equal([2, "a", "b", {c: 1}], Bug::ScanArgs.lead_opt_hash("a", "b", c: 1))
-    assert_equal([1, {c: 1}, nil, nil], Bug::ScanArgs.lead_opt_hash(c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, {c: 1}, nil, nil], Bug::ScanArgs.lead_opt_hash(c: 1))
+    end
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_opt_hash("a", "b", "c")}
-    assert_equal([2, "a", {"b"=>0}, {c: 1}], Bug::ScanArgs.lead_opt_hash("a", "b"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([2, "a", {"b"=>0}, {c: 1}], Bug::ScanArgs.lead_opt_hash("a", "b"=>0, c: 1))
+    end
   end
 
   def test_var_hash
@@ -120,7 +134,9 @@ def test_var_hash
     assert_equal([1, ["a"], nil], Bug::ScanArgs.var_hash("a"))
     assert_equal([1, ["a"], {b: 1}], Bug::ScanArgs.var_hash("a", b: 1))
     assert_equal([0, [], {b: 1}], Bug::ScanArgs.var_hash(b: 1))
-    assert_equal([1, [{"a"=>0}], {b: 1}], Bug::ScanArgs.var_hash("a"=>0, b: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([1, [{"a"=>0}], {b: 1}], Bug::ScanArgs.var_hash("a"=>0, b: 1))
+    end
   end
 
   def test_lead_var_hash
@@ -129,9 +145,13 @@ def test_lead_var_hash
     assert_equal([2, "a", ["b"], nil], Bug::ScanArgs.lead_var_hash("a", "b"))
     assert_equal([2, "a", ["b"], {c: 1}], Bug::ScanArgs.lead_var_hash("a", "b", c: 1))
     assert_equal([1, "a", [], {c: 1}], Bug::ScanArgs.lead_var_hash("a", c: 1))
-    assert_equal([1, {c: 1}, [], nil], Bug::ScanArgs.lead_var_hash(c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, {c: 1}, [], nil], Bug::ScanArgs.lead_var_hash(c: 1))
+    end
     assert_equal([3, "a", ["b", "c"], nil], Bug::ScanArgs.lead_var_hash("a", "b", "c"))
-    assert_equal([2, "a", [{"b"=>0}], {c: 1}], Bug::ScanArgs.lead_var_hash("a", "b"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([2, "a", [{"b"=>0}], {c: 1}], Bug::ScanArgs.lead_var_hash("a", "b"=>0, c: 1))
+    end
   end
 
   def test_opt_var_hash
@@ -142,7 +162,9 @@ def test_opt_var_hash
     assert_equal([1, "a", [], {c: 1}], Bug::ScanArgs.opt_var_hash("a", c: 1))
     assert_equal([0, nil, [], {c: 1}], Bug::ScanArgs.opt_var_hash(c: 1))
     assert_equal([3, "a", ["b", "c"], nil], Bug::ScanArgs.opt_var_hash("a", "b", "c"))
-    assert_equal([2, "a", [{"b"=>0}], {c: 1}], Bug::ScanArgs.opt_var_hash("a", "b"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([2, "a", [{"b"=>0}], {c: 1}], Bug::ScanArgs.opt_var_hash("a", "b"=>0, c: 1))
+    end
   end
 
   def test_lead_opt_var_hash
@@ -151,10 +173,14 @@ def test_lead_opt_var_hash
     assert_equal([2, "a", "b", [], nil], Bug::ScanArgs.lead_opt_var_hash("a", "b"))
     assert_equal([2, "a", "b", [], {c: 1}], Bug::ScanArgs.lead_opt_var_hash("a", "b", c: 1))
     assert_equal([1, "a", nil, [], {c: 1}], Bug::ScanArgs.lead_opt_var_hash("a", c: 1))
-    assert_equal([1, {c: 1}, nil, [], nil], Bug::ScanArgs.lead_opt_var_hash(c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, {c: 1}, nil, [], nil], Bug::ScanArgs.lead_opt_var_hash(c: 1))
+    end
     assert_equal([3, "a", "b", ["c"], nil], Bug::ScanArgs.lead_opt_var_hash("a", "b", "c"))
     assert_equal([3, "a", "b", ["c"], {d: 1}], Bug::ScanArgs.lead_opt_var_hash("a", "b", "c", d: 1))
-    assert_equal([3, "a", "b", [{"c"=>0}], {d: 1}], Bug::ScanArgs.lead_opt_var_hash("a", "b", "c"=>0, d: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([3, "a", "b", [{"c"=>0}], {d: 1}], Bug::ScanArgs.lead_opt_var_hash("a", "b", "c"=>0, d: 1))
+    end
   end
 
   def test_opt_trail_hash
@@ -163,9 +189,13 @@ def test_opt_trail_hash
     assert_equal([2, "a", "b", nil], Bug::ScanArgs.opt_trail_hash("a", "b"))
     assert_equal([1, nil, "a", {c: 1}], Bug::ScanArgs.opt_trail_hash("a", c: 1))
     assert_equal([2, "a", "b", {c: 1}], Bug::ScanArgs.opt_trail_hash("a", "b", c: 1))
-    assert_equal([1, nil, {c: 1}, nil], Bug::ScanArgs.opt_trail_hash(c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, nil, {c: 1}, nil], Bug::ScanArgs.opt_trail_hash(c: 1))
+    end
     assert_raise(ArgumentError) {Bug::ScanArgs.opt_trail_hash("a", "b", "c")}
-    assert_equal([2, "a", {"b"=>0}, {c: 1}], Bug::ScanArgs.opt_trail_hash("a", "b"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([2, "a", {"b"=>0}, {c: 1}], Bug::ScanArgs.opt_trail_hash("a", "b"=>0, c: 1))
+    end
   end
 
   def test_lead_opt_trail_hash
@@ -173,12 +203,16 @@ def test_lead_opt_trail_hash
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_opt_trail_hash("a")}
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_opt_trail_hash(c: 1)}
     assert_equal([2, "a", nil, "b", nil], Bug::ScanArgs.lead_opt_trail_hash("a", "b"))
-    assert_equal([2, "a", nil, {c: 1}, nil], Bug::ScanArgs.lead_opt_trail_hash("a", c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([2, "a", nil, {c: 1}, nil], Bug::ScanArgs.lead_opt_trail_hash("a", c: 1))
+    end
     assert_equal([2, "a", nil, "b", {c: 1}], Bug::ScanArgs.lead_opt_trail_hash("a", "b", c: 1))
     assert_equal([3, "a", "b", "c", nil], Bug::ScanArgs.lead_opt_trail_hash("a", "b", "c"))
     assert_equal([3, "a", "b", "c", {c: 1}], Bug::ScanArgs.lead_opt_trail_hash("a", "b", "c", c: 1))
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_opt_trail_hash("a", "b", "c", "d")}
-    assert_equal([3, "a", "b", {"c"=>0}, {c: 1}], Bug::ScanArgs.lead_opt_trail_hash("a", "b", "c"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([3, "a", "b", {"c"=>0}, {c: 1}], Bug::ScanArgs.lead_opt_trail_hash("a", "b", "c"=>0, c: 1))
+    end
   end
 
   def test_var_trail_hash
@@ -187,45 +221,104 @@ def test_var_trail_hash
     assert_equal([2, ["a"], "b", nil], Bug::ScanArgs.var_trail_hash("a", "b"))
     assert_equal([1, [], "a", {c: 1}], Bug::ScanArgs.var_trail_hash("a", c: 1))
     assert_equal([2, ["a"], "b", {c: 1}], Bug::ScanArgs.var_trail_hash("a", "b", c: 1))
-    assert_equal([1, [], {c: 1}, nil], Bug::ScanArgs.var_trail_hash(c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, [], {c: 1}, nil], Bug::ScanArgs.var_trail_hash(c: 1))
+    end
     assert_equal([3, ["a", "b"], "c", nil], Bug::ScanArgs.var_trail_hash("a", "b", "c"))
     assert_equal([3, ["a", "b"], "c", {c: 1}], Bug::ScanArgs.var_trail_hash("a", "b", "c", c: 1))
-    assert_equal([3, ["a", "b"], {"c"=>0}, {c: 1}], Bug::ScanArgs.var_trail_hash("a", "b", "c"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([3, ["a", "b"], {"c"=>0}, {c: 1}], Bug::ScanArgs.var_trail_hash("a", "b", "c"=>0, c: 1))
+    end
   end
 
   def test_lead_var_trail_hash
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_var_trail_hash()}
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_var_trail_hash("a")}
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_var_trail_hash(c: 1)}
-    assert_equal([2, "a", [], {c: 1}, nil], Bug::ScanArgs.lead_var_trail_hash("a", c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([2, "a", [], {c: 1}, nil], Bug::ScanArgs.lead_var_trail_hash("a", c: 1))
+    end
     assert_equal([2, "a", [], "b", nil], Bug::ScanArgs.lead_var_trail_hash("a", "b"))
     assert_equal([2, "a", [], "b", {c: 1}], Bug::ScanArgs.lead_var_trail_hash("a", "b", c: 1))
     assert_equal([3, "a", ["b"], "c", nil], Bug::ScanArgs.lead_var_trail_hash("a", "b", "c"))
     assert_equal([3, "a", ["b"], "c", {c: 1}], Bug::ScanArgs.lead_var_trail_hash("a", "b", "c", c: 1))
-    assert_equal([3, "a", ["b"], {"c"=>0}, {c: 1}], Bug::ScanArgs.lead_var_trail_hash("a", "b", c: 1, "c"=>0))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([3, "a", ["b"], {"c"=>0}, {c: 1}], Bug::ScanArgs.lead_var_trail_hash("a", "b", c: 1, "c"=>0))
+    end
   end
 
   def test_opt_var_trail_hash
     assert_raise(ArgumentError) {Bug::ScanArgs.opt_var_trail_hash()}
     assert_equal([1, nil, [], "a", nil], Bug::ScanArgs.opt_var_trail_hash("a"))
-    assert_equal([1, nil, [], {c: 1}, nil], Bug::ScanArgs.opt_var_trail_hash(c: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, nil, [], {c: 1}, nil], Bug::ScanArgs.opt_var_trail_hash(c: 1))
+    end
     assert_equal([1, nil, [], "a", {c: 1}], Bug::ScanArgs.opt_var_trail_hash("a", c: 1))
     assert_equal([2, "a", [], "b", nil], Bug::ScanArgs.opt_var_trail_hash("a", "b"))
     assert_equal([2, "a", [], "b", {c: 1}], Bug::ScanArgs.opt_var_trail_hash("a", "b", c: 1))
     assert_equal([3, "a", ["b"], "c", nil], Bug::ScanArgs.opt_var_trail_hash("a", "b", "c"))
     assert_equal([3, "a", ["b"], "c", {c: 1}], Bug::ScanArgs.opt_var_trail_hash("a", "b", "c", c: 1))
-    assert_equal([3, "a", ["b"], {"c"=>0}, {c: 1}], Bug::ScanArgs.opt_var_trail_hash("a", "b", "c"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([3, "a", ["b"], {"c"=>0}, {c: 1}], Bug::ScanArgs.opt_var_trail_hash("a", "b", "c"=>0, c: 1))
+    end
   end
 
   def test_lead_opt_var_trail_hash
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_opt_var_trail_hash()}
     assert_raise(ArgumentError) {Bug::ScanArgs.lead_opt_var_trail_hash("a")}
-    assert_equal([2, "a", nil, [], {b: 1}, nil], Bug::ScanArgs.lead_opt_var_trail_hash("a", b: 1))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([2, "a", nil, [], {b: 1}, nil], Bug::ScanArgs.lead_opt_var_trail_hash("a", b: 1))
+    end
     assert_equal([2, "a", nil, [], "b", nil], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b"))
     assert_equal([2, "a", nil, [], "b", {c: 1}], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b", c: 1))
     assert_equal([3, "a", "b", [], "c", nil], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b", "c"))
     assert_equal([3, "a", "b", [], "c", {c: 1}], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b", "c", c: 1))
     assert_equal([4, "a", "b", ["c"], "d", nil], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b", "c", "d"))
-    assert_equal([4, "a", "b", ["c"], {"d"=>0}, {c: 1}], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b", "c", "d"=>0, c: 1))
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([4, "a", "b", ["c"], {"d"=>0}, {c: 1}], Bug::ScanArgs.lead_opt_var_trail_hash("a", "b", "c", "d"=>0, c: 1))
+    end
+  end
+
+  def test_k_lead_opt_hash
+    assert_raise(ArgumentError) {Bug::ScanArgs.k_lead_opt_hash}
+    assert_equal([1, "a", nil, {c: 1}], Bug::ScanArgs.k_lead_opt_hash("a", c: 1))
+    assert_equal([1, "a", nil, {c: 1}], Bug::ScanArgs.k_lead_opt_hash("a", {c: 1}))
+    assert_equal([2, "a", "b", {c: 1}], Bug::ScanArgs.k_lead_opt_hash("a", "b", c: 1))
+    assert_equal([2, "a", "b", {c: 1}], Bug::ScanArgs.k_lead_opt_hash("a", "b", {c: 1}))
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, {c: 1}, nil, nil], Bug::ScanArgs.k_lead_opt_hash(c: 1))
+    end
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([2, "a", {"b"=>0}, {c: 1}], Bug::ScanArgs.k_lead_opt_hash("a", "b"=>0, c: 1))
+    end
+  end
+
+  def test_e_lead_opt_hash
+    assert_warn(/The keyword argument is passed as the last hash parameter/) do
+      assert_equal([1, {}, nil, nil], Bug::ScanArgs.e_lead_opt_hash)
+    end
+    assert_equal([1, "a", nil, nil], Bug::ScanArgs.e_lead_opt_hash("a"))
+    assert_equal([2, "a", "b", nil], Bug::ScanArgs.e_lead_opt_hash("a", "b"))
+    assert_equal([2, "a", {c: 1}, nil], Bug::ScanArgs.e_lead_opt_hash("a", c: 1))
+    assert_raise(ArgumentError) {Bug::ScanArgs.e_lead_opt_hash("a", "b", c: 1)}
+    assert_equal([1, {c: 1}, nil, nil], Bug::ScanArgs.e_lead_opt_hash(c: 1))
+    assert_raise(ArgumentError) {Bug::ScanArgs.e_lead_opt_hash("a", "b", "c")}
+    assert_equal([2, "a", {"b"=>0, c: 1}, nil], Bug::ScanArgs.e_lead_opt_hash("a", "b"=>0, c: 1))
+  end
+
+  def test_n_lead_opt_hash
+    assert_raise(ArgumentError) {Bug::ScanArgs.n_lead_opt_hash}
+    assert_equal([1, "a", nil, nil], Bug::ScanArgs.n_lead_opt_hash("a"))
+    assert_equal([2, "a", "b", nil], Bug::ScanArgs.n_lead_opt_hash("a", "b"))
+    assert_equal([1, "a", nil, {c: 1}], Bug::ScanArgs.n_lead_opt_hash("a", c: 1))
+    assert_equal([1, "a", nil, {c: 1}], Bug::ScanArgs.n_lead_opt_hash("a", {c: 1}))
+    assert_equal([2, "a", "b", {c: 1}], Bug::ScanArgs.n_lead_opt_hash("a", "b", c: 1))
+    assert_equal([2, "a", "b", {c: 1}], Bug::ScanArgs.n_lead_opt_hash("a", "b", {c: 1}))
+    assert_equal([1, {c: 1}, nil, nil], Bug::ScanArgs.n_lead_opt_hash(c: 1))
+    assert_equal([1, {c: 1}, nil, nil], Bug::ScanArgs.n_lead_opt_hash({c: 1}))
+    assert_raise(ArgumentError) {Bug::ScanArgs.n_lead_opt_hash("a", "b", "c")}
+    assert_warn(/The last argument is split into positional and keyword parameters/) do
+      assert_equal([2, "a", {"b"=>0}, {c: 1}], Bug::ScanArgs.n_lead_opt_hash("a", "b"=>0, c: 1))
+    end
   end
 end
diff --git a/test/pathname/test_pathname.rb b/test/pathname/test_pathname.rb
index eaecc52475..02d25d15c8 100644
--- a/test/pathname/test_pathname.rb
+++ b/test/pathname/test_pathname.rb
@@ -927,7 +927,7 @@ def test_open
       assert_equal(0444 & ~File.umask, File.stat("b").mode & 0777)
       assert_equal("def", File.read("b"))
 
-      Pathname("c").open("w", 0444, {}) {|f| f.write "ghi" }
+      Pathname("c").open("w", 0444, **{}) {|f| f.write "ghi" }
       assert_equal(0444 & ~File.umask, File.stat("c").mode & 0777)
       assert_equal("ghi", File.read("c"))
 
diff --git a/test/reline/helper.rb b/test/reline/helper.rb
index ca0001258d..b1759f1d80 100644
--- a/test/reline/helper.rb
+++ b/test/reline/helper.rb
@@ -31,7 +31,7 @@ class Reline::TestCase < Test::Unit::TestCase
       if Reline::Unicode::EscapedChars.include?(c.ord)
         c
       else
-        c.encode(@line_editor.instance_variable_get(:@encoding), Encoding::UTF_8, options)
+        c.encode(@line_editor.instance_variable_get(:@encoding), Encoding::UTF_8, **options)
       end
     }.join
   rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
diff --git a/test/ruby/test_dir_m17n.rb b/test/ruby/test_dir_m17n.rb
index 7584074c7e..90bf8b399f 100644
--- a/test/ruby/test_dir_m17n.rb
+++ b/test/ruby/test_dir_m17n.rb
@@ -18,7 +18,7 @@ def assert_raw_file_name(code, encoding)
         filename = #{code}.chr('UTF-8').force_encoding("#{encoding}")
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename)
       EOS
 
@@ -26,7 +26,7 @@ def assert_raw_file_name(code, encoding)
       assert_separately(%w[-EASCII-8BIT], <<-EOS, :chdir=>dir)
         filename = #{code}.chr('UTF-8').force_encoding("ASCII-8BIT")
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         expected_filename = #{code}.chr('UTF-8').encode(Encoding.find("filesystem")) rescue expected_filename = "?"
         expected_filename = expected_filename.force_encoding("ASCII-8BIT")
         if /mswin|mingw/ =~ RUBY_PLATFORM
@@ -52,7 +52,7 @@ def test_filename_extutf8
         filename = "\u3042"
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename)
       EOS
     }
@@ -67,7 +67,7 @@ def test_filename_extutf8_invalid
         filename = "\xff".force_encoding("ASCII-8BIT") # invalid byte sequence as UTF-8
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         filename = "%FF" if /darwin/ =~ RUBY_PLATFORM && ents.include?("%FF")
         assert_include(ents, filename)
       EOS
@@ -75,7 +75,7 @@ def test_filename_extutf8_invalid
         filename = "\xff".force_encoding("UTF-8") # invalid byte sequence as UTF-8
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         filename = "%FF" if /darwin/ =~ RUBY_PLATFORM && ents.include?("%FF")
         assert_include(ents, filename)
       EOS
@@ -88,7 +88,7 @@ def test_filename_as_bytes_extutf8
         filename = "\xc2\xa1".force_encoding("utf-8")
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename)
       EOS
       assert_separately(%w[-EUTF-8], <<-'EOS', :chdir=>d)
@@ -125,13 +125,13 @@ def test_filename_extutf8_inteucjp_representable
         filename = "\u3042"
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename)
       EOS
       assert_separately(%w[-EUTF-8:EUC-JP], <<-'EOS', :chdir=>d)
         filename = "\xA4\xA2".force_encoding("euc-jp")
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename)
       EOS
       assert_separately(%w[-EUTF-8:EUC-JP], <<-'EOS', :chdir=>d)
@@ -151,7 +151,7 @@ def test_filename_extutf8_inteucjp_unrepresentable
         File.open(filename1, "w") {}
         File.open(filename2, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename1)
         assert_include(ents, filename2)
       EOS
@@ -159,7 +159,7 @@ def test_filename_extutf8_inteucjp_unrepresentable
         filename1 = "\u2661" # WHITE HEART SUIT which is not representable in EUC-JP
         filename2 = "\xA4\xA2".force_encoding("euc-jp") # HIRAGANA LETTER A in EUC-JP
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         assert_include(ents, filename1)
         assert_include(ents, filename2)
       EOS
@@ -183,7 +183,7 @@ def test_filename_bytes_euc_jp
         filename = "\xA4\xA2".force_encoding("euc-jp")
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         ents.each {|e| e.force_encoding("ASCII-8BIT") }
         if /darwin/ =~ RUBY_PLATFORM
           filename = filename.encode("utf-8")
@@ -200,7 +200,7 @@ def test_filename_euc_jp
         filename = "\xA4\xA2".force_encoding("euc-jp")
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         if /darwin/ =~ RUBY_PLATFORM
           filename = filename.encode("utf-8").force_encoding("euc-jp")
         end
@@ -210,7 +210,7 @@ def test_filename_euc_jp
         filename = "\xA4\xA2".force_encoding('ASCII-8BIT')
         win_expected_filename = filename.encode(Encoding.find("filesystem"), "euc-jp") rescue "?"
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         unless ents.include?(filename)
           case RUBY_PLATFORM
           when /darwin/
@@ -246,7 +246,7 @@ def test_filename_ext_euc_jp_and_int_utf_8
         filename = "\xA4\xA2".force_encoding("euc-jp")
         File.open(filename, "w") {}
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         if /darwin/ =~ RUBY_PLATFORM
           filename = filename.encode("utf-8", "euc-jp").force_encoding("euc-jp")
         end
@@ -255,7 +255,7 @@ def test_filename_ext_euc_jp_and_int_utf_8
       assert_separately(%w[-EEUC-JP:UTF-8], <<-'EOS', :chdir=>d)
         filename = "\u3042"
         opts = {:encoding => Encoding.default_external} if /mswin|mingw/ =~ RUBY_PLATFORM
-        ents = Dir.entries(".", opts)
+        ents = Dir.entries(".", opts||{})
         if /darwin/ =~ RUBY_PLATFORM
           filename = filename.force_encoding("euc-jp")
         end
@@ -420,7 +420,7 @@ def test_entries_compose
       else
         orig.each {|o| o.force_encoding(enc) }
       end
-      ents = Dir.entries(".", opts).reject {|n| /\A\./ =~ n}
+      ents = Dir.entries(".", opts||{}).reject {|n| /\A\./ =~ n}
       ents.sort!
       PP.assert_equal(orig, ents, bug7267)
     }
diff --git a/test/ruby/test_econv.rb b/test/ruby/test_econv.rb
index 6f098db454..115ff73ea8 100644
--- a/test/ruby/test_econv.rb
+++ b/test/ruby/test_econv.rb
@@ -3,7 +3,12 @@
 
 class TestEncodingConverter < Test::Unit::TestCase
   def check_ec(edst, esrc, eres, dst, src, ec, off, len, opts=nil)
-    res = ec.primitive_convert(src, dst, off, len, opts)
+    case opts
+    when Hash
+      res = ec.primitive_convert(src, dst, off, len, **opts)
+    else
+      res = ec.primitive_convert(src, dst, off, len, opts)
+    end
     assert_equal([edst.b, esrc.b, eres],
                  [dst.b,  src.b,  res])
   end
diff --git a/test/ruby/test_exception.rb b/test/ruby/test_exception.rb
index 9de1b2d82c..e5c38091ac 100644
--- a/test/ruby/test_exception.rb
+++ b/test/ruby/test_exception.rb
@@ -1223,6 +1223,14 @@ def (obj = Object.new).w(n) warn("test warning", uplevel: n) end
     assert_equal("#{__FILE__}:#{__LINE__-1}: warning: test warning\n", warning[0])
     assert_raise(ArgumentError) {warn("test warning", uplevel: -1)}
     assert_in_out_err(["-e", "warn 'ok', uplevel: 1"], '', [], /warning:/)
+    warning = capture_warning_warn {warn("test warning", {uplevel: 0})}
+    assert_equal("#{__FILE__}:#{__LINE__-1}: warning: The last argument is used as the keyword parameter\n", warning[0])
+    assert_match(/warning: for method defined here|warning: test warning/, warning[1])
+    warning = capture_warning_warn {warn("test warning", **{uplevel: 0})}
+    assert_equal("#{__FILE__}:#{__LINE__-1}: warning: test warning\n", warning[0])
+    warning = capture_warning_warn {warn("test warning", {uplevel: 0}, **{})}
+    assert_equal("test warning\n{:uplevel=>0}\n", warning[0])
+    assert_raise(ArgumentError) {warn("test warning", foo: 1)}
   end
 
   def test_warning_warn_invalid_argument
diff --git a/test/ruby/test_io.rb b/test/ruby/test_io.rb
index ed60452b36..a610b608aa 100644
--- a/test/ruby/test_io.rb
+++ b/test/ruby/test_io.rb
@@ -423,7 +423,7 @@ def test_rubydev33072
     path = t.path
     t.close!
     assert_raise(Errno::ENOENT, "[ruby-dev:33072]") do
-      File.read(path, nil, nil, {})
+      File.read(path, nil, nil, **{})
     end
   end
 
@@ -2489,15 +2489,15 @@ def test_foreach
       assert_equal(["foo\n", "bar\n", "baz\n"], a)
 
       a = []
-      IO.foreach(t.path, {:mode => "r" }) {|x| a << x }
+      IO.foreach(t.path, :mode => "r") {|x| a << x }
       assert_equal(["foo\n", "bar\n", "baz\n"], a)
 
       a = []
-      IO.foreach(t.path, {:open_args => [] }) {|x| a << x }
+      IO.foreach(t.path, :open_args => []) {|x| a << x }
       assert_equal(["foo\n", "bar\n", "baz\n"], a)
 
       a = []
-      IO.foreach(t.path, {:open_args => ["r"] }) {|x| a << x }
+      IO.foreach(t.path, :open_args => ["r"]) {|x| a << x }
       assert_equal(["foo\n", "bar\n", "baz\n"], a)
 
       a = []
@@ -3119,7 +3119,9 @@ def test_s_write
       assert_equal(1, File.write(path, "f", 0, encoding: "UTF-8"))
       assert_equal("ff", File.read(path))
       assert_raise(TypeError) {
-        File.write(path, "foo", Object.new => Object.new)
+        assert_warn(/The last argument is split into positional and keyword parameters/) do
+          File.write(path, "foo", Object.new => Object.new)
+        end
       }
     end
   end
diff --git a/test/ruby/test_io_m17n.rb b/test/ruby/test_io_m17n.rb
index 8101bfb62f..022ff330b5 100644
--- a/test/ruby/test_io_m17n.rb
+++ b/test/ruby/test_io_m17n.rb
@@ -23,7 +23,8 @@ def with_tmpdir
 
   def pipe(*args, wp, rp)
     re, we = nil, nil
-    r, w = IO.pipe(*args)
+    kw = args.last.is_a?(Hash) ? args.pop : {}
+    r, w = IO.pipe(*args, **kw)
     rt = Thread.new do
       begin
         rp.call(r)
diff --git a/test/ruby/test_literal.rb b/test/ruby/test_literal.rb
index 3f87f860b1..7f4a329c4a 100644
--- a/test/ruby/test_literal.rb
+++ b/test/ruby/test_literal.rb
@@ -189,7 +189,7 @@ def test_frozen_string_in_array_literal
     def test_debug_frozen_string
       src = 'n = 1; _="foo#{n ? "-#{n}" : ""}"'; f = "test.rb"; n = 1
       opt = {frozen_string_literal: true, debug_frozen_string_literal: true}
-      str = RubyVM::InstructionSequence.compile(src, f, f, n, opt).eval
+      str = RubyVM::InstructionSequence.compile(src, f, f, n, **opt).eval
       assert_equal("foo-1", str)
       assert_predicate(str, :frozen?)
       assert_raise_with_message(FrozenError, /created at #{Regexp.quote(f)}:#{n}/) {
@@ -200,7 +200,7 @@ def test_debug_frozen_string
     def test_debug_frozen_string_in_array_literal
       src = '["foo"]'; f = "test.rb"; n = 1
       opt = {frozen_string_literal: true, debug_frozen_string_literal: true}
-      ary = RubyVM::InstructionSequence.compile(src, f, f, n, opt).eval
+      ary = RubyVM::InstructionSequence.compile(src, f, f, n, **opt).eval
       assert_equal("foo", ary.first)
       assert_predicate(ary.first, :frozen?)
       assert_raise_with_message(FrozenError, /created at #{Regexp.quote(f)}:#{n}/) {
diff --git a/test/ruby/test_numeric.rb b/test/ruby/test_numeric.rb
index 6073ec1ee5..f87abbb4a8 100644
--- a/test/ruby/test_numeric.rb
+++ b/test/ruby/test_numeric.rb
@@ -230,7 +230,8 @@ def to_f; -1.5; end
   end
 
   def assert_step(expected, (from, *args), inf: false)
-    enum = from.step(*args)
+    kw = args.last.is_a?(Hash) ? args.pop : {}
+    enum = from.step(*args, **kw)
     size = enum.size
     xsize = expected.size
 
@@ -239,7 +240,7 @@ def assert_step(expected, (from, *args), inf: false)
       assert_send [size, :>, 0], "step size: +infinity"
 
       a = []
-      from.step(*args) { |x| a << x; break if a.size == xsize }
+      from.step(*args, **kw) { |x| a << x; break if a.size == xsize }
       assert_equal expected, a, "step"
 
       a = []
@@ -249,7 +250,7 @@ def assert_step(expected, (from, *args), inf: false)
       assert_equal expected.size, size, "step size"
 
       a = []
-      from.step(*args) { |x| a << x }
+      from.step(*args, **kw) { |x| a << x }
       assert_equal expected, a, "step"
 
       a = []
diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb
index 36d246f9e0..dd403bf9d9 100644
--- a/test/ruby/test_string.rb
+++ b/test/ruby/test_string.rb
@@ -17,8 +17,8 @@ def initialize(*args)
     super
   end
 
-  def S(*args)
-    @cls.new(*args)
+  def S(*args, **kw)
+    @cls.new(*args, **kw)
   end
 
   def test_s_new
-- 
2.22.0

