Skip to content
Merged
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
110 changes: 110 additions & 0 deletions lsp-daemon/src/analyzer/tree_sitter_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ impl ParserPool {
"solidity" | "sol" => Some(tree_sitter_solidity::LANGUAGE),
"crystal" | "cr" => Some(tree_sitter_crystal::LANGUAGE),
"haskell" | "hs" | "lhs" => Some(tree_sitter_haskell::LANGUAGE),
"ruby" | "rb" => Some(tree_sitter_ruby::LANGUAGE),
_ => None,
};

Expand Down Expand Up @@ -453,6 +454,7 @@ impl TreeSitterAnalyzer {
"c" | "cpp" | "c++" => self.map_c_node_to_symbol(node_kind),
"crystal" | "cr" => self.map_crystal_node_to_symbol(node_kind),
"haskell" | "hs" | "lhs" => self.map_haskell_node_to_symbol(node_kind),
"ruby" | "rb" => self.map_ruby_node_to_symbol(node_kind),
_ => self.map_generic_node_to_symbol(node_kind),
};

Expand Down Expand Up @@ -610,6 +612,17 @@ impl TreeSitterAnalyzer {
}
}

/// Map Ruby node kinds to symbol kinds
fn map_ruby_node_to_symbol(&self, node_kind: &str) -> Option<SymbolKind> {
match node_kind {
"method" => Some(SymbolKind::Method),
"singleton_method" => Some(SymbolKind::Method),
"class" => Some(SymbolKind::Class),
"module" => Some(SymbolKind::Module),
_ => None,
}
}

/// Generic node mapping for unknown languages
fn map_generic_node_to_symbol(&self, node_kind: &str) -> Option<SymbolKind> {
if node_kind.contains("function") {
Expand Down Expand Up @@ -934,6 +947,10 @@ impl TreeSitterAnalyzer {
| "data_family"
| "pattern_synonym"
),
"ruby" | "rb" => matches!(
node_kind,
"module" | "class" | "method" | "singleton_method"
),
_ => false,
}
}
Expand Down Expand Up @@ -1319,6 +1336,99 @@ end
);
}

#[test]
fn test_ruby_parser_pool_and_node_mapping() {
let analyzer = create_test_analyzer();
let mut pool = ParserPool::new();

assert!(
pool.get_parser("ruby").is_some(),
"Ruby parser should be available by language name"
);
assert!(
pool.get_parser("rb").is_some(),
"Ruby parser should be available by extension alias"
);
assert_eq!(
analyzer.map_ruby_node_to_symbol("module"),
Some(SymbolKind::Module)
);
assert_eq!(
analyzer.map_ruby_node_to_symbol("class"),
Some(SymbolKind::Class)
);
assert_eq!(
analyzer.map_ruby_node_to_symbol("method"),
Some(SymbolKind::Method)
);
assert_eq!(
analyzer.map_ruby_node_to_symbol("singleton_method"),
Some(SymbolKind::Method)
);
assert!(analyzer.creates_scope("class", "ruby"));
assert!(analyzer.creates_scope("method", "rb"));
}

#[tokio::test]
async fn test_ruby_symbol_extraction_uses_parser_pool() {
let analyzer = create_test_analyzer();
let uid_generator = Arc::new(SymbolUIDGenerator::new());
let context = AnalysisContext::new(
1,
2,
"ruby".to_string(),
PathBuf::from("."),
PathBuf::from("sample.rb"),
uid_generator,
);
let ruby_code = r#"
module Demo
class User
def self.build
new
end

def active?
true
end
end
end
"#;

let result = analyzer
.analyze_file(ruby_code, Path::new("sample.rb"), "ruby", &context)
.await
.expect("Ruby analysis should use the parser pool");

let symbols = result
.symbols
.iter()
.map(|symbol| format!("{}:{:?}", symbol.name, symbol.kind))
.collect::<Vec<_>>();

assert!(
result
.symbols
.iter()
.any(|symbol| symbol.name == "Demo" && symbol.kind == SymbolKind::Module),
"expected Demo module in symbols: {symbols:?}"
);
assert!(
result
.symbols
.iter()
.any(|symbol| symbol.name == "User" && symbol.kind == SymbolKind::Class),
"expected User class in symbols: {symbols:?}"
);
assert!(
result
.symbols
.iter()
.any(|symbol| symbol.name == "active?" && symbol.kind == SymbolKind::Method),
"expected active? method in symbols: {symbols:?}"
);
}

