Skip to content

Commit 25d5a3b

Browse files
authored
Add detection reasons to auto-detection log messages (#3859)
Log messages for auto-detected formatters, linters, test libraries, and type checkers now include the reason why they were detected. For example: - "Auto detected formatter: rubocop_internal (direct dependency matching /^rubocop/)" - "Detected test library: rails (bin/rails present)" This helps users debug false positives by understanding which detection heuristics triggered. Fixes #3845
1 parent cea52a3 commit 25d5a3b

File tree

1 file changed

+65
-33
lines changed

1 file changed

+65
-33
lines changed

lib/ruby_lsp/global_state.rb

Lines changed: 65 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22
# frozen_string_literal: true
33

44
module RubyLsp
5+
# Holds the detected value and the reason for detection
6+
class DetectionResult
7+
#: String
8+
attr_reader :value
9+
10+
#: String
11+
attr_reader :reason
12+
13+
#: (String value, String reason) -> void
14+
def initialize(value, reason)
15+
@value = value
16+
@reason = reason
17+
end
18+
end
19+
520
class GlobalState
621
#: String
722
attr_reader :test_library
@@ -122,8 +137,11 @@ def apply_options(options)
122137
end
123138

124139
if @formatter == "auto"
125-
@formatter = detect_formatter(direct_dependencies, all_dependencies)
126-
notifications << Notification.window_log_message("Auto detected formatter: #{@formatter}")
140+
formatter_result = detect_formatter(direct_dependencies, all_dependencies)
141+
@formatter = formatter_result.value
142+
notifications << Notification.window_log_message(
143+
"Auto detected formatter: #{@formatter} (#{formatter_result.reason})",
144+
)
127145
end
128146

129147
specified_linters = options.dig(:initializationOptions, :linters)
@@ -144,21 +162,28 @@ def apply_options(options)
144162
specified_linters << "rubocop_internal"
145163
end
146164

147-
@linters = specified_linters || detect_linters(direct_dependencies, all_dependencies)
148-
149-
notifications << if specified_linters
150-
Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
165+
if specified_linters
166+
@linters = specified_linters
167+
notifications << Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
151168
else
152-
Notification.window_log_message("Auto detected linters: #{@linters.join(", ")}")
169+
linter_results = detect_linters(direct_dependencies, all_dependencies)
170+
@linters = linter_results.map(&:value)
171+
linter_messages = linter_results.map { |r| "#{r.value} (#{r.reason})" }
172+
notifications << Notification.window_log_message("Auto detected linters: #{linter_messages.join(", ")}")
153173
end
154174

155-
@test_library = detect_test_library(direct_dependencies)
156-
notifications << Notification.window_log_message("Detected test library: #{@test_library}")
175+
test_library_result = detect_test_library(direct_dependencies)
176+
@test_library = test_library_result.value
177+
notifications << Notification.window_log_message(
178+
"Detected test library: #{@test_library} (#{test_library_result.reason})",
179+
)
157180

158-
@has_type_checker = detect_typechecker(all_dependencies)
159-
if @has_type_checker
181+
typechecker_result = detect_typechecker(all_dependencies)
182+
@has_type_checker = !typechecker_result.nil?
183+
if typechecker_result
160184
notifications << Notification.window_log_message(
161-
"Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
185+
"Ruby LSP detected this is a Sorbet project (#{typechecker_result.reason}) and will defer to the " \
186+
"Sorbet LSP for some functionality",
162187
)
163188
end
164189

@@ -228,60 +253,67 @@ def supports_watching_files
228253

229254
private
230255

231-
#: (Array[String] direct_dependencies, Array[String] all_dependencies) -> String
256+
#: (Array[String] direct_dependencies, Array[String] all_dependencies) -> DetectionResult
232257
def detect_formatter(direct_dependencies, all_dependencies)
233258
# NOTE: Intentionally no $ at end, since we want to match rubocop-shopify, etc.
234-
return "rubocop_internal" if direct_dependencies.any?(/^rubocop/)
259+
if direct_dependencies.any?(/^rubocop/)
260+
return DetectionResult.new("rubocop_internal", "direct dependency matching /^rubocop/")
261+
end
235262

236-
syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
237-
return "syntax_tree" if syntax_tree_is_direct_dependency
263+
if direct_dependencies.include?("syntax_tree")
264+
return DetectionResult.new("syntax_tree", "direct dependency")
265+
end
238266

239-
rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
240-
return "rubocop_internal" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
267+
if all_dependencies.include?("rubocop") && dot_rubocop_yml_present
268+
return DetectionResult.new("rubocop_internal", "transitive dependency with .rubocop.yml present")
269+
end
241270

242-
"none"
271+
DetectionResult.new("none", "no formatter detected")
243272
end
244273

245274
# Try to detect if there are linters in the project's dependencies. For auto-detection, we always only consider a
246275
# single linter. To have multiple linters running, the user must configure them manually
247-
#: (Array[String] dependencies, Array[String] all_dependencies) -> Array[String]
276+
#: (Array[String] dependencies, Array[String] all_dependencies) -> Array[DetectionResult]
248277
def detect_linters(dependencies, all_dependencies)
249-
linters = []
278+
linters = [] #: Array[DetectionResult]
250279

251-
if dependencies.any?(/^rubocop/) || (all_dependencies.include?("rubocop") && dot_rubocop_yml_present)
252-
linters << "rubocop_internal"
280+
if dependencies.any?(/^rubocop/)
281+
linters << DetectionResult.new("rubocop_internal", "direct dependency matching /^rubocop/")
282+
elsif all_dependencies.include?("rubocop") && dot_rubocop_yml_present
283+
linters << DetectionResult.new("rubocop_internal", "transitive dependency with .rubocop.yml present")
253284
end
254285

255286
linters
256287
end
257288

258-
#: (Array[String] dependencies) -> String
289+
#: (Array[String] dependencies) -> DetectionResult
259290
def detect_test_library(dependencies)
260291
if dependencies.any?(/^rspec/)
261-
"rspec"
292+
DetectionResult.new("rspec", "direct dependency matching /^rspec/")
262293
# A Rails app may have a dependency on minitest, but we would instead want to use the Rails test runner provided
263294
# by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
264295
# activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
265296
# of bin/rails to support these cases.
266297
elsif bin_rails_present
267-
"rails"
298+
DetectionResult.new("rails", "bin/rails present")
268299
# NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
269300
elsif dependencies.any?(/^minitest$/)
270-
"minitest"
301+
DetectionResult.new("minitest", "direct dependency matching /^minitest$/")
271302
elsif dependencies.any?(/^test-unit/)
272-
"test-unit"
303+
DetectionResult.new("test-unit", "direct dependency matching /^test-unit/")
273304
else
274-
"unknown"
305+
DetectionResult.new("unknown", "no test library detected")
275306
end
276307
end
277308

278-
#: (Array[String] dependencies) -> bool
309+
#: (Array[String] dependencies) -> DetectionResult?
279310
def detect_typechecker(dependencies)
280-
return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
311+
return if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
312+
return if dependencies.none?(/^sorbet-static/)
281313

282-
dependencies.any?(/^sorbet-static/)
314+
DetectionResult.new("sorbet", "sorbet-static in dependencies")
283315
rescue Bundler::GemfileNotFound
284-
false
316+
nil
285317
end
286318

287319
#: -> bool

0 commit comments

Comments
 (0)