Skip to content
Open
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
45 changes: 45 additions & 0 deletions src/kit/queries/csharp/tags.scm
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
;; C# symbol queries (tree-sitter-c-sharp)

;; Classes
(class_declaration
name: (identifier) @name) @definition.class

;; Structs
(struct_declaration
name: (identifier) @name) @definition.struct

;; Records (C# 9+)
(record_declaration
name: (identifier) @name) @definition.record

;; Interfaces
(interface_declaration
name: (identifier) @name) @definition.interface

;; Enums
(enum_declaration
name: (identifier) @name) @definition.enum

;; Delegates
(delegate_declaration
name: (identifier) @name) @definition.delegate

;; Methods
(method_declaration
name: (identifier) @name) @definition.method

;; Constructors
(constructor_declaration
name: (identifier) @name) @definition.constructor

;; Properties
(property_declaration
name: (identifier) @name) @definition.property

;; Namespaces
(namespace_declaration
name: (identifier) @name) @definition.namespace

;; Namespaces with qualified names (e.g., namespace Foo.Bar)
(namespace_declaration
name: (qualified_name) @name) @definition.namespace
2 changes: 2 additions & 0 deletions src/kit/tree_sitter_symbol_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
".hpp": "cpp",
".hxx": "cpp",
".zig": "zig",
".cs": "csharp",
}


Expand Down Expand Up @@ -348,6 +349,7 @@ def reset_plugins(cls) -> None:
".hpp": "cpp",
".hxx": "cpp",
".zig": "zig",
".cs": "csharp",
}
LANGUAGES.clear()
LANGUAGES.update(original_languages)
Expand Down
52 changes: 52 additions & 0 deletions tests/golden_csharp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Golden test file for C# symbol extraction
using System;
using System.Collections.Generic;

namespace GoldenTest.Models
{
public interface IEntity
{
int Id { get; }
}

public class User : IEntity
{
public int Id { get; set; }
public string Name { get; set; }

public User() {}

public User(int id, string name)
{
Id = id;
Name = name;
}

public void UpdateName(string newName)
{
Name = newName;
}
}

public struct Point
{
public int X { get; }
public int Y { get; }

public Point(int x, int y)
{
X = x;
Y = y;
}
}

public record Person(string FirstName, string LastName);

public enum Status
{
Active,
Inactive
}

public delegate void StatusChanged(Status newStatus);
}
244 changes: 244 additions & 0 deletions tests/test_csharp_symbols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
# Tests for C# symbol extraction
import os

from kit import Repository


def _extract(tmpdir: str, filename: str, content: str):
path = os.path.join(tmpdir, filename)
with open(path, "w") as f:
f.write(content)
return Repository(tmpdir).extract_symbols(filename)


def test_csharp_class_and_methods(tmp_path):
"""Test basic class, constructor, and method extraction."""
code = """
namespace MyApp
{
public class Foo
{
public int X { get; set; }

public Foo() {}

public void Bar() {}

private static void Helper() {}
}
}
"""
symbols = _extract(str(tmp_path), "Foo.cs", code)
names = {s["name"] for s in symbols}
assert {"MyApp", "Foo", "Bar", "Helper", "X"}.issubset(names)


