Skip to content

Commit

Permalink
Add Tapioca Addon gem RBI generation support
Browse files Browse the repository at this point in the history
To support gem RBI generation, we needed a way to detect changes in
Gemfile.lock. Currently, changes to this file cause the Ruby LSP to
restart, resulting in loss of access to any previous state information.

By creating a snapshot of Gemfile.lock, we can persist data across
server restarts. Upon restart, we parse both the snapshot and current
Gemfile.lock using Bundler::LockfileParser. If differences are found,
we extract the relevant gem names and specifications, allowing us to
trigger the gem RBI generation.
  • Loading branch information
alexcrocha committed Oct 31, 2024
1 parent 7e68f5c commit 1952bd0
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 0 deletions.
30 changes: 30 additions & 0 deletions lib/ruby_lsp/tapioca/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ module Tapioca
class Addon < ::RubyLsp::Addon
extend T::Sig

GEMFILE_LOCK_SNAPSHOT = "tmp/tapioca/.gemfile_lock_snapshot"

sig { void }
def initialize
super
Expand All @@ -44,6 +46,8 @@ def activate(global_state, outgoing_queue)
@rails_runner_client = addon.rails_runner_client
outgoing_queue << Notification.window_log_message("Activating Tapioca add-on v#{version}")
@rails_runner_client.register_server_addon(File.expand_path("server_addon.rb", __dir__))

check_gemfile_changes
rescue IncompatibleApiError
# The requested version for the Rails add-on no longer matches. We need to upgrade and fix the breaking
# changes
Expand Down Expand Up @@ -106,6 +110,32 @@ def workspace_did_change_watched_files(changes)
constants: constants,
)
end

private

sig { void }
def check_gemfile_changes
current_lockfile = File.read("Gemfile.lock")
snapshot_lockfile = File.read(GEMFILE_LOCK_SNAPSHOT) if File.exist?(GEMFILE_LOCK_SNAPSHOT)

unless snapshot_lockfile
$stdout.puts("Creating initial Gemfile.lock snapshot at #{GEMFILE_LOCK_SNAPSHOT}")
FileUtils.mkdir_p(File.dirname(GEMFILE_LOCK_SNAPSHOT))
File.write(GEMFILE_LOCK_SNAPSHOT, current_lockfile)
return
end

return if current_lockfile == snapshot_lockfile

T.must(@rails_runner_client).delegate_notification(
server_addon_name: "Tapioca",
request_name: "gem",
snapshot_lockfile: snapshot_lockfile,
current_lockfile: current_lockfile,
)

File.write(GEMFILE_LOCK_SNAPSHOT, current_lockfile)
end
end
end
end
30 changes: 30 additions & 0 deletions lib/ruby_lsp/tapioca/server_addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ def execute(request, params)
case request
when "dsl"
dsl(params)
when "gem"
gem(params)
end
end

Expand All @@ -23,6 +25,34 @@ def dsl(params)
load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests
::Tapioca::Cli.start(["dsl", "--lsp_addon", "--workers=1"] + params[:constants])
end

def gem(params)
snapshot_specs = parse_lockfile(params[:snapshot_lockfile])
current_specs = parse_lockfile(params[:current_lockfile])

removed_gems = snapshot_specs.keys - current_specs.keys
changed_gems = current_specs.select { |name, version| snapshot_specs[name] != version }.keys

return $stdout.puts("No gem changes detected") if removed_gems.empty? && changed_gems.empty?

if removed_gems.any?
$stdout.puts("Removing RBIs for deleted gems: #{removed_gems.join(", ")}")
FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi"))
end

if changed_gems.any?
$stdout.puts("Generating RBIs for changed gems: #{changed_gems.join(", ")}")

load("tapioca/cli.rb") # Reload the CLI to reset thor defaults between requests
::Tapioca::Cli.start(["gem"] + changed_gems)
end
end

def parse_lockfile(content)
return {} if content.to_s.empty?

Bundler::LockfileParser.new(content).specs.to_h { |spec| [spec.name, spec.version.to_s] }
end
end
end
end

0 comments on commit 1952bd0

Please sign in to comment.