Skip to content
Open
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
102 changes: 70 additions & 32 deletions lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
# frozen_string_literal: true

module RubyLsp
# Holds the detected value and the reason for detection
class DetectionResult
#: String
attr_reader :value

#: String
attr_reader :reason

#: (String value, String reason) -> void
def initialize(value, reason)
@value = value
@reason = reason
end
end

class GlobalState
#: String
attr_reader :test_library
Expand Down Expand Up @@ -122,8 +137,11 @@ def apply_options(options)
end

if @formatter == "auto"
@formatter = detect_formatter(direct_dependencies, all_dependencies)
notifications << Notification.window_log_message("Auto detected formatter: #{@formatter}")
formatter_result = detect_formatter(direct_dependencies, all_dependencies)
@formatter = formatter_result.value
notifications << Notification.window_log_message(
"Auto detected formatter: #{@formatter} (#{formatter_result.reason})",
)
end

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

@linters = specified_linters || detect_linters(direct_dependencies, all_dependencies)

notifications << if specified_linters
Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
if specified_linters
@linters = specified_linters
notifications << Notification.window_log_message("Using linters specified by user: #{@linters.join(", ")}")
else
Notification.window_log_message("Auto detected linters: #{@linters.join(", ")}")
linter_results = detect_linters(direct_dependencies, all_dependencies)
@linters = linter_results.map(&:value)
linter_messages = linter_results.map { |r| "#{r.value} (#{r.reason})" }
notifications << Notification.window_log_message("Auto detected linters: #{linter_messages.join(", ")}")
end

@test_library = detect_test_library(direct_dependencies)
notifications << Notification.window_log_message("Detected test library: #{@test_library}")
test_library_result = detect_test_library(direct_dependencies)
@test_library = test_library_result.value
notifications << Notification.window_log_message(
"Detected test library: #{@test_library} (#{test_library_result.reason})",
)

@has_type_checker = detect_typechecker(all_dependencies)
typechecker_detected, typechecker_reason = detect_typechecker(all_dependencies)
@has_type_checker = typechecker_detected
if @has_type_checker
notifications << Notification.window_log_message(
"Ruby LSP detected this is a Sorbet project and will defer to the Sorbet LSP for some functionality",
"Ruby LSP detected this is a Sorbet project (#{typechecker_reason}) and will defer to the " \
"Sorbet LSP for some functionality",
)
end

Expand Down Expand Up @@ -228,60 +253,73 @@ def supports_watching_files

private

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

syntax_tree_is_direct_dependency = direct_dependencies.include?("syntax_tree")
return "syntax_tree" if syntax_tree_is_direct_dependency
if direct_dependencies.include?("syntax_tree")
return DetectionResult.new("syntax_tree", "direct dependency")
end

rubocop_is_transitive_dependency = all_dependencies.include?("rubocop")
return "rubocop_internal" if dot_rubocop_yml_present && rubocop_is_transitive_dependency
if all_dependencies.include?("rubocop") && dot_rubocop_yml_present
return DetectionResult.new("rubocop_internal", "transitive dependency with .rubocop.yml present")
end

"none"
DetectionResult.new("none", "no formatter detected")
end

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

if dependencies.any?(/^rubocop/) || (all_dependencies.include?("rubocop") && dot_rubocop_yml_present)
linters << "rubocop_internal"
if dependencies.any?(/^rubocop/)
linters << DetectionResult.new("rubocop_internal", "direct dependency matching /^rubocop/")
elsif all_dependencies.include?("rubocop") && dot_rubocop_yml_present
linters << DetectionResult.new("rubocop_internal", "transitive dependency with .rubocop.yml present")
end

linters
end

#: (Array[String] dependencies) -> String
#: (Array[String] dependencies) -> DetectionResult
def detect_test_library(dependencies)
if dependencies.any?(/^rspec/)
"rspec"
DetectionResult.new("rspec", "direct dependency matching /^rspec/")
# A Rails app may have a dependency on minitest, but we would instead want to use the Rails test runner provided
# by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
# activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
# of bin/rails to support these cases.
elsif bin_rails_present
"rails"
DetectionResult.new("rails", "bin/rails present")
# NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
elsif dependencies.any?(/^minitest$/)
"minitest"
DetectionResult.new("minitest", "direct dependency matching /^minitest$/")
elsif dependencies.any?(/^test-unit/)
"test-unit"
DetectionResult.new("test-unit", "direct dependency matching /^test-unit/")
else
"unknown"
DetectionResult.new("unknown", "no test library detected")
end
end

#: (Array[String] dependencies) -> bool
# Returns [detected, reason] tuple for typechecker detection
#: (Array[String] dependencies) -> [bool, String]
def detect_typechecker(dependencies)
return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
return [false, "RUBY_LSP_BYPASS_TYPECHECKER environment variable set"]
end

dependencies.any?(/^sorbet-static/)
if dependencies.any?(/^sorbet-static/)
[true, "sorbet-static in dependencies"]
else
[false, "sorbet-static not found in dependencies"]
end
rescue Bundler::GemfileNotFound
false
[false, "Gemfile not found"]
end

#: -> bool
Expand Down