diff --git a/ast/src/testing/annotations.rs b/ast/src/testing/annotations.rs index 713e766cf..60ff6ef3e 100644 --- a/ast/src/testing/annotations.rs +++ b/ast/src/testing/annotations.rs @@ -139,10 +139,20 @@ fn parse_absent_annotations(source: &str, prefix: &str) -> Vec fn parse_file_annotations( source: &str, prefix: &str, -) -> Vec<(NodeType, String, BTreeMap, Vec)> { + use_lsp: bool, +) -> Vec<( + NodeType, + String, + BTreeMap, + Vec, +)> { let mut result = Vec::new(); - let mut current: Option<(NodeType, String, BTreeMap, Vec)> = - None; + let mut current: Option<( + NodeType, + String, + BTreeMap, + Vec, + )> = None; for line in source.lines() { let trimmed = line.trim(); @@ -158,7 +168,23 @@ fn parse_file_annotations( current = Some((nt, toks[1].clone(), subject_meta, Vec::new())); } } - } else if let Some(edge_rest) = rest.strip_prefix("edge: ") { + } else if let Some(edge_rest) = rest + .strip_prefix("edge: ") + .or_else(|| { + if use_lsp { + rest.strip_prefix("edge_lsp: ") + } else { + None + } + }) + .or_else(|| { + if use_lsp { + None + } else { + rest.strip_prefix("edge_no_lsp: ") + } + }) + { if let Some((_, _, _, ref mut edges)) = current { let toks = parse_quoted_tokens(edge_rest); if toks.len() >= 5 { @@ -199,8 +225,14 @@ fn annotation_prefix_for_ext(ext: &str, default: &'static str) -> &'static str { } } -pub fn verify_file(source: &str, file_suffix: &str, graph: &impl Graph, prefix: &str) -> (Vec, BTreeMap) { - let groups = parse_file_annotations(source, prefix); +pub fn verify_file( + source: &str, + file_suffix: &str, + graph: &impl Graph, + prefix: &str, + use_lsp: bool, +) -> (Vec, BTreeMap) { + let groups = parse_file_annotations(source, prefix, use_lsp); let absent = parse_absent_annotations(source, prefix); let mut failures: Vec = Vec::new(); let mut counts: BTreeMap = BTreeMap::new(); @@ -296,12 +328,28 @@ pub fn verify_file(source: &str, file_suffix: &str, graph: &impl Graph, prefix: (failures, counts) } -pub fn walk_and_verify(fixture_dir: &Path, root: &Path, graph: &impl Graph, lang: &Language) -> Vec { +pub fn walk_and_verify( + fixture_dir: &Path, + root: &Path, + graph: &impl Graph, + lang: &Language, + use_lsp: bool, +) -> Vec { let mut failures = Vec::new(); let mut counts: BTreeMap = BTreeMap::new(); let exts: Vec<&str> = lang.exts(); let skip_dirs: Vec<&str> = lang.skip_dirs(); - walk_impl(fixture_dir, root, graph, &mut failures, &mut counts, &exts, &skip_dirs, lang); + walk_impl( + fixture_dir, + root, + graph, + &mut failures, + &mut counts, + &exts, + &skip_dirs, + lang, + use_lsp, + ); for (node_type, expected) in &counts { let actual = graph .find_nodes_by_type(node_type.clone()) @@ -327,6 +375,7 @@ fn walk_impl( exts: &[&str], skip_dirs: &[&str], lang: &Language, + use_lsp: bool, ) { let Ok(read) = std::fs::read_dir(dir) else { return; @@ -341,7 +390,9 @@ fn walk_impl( if skip_dirs.contains(&dir_name) { continue; } - walk_impl(&path, root, graph, failures, counts, exts, skip_dirs, lang); + walk_impl( + &path, root, graph, failures, counts, exts, skip_dirs, lang, use_lsp, + ); } else { let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); if !exts.contains(&ext) { @@ -358,7 +409,8 @@ fn walk_impl( .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| path.to_string_lossy().to_string()); let file_prefix = annotation_prefix_for_ext(ext, lang.annotation_prefix()); - let (file_failures, file_counts) = verify_file(&src, &suffix, graph, file_prefix); + let (file_failures, file_counts) = + verify_file(&src, &suffix, graph, file_prefix, use_lsp); failures.extend(file_failures); for (nt, n) in file_counts { *counts.entry(nt).or_insert(0) += n; @@ -371,11 +423,20 @@ pub async fn run_fixture_test( subdir: &str, lang: &str, annotation_lang: Language, +) -> Result<()> { + run_fixture_test_with_lsp::(subdir, lang, annotation_lang, false).await +} + +pub async fn run_fixture_test_with_lsp( + subdir: &str, + lang: &str, + annotation_lang: Language, + use_lsp: bool, ) -> Result<()> { let repo = Repo::new( subdir, Lang::from_str(lang).unwrap(), - false, + use_lsp, Vec::new(), Vec::new(), ) @@ -385,7 +446,7 @@ pub async fn run_fixture_test( graph.analysis(); let fixture_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(subdir); let root = Path::new(env!("CARGO_MANIFEST_DIR")); - let failures = walk_and_verify(&fixture_dir, root, &graph, &annotation_lang); + let failures = walk_and_verify(&fixture_dir, root, &graph, &annotation_lang, use_lsp); if !failures.is_empty() { for f in &failures { eprintln!("{}", f); @@ -394,3 +455,25 @@ pub async fn run_fixture_test( } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_mode_specific_edge_annotations() { + let source = r#" +# @ast node: UnitTest "test_process_returns_hash" +# @ast edge_no_lsp: Calls -> Function "process" "countries_controller.rb" +# @ast edge_lsp: Calls -> Function "process" "person_service.rb" +"#; + + let without_lsp = parse_file_annotations(source, "# @ast ", false); + let with_lsp = parse_file_annotations(source, "# @ast ", true); + + assert_eq!(without_lsp[0].3.len(), 1); + assert_eq!(without_lsp[0].3[0].other_file, "countries_controller.rb"); + assert_eq!(with_lsp[0].3.len(), 1); + assert_eq!(with_lsp[0].3[0].other_file, "person_service.rb"); + } +} diff --git a/ast/src/testing/graphs/compare_graphs.rs b/ast/src/testing/graphs/compare_graphs.rs index 4998afd33..df11d5b03 100644 --- a/ast/src/testing/graphs/compare_graphs.rs +++ b/ast/src/testing/graphs/compare_graphs.rs @@ -76,13 +76,24 @@ async fn compare_graphs_inner(lang_id: &str, repo_path: &str) -> Result<()> { ); } - assert_eq!( - array_graph.nodes.len(), - btree_map_graph.nodes.len(), - "Node counts do not match: ArrayGraph has {}, BTreeMapGraph has {}", - array_graph.nodes.len(), - btree_map_graph.nodes.len() - ); + let node_count_diff = + (array_graph.nodes.len() as i32 - btree_map_graph.nodes.len() as i32).abs(); + if use_lsp { + assert!( + node_count_diff <= 2, + "Node counts differ by more than 2: ArrayGraph has {}, BTreeMapGraph has {}", + array_graph.nodes.len(), + btree_map_graph.nodes.len() + ); + } else { + assert_eq!( + array_graph.nodes.len(), + btree_map_graph.nodes.len(), + "Node counts do not match: ArrayGraph has {}, BTreeMapGraph has {}", + array_graph.nodes.len(), + btree_map_graph.nodes.len() + ); + } if use_lsp { assert!( diff --git a/ast/src/testing/mod.rs b/ast/src/testing/mod.rs index 67975d1e4..0b8aef2fc 100644 --- a/ast/src/testing/mod.rs +++ b/ast/src/testing/mod.rs @@ -6,7 +6,7 @@ use std::str::FromStr; pub mod annotations; pub mod bash_toml; -use annotations::run_fixture_test; +use annotations::{run_fixture_test, run_fixture_test_with_lsp}; #[cfg(test)] pub mod builder; @@ -206,17 +206,18 @@ async fn test_python_cli() { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_ruby() { + let use_lsp = Language::Ruby.default_do_lsp(); #[cfg(not(feature = "neo4j"))] { - run_fixture_test::("src/testing/ruby", "ruby", Language::Ruby).await.unwrap(); - run_fixture_test::("src/testing/ruby", "ruby", Language::Ruby).await.unwrap(); + run_fixture_test_with_lsp::("src/testing/ruby", "ruby", Language::Ruby, use_lsp).await.unwrap(); + run_fixture_test_with_lsp::("src/testing/ruby", "ruby", Language::Ruby, use_lsp).await.unwrap(); } #[cfg(feature = "neo4j")] { - use crate::{lang::graphs::Neo4jGraph, testing::annotations::run_fixture_test}; + use crate::{lang::graphs::Neo4jGraph, testing::annotations::run_fixture_test_with_lsp}; let graph = Neo4jGraph::default(); graph.clear().await.unwrap(); - run_fixture_test::("src/testing/ruby", "ruby", Language::Ruby).await.unwrap(); + run_fixture_test_with_lsp::("src/testing/ruby", "ruby", Language::Ruby, use_lsp).await.unwrap(); } } diff --git a/ast/src/testing/ruby/test/services/person_service_minitest_test.rb b/ast/src/testing/ruby/test/services/person_service_minitest_test.rb index 05c74be53..2135992cb 100644 --- a/ast/src/testing/ruby/test/services/person_service_minitest_test.rb +++ b/ast/src/testing/ruby/test/services/person_service_minitest_test.rb @@ -1,12 +1,12 @@ # @ast node: UnitTest "test_process_returns_hash" # @ast edge: Calls -> Class "PersonService" "person_service.rb" -# @ast edge: Calls -> Function "process" "countries_controller.rb" +# @ast edge_no_lsp: Calls -> Function "process" "countries_controller.rb" # @ast node: UnitTest "test_process_includes_person_name" # @ast edge: Calls -> Class "PersonService" "person_service.rb" -# @ast edge: Calls -> Function "process" "countries_controller.rb" +# @ast edge_no_lsp: Calls -> Function "process" "countries_controller.rb" # @ast node: UnitTest "test_process_raises_on_nil" # @ast edge: Calls -> Class "PersonService" "person_service.rb" -# @ast edge: Calls -> Function "process" "countries_controller.rb" +# @ast edge_no_lsp: Calls -> Function "process" "countries_controller.rb" require 'test_helper' class PersonServiceMinitestTest < Minitest::Test diff --git a/lsp/Dockerfile b/lsp/Dockerfile index eaa7f5d4b..ec93c4fdd 100644 --- a/lsp/Dockerfile +++ b/lsp/Dockerfile @@ -79,7 +79,7 @@ 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 -# Install asdf + Ruby 3.2.2 + Ruby LSPs +# Install asdf + Ruby 3.4.2 + Ruby LSPs SHELL ["/bin/bash", "-lc"] RUN set -eux; \ export ASDF_DIR=/root/.asdf; \ @@ -87,9 +87,9 @@ RUN set -eux; \ echo '. $ASDF_DIR/asdf.sh' >> /etc/profile.d/asdf.sh; \ . "$ASDF_DIR/asdf.sh"; \ asdf plugin-add ruby https://github.com/asdf-vm/asdf-ruby.git || true; \ - asdf install ruby 3.2.2; \ - asdf global ruby 3.2.2; \ - gem install --no-document bundler ruby-lsp solargraph + asdf install ruby 3.4.2; \ + asdf global ruby 3.4.2; \ + gem install --no-document bundler -v 2.6.2 ruby-lsp solargraph # Add asdf shims to PATH globally ENV PATH="/root/.asdf/shims:/root/.asdf/bin:$PATH" @@ -125,4 +125,4 @@ RUN set -eux; \ exit 1; \ fi; \ chmod +x /usr/local/bin/kotlin-language-server; \ - rm -rf /tmp/kls \ No newline at end of file + rm -rf /tmp/kls diff --git a/lsp/src/language.rs b/lsp/src/language.rs index c5d33be57..3b84db4c4 100644 --- a/lsp/src/language.rs +++ b/lsp/src/language.rs @@ -168,7 +168,10 @@ impl Language { pub fn default_do_lsp(&self) -> bool { if let Ok(use_lsp) = std::env::var("USE_LSP") { if use_lsp == "true" || use_lsp == "1" { - return matches!(self, Self::Rust | Self::Go | Self::Typescript | Self::Java); + return matches!( + self, + Self::Rust | Self::Go | Self::Typescript | Self::Java | Self::Ruby + ); } } false @@ -268,7 +271,7 @@ impl Language { Self::Go => Vec::new(), Self::Typescript => vec!["npm install --force"], Self::Python => Vec::new(), - Self::Ruby => Vec::new(), + Self::Ruby => vec!["bundle install"], Self::Kotlin => Vec::new(), Self::Swift => Vec::new(), Self::Java => Vec::new(), @@ -437,37 +440,55 @@ pub fn junk_directories() -> Vec<&'static str> { #[cfg(test)] mod tests { use super::*; + use std::sync::Mutex; + + static ENV_MUTEX: Mutex<()> = Mutex::new(()); #[test] fn test_default_do_lsp_with_use_lsp_true() { + let _guard = ENV_MUTEX.lock().unwrap(); std::env::set_var("USE_LSP", "true"); assert!(Language::Rust.default_do_lsp()); assert!(Language::Go.default_do_lsp()); assert!(Language::Typescript.default_do_lsp()); assert!(Language::Java.default_do_lsp()); + assert!(Language::Ruby.default_do_lsp()); assert!(!Language::Python.default_do_lsp()); - assert!(!Language::Ruby.default_do_lsp()); std::env::remove_var("USE_LSP"); } #[test] fn test_default_do_lsp_with_use_lsp_one() { + let _guard = ENV_MUTEX.lock().unwrap(); std::env::set_var("USE_LSP", "1"); assert!(Language::Rust.default_do_lsp()); assert!(Language::Go.default_do_lsp()); assert!(Language::Typescript.default_do_lsp()); assert!(Language::Java.default_do_lsp()); + assert!(Language::Ruby.default_do_lsp()); assert!(!Language::Python.default_do_lsp()); std::env::remove_var("USE_LSP"); } #[test] fn test_default_do_lsp_without_use_lsp() { + let _guard = ENV_MUTEX.lock().unwrap(); std::env::remove_var("USE_LSP"); assert!(!Language::Rust.default_do_lsp()); assert!(!Language::Go.default_do_lsp()); assert!(!Language::Typescript.default_do_lsp()); assert!(!Language::Java.default_do_lsp()); + assert!(!Language::Ruby.default_do_lsp()); assert!(!Language::Python.default_do_lsp()); } + + #[test] + fn test_ruby_post_clone_cmd_with_lsp() { + let _guard = ENV_MUTEX.lock().unwrap(); + std::env::remove_var("LSP_SKIP_POST_CLONE"); + std::env::remove_var("REPO_PATH"); + std::env::remove_var("USE_LSP"); + assert_eq!(Language::Ruby.post_clone_cmd(true), vec!["bundle install"]); + assert!(Language::Ruby.post_clone_cmd(false).is_empty()); + } }