use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, EntryPoint, FileFacts, RefRole, Reference, Scope, ScopeId,
ScopeKind, Symbol, SymbolKind, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, collect_call_references,
definition_bindings, field_text, import_bindings, innermost_scope, make_symbol, node_span,
node_text, one_line_signature, push_binding, push_import_ref, push_ref, push_scope,
push_type_ref, simple_type_name,
};
const CALL_QUERY: &str = r#"
[
(invocation_expression function: (identifier) @callee)
(invocation_expression function: (generic_name (identifier) @callee))
(invocation_expression function: (member_access_expression expression: (_) @qualifier name: (identifier) @callee))
(invocation_expression function: (member_access_expression expression: (_) @qualifier name: (generic_name (identifier) @callee)))
(object_creation_expression type: (identifier) @callee)
(object_creation_expression type: (qualified_name name: (identifier) @callee))
(object_creation_expression type: (generic_name (identifier) @callee))
]
"#;
pub struct CSharpExtractor;
impl Extractor for CSharpExtractor {
fn lang(&self) -> Language {
Language::CSharp
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::csharp();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let namespaces = csharp_namespaces(&root, bytes, file);
let defs = collect_symbols(&root, bytes, file, &namespaces);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
let mod_sym = super::module_symbol(Language::CSharp, &namespaces, file, source.len());
let module_id = mod_sym.id.to_scip_string();
symbols.push(mod_sym);
let mut references = collect_call_references(
&root,
&ts_language,
CALL_QUERY,
Language::CSharp,
bytes,
file,
)?;
collect_inheritance(&root, bytes, file, &mut references);
collect_imports(&root, bytes, file, &mut references, &module_id);
collect_type_references(&root, bytes, file, &mut references);
collect_read_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, bytes, &scopes);
bindings.extend(def_bindings);
bindings.extend(import_bindings(&references, &scopes));
Ok(FileFacts {
file: file.to_owned(),
lang: Language::CSharp.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn csharp_namespaces(root: &Node, bytes: &[u8], file: &str) -> Vec<String> {
for child in root.children(&mut root.walk()) {
let kind = child.kind();
if kind != "namespace_declaration" && kind != "file_scoped_namespace_declaration" {
continue;
}
if let Some(name_node) = child.child_by_field_name("name") {
let text = node_text(&name_node, bytes);
return text
.split('.')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
}
}
let p = file.strip_suffix(".cs").unwrap_or(file);
let p = p.strip_prefix("src/").unwrap_or(p);
p.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn read_visibility(node: &Node, bytes: &[u8]) -> Visibility {
let mut has_private = false;
let mut has_protected = false;
let mut has_public = false;
for child in node.children(&mut node.walk()) {
if child.kind() == "modifier" {
match node_text(&child, bytes) {
"private" => has_private = true,
"protected" => has_protected = true,
"public" => has_public = true,
_ => {}
}
}
}
if has_private {
Visibility::Private
} else if has_protected {
Visibility::Protected
} else if has_public {
Visibility::Public
} else {
Visibility::Internal
}
}
fn has_modifier(node: &tree_sitter::Node, bytes: &[u8], kw: &str) -> bool {
node.children(&mut node.walk())
.any(|c| c.kind() == "modifier" && node_text(&c, bytes) == kw)
}
fn collect_symbols(root: &Node, bytes: &[u8], file: &str, namespaces: &[String]) -> Vec<Symbol> {
let mut out = Vec::new();
let ctx = ExtractCtx {
bytes,
file,
lang: Language::CSharp,
};
let ns_descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
collect_types_in(root, &ctx, &ns_descriptors, false, &mut out);
out
}
fn collect_types_in(
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
implicit_public: bool,
out: &mut Vec<Symbol>,
) {
for child in node.children(&mut node.walk()) {
match child.kind() {
"namespace_declaration" | "file_scoped_namespace_declaration" => {
if let Some(body) = child.child_by_field_name("body") {
collect_types_in(&body, ctx, prefix, false, out);
} else {
}
}
k @ ("class_declaration"
| "struct_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration") => {
let Some(type_name) = field_text(&child, "name", ctx.bytes) else {
continue;
};
let type_kind = match k {
"class_declaration" | "record_declaration" => SymbolKind::Class,
"struct_declaration" => SymbolKind::Struct,
"interface_declaration" => SymbolKind::Interface,
"enum_declaration" => SymbolKind::Enum,
_ => SymbolKind::Class,
};
let vis = if implicit_public {
Visibility::Public
} else {
read_visibility(&child, ctx.bytes)
};
let mut type_descriptors = prefix.to_vec();
type_descriptors.push(Descriptor::Type(type_name.clone()));
let sig = one_line_signature(node_text(&child, ctx.bytes), &['{', ';']);
out.push(make_symbol(
ctx,
&child,
type_name.clone(),
type_kind,
vis,
type_descriptors.clone(),
sig,
));
let implicit = k == "interface_declaration";
if let Some(body) = child.child_by_field_name("body") {
collect_members(&body, ctx, &type_descriptors, implicit, out);
}
}
_ => {}
}
}
}
fn collect_members(
body: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
implicit_public: bool,
out: &mut Vec<Symbol>,
) {
for member in body.children(&mut body.walk()) {
match member.kind() {
"method_declaration" | "constructor_declaration" | "property_declaration" => {
let Some(name) = field_text(&member, "name", ctx.bytes) else {
continue;
};
let vis = if implicit_public {
Visibility::Public
} else {
read_visibility(&member, ctx.bytes)
};
let mut descriptors = type_prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
let sig = one_line_signature(node_text(&member, ctx.bytes), &['{', ';']);
let is_main = member.kind() == "method_declaration"
&& name == "Main"
&& has_modifier(&member, ctx.bytes, "static");
out.push(make_symbol(
ctx,
&member,
name,
SymbolKind::Method,
vis,
descriptors,
sig,
));
if is_main {
if let Some(s) = out.last_mut() {
s.entry_points.push(EntryPoint::Main);
}
}
}
"field_declaration" => {
let vis = if implicit_public {
Visibility::Public
} else {
read_visibility(&member, ctx.bytes)
};
collect_field_declarators(&member, ctx, type_prefix, vis, out);
}
"enum_member_declaration" => {
let Some(name) = field_text(&member, "name", ctx.bytes) else {
continue;
};
let mut descriptors = type_prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
let sig = one_line_signature(node_text(&member, ctx.bytes), &['{', ';', ',']);
out.push(make_symbol(
ctx,
&member,
name,
SymbolKind::Const,
Visibility::Public,
descriptors,
sig,
));
}
k @ ("class_declaration"
| "struct_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration") => {
let Some(nested_name) = field_text(&member, "name", ctx.bytes) else {
continue;
};
let nested_kind = match k {
"class_declaration" | "record_declaration" => SymbolKind::Class,
"struct_declaration" => SymbolKind::Struct,
"interface_declaration" => SymbolKind::Interface,
"enum_declaration" => SymbolKind::Enum,
_ => SymbolKind::Class,
};
let vis = if implicit_public {
Visibility::Public
} else {
read_visibility(&member, ctx.bytes)
};
let mut nested_descriptors = type_prefix.to_vec();
nested_descriptors.push(Descriptor::Type(nested_name.clone()));
let sig = one_line_signature(node_text(&member, ctx.bytes), &['{', ';']);
out.push(make_symbol(
ctx,
&member,
nested_name.clone(),
nested_kind,
vis,
nested_descriptors.clone(),
sig,
));
let implicit = k == "interface_declaration";
if let Some(nested_body) = member.child_by_field_name("body") {
collect_members(&nested_body, ctx, &nested_descriptors, implicit, out);
}
}
_ => {}
}
}
}
fn collect_field_declarators(
field: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
visibility: Visibility,
out: &mut Vec<Symbol>,
) {
for child in field.children(&mut field.walk()) {
if child.kind() != "variable_declaration" {
continue;
}
for decl in child.children(&mut child.walk()) {
if decl.kind() != "variable_declarator" {
continue;
}
for id_node in decl.children(&mut decl.walk()) {
if id_node.kind() == "identifier" {
let name = node_text(&id_node, ctx.bytes).to_owned();
let mut descriptors = type_prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
let sig = one_line_signature(node_text(field, ctx.bytes), &['{', ';']);
out.push(make_symbol(
ctx,
field,
name,
SymbolKind::Static,
visibility,
descriptors,
sig,
));
break; }
}
}
}
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "base_list" {
for child in node.children(&mut node.walk()) {
match child.kind() {
"identifier" => {
push_ref(
out,
node_text(&child, bytes),
&child,
file,
RefRole::IsImplementation,
);
}
"qualified_name" | "generic_name" => {
let name = simple_type_name(node_text(&child, bytes), ".");
push_ref(out, name, &child, file, RefRole::IsImplementation);
}
_ => {}
}
}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn collect_imports(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
if node.kind() == "using_directive" {
let has_alias = node
.children(&mut node.walk())
.any(|c| c.kind() == "name_equals");
if !has_alias {
let mut name_node: Option<Node> = None;
for child in node.children(&mut node.walk()) {
match child.kind() {
"qualified_name" | "identifier" => {
name_node = Some(child);
break;
}
_ => {}
}
}
if let Some(qn) = name_node {
let full_text = node_text(&qn, bytes);
if qn.kind() == "qualified_name" {
if let Some(dot) = full_text.rfind('.') {
let from_path = &full_text[..dot];
let leaf = &full_text[dot + 1..];
if let Some(name_field) = qn.child_by_field_name("name") {
push_import_ref(out, leaf, &name_field, file, module_id, from_path);
} else {
push_import_ref(out, leaf, &qn, file, module_id, from_path);
}
} else {
push_import_ref(out, full_text, &qn, file, module_id, "");
}
} else {
push_import_ref(out, full_text, &qn, file, module_id, "");
}
}
}
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
if parent.child_by_field_name("type").as_ref() == Some(node) {
return true;
}
if parent.child_by_field_name("returns").as_ref() == Some(node) {
return true;
}
match parent.kind() {
"invocation_expression" => parent.child_by_field_name("function").as_ref() == Some(node),
"method_declaration"
| "constructor_declaration"
| "property_declaration"
| "class_declaration"
| "struct_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
| "enum_member_declaration" => parent.child_by_field_name("name").as_ref() == Some(node),
"variable_declarator" => parent.child_by_field_name("name").as_ref() == Some(node),
"parameter" => parent.child_by_field_name("name").as_ref() == Some(node),
"using_directive" => true,
"member_access_expression" => parent.child_by_field_name("name").as_ref() == Some(node),
"qualified_name" => true,
"generic_name" => parent.child_by_field_name("name").as_ref() == Some(node),
"assignment_expression" => parent.child_by_field_name("left").as_ref() == Some(node),
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "assignment_expression" {
if let Some(lhs) = node.child_by_field_name("left") {
if lhs.kind() == "identifier" {
let name = node_text(&lhs, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &lhs, file, RefRole::Write);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"method_declaration" => {
if let Some(ret) = node.child_by_field_name("returns") {
type_leaf(&ret, bytes, file, TypeRefContext::ReturnType, out);
}
}
"parameter" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::ParameterType, out);
}
}
"field_declaration" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "variable_declaration" {
if let Some(ty) = child.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::Field, out);
}
}
}
}
"property_declaration" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::Field, out);
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
fn type_leaf(node: &Node, bytes: &[u8], file: &str, ctx: TypeRefContext, out: &mut Vec<Reference>) {
match node.kind() {
"predefined_type" | "void_keyword" => {}
"identifier" => {
let name = node_text(node, bytes);
push_type_ref(out, name, node, file, ctx);
}
"qualified_name" | "generic_name" => {
let name = simple_type_name(node_text(node, bytes), ".");
push_type_ref(out, name, node, file, ctx);
}
"nullable_type" | "array_type" => {
if let Some(elem) = node.named_children(&mut node.walk()).next() {
type_leaf(&elem, bytes, file, ctx, out);
}
}
_ => {
for child in node.named_children(&mut node.walk()) {
type_leaf(&child, bytes, file, ctx, out);
}
}
}
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"namespace_declaration" | "file_scoped_namespace_declaration" => {
let ns_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Module);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, ns_id, scopes);
}
}
}
"class_declaration"
| "struct_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, type_id, scopes);
}
}
}
"method_declaration" | "constructor_declaration" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"block" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"method_declaration" | "constructor_declaration" => {
if let Some(params) = node.child_by_field_name("parameters") {
collect_params(¶ms, bytes, scopes, out);
}
}
"local_declaration_statement" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "variable_declaration" {
for decl in child.children(&mut child.walk()) {
if decl.kind() == "variable_declarator" {
for id_node in decl.children(&mut decl.walk()) {
if id_node.kind() == "identifier" {
let name = node_text(&id_node, bytes);
let intro = id_node.start_byte();
if name.len() >= MIN_REF_LEN
&& innermost_scope(intro, scopes) != Some(0)
{
push_binding(
out,
name.to_owned(),
intro,
BindingKind::Local,
scopes,
);
}
break;
}
}
}
}
}
}
}
"for_each_statement" => {
if let Some(name_node) = node.child_by_field_name("left") {
if name_node.kind() == "identifier" {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
if innermost_scope(intro, scopes) != Some(0) {
push_binding(out, name.to_owned(), intro, BindingKind::Local, scopes);
}
}
}
}
"catch_clause" => {
if let Some(decl) = node.child_by_field_name("declaration") {
if let Some(name_node) = decl.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
fn collect_params(params: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
for child in params.named_children(&mut params.walk()) {
if child.kind() == "parameter" {
if let Some(name_node) = child.child_by_field_name("name") {
let name = node_text(&name_node, bytes);
let intro = name_node.start_byte();
push_binding(out, name.to_owned(), intro, BindingKind::Param, scopes);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_main_is_entry_point() {
let src = r#"
class Program {
static void Main(string[] args) {}
}
"#;
let facts = CSharpExtractor.extract(src, "src/Program.cs").unwrap();
let main = facts.symbols.iter().find(|s| s.name == "Main").unwrap();
assert!(
main.entry_points
.iter()
.any(|e| matches!(e, EntryPoint::Main))
);
}
#[test]
fn instance_main_is_not_entry_point() {
let src = r#"
class Program {
void Main() {}
}
"#;
let facts = CSharpExtractor.extract(src, "src/Program.cs").unwrap();
let main = facts.symbols.iter().find(|s| s.name == "Main").unwrap();
assert!(
!main
.entry_points
.iter()
.any(|e| matches!(e, EntryPoint::Main))
);
}
#[test]
fn class_and_method_get_correct_scip_strings() {
let src = r#"
namespace MyApp.Auth {
public class SessionManager {
public bool Validate(string token) { return true; }
private void Secret() {}
}
}
"#;
let facts = CSharpExtractor
.extract(src, "src/MyApp/Auth/SessionManager.cs")
.unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let sm = by_name("SessionManager").unwrap();
assert_eq!(sm.kind, SymbolKind::Class);
assert_eq!(sm.visibility, Visibility::Public);
assert_eq!(
sm.id.to_scip_string(),
"codegraph . . . MyApp/Auth/SessionManager#"
);
let validate = by_name("Validate").unwrap();
assert_eq!(validate.kind, SymbolKind::Method);
assert_eq!(validate.visibility, Visibility::Public);
assert_eq!(
validate.id.to_scip_string(),
"codegraph . . . MyApp/Auth/SessionManager#Validate()."
);
let secret = by_name("Secret").expect("private method 'Secret' must now be emitted");
assert_eq!(secret.visibility, Visibility::Private);
assert_eq!(
secret.id.to_scip_string(),
"codegraph . . . MyApp/Auth/SessionManager#Secret()."
);
assert_eq!(facts.lang, "csharp");
}
#[test]
fn namespace_block_yields_correct_descriptors() {
let src = r#"
namespace A.B {
public class C {
public void M() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/A/B/C.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let c = by_name("C").unwrap();
assert_eq!(c.id.to_scip_string(), "codegraph . . . A/B/C#");
let m = by_name("M").unwrap();
assert_eq!(m.id.to_scip_string(), "codegraph . . . A/B/C#M().");
}
#[test]
fn file_scoped_namespace_works() {
let src = r#"
namespace A.B;
public class Foo {
public void Bar() {}
}
"#;
let facts = CSharpExtractor.extract(src, "src/A/B/Foo.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let foo = by_name("Foo").unwrap();
assert_eq!(foo.id.to_scip_string(), "codegraph . . . A/B/Foo#");
let bar = by_name("Bar").unwrap();
assert_eq!(bar.id.to_scip_string(), "codegraph . . . A/B/Foo#Bar().");
}
#[test]
fn enum_and_enum_member_are_extracted() {
let src = r#"
namespace N {
public enum Color { Red, Green, Blue }
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Color.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let color = by_name("Color").unwrap();
assert_eq!(color.kind, SymbolKind::Enum);
assert_eq!(color.id.to_scip_string(), "codegraph . . . N/Color#");
let red = by_name("Red").unwrap();
assert_eq!(red.kind, SymbolKind::Const);
assert_eq!(red.id.to_scip_string(), "codegraph . . . N/Color#Red.");
}
#[test]
fn field_is_extracted() {
let src = r#"
namespace N {
public class C {
public int Count;
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/C.cs").unwrap();
let count = facts
.symbols
.iter()
.find(|s| s.name == "Count")
.expect("expected field Count");
assert_eq!(count.kind, SymbolKind::Static);
assert_eq!(count.id.to_scip_string(), "codegraph . . . N/C#Count.");
}
#[test]
fn qualified_call_captures_qualifier() {
let src = r#"
public class Client {
public void Run() {
var obj = new Service();
obj.Foo();
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/Client.cs").unwrap();
let foo = facts
.references
.iter()
.find(|r| r.name == "Foo")
.expect("expected Call ref for 'Foo'");
assert_eq!(foo.role, RefRole::Call);
assert_eq!(
foo.qualifier.as_deref(),
Some("obj"),
"expected qualifier 'obj' on the Foo call ref",
);
}
#[test]
fn using_directive_produces_import_reference() {
let src = r#"
using System.Collections.Generic;
public class C {}
"#;
let facts = CSharpExtractor.extract(src, "src/C.cs").unwrap();
let import_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.map(|r| r.name.as_str())
.collect();
assert!(
import_names.contains(&"Generic"),
"expected 'Generic' in import refs: {import_names:?}"
);
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::Import && r.name == "Generic")
.unwrap();
assert_eq!(
r.from_path,
Some("System.Collections".to_owned()),
"from_path should be 'System.Collections', got {:?}",
r.from_path
);
}
#[test]
fn inheritance_produces_is_implementation_references() {
let src = r#"
public class Foo : Bar, IBaz {}
"#;
let facts = CSharpExtractor.extract(src, "src/Foo.cs").unwrap();
let inherit_names: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::IsImplementation)
.map(|r| r.name.as_str())
.collect();
assert!(
inherit_names.contains(&"Bar"),
"expected 'Bar' in {inherit_names:?}"
);
assert!(
inherit_names.contains(&"IBaz"),
"expected 'IBaz' in {inherit_names:?}"
);
}
#[test]
fn interface_members_emitted_without_public_modifier() {
let src = r#"
namespace Svc {
public interface IReader {
int Read();
void Close();
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/Svc/IReader.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let read = by_name("Read").unwrap();
assert_eq!(read.kind, SymbolKind::Method);
let close = by_name("Close").unwrap();
assert_eq!(close.kind, SymbolKind::Method);
}
#[test]
fn reassignment_emits_write_for_lhs_and_read_for_rhs() {
let src = r#"
public class Calc {
public int Run(int total, int bonus) {
total = total + bonus;
return total;
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/Calc.cs").unwrap();
let writes: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write)
.map(|r| r.name.as_str())
.collect();
assert!(
writes.contains(&"total"),
"expected Write for 'total', got: {writes:?}",
);
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.contains(&"bonus"),
"expected Read for 'bonus', got: {reads:?}",
);
assert!(
reads.contains(&"total"),
"expected Read for RHS 'total', got: {reads:?}",
);
}
#[test]
fn local_declaration_does_not_emit_write() {
let src = r#"
public class Worker {
public int Run() {
var result = Compute();
return result;
}
private int Compute() { return 42; }
}
"#;
let facts = CSharpExtractor.extract(src, "src/Worker.cs").unwrap();
let writes: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write)
.map(|r| r.name.as_str())
.collect();
assert!(
!writes.contains(&"result"),
"declaration binding must NOT produce a Write ref; got writes: {writes:?}",
);
}
#[test]
fn call_argument_emits_read_but_not_callee() {
let src = r#"
public class App {
public void Run(object config) {
Log(config);
}
private void Log(object msg) {}
}
"#;
let facts = CSharpExtractor.extract(src, "src/App.cs").unwrap();
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.contains(&"config"),
"expected Read for 'config', got: {reads:?}",
);
assert!(
!reads.contains(&"Log"),
"callee 'Log' must NOT appear as a Read ref; reads: {reads:?}",
);
}
#[test]
fn member_access_emits_read_for_base_not_leaf() {
let src = r#"
public class Copier {
public int Run(DataObj source) {
int value = source.Field;
return value;
}
}
public class DataObj { public int Field; }
"#;
let facts = CSharpExtractor.extract(src, "src/Copier.cs").unwrap();
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
reads.contains(&"source"),
"expected Read for 'source', got: {reads:?}",
);
assert!(
!reads.contains(&"Field"),
"member-access leaf 'Field' must NOT be a Read ref; reads: {reads:?}",
);
}
#[test]
fn type_name_in_typed_local_is_not_a_read() {
let src = r#"
class C {
void M(Helper source) {
Helper result = source;
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/C.cs").unwrap();
let reads: Vec<&str> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read)
.map(|r| r.name.as_str())
.collect();
assert!(
!reads.contains(&"Helper"),
"type name 'Helper' must NOT appear as a Read ref; reads: {reads:?}",
);
assert!(
reads.contains(&"source"),
"initializer value 'source' must appear as a Read ref; reads: {reads:?}",
);
}
#[test]
fn public_visibility_tagged_correctly() {
let src = r#"
namespace N {
public class Svc {
public void Open() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Svc.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
assert_eq!(by_name("Svc").unwrap().visibility, Visibility::Public);
assert_eq!(by_name("Open").unwrap().visibility, Visibility::Public);
}
#[test]
fn private_def_emitted_with_private_visibility() {
let src = r#"
namespace N {
public class Worker {
private void Helper() {}
private int count;
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Worker.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let helper = by_name("Helper").expect("private method must be emitted");
assert_eq!(helper.visibility, Visibility::Private);
let count = by_name("count").expect("private field must be emitted");
assert_eq!(count.visibility, Visibility::Private);
}
#[test]
fn protected_def_emitted_with_protected_visibility() {
let src = r#"
namespace N {
public class Base {
protected void OnInit() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Base.cs").unwrap();
let on_init = facts
.symbols
.iter()
.find(|s| s.name == "OnInit")
.expect("protected method must be emitted");
assert_eq!(on_init.visibility, Visibility::Protected);
}
#[test]
fn internal_def_emitted_with_internal_visibility() {
let src = r#"
namespace N {
internal class Cache {
internal void Flush() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Cache.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let cache = by_name("Cache").expect("internal class must be emitted");
assert_eq!(cache.visibility, Visibility::Internal);
let flush = by_name("Flush").expect("internal method must be emitted");
assert_eq!(flush.visibility, Visibility::Internal);
}
#[test]
fn no_modifier_maps_to_internal() {
let src = r#"
namespace N {
class Hidden {
void DoWork() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Hidden.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let hidden = by_name("Hidden").expect("no-modifier class must be emitted");
assert_eq!(hidden.visibility, Visibility::Internal);
let do_work = by_name("DoWork").expect("no-modifier method must be emitted");
assert_eq!(do_work.visibility, Visibility::Internal);
}
#[test]
fn private_protected_maps_to_private() {
let src = r#"
namespace N {
public class Base {
private protected void Hook() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Base.cs").unwrap();
let hook = facts
.symbols
.iter()
.find(|s| s.name == "Hook")
.expect("private protected method must be emitted");
assert_eq!(hook.visibility, Visibility::Private);
}
#[test]
fn protected_internal_maps_to_protected() {
let src = r#"
namespace N {
public class Base {
protected internal void Extend() {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Base.cs").unwrap();
let extend = facts
.symbols
.iter()
.find(|s| s.name == "Extend")
.expect("protected internal method must be emitted");
assert_eq!(extend.visibility, Visibility::Protected);
}
#[test]
fn interface_members_tagged_public_implicitly() {
let src = r#"
namespace N {
public interface IFoo {
void Bar();
int Baz { get; }
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/IFoo.cs").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
assert_eq!(by_name("Bar").unwrap().visibility, Visibility::Public);
assert_eq!(by_name("Baz").unwrap().visibility, Visibility::Public);
}
#[test]
fn nested_private_type_emitted_with_private_visibility() {
let src = r#"
namespace N {
public class Outer {
private class Inner {}
}
}
"#;
let facts = CSharpExtractor.extract(src, "src/N/Outer.cs").unwrap();
let inner = facts
.symbols
.iter()
.find(|s| s.name == "Inner")
.expect("nested private class must be emitted");
assert_eq!(inner.visibility, Visibility::Private);
}
}