#[test]
fn test_haskell_parser_pool_and_node_mapping() {
let analyzer = create_test_analyzer();
Expand Down
36 changes: 31 additions & 5 deletions lsp-daemon/src/lsp_database_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,10 @@ impl LspDatabaseAdapter {
debug!("[TREE_SITTER] Using tree-sitter-php");
Some(tree_sitter_php::LANGUAGE_PHP.into())
}
"ruby" | "rb" => {
debug!("[TREE_SITTER] Using tree-sitter-ruby");
Some(tree_sitter_ruby::LANGUAGE.into())
}
_ => {
debug!(
"[TREE_SITTER] No parser available for language: {}",
Expand Down Expand Up @@ -971,6 +975,8 @@ impl LspDatabaseAdapter {
| "class" | "instance" | "type_synomym" | "type_family" | "type_instance"
| "data_family" | "data_instance" | "kind_signature" | "foreign_import"
| "foreign_export" | "pattern_synonym" => true,
// Ruby symbols
"module" | "method" | "singleton_method" => true,
_ => false,
}
}
Expand Down Expand Up @@ -1263,9 +1269,12 @@ impl LspDatabaseAdapter {
| "function_declaration"
| "function_definition"
| "func_declaration" => SymbolKind::Function,
"method_definition" | "method_declaration" | "method_def" | "abstract_method_def" => {
SymbolKind::Method
}
"method"
| "singleton_method"
| "method_definition"
| "method_declaration"
| "method_def"
| "abstract_method_def" => SymbolKind::Method,
"function" | "bind" | "signature" | "default_signature" | "foreign_import"
| "foreign_export" => SymbolKind::Function,
"macro_def" => SymbolKind::Macro,
Expand All @@ -1277,7 +1286,7 @@ impl LspDatabaseAdapter {
"trait_item" => SymbolKind::Trait,
"interface_declaration" | "lib_def" => SymbolKind::Interface,
"impl_item" => SymbolKind::Impl,
"mod_item" | "namespace" | "module_def" => SymbolKind::Module,
"mod_item" | "namespace" | "module" | "module_def" => SymbolKind::Module,
"type_declaration"
| "type_alias_declaration"
| "alias"
Expand Down Expand Up @@ -2491,6 +2500,7 @@ impl LspDatabaseAdapter {
kind,
"method_def" | "abstract_method_def" | "macro_def" | "fun_def"
),
"ruby" | "rb" => matches!(kind, "method" | "singleton_method"),
"hs" | "lhs" => matches!(
kind,
"function"
Expand Down Expand Up @@ -2535,6 +2545,7 @@ impl LspDatabaseAdapter {
kind,
"class_def" | "module_def" | "struct_def" | "enum_def" | "lib_def" | "union_def"
),
"ruby" | "rb" => matches!(kind, "class" | "module"),
"hs" | "lhs" => matches!(
kind,
"class"
Expand Down Expand Up @@ -3251,7 +3262,7 @@ pub fn test_function() -> i32 {
);
assert_eq!(
adapter.node_kind_to_symbol_kind("impl_item"),
SymbolKind::Class
SymbolKind::Impl
);

// Test Python mappings
Expand Down Expand Up @@ -3282,6 +3293,21 @@ pub fn test_function() -> i32 {
SymbolKind::Interface
);

// Test Ruby mappings
assert_eq!(
adapter.node_kind_to_symbol_kind("method"),
SymbolKind::Method
);
assert_eq!(
adapter.node_kind_to_symbol_kind("singleton_method"),
SymbolKind::Method
);
assert_eq!(
adapter.node_kind_to_symbol_kind("module"),
SymbolKind::Module
);
assert_eq!(adapter.node_kind_to_symbol_kind("class"), SymbolKind::Class);

// Test fallback
assert_eq!(
adapter.node_kind_to_symbol_kind("unknown_node"),
Expand Down
26 changes: 26 additions & 0 deletions lsp-daemon/src/symbol/language_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,30 @@ impl LanguageRules {
}
}

/// Create rules for Ruby
pub fn ruby() -> Self {
Self {
scope_separator: "::".to_string(),
anonymous_prefix: "anon".to_string(),
supports_overloading: false,
case_sensitive: true,
signature_normalization: SignatureNormalization::RemoveParameterNames,
visibility_affects_uid: false,
default_visibility: "public".to_string(),
file_extensions: vec!["rb".to_string(), "rake".to_string()],
signature_keywords: vec![
"def".to_string(),
"self".to_string(),
"class".to_string(),
"module".to_string(),
"private".to_string(),
"protected".to_string(),
"public".to_string(),
],
type_aliases: vec![],
}
}

/// Create rules for Go
pub fn go() -> Self {
Self {
Expand Down Expand Up @@ -573,6 +597,7 @@ impl LanguageRulesFactory {
"typescript" | "ts" => Some(LanguageRules::typescript()),
"javascript" | "js" => Some(LanguageRules::javascript()),
"python" | "py" => Some(LanguageRules::python()),
"ruby" | "rb" => Some(LanguageRules::ruby()),
"go" => Some(LanguageRules::go()),
"java" => Some(LanguageRules::java()),
"c" => Some(LanguageRules::c()),
Expand All @@ -591,6 +616,7 @@ impl LanguageRulesFactory {
"typescript".to_string(),
"javascript".to_string(),
"python".to_string(),
"ruby".to_string(),
"go".to_string(),
"java".to_string(),
"c".to_string(),
Expand Down
3 changes: 3 additions & 0 deletions lsp-daemon/src/symbol/uid_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ impl SymbolUIDGenerator {
// Python
rules.insert("python".to_string(), LanguageRules::python());

// Ruby
rules.insert("ruby".to_string(), LanguageRules::ruby());

// Go
rules.insert("go".to_string(), LanguageRules::go());

Expand Down
45 changes: 45 additions & 0 deletions src/extract/symbol_finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
if child.kind() == "identifier"
|| child.kind() == "type_identifier"
|| child.kind() == "field_identifier"
|| child.kind() == "constant"
|| child.kind() == "name"
{
if let Ok(name) = child.utf8_text(content) {
Expand Down Expand Up @@ -84,6 +85,7 @@
|| child.kind() == "field_identifier"
|| child.kind() == "type_identifier"
|| child.kind() == "property_identifier"
|| child.kind() == "constant"
|| child.kind() == "name"
|| child.kind() == "variable"
|| child.kind() == "constructor"
Expand Down Expand Up @@ -833,6 +835,49 @@
let _ = fs::remove_file(&test_file);
}

#[test]
fn test_ruby_class_and_nested_method_extraction() {
let temp_dir = std::env::temp_dir();
let test_file = temp_dir.join("test_ruby_nested_symbols.rb");

let content = r#"module RuboCop
module Cop
class Base
def self.documentation_url(config = nil)
Documentation.url_for(self, config)
end

def add_offense(node_or_range, message: nil, severity: nil, &block)
current_offenses << node_or_range
end
end
end
end
"#;

let mut file = fs::File::create(&test_file).unwrap();
write!(file, "{content}").unwrap();

let class_result = find_symbol_in_file(&test_file, "Base", content, true, 0)
.expect("Ruby class lookup should use AST instead of text fallback");
assert_eq!(class_result.node_type, "class");
assert!(class_result.code.contains("class Base"));

let method_result = find_symbol_in_file(&test_file, "Base.add_offense", content, true, 0)
.expect("Ruby nested method lookup should resolve inside class");
assert_eq!(method_result.node_type, "method");

Check warning on line 868 in src/extract/symbol_finder.rs

View check run for this annotation

probelabs / Visor: quality

logic Issue

Test assertion checks for 'current_offenses' string in method body without explaining why this specific value should be present. The test validates implementation details rather than behavior - it would pass if the method contained any string matching 'current_offenses' regardless of actual method logic.
Raw output
Add a comment explaining why 'current_offenses' should appear in the method body, or restructure the test to validate that the method actually performs offense tracking behavior rather than just containing a specific string.
assert!(method_result.code.contains("def add_offense"));
assert!(method_result.code.contains("current_offenses"));

let singleton_result =
find_symbol_in_file(&test_file, "Base.documentation_url", content, true, 0)
.expect("Ruby singleton method lookup should resolve inside class");
assert_eq!(singleton_result.node_type, "singleton_method");
assert!(singleton_result.code.contains("def self.documentation_url"));

let _ = fs::remove_file(&test_file);
}

#[test]
fn test_find_all_symbols_with_duplicate_names() {
let temp_dir = std::env::temp_dir();
Expand Down
Loading
Loading