Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Tapioca Addon] Support gem RBI generation #2081

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions lib/ruby_lsp/tapioca/addon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I noticed is we keep generating RBIs for updated gems. An ideal solution would be like GemSync where we determine that gem RBI is up to date and not generate. It may not be needed yet assuming restarts are rare and folks will stage their lockfile changes. Any thoughts?


require "zlib"
require "ruby_lsp/tapioca/lockfile_diff_parser"

module RubyLsp
module Tapioca
Expand All @@ -27,6 +28,7 @@ def initialize
@rails_runner_client = T.let(nil, T.nilable(RubyLsp::Rails::RunnerClient))
@index = T.let(nil, T.nilable(RubyIndexer::Index))
@file_checksums = T.let({}, T::Hash[String, String])
@lockfile_diff = T.let(nil, T.nilable(String))
@outgoing_queue = T.let(nil, T.nilable(Thread::Queue))
end

Expand All @@ -45,6 +47,10 @@ 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__))

if git_repo?
lockfile_changed? ? generate_gem_rbis : cleanup_orphaned_rbis
vinistock marked this conversation as resolved.
Show resolved Hide resolved
end
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 @@ -127,6 +133,86 @@ def file_updated?(change, path)

false
end

sig { returns(T::Boolean) }
def git_repo?
require "open3"

_, status = Open3.capture2e("git rev-parse --is-inside-work-tree")

T.must(status.success?)
end

sig { returns(T::Boolean) }
def lockfile_changed?
fetch_lockfile_diff
!T.must(@lockfile_diff).empty?
end

sig { returns(String) }
def fetch_lockfile_diff
@lockfile_diff = %x(git diff HEAD Gemfile.lock).strip
end

sig { void }
def generate_gem_rbis
parser = LockfileDiffParser.new(@lockfile_diff)

removed_gems = parser.removed_gems
added_or_modified_gems = parser.added_or_modified_gems

if added_or_modified_gems.any?
# Resetting BUNDLE_GEMFILE to root folder to use the project's Gemfile instead of Ruby LSP's composed Gemfile
stdout, stderr, status = T.unsafe(Open3).capture3(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tapioca doesn't log clearly before starting generating RBIs. I think it'll be good to mention here, something like:

Identified lockfile changes, attempting to generate gem RBIs

{ "BUNDLE_GEMFILE" => "Gemfile" },
"bin/tapioca",
"gem",
"--lsp_addon",
*added_or_modified_gems,
)
T.must(@outgoing_queue) << if status.success?
Notification.window_log_message(
stdout,
type: Constant::MessageType::INFO,
)
else
Notification.window_log_message(
stderr,
type: Constant::MessageType::ERROR,
)
end
elsif removed_gems.any?
FileUtils.rm_f(Dir.glob("sorbet/rbi/gems/{#{removed_gems.join(",")}}@*.rbi"))
T.must(@outgoing_queue) << Notification.window_log_message(
"Removed RBIs for: #{removed_gems.join(", ")}",
type: Constant::MessageType::INFO,
)
end
end

sig { void }
def cleanup_orphaned_rbis
untracked_files = %x(git ls-files --others --exclude-standard sorbet/rbi/gems/).lines.map(&:strip)
deleted_files = %x(git ls-files --deleted sorbet/rbi/gems/).lines.map(&:strip)

untracked_files.each do |file|
File.delete(file)

T.must(@outgoing_queue) << Notification.window_log_message(
"Deleted untracked RBI: #{file}",
type: Constant::MessageType::INFO,
)
end

deleted_files.each do |file|
%x(git checkout -- #{file})

T.must(@outgoing_queue) << Notification.window_log_message(
"Restored deleted RBI: #{file}",
type: Constant::MessageType::INFO,
)
end
end
end
end
end
43 changes: 43 additions & 0 deletions lib/ruby_lsp/tapioca/lockfile_diff_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# typed: true
andyw8 marked this conversation as resolved.
Show resolved Hide resolved
# frozen_string_literal: true

module RubyLsp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on this!

module Tapioca
class LockfileDiffParser
GEM_NAME_PATTERN = /[\w\-]+/
DIFF_LINE_PATTERN = /[+-](.*#{GEM_NAME_PATTERN})\s*\(/
ADDED_LINE_PATTERN = /^\+.*#{GEM_NAME_PATTERN} \(.*\)/
REMOVED_LINE_PATTERN = /^-.*#{GEM_NAME_PATTERN} \(.*\)/

attr_reader :added_or_modified_gems
attr_reader :removed_gems

def initialize(diff_content)
@diff_content = diff_content.lines
@added_or_modified_gems = parse_added_or_modified_gems
@removed_gems = parse_removed_gems
end

private

def parse_added_or_modified_gems
@diff_content
.filter { |line| line.match?(ADDED_LINE_PATTERN) }
.map { |line| extract_gem(line) }
.uniq
end

def parse_removed_gems
@diff_content
.filter { |line| line.match?(REMOVED_LINE_PATTERN) }
.map { |line| extract_gem(line) }
.reject { |gem| @added_or_modified_gems.include?(gem) }
.uniq
end

def extract_gem(line)
line.match(DIFF_LINE_PATTERN)[1].strip
end
end
end
end
62 changes: 62 additions & 0 deletions spec/tapioca/ruby_lsp/tapioca/lockfile_diff_parser_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# typed: strict
# frozen_string_literal: true

require "spec_helper"
require "ruby_lsp/tapioca/lockfile_diff_parser"

module RubyLsp
module Tapioca
class LockFileDiffParserSpec < Minitest::Spec
describe "#parse_added_or_modified_gems" do
it "parses added or modified gems from git diff" do
diff_output = <<~DIFF
+ new_gem (1.0.0)
+ updated_gem (2.0.0)
- removed_gem (1.0.0)
DIFF

lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output)
assert_equal ["new_gem", "updated_gem"], lockfile_parser.added_or_modified_gems
end

it "is empty when there is no diff" do
diff_output = ""

lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output)
assert_empty lockfile_parser.added_or_modified_gems
end
end

describe "#parse_removed_gems" do
it "parses removed gems from git diff" do
diff_output = <<~DIFF
+ new_gem (1.0.0)
- removed_gem (1.0.0)
- outdated_gem (2.3.4)
DIFF

lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output)
assert_equal ["removed_gem", "outdated_gem"], lockfile_parser.removed_gems
end
end

it "handles gem names with hyphens and underscores" do
diff_output = <<~DIFF
- my-gem_extra2 (1.0.0.beta1)
alexcrocha marked this conversation as resolved.
Show resolved Hide resolved
DIFF

lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output)
assert_equal ["my-gem_extra2"], lockfile_parser.removed_gems
end

it "handles gem names with multiple hyphens" do
diff_output = <<~DIFF
- sorbet-static-and-runtime (0.5.0)
DIFF

lockfile_parser = RubyLsp::Tapioca::LockfileDiffParser.new(diff_output)
assert_equal ["sorbet-static-and-runtime"], lockfile_parser.removed_gems
end
end
end
end
Loading