Skip to content

Commit 7e99184

Browse files
authored
Added C# Support (#166)
- Added tags for C# - Added .cs extension to tree-sitter-languages - Added tests
1 parent a7e88dc commit 7e99184

5 files changed

Lines changed: 376 additions & 0 deletions

File tree

src/kit/queries/csharp/tags.scm

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
;; C# symbol queries (tree-sitter-c-sharp)
2+
3+
;; Classes
4+
(class_declaration
5+
name: (identifier) @name) @definition.class
6+
7+
;; Structs
8+
(struct_declaration
9+
name: (identifier) @name) @definition.struct
10+
11+
;; Records (C# 9+)
12+
(record_declaration
13+
name: (identifier) @name) @definition.record
14+
15+
;; Interfaces
16+
(interface_declaration
17+
name: (identifier) @name) @definition.interface
18+
19+
;; Enums
20+
(enum_declaration
21+
name: (identifier) @name) @definition.enum
22+
23+
;; Delegates
24+
(delegate_declaration
25+
name: (identifier) @name) @definition.delegate
26+
27+
;; Methods
28+
(method_declaration
29+
name: (identifier) @name) @definition.method
30+
31+
;; Constructors
32+
(constructor_declaration
33+
name: (identifier) @name) @definition.constructor
34+
35+
;; Properties
36+
(property_declaration
37+
name: (identifier) @name) @definition.property
38+
39+
;; Namespaces
40+
(namespace_declaration
41+
name: (identifier) @name) @definition.namespace
42+
43+
;; Namespaces with qualified names (e.g., namespace Foo.Bar)
44+
(namespace_declaration
45+
name: (qualified_name) @name) @definition.namespace

src/kit/tree_sitter_symbol_extractor.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
".hpp": "cpp",
3434
".hxx": "cpp",
3535
".zig": "zig",
36+
".cs": "csharp",
3637
}
3738

3839

@@ -348,6 +349,7 @@ def reset_plugins(cls) -> None:
348349
".hpp": "cpp",
349350
".hxx": "cpp",
350351
".zig": "zig",
352+
".cs": "csharp",
351353
}
352354
LANGUAGES.clear()
353355
LANGUAGES.update(original_languages)

tests/golden_csharp.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Golden test file for C# symbol extraction
2+
using System;
3+
using System.Collections.Generic;
4+
5+
namespace GoldenTest.Models
6+
{
7+
public interface IEntity
8+
{
9+
int Id { get; }
10+
}
11+
12+
public class User : IEntity
13+
{
14+
public int Id { get; set; }
15+
public string Name { get; set; }
16+
17+
public User() {}
18+
19+
public User(int id, string name)
20+
{
21+
Id = id;
22+
Name = name;
23+
}
24+
25+
public void UpdateName(string newName)
26+
{
27+
Name = newName;
28+
}
29+
}
30+
31+
public struct Point
32+
{
33+
public int X { get; }
34+
public int Y { get; }
35+
36+
public Point(int x, int y)
37+
{
38+
X = x;
39+
Y = y;
40+
}
41+
}
42+
43+
public record Person(string FirstName, string LastName);
44+
45+
public enum Status
46+
{
47+
Active,
48+
Inactive
49+
}
50+
51+
public delegate void StatusChanged(Status newStatus);
52+
}

