Skip to content
Open
Show file tree
Hide file tree
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
115 changes: 108 additions & 7 deletions ast/src/testing/ruby/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<G: Graph>() -> Result<(), anyhow::Error> {
let repo = Repo::new(
"src/testing/ruby",
Expand All @@ -29,7 +33,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -51,7 +55,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -60,7 +64,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -69,7 +73,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -78,7 +82,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -89,7 +93,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -101,7 +105,7 @@ pub async fn test_ruby_generic<G: Graph>() -> 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"
);

Expand All @@ -111,9 +115,106 @@ pub async fn test_ruby_generic<G: Graph>() -> Result<(), anyhow::Error> {
Ok(())
}

pub async fn test_ruby_lsp_generic<G: Graph>() -> 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::<G>().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::<ArrayGraph>().await.unwrap();
test_ruby_generic::<BTreeMapGraph>().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::<ArrayGraph>().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::<BTreeMapGraph>().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);
}
}
}
}
112 changes: 109 additions & 3 deletions lsp/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 "\\<rails\\>" 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 "\\<rspec\\>" 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 \
Expand All @@ -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 \
Expand Down
48 changes: 48 additions & 0 deletions lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,54 @@ fn start(
ControlFlow::Continue(())
});
}
Language::Ruby => {
router.notification::<Progress>(|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(())
});
}
_ => {
//
}
Expand Down
6 changes: 3 additions & 3 deletions lsp/src/language.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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",
Expand Down Expand Up @@ -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(),
Expand Down
Loading