use std::path::Path;
use anyhow::Result;
use tree_sitter::{Language, Node};
use super::{Entity, FileInsight, ImportStmt, KindRule, LanguageProcessor, SharedProcessor};
pub struct CSharpProcessor;
impl CSharpProcessor {
pub fn new() -> Result<Self> {
Ok(Self)
}
fn node_name<'a>(node: &tree_sitter::Node, bytes: &'a [u8]) -> Option<&'a str> {
node.child_by_field_name("name").and_then(|n| n.utf8_text(bytes).ok())
}
}
const KINDS: &[KindRule] = &[
KindRule::with_sig("class_declaration", "class", '{'),
KindRule::with_sig("record_declaration", "class", '{'),
KindRule::with_sig("struct_declaration", "struct", '{'),
KindRule::with_sig("interface_declaration", "interface", '{'),
KindRule::with_sig("enum_declaration", "enum", '{'),
KindRule::with_sig("method_declaration", "function", '{'),
KindRule::with_sig("constructor_declaration", "function", '{'),
KindRule::plain("property_declaration", "property"),
KindRule::plain("namespace_declaration", "mod"),
];
impl SharedProcessor for CSharpProcessor {
fn language() -> &'static str { "C#" }
fn grammar() -> Language { tree_sitter_c_sharp::LANGUAGE.into() }
fn kinds() -> &'static [KindRule] {
KINDS
}
fn handle_special(node: Node, bytes: &[u8], entities: &mut Vec<Entity>, imports: &mut Vec<ImportStmt>) {
match node.kind() {
"field_declaration" => {
let mut fd_cursor = node.walk();
for decl in node.children(&mut fd_cursor) {
if decl.kind() != "variable_declaration" {
continue;
}
let mut decl_cursor = decl.walk();
for child in decl.children(&mut decl_cursor) {
if child.kind() != "variable_declarator" {
continue;
}
let name = Self::node_name(&child, bytes)
.map(|s| s.to_string())
.or_else(|| child.utf8_text(bytes).ok().map(|s| s.trim().to_string()));
if let Some(name) = name {
entities.push(Entity {
name: name.to_string(), kind: "variable".to_string(),
line_start: child.start_position().row + 1,
line_end: child.end_position().row + 1,
doc_comment: None, signature: None, visibility: None,
});
}
}
}
}
"using_directive" => {
if let Ok(text) = node.utf8_text(bytes) {
let name = text
.strip_prefix("using ")
.and_then(|s| s.strip_suffix(';'))
.map(|s| s.trim())
.unwrap_or(text.trim());
imports.push(ImportStmt {
source: name.to_string(),
alias: None,
line: node.start_position().row + 1,
});
}
}
_ => {}
}
}
fn fallback(source: &str) -> (Vec<Entity>, Vec<ImportStmt>) {
let mut entities = Vec::new();
let mut imports = Vec::new();
for (i, line) in source.lines().enumerate() {
let line_no = i + 1;
let t = line.trim();
if let Some(rest) = t.strip_prefix("using ") {
let ns = rest.trim_end_matches(';').trim();
if !ns.contains('=') && !ns.starts_with("static ") {
imports.push(ImportStmt {
source: ns.to_string(), alias: None, line: line_no,
});
}
continue;
}
if let Some(rest) = t.strip_prefix("namespace ") {
let name = rest.split(&['{', ' ', ';'][..]).next().unwrap_or("").trim();
if !name.is_empty() {
entities.push(Entity {
name: name.to_string(), kind: "mod".into(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: None, visibility: None,
});
}
continue;
}
for prefix in &["class ", "struct ", "interface ", "enum ", "record "] {
if let Some(rest) = t.find(prefix).map(|pos| &t[pos..]) {
let name = rest.strip_prefix(prefix)
.and_then(|s| s.split(&['{', ' ', ':', '<', ';', '(', '}'][..]).next())
.map(|s| s.trim());
if let Some(name) = name
&& !name.is_empty() && name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false)
{
let kind = match *prefix {
"class " | "record " => "class",
"struct " => "struct",
"interface " => "interface",
"enum " => "enum",
_ => "class",
};
entities.push(Entity {
name: name.to_string(), kind: kind.to_string(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: Some(t.to_string()), visibility: None,
});
break;
}
}
}
if !entities.iter().any(|e| e.line_start == line_no) {
let method_candidate = t.split(&['(', '{'][..]).next().unwrap_or("");
let tokens: Vec<&str> = method_candidate.split_whitespace().collect();
if tokens.len() >= 2 {
if method_candidate.contains('(') {
let name_token = tokens.last().unwrap_or(&"");
let name = name_token.trim_end_matches('(');
if name.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false)
&& !["class", "struct", "interface", "enum", "namespace", "using", "if", "while", "for", "foreach", "switch", "return"].contains(&name)
{
entities.push(Entity {
name: name.to_string(), kind: "function".into(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: Some(t.to_string()), visibility: None,
});
}
}
}
}
if !entities.iter().any(|e| e.line_start == line_no) && t.contains("{") && !t.contains("(") {
let tokens: Vec<&str> = t.split_whitespace().collect();
if tokens.len() >= 2 {
let name = tokens.iter()
.position(|s| s.contains('{'))
.and_then(|pos| tokens.get(pos - 1))
.map(|s| s.trim())
.filter(|s| s.chars().next().map(|c| c.is_alphabetic() || c == '_').unwrap_or(false));
if let Some(name) = name {
entities.push(Entity {
name: name.to_string(), kind: "property".into(),
line_start: line_no, line_end: line_no,
doc_comment: None, signature: Some(t.to_string()), visibility: None,
});
}
}
}
}
(entities, imports)
}
}
impl LanguageProcessor for CSharpProcessor {
fn name(&self) -> &'static str { Self::language() }
fn extensions(&self) -> &[&str] { &[".cs"] }
fn parse(&self, source: &str, path: &Path) -> Result<FileInsight> {
Self::parse_file(source, path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_csharp_basics() {
let source = r#"using System;
using System.Collections.Generic;
namespace MyApp {
class Player { }
struct Point { public int X; }
interface ILogger { void Log(string msg); }
enum Color { Red, Green, Blue }
}
"#;
let proc = CSharpProcessor::new().unwrap();
let result = proc.parse(source, Path::new("test.cs")).unwrap();
assert!(result.imports.iter().any(|i| i.source == "System"));
assert!(result.imports.iter().any(|i| i.source == "System.Collections.Generic"));
assert!(result.entities.iter().any(|e| e.name == "MyApp"));
assert!(result.entities.iter().any(|e| e.name == "Player" && e.kind == "class"));
assert!(result.entities.iter().any(|e| e.name == "Point" && e.kind == "struct"));
assert!(result.entities.iter().any(|e| e.name == "ILogger" && e.kind == "interface"));
assert!(result.entities.iter().any(|e| e.name == "Color" && e.kind == "enum"));
}
#[test]
fn test_parse_csharp_method_and_property() {
let source = r#"class Calculator {
public int Add(int a, int b) { return a + b; }
public string Name { get; set; }
private int _count;
}
"#;
let proc = CSharpProcessor::new().unwrap();
let result = proc.parse(source, Path::new("test.cs")).unwrap();
assert!(result.entities.iter().any(|e| e.name == "Calculator"));
assert!(result.entities.iter().any(|e| e.name == "Add" && e.kind == "function"));
assert!(result.entities.iter().any(|e| e.name == "Name" && e.kind == "property"));
}
}
#[test]
fn test_parse_csharp_unity_morphology() {
let source = r#"using UnityEngine;
namespace Test.Unity
{
public class PlayerController : MonoBehaviour
{
[SerializeField]
private float moveSpeed = 5f;
[SerializeField]
private string playerName;
public int Score { get; private set; }
void Awake() { this.moveSpeed = 1f; }
void Start() { this.playerName = "hero"; }
void Update() { this.Score++; }
public void Move(Vector3 dir) { }
}
}
"#;
let proc = CSharpProcessor::new().unwrap();
let result = proc.parse(source, Path::new("PlayerController.cs")).unwrap();
assert!(result.entities.iter().any(|e| e.name == "PlayerController" && e.kind == "class"));
assert!(result.entities.iter().any(|e| e.name == "moveSpeed" && e.kind == "variable"), "SerializeField 字段应解析: {:?}", result.entities);
assert!(result.entities.iter().any(|e| e.name == "playerName" && e.kind == "variable"));
assert!(result.entities.iter().any(|e| e.name == "Score" && e.kind == "property"));
for lifecycle in ["Awake", "Start", "Update", "Move"] {
assert!(result.entities.iter().any(|e| e.name == lifecycle && e.kind == "function"), "方法 {lifecycle} 应解析");
}
}