tests/test_csharp_symbols.py

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Tests for C# symbol extraction
2+
import os
3+
4+
from kit import Repository
5+
6+
7+
def _extract(tmpdir: str, filename: str, content: str):
8+
path = os.path.join(tmpdir, filename)
9+
with open(path, "w") as f:
10+
f.write(content)
11+
return Repository(tmpdir).extract_symbols(filename)
12+
13+
14+
def test_csharp_class_and_methods(tmp_path):
15+
"""Test basic class, constructor, and method extraction."""
16+
code = """
17+
namespace MyApp
18+
{
19+
public class Foo
20+
{
21+
public int X { get; set; }
22+
23+
public Foo() {}
24+
25+
public void Bar() {}
26+
27+
private static void Helper() {}
28+
}
29+
}
30+
"""
31+
symbols = _extract(str(tmp_path), "Foo.cs", code)
32+
names = {s["name"] for s in symbols}
33+
assert {"MyApp", "Foo", "Bar", "Helper", "X"}.issubset(names)
34+
35+
36+
def test_csharp_interface(tmp_path):
37+
"""Test interface extraction."""
38+
code = """
39+
public interface IService
40+
{
41+
void Execute();
42+
string GetName();
43+
}
44+
"""
45+
symbols = _extract(str(tmp_path), "IService.cs", code)
46+
names = {s["name"] for s in symbols}
47+
types = {s["type"] for s in symbols}
48+
assert "IService" in names
49+
assert "interface" in types
50+
51+
52+
def test_csharp_struct(tmp_path):
53+
"""Test struct extraction."""
54+
code = """
55+
public struct Point
56+
{
57+
public int X;
58+
public int Y;
59+
60+
public Point(int x, int y)
61+
{
62+
X = x;
63+
Y = y;
64+
}
65+
}
66+
"""
67+
symbols = _extract(str(tmp_path), "Point.cs", code)
68+
names = {s["name"] for s in symbols}
69+
types = {s["type"] for s in symbols}
70+
assert "Point" in names
71+
assert "struct" in types
72+
73+
74+
def test_csharp_enum(tmp_path):
75+
"""Test enum extraction."""
76+
code = """
77+
public enum Color
78+
{
79+
Red,
80+
Green,
81+
Blue
82+
}
83+
"""
84+
symbols = _extract(str(tmp_path), "Color.cs", code)
85+
names = {s["name"] for s in symbols}
86+
types = {s["type"] for s in symbols}
87+
assert "Color" in names
88+
assert "enum" in types
89+
90+
91+
def test_csharp_record(tmp_path):
92+
"""Test C# 9+ record extraction."""
93+
code = """
94+
public record Person(string FirstName, string LastName);
95+
96+
public record Employee(string FirstName, string LastName, string Department) : Person(FirstName, LastName);
97+
"""
98+
symbols = _extract(str(tmp_path), "Records.cs", code)
99+
names = {s["name"] for s in symbols}
100+
types = {s["type"] for s in symbols}
101+
assert "Person" in names
102+
assert "Employee" in names
103+
assert "record" in types
104+
105+
106+
def test_csharp_delegate(tmp_path):
107+
"""Test delegate extraction."""
108+
code = """
109+
public delegate void EventHandler(object sender, EventArgs e);
110+
public delegate int Calculator(int a, int b);
111+
"""
112+
symbols = _extract(str(tmp_path), "Delegates.cs", code)
113+
names = {s["name"] for s in symbols}
114+
types = {s["type"] for s in symbols}
115+
assert "EventHandler" in names
116+
assert "Calculator" in names
117+
assert "delegate" in types
118+
119+
120+
def test_csharp_properties(tmp_path):
121+
"""Test property extraction."""
122+
code = """
123+
public class Config
124+
{
125+
public string Name { get; set; }
126+
public int Count { get; private set; }
127+
public bool IsEnabled => true;
128+
}
129+
"""
130+
symbols = _extract(str(tmp_path), "Config.cs", code)
131+
names = {s["name"] for s in symbols}
132+
types = {s["type"] for s in symbols}
133+
assert {"Config", "Name", "Count", "IsEnabled"}.issubset(names)
134+
assert "property" in types
135+
136+
137+
def test_csharp_namespace_qualified(tmp_path):
138+
"""Test qualified namespace extraction (e.g., Foo.Bar.Baz)."""
139+
code = """
140+
namespace Foo.Bar.Baz
141+
{
142+
public class Widget {}
143+
}
144+
"""
145+
symbols = _extract(str(tmp_path), "Widget.cs", code)
146+
names = {s["name"] for s in symbols}
147+
# Qualified name should be captured
148+
assert "Widget" in names
149+
# Check for namespace - could be "Foo.Bar.Baz" as text
150+
namespace_symbols = [s for s in symbols if s["type"] == "namespace"]
151+
assert len(namespace_symbols) >= 1
152+
153+
154+
def test_csharp_complex_file(tmp_path):
155+
"""Test a more complex C# file with multiple constructs."""
156+
code = """
157+
using System;
158+
using System.Collections.Generic;
159+
160+
namespace MyCompany.MyProduct.Core
161+
{
162+
public interface IRepository<T>
163+
{
164+
T GetById(int id);
165+
void Save(T entity);
166+
}
167+
168+
public class UserRepository : IRepository<User>
169+
{
170+
private readonly List<User> _users;
171+
172+
public UserRepository()
173+
{
174+
_users = new List<User>();
175+
}
176+
177+
public User GetById(int id)
178+
{
179+
return _users.Find(u => u.Id == id);
180+
}
181+
182+
public void Save(User entity)
183+
{
184+
_users.Add(entity);
185+
}
186+
187+
public IEnumerable<User> GetAll() => _users;
188+
}
189+
190+
public record User(int Id, string Name, string Email);
191+
192+
public struct UserId
193+
{
194+
public int Value { get; }
195+
public UserId(int value) => Value = value;
196+
}
197+
198+
public enum UserStatus
199+
{
200+
Active,
201+
Inactive,
202+
Pending
203+
}
204+
205+
public delegate void UserChangedHandler(User user);
206+
}
207+
"""
208+
symbols = _extract(str(tmp_path), "Repository.cs", code)
209+
names = {s["name"] for s in symbols}
210+
types = {s["type"] for s in symbols}
211+
212+
# Check classes
213+
assert "UserRepository" in names
214+
215+
# Check interfaces
216+
assert "IRepository" in names
217+
assert "interface" in types
218+
219+
# Check records
220+
assert "User" in names
221+
assert "record" in types
222+
223+
# Check structs
224+
assert "UserId" in names
225+
assert "struct" in types
226+
227+
# Check enums
228+
assert "UserStatus" in names
229+
assert "enum" in types
230+
231+
# Check delegates
232+
assert "UserChangedHandler" in names
233+
assert "delegate" in types
234+
235+
# Check methods
236+
assert {"GetById", "Save", "GetAll"}.issubset(names)
237+
assert "method" in types
238+
239+
# Check properties
240+
assert "Value" in names
241+
assert "property" in types
242+
243+
# Check constructors
244+
assert "constructor" in types

