22# frozen_string_literal: true
33
44module 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