diff --git a/_posts/2023-09-01-ruby-local-variables.markdown b/_posts/2023-09-01-ruby-local-variables.markdown index 2192889..da85404 100644 --- a/_posts/2023-09-01-ruby-local-variables.markdown +++ b/_posts/2023-09-01-ruby-local-variables.markdown @@ -2,6 +2,7 @@ layout: post title: "Is it possible to conditionally define a local variable in Ruby?" date: 2023-09-01 +last_modified_at: 2024-06-10 categories: articles tags: ruby --- @@ -107,7 +108,7 @@ puts RubyVM::InstructionSequence.compile("if defined?(x); x = true; end").disasm ``` Gives: ``` -== disasm: #@:1 (1,0)-(1,29)> (catch: false) +== disasm: #@:1 (1,0)-(1,29)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0 0000 putself ( 1)[Li] @@ -127,7 +128,7 @@ puts RubyVM::InstructionSequence.compile("x = true if defined?(x)").disasm ``` Gives: ``` -== disasm: #@:1 (1,0)-(1,23)> (catch: false) +== disasm: #@:1 (1,0)-(1,23)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0 0000 putobject true ( 1)[Li] diff --git a/_posts/2023-09-27-locating-source-of-ruby-method.markdown b/_posts/2023-09-27-locating-source-of-ruby-method.markdown index 3308370..1599678 100644 --- a/_posts/2023-09-27-locating-source-of-ruby-method.markdown +++ b/_posts/2023-09-27-locating-source-of-ruby-method.markdown @@ -2,11 +2,15 @@ layout: post title: "How to locate the source of a Ruby method" date: 2023-09-27 -last_modified_at: 2023-09-29 +last_modified_at: 2024-06-10 categories: articles tags: ruby metaprogramming --- +**UPDATE Jun 10, 2024:** Modify the article to account for Ruby 3.3 [adding default location for eval'd methods](https://github.com/ruby/ruby/commit/43a5c191358699fe8b19314763998cb8ca77ed90){:target="_blank"}. + +--- + _"Wait, where the hell is this method coming from??"_" One of Ruby's strengths is how highly dynamic it is. This makes it very expressive but it also means that it can often be hard to figure out where a method that you see on an object has come from. Full featured Ruby editors like [RubyMine](https://www.jetbrains.com/ruby/){:target="_blank"} and various plugins for other editors based on gems like [Solargraph](https://solargraph.org/){:target="_blank"} have advanced in recent years and have excellent ability to find the definition of a method. @@ -14,6 +18,7 @@ One of Ruby's strengths is how highly dynamic it is. This makes it very expressi However, Ruby is so dynamic that there are cases where it is impossible to determine reliably without actually running the code. Static analysis having its limits is the cost of super high dynamism and you will eventually find yourself in a ruby REPL wondering: "Where is this method actually defined?" There are multiple cases to consider, let's dig in. + ## The easy case: Regular Ruby method Assume you have a source file defined as follows: @@ -84,18 +89,18 @@ end ``` Both `source_location` and Pry's `show-source` will point to the place where the method is defined, in this case the line number 6, the one calling `define_method`. Easy. -However, in a case where you're calling one of the `eval` methods with a string, all of the information gets destroyed while parsing the string: +Since Ruby 3.3., if you call one of the `eval` methods with a string, the caller location be shown in the location string: ```ruby # in dynamic.rb Dynamic.class_eval "def evald; 'EVALD!'; end" # later -Dynamic.new.method(:evald).source_location # => returns [(eval), 1] +Dynamic.new.method(:evald).source_location # => returns [(eval at dynamic.rb:1), 1] ``` -The reason is that `eval` requires you to explicitly pass what you consider the correct location of the source. I.e. if you do the following: +However, this is just a default and eval allows you to pass what you consider the correct location of the source. I.e. if you do the following: ```ruby -Dynamic.class_eval "def evald_with_source; 'EVALD!'; end", __FILE__, __LINE__ +Dynamic.class_eval "def evald_with_source; 'EVALD!'; end", "intended_source_file.rb", 42 ``` -Then `source_location` will return the location where this was called, just like in the `define_method` with block case above. +Then `source_location` will return the custom, and presumably correct, location you specified. To help your future self, get into the habit of specifying the file and line number when doing meta-programming with `eval` and friends. :) diff --git a/spec/posts/2023-09-01-ruby-local-variables_spec.rb b/spec/posts/2023-09-01-ruby-local-variables_spec.rb index d7e4c6b..4b6e168 100644 --- a/spec/posts/2023-09-01-ruby-local-variables_spec.rb +++ b/spec/posts/2023-09-01-ruby-local-variables_spec.rb @@ -55,7 +55,7 @@ specify do expect(RubyVM::InstructionSequence.compile("if defined?(x); x = true; end").disasm).to eq(<<~ASM) - == disasm: #@:1 (1,0)-(1,29)> (catch: false) + == disasm: #@:1 (1,0)-(1,29)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0 0000 putself ( 1)[Li] @@ -72,7 +72,7 @@ specify do expect(RubyVM::InstructionSequence.compile("x = true if defined?(x)").disasm).to eq(<<~ASM) - == disasm: #@:1 (1,0)-(1,23)> (catch: false) + == disasm: #@:1 (1,0)-(1,23)> local table (size: 1, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1]) [ 1] x@0 0000 putobject true ( 1)[Li] diff --git a/spec/posts/2023-09-27-locating-source-of-ruby-method_spec.rb b/spec/posts/2023-09-27-locating-source-of-ruby-method_spec.rb index 936a1c4..7dcbafe 100644 --- a/spec/posts/2023-09-27-locating-source-of-ruby-method_spec.rb +++ b/spec/posts/2023-09-27-locating-source-of-ruby-method_spec.rb @@ -52,7 +52,7 @@ def bar it "works for dynamic method with string source" do expect(Dynamic.new.method(:evald).source_location).to eq [ - "(eval)", + "(eval at #{File.dirname(__FILE__)}/#{DYNAMIC}:11)", 1 ] end