tests/test_golden_symbols.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,36 @@ def test_go_symbol_extraction():
178178

179179
# Assert the exact set matches
180180
assert names_types == expected, f"Mismatch: Got {names_types}, Expected {expected}"
181+
182+
183+
# --- C# Test ---
184+
def test_csharp_symbol_extraction():
185+
with tempfile.TemporaryDirectory() as tmpdir:
186+
golden_content = open(os.path.join(os.path.dirname(__file__), "golden_csharp.cs")).read()
187+
symbols = run_extraction(tmpdir, "golden_csharp.cs", golden_content)
188+
names_types = {(s["name"], s["type"]) for s in symbols}
189+
190+
# Expected symbols based on C# query
191+
expected = {
192+
("GoldenTest.Models", "namespace"), # namespace GoldenTest.Models
193+
("IEntity", "interface"), # interface IEntity
194+
("Id", "property"), # property in interface and class
195+
("User", "class"), # class User
196+
("Name", "property"), # property Name
197+
("User", "constructor"), # constructor User()
198+
("UpdateName", "method"), # method UpdateName
199+
("Point", "struct"), # struct Point
200+
("X", "property"), # property X
201+
("Y", "property"), # property Y
202+
("Point", "constructor"), # struct constructor
203+
("Person", "record"), # record Person
204+
("Status", "enum"), # enum Status
205+
("StatusChanged", "delegate"), # delegate StatusChanged
206+
}
207+
208+
# Assert individual expected symbols exist
209+
for item in expected:
210+
assert item in names_types, f"Expected symbol {item} not found in {names_types}"
211+
212+
# Assert the exact set matches
213+
assert names_types == expected, f"Mismatch: Got {names_types}, Expected {expected}"

0 commit comments

Comments
 (0)