diff --git a/core/hash/hash_spec.rb b/core/hash/hash_spec.rb index 19eb806dc..3649d4d8d 100644 --- a/core/hash/hash_spec.rb +++ b/core/hash/hash_spec.rb @@ -41,13 +41,4 @@ h.hash.should == {x: [h]}.hash # Like above, because h.eql?(x: [h]) end - - ruby_version_is "3.1" do - it "allows omitting values" do - a = 1 - b = 2 - - eval('{a:, b:}.should == { a: 1, b: 2 }') - end - end end diff --git a/language/hash_spec.rb b/language/hash_spec.rb index 068ac0f39..acdd8f299 100644 --- a/language/hash_spec.rb +++ b/language/hash_spec.rb @@ -4,12 +4,12 @@ require_relative 'fixtures/hash_strings_usascii' describe "Hash literal" do - it "{} should return an empty hash" do + it "{} should return an empty Hash" do {}.size.should == 0 {}.should == {} end - it "{} should return a new hash populated with the given elements" do + it "{} should return a new Hash populated with the given elements" do h = {a: 'a', 'b' => 3, 44 => 2.3} h.size.should == 3 h.should == {a: "a", "b" => 3, 44 => 2.3} @@ -110,7 +110,7 @@ -> { eval("{:a?=> 1}") }.should raise_error(SyntaxError) end - it "constructs a new hash with the given elements" do + it "constructs a new Hash with the given elements" do {foo: 123}.should == {foo: 123} h = {rbx: :cool, specs: 'fail_sometimes'} {rbx: :cool, specs: 'fail_sometimes'}.should == h @@ -232,6 +232,184 @@ def h.to_hash; {:b => 2, :c => 3}; end ScratchPad.recorded.should == [] end end + + describe "with omitted values" do # a.k.a. "Hash punning" or "Shorthand Hash syntax" + it "accepts short notation 'key' for 'key: value' syntax" do + a, b, c = 1, 2, 3 + h = eval('{a:}') + {a: 1}.should == h + h = eval('{a:, b:, c:}') + {a: 1, b: 2, c: 3}.should == h + end + + it "ignores hanging comma on short notation" do + a, b, c = 1, 2, 3 + h = eval('{a:, b:, c:,}') + {a: 1, b: 2, c: 3}.should == h + end + + it "accepts mixed syntax" do + a, e = 1, 5 + h = eval('{a:, b: 2, "c" => 3, :d => 4, e:}') + eval('{a: 1, :b => 2, "c" => 3, "d": 4, e: 5}').should == h + end + + # Copied from Prism::Translation::Ripper + keywords = [ + "alias", + "and", + "begin", + "BEGIN", + "break", + "case", + "class", + "def", + "defined?", + "do", + "else", + "elsif", + "end", + "END", + "ensure", + "false", + "for", + "if", + "in", + "module", + "next", + "nil", + "not", + "or", + "redo", + "rescue", + "retry", + "return", + "self", + "super", + "then", + "true", + "undef", + "unless", + "until", + "when", + "while", + "yield", + "__ENCODING__", + "__FILE__", + "__LINE__" + ] + + invalid_kw_param_names = [ + "BEGIN", + "END", + "defined?", + ] + + invalid_method_names = [ + "BEGIN", + "END", + "defined?", + ] + + it "can resolve local variables" do + a = 1 + b = 2 + + eval('{ a:, b: }.should == { a: 1, b: 2 }') + end + + it "cannot find dynamically defined local variables" do + b = binding + b.local_variable_set(:abc, "a dynamically defined local var") + + eval <<~RUBY + # The local variable definitely exists: + b.local_variable_get(:abc).should == "a dynamically defined local var" + # but we can't get it via value omission: + -> { { abc: } }.should raise_error(NameError) + RUBY + end + + it "can call methods" do + result = sandboxed_eval <<~RUBY + def m = "a statically defined method" + + { m: } + RUBY + + result.should == { m: "a statically defined method" } + end + + it "can call dynamically defined methods" do + result = sandboxed_eval <<~RUBY + define_method(:m) { "a dynamically defined method" } + + { m: } + RUBY + + result.should == { m: "a dynamically defined method" } + end + + it "prefers local variables over methods" do + result = sandboxed_eval <<~RUBY + x = "from a local var" + def x; "from a method"; end + { x: } + RUBY + + result.should == { x: "from a local var" } + end + + describe "handling keywords" do + keywords.each do |kw| + describe "keyword '#{kw}'" do + # None of these keywords can be used as local variables, + # so it's not possible to resolve them via shorthand Hash syntax. + # See `reserved_keywords.rb` + + unless invalid_kw_param_names.include?(kw) + it "can resolve to a parameter whose name is a keyword" do + result = sandboxed_eval <<~RUBY + def m(#{kw}:) = { #{kw}: } + + m(#{kw}: "an argument to '#{kw}'") + RUBY + + result.should == { kw.to_sym => "an argument to '#{kw}'" } + end + end + + unless invalid_method_names.include?(kw) + it "can resolve to a method whose name is a keyword" do + result = sandboxed_eval <<~RUBY + def #{kw} = "a method named '#{kw}'" + + { #{kw}: } + RUBY + + result.should == { kw.to_sym => "a method named '#{kw}'" } + end + end + end + end + + describe "keyword 'self:'" do + it "does not refer to actual 'self'" do + eval <<~RUBY + -> { { self: } }.should raise_error(NameError) + RUBY + end + end + end + + it "raises a SyntaxError when the Hash key ends with `!`" do + -> { eval("{a!:}") }.should raise_error(SyntaxError, /identifier a! is not valid to get/) + end + + it "raises a SyntaxError when the Hash key ends with `?`" do + -> { eval("{a?:}") }.should raise_error(SyntaxError, /identifier a\? is not valid to get/) + end + end end describe "The ** operator" do @@ -258,51 +436,4 @@ def m(h) h.should == { one: 1, two: 2 } end end - - ruby_version_is "3.1" do - describe "hash with omitted value" do - it "accepts short notation 'key' for 'key: value' syntax" do - a, b, c = 1, 2, 3 - h = eval('{a:}') - {a: 1}.should == h - h = eval('{a:, b:, c:}') - {a: 1, b: 2, c: 3}.should == h - end - - it "ignores hanging comma on short notation" do - a, b, c = 1, 2, 3 - h = eval('{a:, b:, c:,}') - {a: 1, b: 2, c: 3}.should == h - end - - it "accepts mixed syntax" do - a, e = 1, 5 - h = eval('{a:, b: 2, "c" => 3, :d => 4, e:}') - eval('{a: 1, :b => 2, "c" => 3, "d": 4, e: 5}').should == h - end - - it "works with methods and local vars" do - a = Class.new - a.class_eval(<<-RUBY) - def bar - "baz" - end - - def foo(val) - {bar:, val:} - end - RUBY - - a.new.foo(1).should == {bar: "baz", val: 1} - end - - it "raises a SyntaxError when the hash key ends with `!`" do - -> { eval("{a!:}") }.should raise_error(SyntaxError, /identifier a! is not valid to get/) - end - - it "raises a SyntaxError when the hash key ends with `?`" do - -> { eval("{a?:}") }.should raise_error(SyntaxError, /identifier a\? is not valid to get/) - end - end - end end diff --git a/language/reserved_keywords.rb b/language/reserved_keywords.rb new file mode 100644 index 000000000..ca0f368a9 --- /dev/null +++ b/language/reserved_keywords.rb @@ -0,0 +1,155 @@ +require_relative '../spec_helper' + +describe "Ruby's reserved keywords" do + # Copied from Prism::Translation::Ripper + keywords = [ + "alias", + "and", + "begin", + "BEGIN", + "break", + "case", + "class", + "def", + "defined?", + "do", + "else", + "elsif", + "end", + "END", + "ensure", + "false", + "for", + "if", + "in", + "module", + "next", + "nil", + "not", + "or", + "redo", + "rescue", + "retry", + "return", + "self", + "super", + "then", + "true", + "undef", + "unless", + "until", + "when", + "while", + "yield", + "__ENCODING__", + "__FILE__", + "__LINE__" + ] + + keywords.each do |kw| + describe "keyword '#{kw}'" do + it "can't be used as local variable name" do + expect_syntax_error <<~RUBY + #{kw} = "a local variable named '#{kw}'" + RUBY + end + + invalid_ivar_names = ["defined?"] + + if invalid_ivar_names.include?(kw) + it "can't be used as an instance variable name" do + expect_syntax_error <<~RUBY + @#{kw} = "an instance variable named '#{kw}'" + RUBY + end + else + it "can be used as an instance variable name" do + result = sandboxed_eval <<~RUBY + @#{kw} = "an instance variable named '#{kw}'" + @#{kw} + RUBY + + result.should == "an instance variable named '#{kw}'" + end + end + + invalid_class_var_names = ["defined?"] + + if invalid_class_var_names.include?(kw) + it "can't be used as a class variable name" do + expect_syntax_error <<~RUBY + @@#{kw} = "a class variable named '#{kw}'" + RUBY + end + else + it "can be used as a class variable name" do + result = sandboxed_eval <<~RUBY + @@#{kw} = "a class variable named '#{kw}'" + @@#{kw} + RUBY + + result.should == "a class variable named '#{kw}'" + end + end + + invalid_global_var_names = ["defined?"] + + if invalid_global_var_names.include?(kw) + it "can't be used as a global variable name" do + expect_syntax_error <<~RUBY + $#{kw} = "a global variable named '#{kw}'" + RUBY + end + else + it "can be used as a global variable name" do + result = sandboxed_eval <<~RUBY + $#{kw} = "a global variable named '#{kw}'" + $#{kw} + RUBY + + result.should == "a global variable named '#{kw}'" + end + end + + it "can't be used as a positional parameter name" do + expect_syntax_error <<~RUBY + def x(#{kw}); end + RUBY + end + + invalid_kw_param_names = ["BEGIN","END","defined?"] + + if invalid_kw_param_names.include?(kw) + it "can't be used a keyword parameter name" do + expect_syntax_error <<~RUBY + def m(#{kw}:); end + RUBY + end + else + it "can be used a keyword parameter name" do + result = sandboxed_eval <<~RUBY + def m(#{kw}:) + binding.local_variable_get(:#{kw}) + end + + m(#{kw}: "an argument to '#{kw}'") + RUBY + + result.should == "an argument to '#{kw}'" + end + end + + it "can be used as a method name" do + result = sandboxed_eval <<~RUBY + def #{kw} + "a method named '#{kw}'" + end + + send(:#{kw}) + RUBY + + result.should == "a method named '#{kw}'" + end + end + end +end diff --git a/spec_helper.rb b/spec_helper.rb index af1c38587..21029ff8d 100644 --- a/spec_helper.rb +++ b/spec_helper.rb @@ -36,3 +36,17 @@ def report_on_exception=(value) ARGV.unshift $0 MSpecRun.main end + +def expect_syntax_error(ruby_src) + -> { eval(ruby_src) }.should raise_error(SyntaxError) +end + +# Evaluates the given Ruby source in a temporary Module, to prevent +# the surrounding context from being polluted with the new methods. +def sandboxed_eval(ruby_src) + Module.new do + # Allows instance methods defined by `ruby_src` to be called directly. + extend self + end + .class_eval(ruby_src) +end