From 4cae6f29e01d5b146cc836586e38995da29bc819 Mon Sep 17 00:00:00 2001 From: MuhammadUmer44 Date: Sat, 17 May 2025 12:56:45 +0500 Subject: [PATCH] Add Ruby LSP support with version flexibility --- ast/src/testing/ruby/mod.rs | 115 +++++++++++++++++++++++++++++++++--- lsp/Dockerfile | 112 ++++++++++++++++++++++++++++++++++- lsp/src/client.rs | 48 +++++++++++++++ lsp/src/language.rs | 6 +- 4 files changed, 268 insertions(+), 13 deletions(-) diff --git a/ast/src/testing/ruby/mod.rs b/ast/src/testing/ruby/mod.rs index dc1c1aa4f..6bb73198a 100644 --- a/ast/src/testing/ruby/mod.rs +++ b/ast/src/testing/ruby/mod.rs @@ -4,6 +4,10 @@ use crate::{lang::Lang, repo::Repo}; use std::str::FromStr; use test_log::test; +fn normalize_path(path: &str) -> String { + path.replace('\\', "/") +} + pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { let repo = Repo::new( "src/testing/ruby", @@ -29,7 +33,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { "Language node name should be 'ruby'" ); assert_eq!( - language_nodes[0].file, "src/testing/ruby/", + normalize_path(&language_nodes[0].file), "src/testing/ruby/", "Language node file path is incorrect" ); @@ -51,7 +55,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { .find(|e| e.name == "person/:id" && e.meta.get("verb") == Some(&"GET".to_string())) .expect("GET person/:id endpoint not found"); assert_eq!( - get_person_endpoint.file, "src/testing/ruby/config/routes.rb", + normalize_path(&get_person_endpoint.file), "src/testing/ruby/config/routes.rb", "Endpoint file path is incorrect" ); @@ -60,7 +64,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { .find(|e| e.name == "person" && e.meta.get("verb") == Some(&"POST".to_string())) .expect("POST person endpoint not found"); assert_eq!( - post_person_endpoint.file, "src/testing/ruby/config/routes.rb", + normalize_path(&post_person_endpoint.file), "src/testing/ruby/config/routes.rb", "Endpoint file path is incorrect" ); @@ -69,7 +73,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { .find(|e| e.name == "/people/:id" && e.meta.get("verb") == Some(&"DELETE".to_string())) .expect("DELETE /people/:id endpoint not found"); assert_eq!( - delete_people_endpoint.file, "src/testing/ruby/config/routes.rb", + normalize_path(&delete_people_endpoint.file), "src/testing/ruby/config/routes.rb", "Endpoint file path is incorrect" ); @@ -78,7 +82,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { .find(|e| e.name == "/people/articles" && e.meta.get("verb") == Some(&"GET".to_string())) .expect("GET /people/articles endpoint not found"); assert_eq!( - get_articles_endpoint.file, "src/testing/ruby/config/routes.rb", + normalize_path(&get_articles_endpoint.file), "src/testing/ruby/config/routes.rb", "Endpoint file path is incorrect" ); @@ -89,7 +93,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { }) .expect("POST /people/:id/articles endpoint not found"); assert_eq!( - post_articles_endpoint.file, "src/testing/ruby/config/routes.rb", + normalize_path(&post_articles_endpoint.file), "src/testing/ruby/config/routes.rb", "Endpoint file path is incorrect" ); @@ -101,7 +105,7 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { }) .expect("POST /countries/:country_id/process endpoint not found"); assert_eq!( - post_countries_endpoint.file, "src/testing/ruby/config/routes.rb", + normalize_path(&post_countries_endpoint.file), "src/testing/ruby/config/routes.rb", "Endpoint file path is incorrect" ); @@ -111,9 +115,106 @@ pub async fn test_ruby_generic() -> Result<(), anyhow::Error> { Ok(()) } +pub async fn test_ruby_lsp_generic() -> Result<(), anyhow::Error> { + let repo = Repo::new( + "src/testing/ruby", + Lang::from_str("ruby").unwrap(), + true, + Vec::new(), + Vec::new(), + ) + .unwrap(); + + let graph = repo.build_graph_inner::().await?; + + graph.analysis(); + + let (num_nodes, num_edges) = graph.get_graph_size(); + assert!(num_nodes > 61, "Expected more than 61 nodes with LSP"); + assert!(num_edges > 99, "Expected more than 99 edges with LSP"); + + let language_nodes = graph.find_nodes_by_type(NodeType::Language); + assert_eq!(language_nodes.len(), 1, "Expected 1 language node"); + assert_eq!( + language_nodes[0].name, "ruby", + "Language node name should be 'ruby'" + ); + + let pkg_files = graph.find_nodes_by_name(NodeType::File, "Gemfile"); + assert_eq!(pkg_files.len(), 1, "Expected 1 Gemfile"); + + let endpoints = graph.find_nodes_by_type(NodeType::Endpoint); + assert_eq!(endpoints.len(), 7, "Expected 7 endpoints"); + + let classes = graph.find_nodes_by_type(NodeType::Class); + assert!(classes.len() >= 3, "Expected at least 3 classes with LSP"); + + let _people_controller = classes + .iter() + .find(|c| c.name.contains("PeopleController")) + .expect("PeopleController class not found"); + + let methods = graph.find_nodes_by_type(NodeType::Function); + assert!(methods.len() >= 5, "Expected at least 5 methods with LSP"); + + let _create_method = methods + .iter() + .find(|m| m.name == "create") + .expect("Create method not found"); + + let _show_method = methods + .iter() + .find(|m| m.name == "show") + .expect("Show method not found"); + + let calls_edges_count = graph.count_edges_of_type(EdgeType::Calls(Default::default())); + assert!(calls_edges_count > 0, "Expected function call edges with LSP"); + + let imports_edges_count = graph.count_edges_of_type(EdgeType::Imports); + assert!(imports_edges_count > 0, "Expected import edges with LSP"); + + let models = graph.find_nodes_by_type(NodeType::DataModel); + assert!(models.len() > 0, "Expected data models with LSP"); + + Ok(()) +} + #[test(tokio::test)] async fn test_ruby() { use crate::lang::graphs::{ArrayGraph, BTreeMapGraph}; test_ruby_generic::().await.unwrap(); test_ruby_generic::().await.unwrap(); } + +#[test(tokio::test)] +async fn test_ruby_lsp() { + use crate::lang::graphs::{ArrayGraph, BTreeMapGraph}; + + if std::env::var("SKIP_LSP_TESTS").is_ok() { + println!("Skipping Ruby LSP test due to SKIP_LSP_TESTS environment variable"); + return; + } + + match test_ruby_lsp_generic::().await { + Ok(_) => println!("Ruby LSP test with ArrayGraph passed"), + Err(e) => { + if e.to_string().contains("sending on a closed channel") { + println!("Ruby LSP test skipped: LSP server might not be available: {}", e); + return; + } else { + panic!("Ruby LSP test with ArrayGraph failed: {}", e); + } + } + } + + match test_ruby_lsp_generic::().await { + Ok(_) => println!("Ruby LSP test with BTreeMapGraph passed"), + Err(e) => { + if e.to_string().contains("sending on a closed channel") { + println!("Ruby LSP test skipped: LSP server might not be available: {}", e); + } else { + panic!("Ruby LSP test with BTreeMapGraph failed: {}", e); + } + } + } +} diff --git a/lsp/Dockerfile b/lsp/Dockerfile index ea6baf41e..de77301c6 100644 --- a/lsp/Dockerfile +++ b/lsp/Dockerfile @@ -20,9 +20,118 @@ RUN apt-get update && apt-get install -y \ gcc \ g++ \ sed \ + sqlite3 \ + libsqlite3-dev \ + zlib1g-dev \ + libreadline-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +# Install rbenv for Ruby version management +RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ + && cd ~/.rbenv && src/configure && make -C src \ + && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ + && echo 'eval "$(rbenv init -)"' >> ~/.bashrc \ + && git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build + +# Add rbenv to PATH +ENV PATH="/root/.rbenv/shims:/root/.rbenv/bin:${PATH}" + +# Install latest stable Ruby as default +RUN rbenv install $(rbenv install -l | grep -v - | tail -1) \ + && rbenv global $(rbenv install -l | grep -v - | tail -1) + +# Ruby LSP and essential gems +RUN gem install bundler --no-document \ + && gem install ruby-lsp ruby-lsp-rails ruby-lsp-rspec sorbet-runtime --no-document + +# Create a smart Ruby LSP wrapper script +RUN echo '#!/bin/bash\n\ +project_dir="$1"\n\ +cd "$project_dir" || exit 1\n\ +\n\ +# Check for specific Ruby version\n\ +if [ -f ".ruby-version" ]; then\n\ + ruby_version=$(cat .ruby-version | tr -d "\\n")\n\ + echo "Detected Ruby version: $ruby_version"\n\ + if ! rbenv versions | grep -q "$ruby_version"; then\n\ + echo "Installing Ruby $ruby_version..."\n\ + rbenv install "$ruby_version" || echo "Failed to install Ruby $ruby_version, using default"\n\ + fi\n\ + export RBENV_VERSION="$ruby_version"\n\ + echo "Using Ruby $ruby_version"\n\ +fi\n\ +\n\ +# Handle project dependencies\n\ +if [ -f "Gemfile" ]; then\n\ + # Check if bundler is installed for this Ruby version\n\ + if ! gem list -i bundler > /dev/null 2>&1; then\n\ + gem install bundler --no-document\n\ + fi\n\ + \n\ + # Install project gems\n\ + bundle config set --local path vendor/bundle 2>/dev/null || true\n\ + bundle install --quiet --jobs=4 --retry=3 || echo "Warning: bundle install failed, continuing anyway"\n\ + \n\ + # Install ruby-lsp gems if needed\n\ + ruby_lsp_needed=true\n\ + ruby_lsp_rails_needed=true\n\ + ruby_lsp_rspec_needed=true\n\ + \n\ + if bundle show ruby-lsp > /dev/null 2>&1; then\n\ + ruby_lsp_needed=false\n\ + fi\n\ + \n\ + if bundle show ruby-lsp-rails > /dev/null 2>&1; then\n\ + ruby_lsp_rails_needed=false\n\ + fi\n\ + \n\ + if bundle show ruby-lsp-rspec > /dev/null 2>&1; then\n\ + ruby_lsp_rspec_needed=false\n\ + fi\n\ + \n\ + # Check if this is a Rails project\n\ + is_rails=false\n\ + if grep -q "\\" Gemfile; then\n\ + is_rails=true\n\ + fi\n\ + \n\ + # Check if this is an RSpec project\n\ + is_rspec=false\n\ + if grep -q "\\" Gemfile; then\n\ + is_rspec=true\n\ + fi\n\ + \n\ + # Inject necessary ruby-lsp gems if needed\n\ + if [ "$ruby_lsp_needed" = true ]; then\n\ + echo "Installing ruby-lsp..."\n\ + gem install ruby-lsp --no-document\n\ + fi\n\ + \n\ + if [ "$is_rails" = true ] && [ "$ruby_lsp_rails_needed" = true ]; then\n\ + echo "Installing ruby-lsp-rails..."\n\ + gem install ruby-lsp-rails --no-document\n\ + fi\n\ + \n\ + if [ "$is_rspec" = true ] && [ "$ruby_lsp_rspec_needed" = true ]; then\n\ + echo "Installing ruby-lsp-rspec..."\n\ + gem install ruby-lsp-rspec --no-document\n\ + fi\n\ + \n\ + # Use bundled ruby-lsp if available\n\ + if bundle show ruby-lsp > /dev/null 2>&1; then\n\ + echo "Using bundled ruby-lsp"\n\ + bundle exec ruby-lsp --stdio\n\ + else\n\ + echo "Using global ruby-lsp"\n\ + ruby-lsp --stdio\n\ + fi\n\ +else\n\ + ruby-lsp --stdio\n\ +fi' > /usr/local/bin/ruby-lsp-wrapper + +RUN chmod +x /usr/local/bin/ruby-lsp-wrapper + # rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup.sh \ && chmod +x rustup.sh \ @@ -48,9 +157,6 @@ ENV PATH=$PATH:$GOROOT/bin:$GOPATH/bin RUN mkdir -p $GOPATH/bin \ && CGO_ENABLED=0 go install -v golang.org/x/tools/gopls@v0.16.2 -# ruby -RUN gem install ruby-lsp - # rust-analyzer RUN curl -LO "https://github.com/rust-lang/rust-analyzer/releases/download/2025-01-20/rust-analyzer-x86_64-unknown-linux-gnu.gz" \ && gzip -cd rust-analyzer-x86_64-unknown-linux-gnu.gz > /bin/rust-analyzer \ diff --git a/lsp/src/client.rs b/lsp/src/client.rs index 6b21433e2..7b3e2799f 100644 --- a/lsp/src/client.rs +++ b/lsp/src/client.rs @@ -245,6 +245,54 @@ fn start( ControlFlow::Continue(()) }); } + Language::Ruby => { + router.notification::(|this, prog| { + info!("Ruby progress: {:?} {:?}", prog.token, prog.value); + + let is_ruby_token = if let NumberOrString::String(ref s) = prog.token { + s.contains("ruby") || s.contains("ruby-lsp") + } else { + false + }; + + match &prog.value { + ProgressParamsValue::WorkDone(wd) => { + match wd { + WorkDoneProgress::Begin(begin) => { + if begin.title == "Indexing" || begin.title == "Loading" { + debug!("Ruby LSP indexing started: {}", begin.title); + } + } + WorkDoneProgress::Report(report) => { + if let Some(per) = report.percentage { + if let Some(msg) = &report.message { + debug!("Ruby LSP indexing: {msg} ({per}%)"); + } + } + } + WorkDoneProgress::End(_) => { + debug!("Ruby LSP indexing completed"); + if let Some(tx) = this.indexed_tx.take() { + let _: Result<_, _> = tx.send(()); + } + } + } + } + _ => { + debug!("Ruby non-WorkDone progress type"); + } + } + + if is_ruby_token && this.indexed_tx.is_some() { + debug!("Ruby LSP token match detected"); + if let Some(tx) = this.indexed_tx.take() { + let _: Result<_, _> = tx.send(()); + } + } + + ControlFlow::Continue(()) + }); + } _ => { // } diff --git a/lsp/src/language.rs b/lsp/src/language.rs index 2273489ff..54dacf50f 100644 --- a/lsp/src/language.rs +++ b/lsp/src/language.rs @@ -139,7 +139,7 @@ impl Language { Self::Go => true, Self::Typescript | Self::React => true, Self::Python => false, - Self::Ruby => false, + Self::Ruby => true, Self::Kotlin => true, Self::Swift => false, Self::Java => true, @@ -156,7 +156,7 @@ impl Language { Self::Go => "gopls", Self::Typescript | Self::React => "typescript-language-server", Self::Python => "pylsp", - Self::Ruby => "ruby-lsp", + Self::Ruby => "ruby-lsp-wrapper", Self::Kotlin => "kotlin-language-server", Self::Swift => "sourcekit-lsp", Self::Java => "jdtls", @@ -192,7 +192,7 @@ impl Language { Self::Go => Vec::new(), Self::Typescript | Self::React => vec!["--stdio".to_string()], Self::Python => Vec::new(), - Self::Ruby => Vec::new(), + Self::Ruby => vec![".".to_string()], Self::Kotlin => Vec::new(), Self::Swift => Vec::new(), Self::Java => Vec::new(),