From 81f639dfc2261778dcecf446aba0a8c62c4c44f5 Mon Sep 17 00:00:00 2001 From: Zeeshan Khan Date: Fri, 17 Jul 2026 11:53:02 -0700 Subject: [PATCH 1/2] Avoid treating empty DSL matches as errors in add-on mode. When the LSP requests generation for an existing constant with no processable DSL, return quietly instead of raising Tapioca::Error. Co-authored-by: Cursor --- lib/tapioca/dsl/pipeline.rb | 3 ++ spec/dummy/app/models/foo.rb | 5 ++++ spec/tapioca/addon_spec.rb | 54 ++++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 spec/dummy/app/models/foo.rb diff --git a/lib/tapioca/dsl/pipeline.rb b/lib/tapioca/dsl/pipeline.rb index 1a7bd2f11..33ff96c05 100644 --- a/lib/tapioca/dsl/pipeline.rb +++ b/lib/tapioca/dsl/pipeline.rb @@ -64,6 +64,9 @@ def run(&blk) # It's OK if there are no constants to process if we received a valid file/path. if constants_to_process.empty? && requested_paths.none? { |p| File.exist?(p) } + # When running within the add-on, return early so this expected case is not logged as an error + return [] if @lsp_addon + report_error(<<~ERROR) No classes/modules can be matched for RBI generation. Please check that the requested classes/modules include processable DSL methods. diff --git a/spec/dummy/app/models/foo.rb b/spec/dummy/app/models/foo.rb new file mode 100644 index 000000000..9d430d21a --- /dev/null +++ b/spec/dummy/app/models/foo.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Plain class with no processable DSL methods, used by addon_spec. +class Foo +end diff --git a/spec/tapioca/addon_spec.rb b/spec/tapioca/addon_spec.rb index 38b2cadcc..ac86a4b57 100644 --- a/spec/tapioca/addon_spec.rb +++ b/spec/tapioca/addon_spec.rb @@ -28,6 +28,43 @@ class AddonSpec < Minitest::HooksSpec FileUtils.rm_rf("spec/dummy/sorbet/rbi") end + it "does not error when a constant has no processable DSL" do + create_client + + # Foo exists but has no processable DSL. dsl requests are forked, so wait on a + # subsequent successful generation and keep draining logs briefly afterward to + # avoid missing a concurrent Foo error notification. + @client.delegate_notification( + server_addon_name: "Tapioca", + request_name: "dsl", + constants: ["Foo"], + ) + @client.delegate_notification( + server_addon_name: "Tapioca", + request_name: "dsl", + constants: ["NotifyUserJob"], + ) + wait_until_exists("spec/dummy/sorbet/rbi/dsl/notify_user_job.rbi") + + error_logs = [] + deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1.0 + loop do + error_logs.concat(drain_log_messages.select do |message| + message.include?("Tapioca::Error") || message.include?("failed with StandardError") + end) + break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline + + sleep(0.1) + end + + shutdown_client + + assert_empty(error_logs) + refute_path_exists("spec/dummy/sorbet/rbi/dsl/foo.rbi") + ensure + FileUtils.rm_rf("spec/dummy/sorbet/rbi") + end + it "triggers route DSL generation if routes.rb is modified" do create_client @@ -182,6 +219,23 @@ def wait_until_exists(path) rescue Timeout::Error flunk("#{path} was not created in time") end + + def drain_log_messages + messages = [] + + loop do + notification = @outgoing_queue.pop(true) + message = case notification + when Hash + notification.dig(:params, :message).to_s + else + notification.params&.message.to_s + end + messages << message unless message.empty? + end + rescue ThreadError + messages + end end end end From 3f861f3784fc77121a165b4162bb92ad6ee1a218 Mon Sep 17 00:00:00 2001 From: Zeeshan Khan Date: Fri, 17 Jul 2026 12:27:48 -0700 Subject: [PATCH 2/2] Make add-on DSL test completion deterministic. Co-authored-by: Cursor --- spec/tapioca/addon_spec.rb | 67 +++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/spec/tapioca/addon_spec.rb b/spec/tapioca/addon_spec.rb index ac86a4b57..ac61b5379 100644 --- a/spec/tapioca/addon_spec.rb +++ b/spec/tapioca/addon_spec.rb @@ -29,37 +29,19 @@ class AddonSpec < Minitest::HooksSpec end it "does not error when a constant has no processable DSL" do - create_client + create_client(supports_progress: true) - # Foo exists but has no processable DSL. dsl requests are forked, so wait on a - # subsequent successful generation and keep draining logs briefly afterward to - # avoid missing a concurrent Foo error notification. @client.delegate_notification( server_addon_name: "Tapioca", request_name: "dsl", constants: ["Foo"], ) - @client.delegate_notification( - server_addon_name: "Tapioca", - request_name: "dsl", - constants: ["NotifyUserJob"], - ) - wait_until_exists("spec/dummy/sorbet/rbi/dsl/notify_user_job.rbi") - error_logs = [] - deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1.0 - loop do - error_logs.concat(drain_log_messages.select do |message| - message.include?("Tapioca::Error") || message.include?("failed with StandardError") - end) - break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline - - sleep(0.1) - end + messages = wait_for_dsl_completion shutdown_client - assert_empty(error_logs) + assert_empty(messages.select { |message| error_message?(message) }) refute_path_exists("spec/dummy/sorbet/rbi/dsl/foo.rbi") ensure FileUtils.rm_rf("spec/dummy/sorbet/rbi") @@ -187,9 +169,12 @@ def decorate private # Starts a new client - def create_client + def create_client(supports_progress: false) @outgoing_queue = Thread::Queue.new global_state = GlobalState.new + if supports_progress + global_state.apply_options(capabilities: { window: { workDoneProgress: true } }) + end @client = FileUtils.chdir("spec/dummy") do RubyLsp::Rails::RunnerClient.new(@outgoing_queue, global_state) end @@ -220,21 +205,37 @@ def wait_until_exists(path) flunk("#{path} was not created in time") end - def drain_log_messages + def wait_for_dsl_completion(token: "dsl") messages = [] - - loop do - notification = @outgoing_queue.pop(true) - message = case notification - when Hash - notification.dig(:params, :message).to_s - else - notification.params&.message.to_s + Timeout.timeout(10) do + loop do + notification = @outgoing_queue.pop + messages << notification + break if dsl_progress_end?(notification, token) end - messages << message unless message.empty? end - rescue ThreadError messages + rescue Timeout::Error + flunk("dsl request for #{token} did not complete in time") + end + + def dsl_progress_end?(notification, token) + return false unless notification.is_a?(Hash) + + params = notification[:params] + return false unless params + + params[:token] == token && params.dig(:value, :kind) == "end" + end + + def error_message?(notification) + message = if notification.is_a?(Hash) + notification.dig(:params, :message).to_s + else + notification.params&.message.to_s + end + + message.include?("Tapioca::Error") || message.include?("failed with StandardError") end end end