use crate::parser::language::{
Export, Import, LanguageSupport, ParseResult, Symbol, SymbolKind, Visibility,
};
use tree_sitter::Language as TsLanguage;
pub struct CSharpLanguage;
impl CSharpLanguage {
fn node_text<'a>(node: &tree_sitter::Node, source: &'a [u8]) -> &'a str {
node.utf8_text(source).unwrap_or("")
}
fn first_line(node: &tree_sitter::Node, source: &[u8]) -> String {
let text = Self::node_text(node, source);
text.lines().next().unwrap_or("").trim().to_string()
}
fn extract_name(node: &tree_sitter::Node, source: &[u8]) -> String {
if let Some(name_node) = node.child_by_field_name("name") {
return Self::node_text(&name_node, source).to_string();
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
return Self::node_text(&child, source).to_string();
}
}
String::new()
}
fn extract_visibility(node: &tree_sitter::Node, source: &[u8]) -> Visibility {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "modifier" {
let text = Self::node_text(&child, source);
if text == "public" {
return Visibility::Public;
}
}
}
Visibility::Private
}
fn extract_fn_signature(node: &tree_sitter::Node, source: &[u8]) -> String {
let full_text = Self::node_text(node, source);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "block" {
let body_start = child.start_byte() - node.start_byte();
return full_text[..body_start].trim().to_string();
}
}
full_text.lines().next().unwrap_or("").trim().to_string()
}
fn extract_fn_body(node: &tree_sitter::Node, source: &[u8]) -> String {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "block" {
let text = &source[child.start_byte()..child.end_byte()];
return String::from_utf8_lossy(text).into_owned();
}
}
String::new()
}
fn extract_using(node: &tree_sitter::Node, source: &[u8]) -> Option<Import> {
let text = Self::node_text(node, source);
let inner = text
.trim_start_matches("using")
.trim()
.trim_end_matches(';')
.trim();
if inner.is_empty() {
return None;
}
if let Some(sep) = inner.rfind('.') {
let ns = inner[..sep].to_string();
let name = inner[sep + 1..].to_string();
Some(Import {
source: ns,
names: vec![name],
})
} else {
Some(Import {
source: String::new(),
names: vec![inner.to_string()],
})
}
}
}
impl LanguageSupport for CSharpLanguage {
fn ts_language(&self) -> TsLanguage {
tree_sitter_c_sharp::LANGUAGE.into()
}
fn name(&self) -> &str {
"csharp"
}
fn extract(&self, source: &str, tree: &tree_sitter::Tree) -> ParseResult {
let source_bytes = source.as_bytes();
let root = tree.root_node();
let mut symbols: Vec<Symbol> = Vec::new();
let mut imports: Vec<Import> = Vec::new();
let mut exports: Vec<Export> = Vec::new();
let mut stack: Vec<tree_sitter::Node> = root.children(&mut root.walk()).collect();
while let Some(node) = stack.pop() {
match node.kind() {
"using_directive" => {
if let Some(imp) = Self::extract_using(&node, source_bytes) {
imports.push(imp);
}
}
"class_declaration" => {
let name = Self::extract_name(&node, source_bytes);
let visibility = Self::extract_visibility(&node, source_bytes);
let is_pub = visibility == Visibility::Public;
let signature = Self::first_line(&node, source_bytes);
let body = Self::node_text(&node, source_bytes).to_string();
let start_line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
if is_pub {
exports.push(Export {
name: name.clone(),
kind: SymbolKind::Class,
});
}
symbols.push(Symbol {
name,
kind: SymbolKind::Class,
visibility,
signature,
body,
start_line,
end_line,
});
if let Some(body_node) = node.child_by_field_name("body") {
let mut cursor = body_node.walk();
for child in body_node.children(&mut cursor) {
stack.push(child);
}
}
}
"interface_declaration" => {
let name = Self::extract_name(&node, source_bytes);
let visibility = Self::extract_visibility(&node, source_bytes);
let is_pub = visibility == Visibility::Public;
let signature = Self::first_line(&node, source_bytes);
let body = Self::node_text(&node, source_bytes).to_string();
let start_line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
if is_pub {
exports.push(Export {
name: name.clone(),
kind: SymbolKind::Interface,
});
}
symbols.push(Symbol {
name,
kind: SymbolKind::Interface,
visibility,
signature,
body,
start_line,
end_line,
});
}
"struct_declaration" => {
let name = Self::extract_name(&node, source_bytes);
let visibility = Self::extract_visibility(&node, source_bytes);
let is_pub = visibility == Visibility::Public;
let signature = Self::first_line(&node, source_bytes);
let body = Self::node_text(&node, source_bytes).to_string();
let start_line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
if is_pub {
exports.push(Export {
name: name.clone(),
kind: SymbolKind::Struct,
});
}
symbols.push(Symbol {
name,
kind: SymbolKind::Struct,
visibility,
signature,
body,
start_line,
end_line,
});
}
"enum_declaration" => {
let name = Self::extract_name(&node, source_bytes);
let visibility = Self::extract_visibility(&node, source_bytes);
let is_pub = visibility == Visibility::Public;
let signature = Self::first_line(&node, source_bytes);
let body = Self::node_text(&node, source_bytes).to_string();
let start_line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
if is_pub {
exports.push(Export {
name: name.clone(),
kind: SymbolKind::Enum,
});
}
symbols.push(Symbol {
name,
kind: SymbolKind::Enum,
visibility,
signature,
body,
start_line,
end_line,
});
}
"method_declaration" => {
let name = Self::extract_name(&node, source_bytes);
let visibility = Self::extract_visibility(&node, source_bytes);
let is_pub = visibility == Visibility::Public;
let signature = Self::extract_fn_signature(&node, source_bytes);
let body = Self::extract_fn_body(&node, source_bytes);
let start_line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
if is_pub {
exports.push(Export {
name: name.clone(),
kind: SymbolKind::Method,
});
}
symbols.push(Symbol {
name,
kind: SymbolKind::Method,
visibility,
signature,
body,
start_line,
end_line,
});
}
"namespace_declaration"
| "file_scoped_namespace_declaration"
| "declaration_list" => {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
_ => {}
}
}
ParseResult {
symbols,
imports,
exports,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::language::{SymbolKind, Visibility};
fn make_parser() -> tree_sitter::Parser {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_c_sharp::LANGUAGE.into())
.expect("failed to set language");
parser
}
#[test]
fn test_extract_public_class() {
let source = r#"public class HelloWorld {
public void Greet() {
Console.WriteLine("Hello");
}
}
"#;
let mut parser = make_parser();
let tree = parser.parse(source, None).expect("parse failed");
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert!(!classes.is_empty(), "expected class symbol");
assert_eq!(classes[0].name, "HelloWorld");
assert_eq!(classes[0].visibility, Visibility::Public);
let exported: Vec<_> = result
.exports
.iter()
.filter(|e| e.name == "HelloWorld")
.collect();
assert!(!exported.is_empty(), "HelloWorld should be exported");
}
#[test]
fn test_extract_method_visibility() {
let source = r#"public class Foo {
public void PublicMethod() {}
private void PrivateMethod() {}
}
"#;
let mut parser = make_parser();
let tree = parser.parse(source, None).expect("parse failed");
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 2, "expected 2 methods, got {:?}", methods);
let pub_method = methods
.iter()
.find(|m| m.name == "PublicMethod")
.expect("PublicMethod not found");
assert_eq!(pub_method.visibility, Visibility::Public);
let priv_method = methods
.iter()
.find(|m| m.name == "PrivateMethod")
.expect("PrivateMethod not found");
assert_eq!(priv_method.visibility, Visibility::Private);
}
#[test]
fn test_extract_using_import() {
let source = r#"using System.Collections.Generic;
using System.Linq;
"#;
let mut parser = make_parser();
let tree = parser.parse(source, None).expect("parse failed");
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
assert_eq!(
result.imports.len(),
2,
"expected 2 imports, got {:?}",
result.imports
);
let names: Vec<&str> = result
.imports
.iter()
.flat_map(|i| i.names.iter().map(|n| n.as_str()))
.collect();
assert!(
names.contains(&"Generic"),
"expected Generic import, got: {:?}",
names
);
assert!(
names.contains(&"Linq"),
"expected Linq import, got: {:?}",
names
);
}
#[test]
fn test_extract_interface() {
let source = "public interface IDrawable {\n void Draw();\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let interfaces: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Interface)
.collect();
assert!(!interfaces.is_empty(), "expected interface symbol");
assert_eq!(interfaces[0].name, "IDrawable");
assert_eq!(interfaces[0].visibility, Visibility::Public);
}
#[test]
fn test_extract_struct() {
let source = "public struct Point {\n public int X;\n public int Y;\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let structs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Struct)
.collect();
assert!(!structs.is_empty(), "expected struct symbol");
assert_eq!(structs[0].name, "Point");
assert_eq!(structs[0].visibility, Visibility::Public);
}
#[test]
fn test_extract_enum() {
let source = "public enum Color {\n Red,\n Green,\n Blue\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let enums: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Enum)
.collect();
assert!(!enums.is_empty(), "expected enum symbol");
assert_eq!(enums[0].name, "Color");
assert_eq!(enums[0].visibility, Visibility::Public);
}
#[test]
fn test_extract_nested_class() {
let source =
"public class Outer {\n public class Inner {\n public void DoWork() {}\n }\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert!(
classes.len() >= 2,
"expected Outer and Inner classes, got {:?}",
classes.iter().map(|c| &c.name).collect::<Vec<_>>()
);
}
#[test]
fn test_extract_private_class() {
let source = "class InternalClass {\n void Method() {}\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert!(!classes.is_empty());
assert_eq!(classes[0].visibility, Visibility::Private);
assert!(result.exports.iter().all(|e| e.name != "InternalClass"));
}
#[test]
fn test_extract_namespace_class() {
let source =
"namespace MyApp {\n public class Service {\n public void Run() {}\n }\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert!(!classes.is_empty(), "expected class inside namespace");
assert_eq!(classes[0].name, "Service");
}
#[test]
fn test_empty_source() {
let source = "";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
assert!(result.symbols.is_empty());
assert!(result.imports.is_empty());
assert!(result.exports.is_empty());
}
#[test]
fn test_extract_multiple_classes() {
let source = "public class Foo {}\npublic class Bar {}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 2);
assert_eq!(result.exports.len(), 2);
}
#[test]
fn test_extract_static_method() {
let source = "public class Program {\n public static void Main(string[] args) {}\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert!(!methods.is_empty(), "expected Main method");
assert_eq!(methods[0].name, "Main");
}
#[test]
fn test_extract_simple_using() {
let source = "using System;\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].source, "");
assert!(result.imports[0].names.contains(&"System".to_string()));
}
#[test]
fn test_extract_method_visibility_variants() {
let source = "public class Svc {\n private void Secret() {}\n protected void Middle() {}\n internal void Local() {}\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert!(
methods.len() >= 2,
"expected multiple methods, got: {:?}",
methods.iter().map(|m| &m.name).collect::<Vec<_>>()
);
}
#[test]
fn test_interface_method_no_body() {
let source =
"public interface IService {\n void Execute(string cmd);\n int GetCount();\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
for m in &methods {
assert!(
m.body.is_empty(),
"interface method should have no body: {}",
m.name
);
}
}
#[test]
fn test_empty_using_directive() {
let source = "using ;\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let _ = result;
}
#[test]
fn test_using_single_name() {
let source = "using Global;\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
if !result.imports.is_empty() {
assert_eq!(result.imports[0].names, vec!["Global".to_string()]);
}
}
#[test]
fn test_abstract_class_method() {
let source = "public abstract class Base {\n public abstract void Process();\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let _ = result;
}
#[test]
fn test_extract_name_fallback_to_identifier() {
let source = "public class MyClass {\n public void Run() {}\n}\n";
let mut parser = make_parser();
let tree = parser.parse(source, None).unwrap();
let lang = CSharpLanguage;
let result = lang.extract(source, &tree);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert!(!classes.is_empty());
assert_eq!(classes[0].name, "MyClass");
}
}