From 982798f7c28f11a0c9f9187195d9bc300a233ee6 Mon Sep 17 00:00:00 2001 From: jesus-mgtc Date: Mon, 22 Dec 2025 16:53:43 +0100 Subject: [PATCH] Added C# Support - Added tags for C# - Added .cs extension to tree-sitter-languages - Added tests --- src/kit/queries/csharp/tags.scm | 45 +++++ src/kit/tree_sitter_symbol_extractor.py | 2 + tests/golden_csharp.cs | 52 +++++ tests/test_csharp_symbols.py | 244 ++++++++++++++++++++++++ tests/test_golden_symbols.py | 33 ++++ 5 files changed, 376 insertions(+) create mode 100644 src/kit/queries/csharp/tags.scm create mode 100644 tests/golden_csharp.cs create mode 100644 tests/test_csharp_symbols.py diff --git a/src/kit/queries/csharp/tags.scm b/src/kit/queries/csharp/tags.scm new file mode 100644 index 00000000..dff2bd4a --- /dev/null +++ b/src/kit/queries/csharp/tags.scm @@ -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 diff --git a/src/kit/tree_sitter_symbol_extractor.py b/src/kit/tree_sitter_symbol_extractor.py index 459c8520..273346f5 100644 --- a/src/kit/tree_sitter_symbol_extractor.py +++ b/src/kit/tree_sitter_symbol_extractor.py @@ -33,6 +33,7 @@ ".hpp": "cpp", ".hxx": "cpp", ".zig": "zig", + ".cs": "csharp", } @@ -348,6 +349,7 @@ def reset_plugins(cls) -> None: ".hpp": "cpp", ".hxx": "cpp", ".zig": "zig", + ".cs": "csharp", } LANGUAGES.clear() LANGUAGES.update(original_languages) diff --git a/tests/golden_csharp.cs b/tests/golden_csharp.cs new file mode 100644 index 00000000..6e152c76 --- /dev/null +++ b/tests/golden_csharp.cs @@ -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); +} diff --git a/tests/test_csharp_symbols.py b/tests/test_csharp_symbols.py new file mode 100644 index 00000000..4d962b1d --- /dev/null +++ b/tests/test_csharp_symbols.py @@ -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 GetById(int id); + void Save(T entity); + } + + public class UserRepository : IRepository + { + private readonly List _users; + + public UserRepository() + { + _users = new List(); + } + + public User GetById(int id) + { + return _users.Find(u => u.Id == id); + } + + public void Save(User entity) + { + _users.Add(entity); + } + + public IEnumerable 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 diff --git a/tests/test_golden_symbols.py b/tests/test_golden_symbols.py index 5ec5da12..75e76e12 100644 --- a/tests/test_golden_symbols.py +++ b/tests/test_golden_symbols.py @@ -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}"