Skip to content

Commit

Permalink
Make Rails app detection based on Rails::Application superclass
Browse files Browse the repository at this point in the history
This change makes the Rails app detection based on the superclass of the
main class being `Rails::Application`. This is a more reliable way to
detect Rails apps than looking for the presence of the `rails` gem in
the Gemfile. Application can require some of the Rails components
without requiring the `rails` gem itself.

co-authored-by: Earlopain <[email protected]>
  • Loading branch information
louim and Earlopain committed Jun 21, 2024
1 parent 4abb4e1 commit 2d5c93f
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 19 deletions.
28 changes: 28 additions & 0 deletions lib/ruby_lsp/listeners/rails_app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# typed: strict
# frozen_string_literal: true

module RubyLsp
module Listeners
class RailsApp
extend T::Sig

sig { returns(T::Boolean) }
attr_reader :rails_app

sig { params(dispatcher: Prism::Dispatcher).void }
def initialize(dispatcher)
@rails_app = T.let(false, T::Boolean)
dispatcher.register(self, :on_class_node_enter)
end

sig { params(node: Prism::ClassNode).void }
def on_class_node_enter(node)
superclass = node.superclass
case superclass
when Prism::ConstantPathNode
@rails_app = true if superclass.full_name == "Rails::Application"
end
end
end
end
end
33 changes: 29 additions & 4 deletions lib/ruby_lsp/setup_bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
require "pathname"
require "digest"
require "time"
require "prism"
require "ruby_lsp/listeners/rails_app"

# This file is a script that will configure a custom bundle for the Ruby LSP. The custom bundle allows developers to use
# the Ruby LSP without including the gem in their application's Gemfile while at the same time giving us access to the
Expand Down Expand Up @@ -50,6 +52,7 @@ def initialize(project_path, **options)
@last_updated_path = T.let(@custom_dir + "last_updated", Pathname)

@dependencies = T.let(load_dependencies, T::Hash[String, T.untyped])
@rails_app = T.let(rails_app?, T::Boolean)
@retry = T.let(false, T::Boolean)
end

Expand All @@ -62,7 +65,7 @@ def setup!
# Do not set up a custom bundle if LSP dependencies are already in the Gemfile
if @dependencies["ruby-lsp"] &&
@dependencies["debug"] &&
(@dependencies["rails"] ? @dependencies["ruby-lsp-rails"] : true)
(@rails_app ? @dependencies["ruby-lsp-rails"] : true)
$stderr.puts(
"Ruby LSP> Skipping custom bundle setup since LSP dependencies are already in #{@gemfile}",
)
Expand Down Expand Up @@ -148,7 +151,7 @@ def write_custom_gemfile
parts << 'gem "debug", require: false, group: :development, platforms: :mri'
end

if @dependencies["rails"] && !@dependencies["ruby-lsp-rails"]
if @rails_app && !@dependencies["ruby-lsp-rails"]
parts << 'gem "ruby-lsp-rails", require: false, group: :development'
end

Expand Down Expand Up @@ -209,7 +212,7 @@ def run_bundle_install(bundle_gemfile = @gemfile)
command << " && bundle update "
command << "ruby-lsp " unless @dependencies["ruby-lsp"]
command << "debug " unless @dependencies["debug"]
command << "ruby-lsp-rails " if @dependencies["rails"] && !@dependencies["ruby-lsp-rails"]
command << "ruby-lsp-rails " if @rails_app && !@dependencies["ruby-lsp-rails"]
command << "--pre" if @experimental
command.delete_suffix!(" ")
command << ")"
Expand Down Expand Up @@ -244,7 +247,7 @@ def run_bundle_install(bundle_gemfile = @gemfile)
def should_bundle_update?
# If `ruby-lsp`, `ruby-lsp-rails` and `debug` are in the Gemfile, then we shouldn't try to upgrade them or else it
# will produce version control changes
if @dependencies["rails"]
if @rails_app
return false if @dependencies.values_at("ruby-lsp", "ruby-lsp-rails", "debug").all?

# If the custom lockfile doesn't include `ruby-lsp`, `ruby-lsp-rails` or `debug`, we need to run bundle install
Expand Down Expand Up @@ -280,5 +283,27 @@ def correct_relative_remote_paths

@custom_lockfile.write(content)
end

# Detects if the project is a Rails app by looking if the superclass of the main class is `Rails::Application`
sig { returns(T::Boolean) }
def rails_app?
config = rails_app_config
return false unless config

result = Prism.parse(config)
return false unless result

dispatcher = Prism::Dispatcher.new
listener = Listeners::RailsApp.new(dispatcher)
dispatcher.dispatch(result.value)

listener.rails_app
end

sig { returns(T.nilable(String)) }
def rails_app_config
config = Pathname.new("config/application.rb").expand_path(Dir.pwd)
config.read if config.exist?
end
end
end
49 changes: 34 additions & 15 deletions test/setup_bundler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,33 @@ def test_creates_custom_bundle
end

def test_creates_custom_bundle_for_a_rails_app
Object.any_instance.expects(:system).with(
bundle_env(".ruby-lsp/Gemfile"),
"(bundle check || bundle install) 1>&2",
).returns(true)
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "rails" => true }).at_least_once
run_script
Dir.mktmpdir do |dir|
Dir.chdir(dir) do
FileUtils.mkdir(File.join(dir, "config"))
File.write(File.join(dir, "config", "application.rb"), <<~RUBY)
module MyApp
class Application < Rails::Application
end
end
RUBY

assert_path_exists(".ruby-lsp")
assert_path_exists(".ruby-lsp/Gemfile")
assert_path_exists(".ruby-lsp/Gemfile.lock")
assert_path_exists(".ruby-lsp/main_lockfile_hash")
assert_match("ruby-lsp", File.read(".ruby-lsp/Gemfile"))
assert_match("debug", File.read(".ruby-lsp/Gemfile"))
assert_match("ruby-lsp-rails", File.read(".ruby-lsp/Gemfile"))
ensure
FileUtils.rm_r(".ruby-lsp") if Dir.exist?(".ruby-lsp")
Object.any_instance.expects(:system).with(
bundle_env(".ruby-lsp/Gemfile"),
"(bundle check || bundle install) 1>&2",
).returns(true)
Bundler::LockfileParser.any_instance.expects(:dependencies).returns({ "rails" => true }).at_least_once
run_script

assert_path_exists(".ruby-lsp")
assert_path_exists(".ruby-lsp/Gemfile")
assert_path_exists(".ruby-lsp/Gemfile.lock")
assert_path_exists(".ruby-lsp/main_lockfile_hash")
gemfile_content = File.read(".ruby-lsp/Gemfile")
assert_match("ruby-lsp", gemfile_content)
assert_match("debug", gemfile_content)
assert_match("ruby-lsp-rails", gemfile_content)
end
end
end

def test_changing_lockfile_causes_custom_bundle_to_be_rebuilt
Expand Down Expand Up @@ -486,6 +497,14 @@ def test_ruby_lsp_rails_is_automatically_included_in_rails_apps
gem "rails"
GEMFILE

FileUtils.mkdir(File.join(dir, "config"))
File.write(File.join(dir, "config", "application.rb"), <<~RUBY)
module MyApp
class Application < Rails::Application
end
end
RUBY

capture_subprocess_io do
Bundler.with_unbundled_env do
# Run bundle install to generate the lockfile
Expand Down

0 comments on commit 2d5c93f

Please sign in to comment.