From e8f4687837e2aa9847786c8c59ef089c80652503 Mon Sep 17 00:00:00 2001 From: Jeremy Evans 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 6b3b6623f1d071..645d78ae8e727b 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 87357a4ebe065e..bef509001170a4 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 dca353f6436308..7593e57b7150f4 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 70f82583a1b42b..52ddda1f8fec81 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 4a81e8418308ea..ab533a03fb0f7d 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 c438b0aea724f4..258df8d60f3826 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 3dbe9ad38e6316..76cf49c5d19f8f 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 2520725814df3d..ffa484f0b82843 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 61702137d0bf87..60dbbcc23008e9 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 d9517e4b9e3b33..f4cb53600688fd 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 4daa2e003f4dff..1180fd50a0a584 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 3e531441edc385..6c5b387e88c088 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 2bd124abeaa4f7..f7caf65f0cb000 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 0dfc8abe120954..bd71c215eeb846 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 cb2dab57603274..7150ce25b7d8fe 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 eaecc5247543ff..02d25d15c8c4c1 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 ca0001258d6f80..b1759f1d80ff03 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 7584074c7ed359..90bf8b399fc7ea 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 6f098db4545e10..115ff73ea8747e 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 9de1b2d82c9b96..e5c38091ac5d73 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 ed60452b36b5b6..a610b608aa1c0f 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 8101bfb62f3a48..022ff330b53f94 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 3f87f860b1be7b..7f4a329c4a4aa2 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 6073ec1ee58068..f87abbb4a8d89e 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 36d246f9e05fc9..dd403bf9d9f62b 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