diff --git a/ast/src/repo.rs b/ast/src/repo.rs index daf79de4e..daa7a9efd 100644 --- a/ast/src/repo.rs +++ b/ast/src/repo.rs @@ -567,7 +567,7 @@ impl Repo { Ok(()) } fn start_lsp(root: &str, lang: &Lang, lsp: bool) -> Result> { - Ok(if lsp { + Ok(if lsp && lang.kind.has_lsp_support() { let (tx, rx) = tokio::sync::mpsc::channel(10000); spawn_analyzer(&root.into(), &lang.kind, rx)?; Some(tx) diff --git a/ast/src/testing/annotations.rs b/ast/src/testing/annotations.rs index 713e766cf..389d753c9 100644 --- a/ast/src/testing/annotations.rs +++ b/ast/src/testing/annotations.rs @@ -108,6 +108,22 @@ fn parse_meta_filter(s: &str) -> BTreeMap { map } +fn strip_mode_annotation<'a>( + trimmed: &'a str, + prefix: &str, + keyword: &str, + use_lsp: bool, +) -> Option<&'a str> { + let common_prefix = format!("{}{}: ", prefix, keyword); + if let Some(rest) = trimmed.strip_prefix(common_prefix.as_str()) { + return Some(rest); + } + + let mode = if use_lsp { "lsp" } else { "no_lsp" }; + let mode_prefix = format!("{}{}_{}: ", prefix, keyword, mode); + trimmed.strip_prefix(mode_prefix.as_str()) +} + #[derive(Debug, Clone)] struct AbsentAnnotation { node_type: NodeType, @@ -115,12 +131,11 @@ struct AbsentAnnotation { file_suffix: String, } -fn parse_absent_annotations(source: &str, prefix: &str) -> Vec { - let absent_prefix = format!("{}absent: ", prefix); +fn parse_absent_annotations(source: &str, prefix: &str, use_lsp: bool) -> Vec { let mut result = Vec::new(); for line in source.lines() { let trimmed = line.trim(); - if let Some(rest) = trimmed.strip_prefix(absent_prefix.as_str()) { + if let Some(rest) = strip_mode_annotation(trimmed, prefix, "absent", use_lsp) { let toks = parse_quoted_tokens(rest); if toks.len() >= 3 { if let Some(nt) = parse_node_type(&toks[0]) { @@ -139,6 +154,7 @@ fn parse_absent_annotations(source: &str, prefix: &str) -> Vec fn parse_file_annotations( source: &str, prefix: &str, + use_lsp: bool, ) -> Vec<(NodeType, String, BTreeMap, Vec)> { let mut result = Vec::new(); let mut current: Option<(NodeType, String, BTreeMap, Vec)> = @@ -146,8 +162,8 @@ fn parse_file_annotations( for line in source.lines() { let trimmed = line.trim(); - if let Some(rest) = trimmed.strip_prefix(prefix) { - if let Some(node_rest) = rest.strip_prefix("node: ") { + if trimmed.strip_prefix(prefix).is_some() { + if let Some(node_rest) = strip_mode_annotation(trimmed, prefix, "node", use_lsp) { if let Some(prev) = current.take() { result.push(prev); } @@ -158,7 +174,7 @@ 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) = strip_mode_annotation(trimmed, prefix, "edge", use_lsp) { if let Some((_, _, _, ref mut edges)) = current { let toks = parse_quoted_tokens(edge_rest); if toks.len() >= 5 { @@ -199,9 +215,9 @@ 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); - let absent = parse_absent_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, use_lsp); let mut failures: Vec = Vec::new(); let mut counts: BTreeMap = BTreeMap::new(); @@ -296,17 +312,31 @@ 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 { +fn node_file_is_in_fixture(file: &str, fixture_prefix: &str) -> bool { + let file = file.replace('\\', "/"); + file == fixture_prefix + || file.starts_with(&format!("{}/", fixture_prefix)) + || file.ends_with(&format!("/{}", fixture_prefix)) + || file.contains(&format!("/{}/", fixture_prefix)) +} + +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); + let fixture_prefix = fixture_dir + .strip_prefix(root) + .unwrap_or(fixture_dir) + .to_string_lossy() + .replace('\\', "/"); + 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()) .iter() .filter(|n| !n.name.contains('\n')) + .filter(|n| node_file_is_in_fixture(&n.file, &fixture_prefix)) .count(); if actual != *expected { failures.push(format!( @@ -327,6 +357,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 +372,7 @@ 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 +389,7 @@ 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 +402,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 +425,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); diff --git a/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MainActivity.kt b/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MainActivity.kt index c9dc6fcb7..fab7bb4ec 100644 --- a/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MainActivity.kt +++ b/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/MainActivity.kt @@ -6,6 +6,7 @@ package com.kotlintestapp // @ast node: Function "PersonItem" // @ast node: Function "PersonList" // @ast edge: Calls -> Function "PersonItem" "MainActivity.kt" +// @ast edge_lsp: Calls -> Function "updatePerson" "PersonViewModel.kt" // @ast node: Function "UpdateProfileDialog" // @ast node: Import "import-imports-srctestingkotlinappsrcmainjavacomkotlintestappmainactivitykt-0" @@ -150,4 +151,3 @@ fun UpdateProfileDialog( } ) } - diff --git a/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/ui/viewmodels/HomeViewModel.kt b/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/ui/viewmodels/HomeViewModel.kt index 61b36da57..6ec69b8a6 100644 --- a/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/ui/viewmodels/HomeViewModel.kt +++ b/ast/src/testing/kotlin/app/src/main/java/com/kotlintestapp/ui/viewmodels/HomeViewModel.kt @@ -4,6 +4,7 @@ package com.kotlintestapp.ui.viewmodels // @ast edge: Operand -> Function "onUserClicked" "HomeViewModel.kt" // @ast node: DataModel "HomeViewModel" // @ast node: Function "fetchUsers" +// @ast edge_lsp: Calls -> Function "getUsers" "UserRepository.kt" // @ast node: Function "onUserClicked" // @ast node: Import "import-imports-srctestingkotlinappsrcmainjavacomkotlintestappuiviewmodelshomeviewmodelkt-0" diff --git a/ast/src/testing/mod.rs b/ast/src/testing/mod.rs index 67975d1e4..bcc711aef 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; @@ -220,6 +220,39 @@ async fn test_ruby() { } } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_kotlin() { + let use_lsp = Language::Kotlin.default_do_lsp(); + + #[cfg(not(feature = "neo4j"))] + { + run_fixture_test_with_lsp::( + "src/testing/kotlin", + "kotlin", + Language::Kotlin, + use_lsp, + ).await.unwrap(); + run_fixture_test_with_lsp::( + "src/testing/kotlin", + "kotlin", + Language::Kotlin, + use_lsp, + ).await.unwrap(); + } + #[cfg(feature = "neo4j")] + { + use crate::{lang::graphs::Neo4jGraph, testing::annotations::run_fixture_test_with_lsp}; + let graph = Neo4jGraph::default(); + graph.clear().await.unwrap(); + run_fixture_test_with_lsp::( + "src/testing/kotlin", + "kotlin", + Language::Kotlin, + use_lsp, + ).await.unwrap(); + } +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_c() { #[cfg(not(feature = "neo4j"))] diff --git a/lsp/src/language.rs b/lsp/src/language.rs index c5d33be57..e506766f6 100644 --- a/lsp/src/language.rs +++ b/lsp/src/language.rs @@ -168,7 +168,7 @@ 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::Kotlin); } } false @@ -445,6 +445,7 @@ mod tests { assert!(Language::Go.default_do_lsp()); assert!(Language::Typescript.default_do_lsp()); assert!(Language::Java.default_do_lsp()); + assert!(Language::Kotlin.default_do_lsp()); assert!(!Language::Python.default_do_lsp()); assert!(!Language::Ruby.default_do_lsp()); std::env::remove_var("USE_LSP"); @@ -457,6 +458,7 @@ mod tests { assert!(Language::Go.default_do_lsp()); assert!(Language::Typescript.default_do_lsp()); assert!(Language::Java.default_do_lsp()); + assert!(Language::Kotlin.default_do_lsp()); assert!(!Language::Python.default_do_lsp()); std::env::remove_var("USE_LSP"); } @@ -468,6 +470,7 @@ mod tests { assert!(!Language::Go.default_do_lsp()); assert!(!Language::Typescript.default_do_lsp()); assert!(!Language::Java.default_do_lsp()); + assert!(!Language::Kotlin.default_do_lsp()); assert!(!Language::Python.default_do_lsp()); } }