Skip to content

Instantly share code, notes, and snippets.

@ksss
Last active August 4, 2023 07:06
Show Gist options
  • Save ksss/15e7d6afacc139567ccfa3b2c92d187b to your computer and use it in GitHub Desktop.
Save ksss/15e7d6afacc139567ccfa3b2c92d187b to your computer and use it in GitHub Desktop.
#! /usr/bin/env ruby
# $ rbs_diff --help
# Usage: rbs_diff [options]
# -f, --format FORMAT
# -t, --type_name NAME
# -b, --before_path PATH
# -a, --after_path PATH
# --no_collection
# --with_defined_in
require 'rbs'
require 'rbs/cli'
require 'optparse'
module RBSDiff
Option = Struct.new(
:format,
:type_name,
:before_path,
:after_path,
:no_collection,
:with_defined_in,
keyword_init: true
) do
def initialize
super
# defaults
self.no_collection = false
self.with_defined_in = false
self.before_path = []
self.after_path = []
end
def validate!
unless ["diff", "markdown"].include?(format)
error! "format should be in [diff, markdown]"
end
end
def error!(msg)
$stderr.puts("error: #{msg}")
exit 1
end
end
class Cli
def initialize(argv)
@option = Option.new
OptionParser.new do |o|
o.on("-f", "--format FORMAT") do |arg|
@option.format = arg
end
o.on("-t", "--type_name NAME") do |arg|
@option.type_name = TypeName(arg).absolute!
end
o.on("-b", "--before_path [PATH]") do |arg|
@option.before_path << arg
end
o.on("-a", "--after_path [PATH]") do |arg|
@option.after_path << arg
end
o.on("--no_collection") do
@option.no_collection = true
end
o.on("--with_defined_in") do
@option.with_defined_in = true
end
end.parse!(argv)
@option.validate!
end
def run
Runner.new(@option).run
end
end
class Runner
def initialize(option)
@option = option
end
def run
__send__("run_#{@option.format}")
end
def run_diff
each_diff do |before, after|
puts "- #{before}"
puts "+ #{after}"
puts
end
end
def run_markdown
puts "| before | after |"
puts "| --- | --- |"
each_diff do |before, after|
puts "| `#{before}` | `#{after}` |"
end
end
def each_diff(&block)
before_instance_methods, before_singleton_methods = build_methods(@option.before_path)
after_instance_methods, after_singleton_methods = build_methods(@option.after_path)
each_diff_by(:instance, before_instance_methods, after_instance_methods, &block)
each_diff_by(:singleton, before_singleton_methods, after_singleton_methods, &block)
end
private
def each_diff_by(kind, before_methods, after_methods)
all_keys = before_methods.keys.to_set + after_methods.keys.to_set
all_keys.each do |key|
before = definition_method_to_s(key, kind, before_methods[key]) or next
after = definition_method_to_s(key, kind, after_methods[key]) or next
next if before == after
yield [before, after]
end
end
def build_methods(path)
env = build_env(path)
builder = build_builder(env)
instance_methods = begin
builder.build_instance(@option.type_name).methods
rescue
{}
end
singleton_methods = begin
builder.build_singleton(@option.type_name).methods
rescue
{}
end
[
instance_methods,
singleton_methods
]
end
def build_env(path)
rbs_options = RBS::CLI::LibraryOptions.new
if @option.no_collection
rbs_options.config_path = nil
end
loader = rbs_options.loader
path&.each do |dir|
loader.add(path: Pathname(dir))
end
RBS::Environment.from_loader(loader)
end
def build_builder(env)
RBS::DefinitionBuilder.new(env: env.resolve_type_names)
end
def definition_method_to_s(key, kind, definition_method)
if definition_method
prefix = kind == :instance ? "" : "self."
defined_in = @option.with_defined_in ? "(#{definition_method.defined_in}) " : ""
if definition_method.alias_of
"#{defined_in}alias #{prefix}#{key} #{prefix}#{definition_method.alias_of.defs.first.member.name}"
else
"#{defined_in}def #{prefix}#{key}: #{definition_method.method_types.join(" | ")}"
end
else
"-"
end
end
end
end
RBSDiff::Cli.new(ARGV).run

Example

$ cat a.rbs
class Foo
  def bar: () -> void
  def self.baz: () -> (Integer | String)
  def qux: (untyped) -> untyped
end

$ cat b.rbs
module Bar
  def bar: () -> void
end

module Baz
  def baz: (Integer) -> Integer?
end

class Foo
  include Bar
  extend Baz
end

$ rbs_diff --with_defined_in --no_collection --format markdown --type_name Foo --before_path a.rbs --after_path b.rbs
| before | after |
| --- | --- |
| `(::Foo) def bar: () -> void` | `(::Bar) def bar: () -> void` |
| `(::Foo) def qux: (untyped) -> untyped` | `-` |
| `(::Foo) def self.baz: () -> (::Integer | ::String)` | `(::Baz) def self.baz: (::Integer) -> ::Integer?` |

$ rbs_diff -f diff -t Foo -b a.rbs -a b.rbs
- def qux: (untyped) -> untyped
+ -

- def self.baz: () -> (::Integer | ::String)
+ def self.baz: (::Integer) -> ::Integer?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment