Skip to content

Latest commit

 

History

History
132 lines (101 loc) · 3.62 KB

metaprogramming-plugins.md

File metadata and controls

132 lines (101 loc) · 3.62 KB
id title
metaprogramming-plugins
Metaprogramming plugins

Warning: This feature is experimental and has known limitations. It may not work as expected and may change without notice.

If you are simply looking for Rails support, checkout sorbet-rails and the community page.

In Ruby, it is possible to dynamically define methods. Consider the following example:

# metaprogramming.rb
# typed: true

class Metaprogramming
  def self.macro(name)
    define_method("#{name}_time") { name }
  end

  macro(:bed)
  macro(:fun)
end

hello = Metaprogramming.new
hello.bed_time # error: Method `bed_time` does not exist on Metaprogramming
hello.fun_time # error: Method `fun_time` does not exist on Metaprogramming

Sorbet rejects this script since Sorbet cannot understand calls to define_method or other similar metaprogramming facilities.

Sorbet has a plugin system for handling metaprogramming patterns similar to the one shown in this example. Specifically, we can write a plugin to teach Sorbet what the method macro does.

Here is a plugin for macro in the example above:

# macro_plugin.rb

# Sorbet calls this plugin with command line arguments similar to the following:
# ruby --class Metaprogramming --method macro --source macro(:bed)
# we only care about the source here, so we use ARGV[5]
source = ARGV[5]
/macro\(:(.*)\)/.match(source) do |match_data|
  puts "def #{match_data[1]}_time; end"
end
# Note that Sorbet treats plugin output as rbi files

We then use a YAML file to tell Sorbet about this plugin.

# triggers.yaml

ruby_extra_args:
  # These options are forwarded to Ruby
  - '--disable-gems' # This option speeds up Ruby boot time. Use it if you don't need gems
triggers:
  macro: macro_plugin.rb # This tells Sorbet to run macro.rb when it sees a call to `macro`

Let's run Sorbet on our initial example again, this time with our new plugin enabled:

❯ srb tc --dsl-plugins triggers.yaml metaprogramming.rb
No errors! Great job.

Sorbet executed our plugin and took into consideration its output. Method definitions generated by our plugin fixed the error we saw initially.

Debugging plugins

We can ask Sorbet to print out the output of all plugin calls using --print plugin-generated-code:

❯ srb tc --print plugin-generated-code --dsl-plugins triggers.yaml metaprogramming.rb
# Path: "metaprogramming.rb//plugin-generated|0.rbi":
class Metaprogramming;
def bed_time; end
end;
# Path: "metaprogramming.rb//plugin-generated|1.rbi":
class Metaprogramming;
def fun_time; end
end;
No errors! Great job.

Anything printed to $stderr within a plugin will show up in the terminal when srb tc runs.

Caveats

  • Sorbet decides which plugin to call using method names only. This might be a problem if different metaprogramming methods use the same name, have similar usages, but behave differently. To illustrate, using the configuration above, the following calls macro_plugin.rb twice but results in an error.
class Metaprogramming
  def self.macro(name)
    define_method("#{name}_time") { name }
  end

  macro(:fun)
end

class DifferentMetaprogramming
  def self.macro(name)
    define_method(:different_macro) { name }
  end

  macro(:not_so_fun)
end

DifferentMetaprogramming.new.different_macro # error: Method `different_macro` does not exist
  • The plugin system spawns many instances of Ruby and is very slow because of it.
  • --cache-dir does not play well with the plugin system. On cached runs, Sorbet behaves as if there were no plugins specified.