Skip to content

Commit a668f4b

Browse files
JunichiItonobu
authored andcommitted
Improve missing keyword detection in Data#initialize
- Treat symbol and string keys equivalently when detecting missing keywords - Consider index keys when detecting missing keywords in Data#initialize
1 parent 72eb59d commit a668f4b

2 files changed

Lines changed: 58 additions & 8 deletions

File tree

struct.c

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ num_members(VALUE klass)
716716
struct struct_hash_set_arg {
717717
VALUE self;
718718
VALUE unknown_keywords;
719+
VALUE found_keywords;
719720
};
720721

721722
static int rb_struct_pos(VALUE s, VALUE *name);
@@ -724,6 +725,8 @@ static int
724725
struct_hash_set_i(VALUE key, VALUE val, VALUE arg)
725726
{
726727
struct struct_hash_set_arg *args = (struct struct_hash_set_arg *)arg;
728+
VALUE klass = rb_obj_class(args->self);
729+
VALUE members = struct_ivar_get(klass, id_members);
727730
int i = rb_struct_pos(args->self, &key);
728731
if (i < 0) {
729732
if (NIL_P(args->unknown_keywords)) {
@@ -734,6 +737,12 @@ struct_hash_set_i(VALUE key, VALUE val, VALUE arg)
734737
else {
735738
rb_struct_modify(args->self);
736739
RSTRUCT_SET_RAW(args->self, i, val);
740+
741+
if (NIL_P(args->found_keywords)) {
742+
args->found_keywords = rb_hash_new();
743+
}
744+
VALUE member = rb_ary_entry(members, i);
745+
rb_hash_aset(args->found_keywords, member, Qtrue);
737746
}
738747
return ST_CONTINUE;
739748
}
@@ -771,6 +780,7 @@ rb_struct_initialize_m(int argc, const VALUE *argv, VALUE self)
771780
rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), n);
772781
arg.self = self;
773782
arg.unknown_keywords = Qnil;
783+
arg.found_keywords = Qnil;
774784
rb_hash_foreach(argv[0], struct_hash_set_i, (VALUE)&arg);
775785
if (arg.unknown_keywords != Qnil) {
776786
rb_raise(rb_eArgError, "unknown keywords: %s",
@@ -1817,19 +1827,21 @@ rb_data_initialize_m(int argc, const VALUE *argv, VALUE self)
18171827
rb_error_arity(argc, 0, 0);
18181828
}
18191829

1820-
if (RHASH_SIZE(argv[0]) < num_members) {
1821-
VALUE missing = rb_ary_diff(members, rb_hash_keys(argv[0]));
1822-
rb_exc_raise(rb_keyword_error_new("missing", missing));
1823-
}
1824-
18251830
struct struct_hash_set_arg arg;
18261831
rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), num_members);
18271832
arg.self = self;
18281833
arg.unknown_keywords = Qnil;
1834+
arg.found_keywords = Qnil;
18291835
rb_hash_foreach(argv[0], struct_hash_set_i, (VALUE)&arg);
18301836
// Freeze early before potentially raising, so that we don't leave an
18311837
// unfrozen copy on the heap, which could get exposed via ObjectSpace.
18321838
OBJ_FREEZE(self);
1839+
if (arg.found_keywords != Qnil) {
1840+
VALUE missing = rb_ary_diff(members, rb_hash_keys(arg.found_keywords));
1841+
if (RARRAY_LEN(missing) > 0) {
1842+
rb_exc_raise(rb_keyword_error_new("missing", missing));
1843+
}
1844+
}
18331845
if (arg.unknown_keywords != Qnil) {
18341846
rb_exc_raise(rb_keyword_error_new("unknown", arg.unknown_keywords));
18351847
}

test/ruby/test_data.rb

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,53 @@ def test_initialize
6969
assert_equal(1, test_kw.foo)
7070
assert_equal(2, test_kw.bar)
7171
assert_equal(test_kw, klass.new(foo: 1, bar: 2))
72+
assert_equal(test_kw, klass.new('foo' => 1, 'bar' => 2))
73+
assert_equal(test_kw, klass.new(0 => 1, 1 => 2))
74+
assert_equal(test_kw, klass.new(-2 => 1, -1 => 2))
75+
assert_equal(test_kw, klass.new(0.1 => 1, 1.1 => 2))
76+
assert_equal(test_kw, klass.new(-2.1 => 1, -1.1 => 2))
77+
assert_equal(test_kw, klass.new(0 => 1, bar: 2))
78+
assert_equal(test_kw, klass.new(foo: 1, -1 => 2))
79+
assert_equal(test_kw, klass.new(foo: 0, bar: 2, 0 => 1))
7280
assert_equal(test_kw, test)
7381

7482
# Wrong protocol
7583
assert_raise(ArgumentError) { klass.new(1) }
7684
assert_raise(ArgumentError) { klass.new(1, 2, 3) }
77-
assert_raise(ArgumentError) { klass.new(foo: 1) }
78-
assert_raise(ArgumentError) { klass.new(foo: 1, bar: 2, baz: 3) }
85+
assert_raise_with_message(ArgumentError, "missing keyword: :bar") do
86+
klass.new(foo: 1)
87+
end
88+
assert_raise_with_message(ArgumentError, "missing keyword: :bar") do
89+
klass.new('foo' => 1)
90+
end
91+
assert_raise_with_message(ArgumentError, "missing keyword: :bar") do
92+
klass.new(foo: 1, 'foo' => 1)
93+
end
94+
assert_raise_with_message(ArgumentError, "missing keyword: :bar") do
95+
klass.new(foo: 1, 0 => 1)
96+
end
97+
assert_raise_with_message(ArgumentError, "unknown keywords: :x, :y") do
98+
klass.new(x: 1, y: 2)
99+
end
100+
assert_raise_with_message(ArgumentError, "missing keyword: :foo") do
101+
klass.new(1 => 1)
102+
end
103+
assert_raise_with_message(ArgumentError, "unknown keyword: :baz") do
104+
klass.new(foo: 1, bar: 2, baz: 3)
105+
end
106+
assert_raise_with_message(ArgumentError, "unknown keyword: 2") do
107+
klass.new(0 => 1, 1 => 2, 2 => 3)
108+
end
109+
assert_raise_with_message(ArgumentError, "unknown keyword: -3") do
110+
klass.new(-2 => 1, -1 => 2, -3 => 3)
111+
end
112+
assert_raise_with_message(ArgumentError, "unknown keyword: 2") do
113+
klass.new(0 => 1, 1 => 2, 2.1 => 3)
114+
end
79115
# Could be converted to foo: 1, bar: 2, but too smart is confusing
80-
assert_raise(ArgumentError) { klass.new(1, bar: 2) }
116+
assert_raise_with_message(ArgumentError, "wrong number of arguments (given 2, expected 0)") do
117+
klass.new(1, bar: 2)
118+
end
81119
end
82120

83121
def test_initialize_redefine

0 commit comments

Comments
 (0)