def test_csharp_interface(tmp_path):
"""Test interface extraction."""
code = """
public interface IService
{
void Execute();
string GetName();
}
"""
symbols = _extract(str(tmp_path), "IService.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}
assert "IService" in names
assert "interface" in types


def test_csharp_struct(tmp_path):
"""Test struct extraction."""
code = """
public struct Point
{
public int X;
public int Y;

public Point(int x, int y)
{
X = x;
Y = y;
}
}
"""
symbols = _extract(str(tmp_path), "Point.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}
assert "Point" in names
assert "struct" in types


def test_csharp_enum(tmp_path):
"""Test enum extraction."""
code = """
public enum Color
{
Red,
Green,
Blue
}
"""
symbols = _extract(str(tmp_path), "Color.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}
assert "Color" in names
assert "enum" in types


def test_csharp_record(tmp_path):
"""Test C# 9+ record extraction."""
code = """
public record Person(string FirstName, string LastName);

public record Employee(string FirstName, string LastName, string Department) : Person(FirstName, LastName);
"""
symbols = _extract(str(tmp_path), "Records.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}
assert "Person" in names
assert "Employee" in names
assert "record" in types


def test_csharp_delegate(tmp_path):
"""Test delegate extraction."""
code = """
public delegate void EventHandler(object sender, EventArgs e);
public delegate int Calculator(int a, int b);
"""
symbols = _extract(str(tmp_path), "Delegates.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}
assert "EventHandler" in names
assert "Calculator" in names
assert "delegate" in types


def test_csharp_properties(tmp_path):
"""Test property extraction."""
code = """
public class Config
{
public string Name { get; set; }
public int Count { get; private set; }
public bool IsEnabled => true;
}
"""
symbols = _extract(str(tmp_path), "Config.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}
assert {"Config", "Name", "Count", "IsEnabled"}.issubset(names)
assert "property" in types


def test_csharp_namespace_qualified(tmp_path):
"""Test qualified namespace extraction (e.g., Foo.Bar.Baz)."""
code = """
namespace Foo.Bar.Baz
{
public class Widget {}
}
"""
symbols = _extract(str(tmp_path), "Widget.cs", code)
names = {s["name"] for s in symbols}
# Qualified name should be captured
assert "Widget" in names
# Check for namespace - could be "Foo.Bar.Baz" as text
namespace_symbols = [s for s in symbols if s["type"] == "namespace"]
assert len(namespace_symbols) >= 1


def test_csharp_complex_file(tmp_path):
"""Test a more complex C# file with multiple constructs."""
code = """
using System;
using System.Collections.Generic;

namespace MyCompany.MyProduct.Core
{
public interface IRepository<T>
{
T GetById(int id);
void Save(T entity);
}

public class UserRepository : IRepository<User>
{
private readonly List<User> _users;

public UserRepository()
{
_users = new List<User>();
}

public User GetById(int id)
{
return _users.Find(u => u.Id == id);
}

public void Save(User entity)
{
_users.Add(entity);
}

public IEnumerable<User> GetAll() => _users;
}

public record User(int Id, string Name, string Email);

public struct UserId
{
public int Value { get; }
public UserId(int value) => Value = value;
}

public enum UserStatus
{
Active,
Inactive,
Pending
}

public delegate void UserChangedHandler(User user);
}
"""
symbols = _extract(str(tmp_path), "Repository.cs", code)
names = {s["name"] for s in symbols}
types = {s["type"] for s in symbols}

# Check classes
assert "UserRepository" in names

# Check interfaces
assert "IRepository" in names
assert "interface" in types

# Check records
assert "User" in names
assert "record" in types

# Check structs
assert "UserId" in names
assert "struct" in types

# Check enums
assert "UserStatus" in names
assert "enum" in types

# Check delegates
assert "UserChangedHandler" in names
assert "delegate" in types

# Check methods
assert {"GetById", "Save", "GetAll"}.issubset(names)
assert "method" in types

# Check properties
assert "Value" in names
assert "property" in types

# Check constructors
assert "constructor" in types
33 changes: 33 additions & 0 deletions tests/test_golden_symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,36 @@ def test_go_symbol_extraction():

# Assert the exact set matches
assert names_types == expected, f"Mismatch: Got {names_types}, Expected {expected}"


# --- C# Test ---
def test_csharp_symbol_extraction():
with tempfile.TemporaryDirectory() as tmpdir:
golden_content = open(os.path.join(os.path.dirname(__file__), "golden_csharp.cs")).read()
symbols = run_extraction(tmpdir, "golden_csharp.cs", golden_content)
names_types = {(s["name"], s["type"]) for s in symbols}

# Expected symbols based on C# query
expected = {
("GoldenTest.Models", "namespace"), # namespace GoldenTest.Models
("IEntity", "interface"), # interface IEntity
("Id", "property"), # property in interface and class
("User", "class"), # class User
("Name", "property"), # property Name
("User", "constructor"), # constructor User()
("UpdateName", "method"), # method UpdateName
("Point", "struct"), # struct Point
("X", "property"), # property X
("Y", "property"), # property Y
("Point", "constructor"), # struct constructor
("Person", "record"), # record Person
("Status", "enum"), # enum Status
("StatusChanged", "delegate"), # delegate StatusChanged
}

# Assert individual expected symbols exist
for item in expected:
assert item in names_types, f"Expected symbol {item} not found in {names_types}"

# Assert the exact set matches
assert names_types == expected, f"Mismatch: Got {names_types}, Expected {expected}"