diff --git a/codegraph/parsers/__init__.py b/codegraph/parsers/__init__.py index d09cfe8..468a35e 100644 --- a/codegraph/parsers/__init__.py +++ b/codegraph/parsers/__init__.py @@ -128,13 +128,33 @@ def get_parser_for_path(path: str | Path) -> BaseParser | None: # --------------------------------------------------------------------------- +# Parser modules whose tree-sitter grammar is an OPTIONAL extra +# (`pip install cgh[langs]`). When the extra is not installed, importing the +# module raises ImportError/ModuleNotFoundError because its grammar package is +# absent. That is expected: we skip the module and the parser simply never +# registers, so cgh keeps working exactly as before. Any OTHER import error +# (a real bug in a hard-dep parser) still propagates. +_OPTIONAL_GRAMMAR_MODULES = frozenset({"csharp", "ruby"}) + + def _discover_parsers(): - """Import all parser modules in this package.""" + """Import all parser modules in this package. + + Optional-grammar modules (see ``_OPTIONAL_GRAMMAR_MODULES``) are skipped + when their grammar package is missing, instead of crashing discovery. + """ package_dir = Path(__file__).parent for _, module_name, _ in pkgutil.iter_modules([str(package_dir)]): if module_name == "base": continue - importlib.import_module(f".{module_name}", package=__package__) + try: + importlib.import_module(f".{module_name}", package=__package__) + except (ImportError, ModuleNotFoundError): + # A parser whose optional grammar package is not installed: skip it. + # Re-raise for non-optional modules so genuine breakage stays loud. + if module_name in _OPTIONAL_GRAMMAR_MODULES: + continue + raise _discover_parsers() @@ -148,5 +168,7 @@ def _discover_parsers(): ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"], ".yaml", ) -register_by_name([".env.example", ".env.local", ".env.staging", ".env.production"], ".env") +register_by_name( + [".env.example", ".env.local", ".env.staging", ".env.production"], ".env" +) register_by_name(["Makefile", "GNUmakefile"], ".sh") diff --git a/codegraph/parsers/csharp.py b/codegraph/parsers/csharp.py new file mode 100644 index 0000000..c4525b3 --- /dev/null +++ b/codegraph/parsers/csharp.py @@ -0,0 +1,190 @@ +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# __creation__ = 2026-06-07 +# __author__ = "jndjama (Joy Ndjama)" +# __copyright__ = "Copyright 2026 ALTIKVA." +# __licence__ = "MIT & CC BY-NC-SA (http://www.altikva.com/licenses/LICENSE-1.0)" +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# Description: C# parser plugin. Extracts classes, interfaces, structs, enums, +# methods, using directives (imports), and call references using +# tree-sitter-c-sharp. Optional: ships behind the `langs` extra, +# so the grammar import only happens when the extra is installed. + +from __future__ import annotations + +import re +from pathlib import Path + +import tree_sitter_c_sharp as tscs +from tree_sitter import Language, Node, Parser + +from . import register_parser +from .base import BaseParser, ClassDef, FileIndex, ImportRef, SymbolDef + +CSHARP_LANGUAGE = Language(tscs.language()) +_parser = Parser(CSHARP_LANGUAGE) + +# Type declarations that map to a ClassDef, with their codegraph kind. +_TYPE_DECLS = { + "class_declaration": "class", + "interface_declaration": "interface", + "struct_declaration": "struct", + "enum_declaration": "enum", + "record_declaration": "record", +} + + +def _text(node: Node, src: bytes) -> str: + return src[node.start_byte : node.end_byte].decode("utf-8", errors="replace") + + +def _ident(node: Node, src: bytes) -> str: + from codegraph.core.utils import normalize_identifier + + return normalize_identifier(_text(node, src)) + + +def _collect_calls(node: Node, src: bytes) -> list[str]: + """Walk a C# method body, return called method names (deduped). + + Covers invocation_expression (`obj.Method()`, `Method()`) and + object_creation_expression (`new Foo()`). + """ + calls: list[str] = [] + visited: set[int] = set() + + def walk(n: Node) -> None: + if id(n) in visited: + return + visited.add(id(n)) + if n.type == "invocation_expression": + fn = n.child_by_field_name("function") + if fn: + name = _ident(fn, src) + # `obj.Method` or `Foo.Bar.Method` -> last segment + if "." in name: + name = name.split(".")[-1] + if re.match(r"^\w+$", name, re.UNICODE): + calls.append(name) + elif n.type == "object_creation_expression": + type_node = n.child_by_field_name("type") + if type_node: + name = _ident(type_node, src) + if "." in name: + name = name.split(".")[-1] + if re.match(r"^\w+$", name, re.UNICODE): + calls.append(name) + for child in n.children: + walk(child) + + walk(node) + return list(dict.fromkeys(calls)) + + +@register_parser(".cs") +class CSharpParser(BaseParser): + """Tree-sitter parser for C# source files.""" + + lang = "csharp" + extensions = [".cs"] + extracts = ["classes", "interfaces", "methods", "imports", "calls"] + description = "C# source files (.cs)" + tree_sitter_lang = "c_sharp" + + def parse(self, path: Path) -> FileIndex: + path = Path(path) + path_str = str(path) + src = path.read_bytes() + tree = _parser.parse(src) + root = tree.root_node + + index = FileIndex(path=path_str, lang=self.lang) + + def _emit_method(method_node: Node, current_class: str | None) -> None: + name_node = method_node.child_by_field_name("name") + name = _ident(name_node, src) if name_node else "?" + body_node = method_node.child_by_field_name("body") + calls = _collect_calls(body_node, src) if body_node else [] + fn_id = ( + f"{path_str}::{current_class}.{name}" + if current_class + else f"{path_str}::{name}" + ) + index.functions.append( + SymbolDef( + id=fn_id, + name=name, + file_path=path_str, + start_line=method_node.start_point[0] + 1, + end_line=method_node.end_point[0] + 1, + docstring="", + class_name=current_class, + calls=calls, + kind="constructor" + if method_node.type == "constructor_declaration" + else "method", + ) + ) + + def _emit_type(decl: Node, kind: str) -> None: + name_node = decl.child_by_field_name("name") + if not name_node: + return + name = _ident(name_node, src) + bases: list[str] = [] + # `base_list` (`: Base, IFoo`) is a positional child, not a named + # field, so look it up by node type. + for child in decl.children: + if child.type == "base_list": + for b in child.children: + if b.type in ("identifier", "qualified_name", "generic_name"): + bases.append(_ident(b, src)) + break + index.classes.append( + ClassDef( + id=f"{path_str}::{name}", + name=name, + file_path=path_str, + start_line=decl.start_point[0] + 1, + end_line=decl.end_point[0] + 1, + docstring="", + bases=bases, + kind=kind, + ) + ) + body = decl.child_by_field_name("body") + if body: + for child in body.children: + if child.type in ("method_declaration", "constructor_declaration"): + _emit_method(child, name) + elif child.type in _TYPE_DECLS: + _emit_type(child, _TYPE_DECLS[child.type]) + + def _emit_using(decl: Node) -> None: + # `using System;` / `using System.Collections.Generic;` + # Skip the leading `using` keyword and any alias `=`; the module is + # the identifier or qualified_name child. + for child in decl.children: + if child.type in ("identifier", "qualified_name"): + mod = _text(child, src) + if mod: + index.imports.append(ImportRef(source_module=mod, symbols=[])) + return + + def _walk(node: Node) -> None: + # Namespaces (block or file-scoped) just wrap declarations, so + # recurse into them rather than treating them as types. + for child in node.children: + t = child.type + if t == "using_directive": + _emit_using(child) + elif t in _TYPE_DECLS: + _emit_type(child, _TYPE_DECLS[t]) + elif t in ( + "namespace_declaration", + "file_scoped_namespace_declaration", + "declaration_list", + ): + _walk(child) + + _walk(root) + return index diff --git a/codegraph/parsers/ruby.py b/codegraph/parsers/ruby.py new file mode 100644 index 0000000..43e6638 --- /dev/null +++ b/codegraph/parsers/ruby.py @@ -0,0 +1,175 @@ +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# __creation__ = 2026-06-07 +# __author__ = "jndjama (Joy Ndjama)" +# __copyright__ = "Copyright 2026 ALTIKVA." +# __licence__ = "MIT & CC BY-NC-SA (http://www.altikva.com/licenses/LICENSE-1.0)" +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# Description: Ruby parser plugin. Extracts classes and modules, methods +# (`def` and `def self.`), require / require_relative (imports), +# and call references using tree-sitter-ruby. Optional: ships +# behind the `langs` extra, so the grammar import only happens +# when the extra is installed. + +from __future__ import annotations + +import re +from pathlib import Path + +import tree_sitter_ruby as tsr +from tree_sitter import Language, Node, Parser + +from . import register_parser +from .base import BaseParser, ClassDef, FileIndex, ImportRef, SymbolDef + +RUBY_LANGUAGE = Language(tsr.language()) +_parser = Parser(RUBY_LANGUAGE) + +_REQUIRE_NAMES = {"require", "require_relative", "load", "autoload"} + + +def _text(node: Node, src: bytes) -> str: + return src[node.start_byte : node.end_byte].decode("utf-8", errors="replace") + + +def _ident(node: Node, src: bytes) -> str: + from codegraph.core.utils import normalize_identifier + + return normalize_identifier(_text(node, src)) + + +def _string_value(node: Node, src: bytes) -> str: + """Pull the literal text out of a `string` node, dropping the quotes.""" + for child in node.children: + if child.type == "string_content": + return _text(child, src) + return _text(node, src).strip("\"'") + + +def _collect_calls(node: Node, src: bytes) -> list[str]: + """Walk a Ruby method body, return called method names (deduped). + + A Ruby `call` node is `recv.method(args)` or a bare `method(args)`; + the method name sits in the `method` field (or is the lone identifier + for a paren-less call). + """ + calls: list[str] = [] + visited: set[int] = set() + + def walk(n: Node) -> None: + if id(n) in visited: + return + visited.add(id(n)) + if n.type == "call": + method = n.child_by_field_name("method") + if method is not None: + name = _ident(method, src) + if re.match(r"^\w+[?!]?$", name, re.UNICODE): + calls.append(name.rstrip("?!")) + for child in n.children: + walk(child) + + walk(node) + return list(dict.fromkeys(calls)) + + +@register_parser(".rb") +class RubyParser(BaseParser): + """Tree-sitter parser for Ruby source files.""" + + lang = "ruby" + extensions = [".rb"] + extracts = ["classes", "modules", "methods", "imports", "calls"] + description = "Ruby source files (.rb)" + tree_sitter_lang = "ruby" + + def parse(self, path: Path) -> FileIndex: + path = Path(path) + path_str = str(path) + src = path.read_bytes() + tree = _parser.parse(src) + root = tree.root_node + + index = FileIndex(path=path_str, lang=self.lang) + + def _emit_method(method_node: Node, current_class: str | None) -> None: + name_node = method_node.child_by_field_name("name") + name = _ident(name_node, src) if name_node else "?" + calls = _collect_calls(method_node, src) + is_singleton = method_node.type == "singleton_method" + fn_id = ( + f"{path_str}::{current_class}.{name}" + if current_class + else f"{path_str}::{name}" + ) + index.functions.append( + SymbolDef( + id=fn_id, + name=name, + file_path=path_str, + start_line=method_node.start_point[0] + 1, + end_line=method_node.end_point[0] + 1, + docstring="", + class_name=current_class, + calls=calls, + kind="singleton_method" if is_singleton else "method", + ) + ) + + def _emit_type(decl: Node, kind: str) -> None: + name_node = decl.child_by_field_name("name") + if not name_node: + return + name = _ident(name_node, src) + bases: list[str] = [] + if kind == "class": + superclass = decl.child_by_field_name("superclass") + if superclass: + for child in superclass.children: + if child.type in ("constant", "scope_resolution"): + bases.append(_ident(child, src)) + index.classes.append( + ClassDef( + id=f"{path_str}::{name}", + name=name, + file_path=path_str, + start_line=decl.start_point[0] + 1, + end_line=decl.end_point[0] + 1, + docstring="", + bases=bases, + kind=kind, + ) + ) + body = decl.child_by_field_name("body") + if body: + for child in body.children: + _dispatch(child, name) + + def _emit_require(call_node: Node) -> None: + method = call_node.child_by_field_name("method") + if not method or _ident(method, src) not in _REQUIRE_NAMES: + return + args = call_node.child_by_field_name("arguments") + if not args: + return + for arg in args.children: + if arg.type == "string": + mod = _string_value(arg, src) + if mod: + index.imports.append(ImportRef(source_module=mod, symbols=[])) + return + + def _dispatch(node: Node, current_class: str | None) -> None: + t = node.type + if t == "class": + _emit_type(node, "class") + elif t == "module": + _emit_type(node, "module") + elif t in ("method", "singleton_method"): + _emit_method(node, current_class) + elif t == "call": + _emit_require(node) + + for node in root.children: + _dispatch(node, None) + + return index diff --git a/pyproject.toml b/pyproject.toml index 76e435f..ed388c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,16 @@ dependencies = [ # you can skip the extra entirely and use the default DuckDB backend. kuzu = ["kuzu>=0.7"] +# Install with `pip install cgh[langs]` to add the optional tree-sitter +# language parsers (C# and Ruby). They are kept out of the core dependency +# list so the base install stays lean and Python-3.14-safe: if a grammar +# lacks a 3.14 wheel it cannot block a core install. When the extra is not +# present these parsers simply never register (see _discover_parsers). +langs = [ + "tree-sitter-c-sharp>=0.23", + "tree-sitter-ruby>=0.23", +] + [project.urls] Homepage = "https://github.com/altikva/cgh" Repository = "https://github.com/altikva/cgh" diff --git a/tests/test_parsers/test_csharp.py b/tests/test_parsers/test_csharp.py new file mode 100644 index 0000000..602b4c9 --- /dev/null +++ b/tests/test_parsers/test_csharp.py @@ -0,0 +1,162 @@ +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# __creation__ = 2026-06-07 +# __author__ = "jndjama (Joy Ndjama)" +# __copyright__ = "Copyright 2026 ALTIKVA." +# __licence__ = "MIT & CC BY-NC-SA (http://www.altikva.com/licenses/LICENSE-1.0)" +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# Description: Tests for the optional C# tree-sitter parser. Skipped when the +# `langs` extra (tree-sitter-c-sharp) is not installed. + +from __future__ import annotations + +import textwrap + +import pytest + +# Optional grammar: skip the whole module when the extra is absent, the same +# way tests/test_core/test_kuzu_optional.py guards the kuzu extra. +pytest.importorskip("tree_sitter_c_sharp") + +from codegraph.core.db import get_connection, reset_connection # noqa: E402 +from codegraph.indexer import index_file # noqa: E402 +from codegraph.parsers.csharp import CSharpParser # noqa: E402 + + +@pytest.fixture +def sample_csharp(tmp_path): + f = tmp_path / "Greeter.cs" + f.write_text( + textwrap.dedent("""\ + using System; + using System.Collections.Generic; + + namespace MyApp.Core + { + interface ISpeaker + { + string Hello(); + } + + public class Greeter : Base, ISpeaker + { + private string name; + + public Greeter(string name) + { + this.name = name; + } + + public string Hello() + { + var parts = new List(); + return Format(parts); + } + } + + class Program + { + static void Main() + { + var g = new Greeter("world"); + Console.WriteLine(g.Hello()); + } + } + } + """) + ) + return f + + +class TestCSharpParser: + def test_classes_extracted(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + kinds = {c.name: c.kind for c in idx.classes} + assert kinds.get("Greeter") == "class" + assert kinds.get("Program") == "class" + assert kinds.get("ISpeaker") == "interface" + + def test_constructor_attached_to_class(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + constructors = [f for f in idx.functions if f.kind == "constructor"] + assert any( + c.name == "Greeter" and c.class_name == "Greeter" for c in constructors + ) + + def test_methods_attached_to_class(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + hello = next( + f for f in idx.functions if f.name == "Hello" and f.class_name == "Greeter" + ) + assert hello.kind == "method" + + def test_base_list_recorded(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + greeter = next(c for c in idx.classes if c.name == "Greeter") + assert "Base" in greeter.bases + assert "ISpeaker" in greeter.bases + + def test_using_imports(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + modules = [imp.source_module for imp in idx.imports] + assert "System" in modules + assert "System.Collections.Generic" in modules + + def test_calls_include_new_and_invocation(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + main = next(f for f in idx.functions if f.name == "Main") + # `new Greeter(...)` from object_creation_expression + assert "Greeter" in main.calls + # `Console.WriteLine(...)` from invocation_expression -> last segment + assert "WriteLine" in main.calls + + def test_file_scoped_namespace(self, tmp_path): + f = tmp_path / "Scoped.cs" + f.write_text( + textwrap.dedent("""\ + namespace Acme; + + public class Widget + { + public void Spin() { } + } + """) + ) + idx = CSharpParser().parse(f) + assert any(c.name == "Widget" for c in idx.classes) + + def test_malformed_does_not_raise(self, tmp_path): + f = tmp_path / "Broken.cs" + f.write_text("public class { void (") + # Must return a FileIndex, never raise. + idx = CSharpParser().parse(f) + assert idx.lang == "csharp" + + def test_lang(self, sample_csharp): + idx = CSharpParser().parse(sample_csharp) + assert idx.lang == "csharp" + + +class TestCSharpRoundTrip: + @pytest.fixture(autouse=True) + def clean_db(self): + reset_connection() + yield + reset_connection() + + def test_index_file_lands_symbols(self, sample_csharp, tmp_path): + ok = index_file(sample_csharp, tmp_path) + assert ok is True + + conn = get_connection(tmp_path) + files = conn.find_nodes("File", return_fields=["path", "lang"]) + assert any(fi["lang"] == "csharp" for fi in files) + + cls_names = [ + c["name"] for c in conn.find_nodes("Class", return_fields=["name"]) + ] + assert "Greeter" in cls_names + + fn_names = [ + f["name"] for f in conn.find_nodes("Function", return_fields=["name"]) + ] + assert "Hello" in fn_names diff --git a/tests/test_parsers/test_ruby.py b/tests/test_parsers/test_ruby.py new file mode 100644 index 0000000..a3b7935 --- /dev/null +++ b/tests/test_parsers/test_ruby.py @@ -0,0 +1,126 @@ +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# __creation__ = 2026-06-07 +# __author__ = "jndjama (Joy Ndjama)" +# __copyright__ = "Copyright 2026 ALTIKVA." +# __licence__ = "MIT & CC BY-NC-SA (http://www.altikva.com/licenses/LICENSE-1.0)" +# -#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-# +# Description: Tests for the optional Ruby tree-sitter parser. Skipped when the +# `langs` extra (tree-sitter-ruby) is not installed. + +from __future__ import annotations + +import textwrap + +import pytest + +# Optional grammar: skip the whole module when the extra is absent, the same +# way tests/test_core/test_kuzu_optional.py guards the kuzu extra. +pytest.importorskip("tree_sitter_ruby") + +from codegraph.core.db import get_connection, reset_connection # noqa: E402 +from codegraph.indexer import index_file # noqa: E402 +from codegraph.parsers.ruby import RubyParser # noqa: E402 + + +@pytest.fixture +def sample_ruby(tmp_path): + f = tmp_path / "greeter.rb" + f.write_text( + textwrap.dedent("""\ + require "json" + require_relative "helper" + + module Greetings + class Greeter < Base + def initialize(name) + @name = name + end + + def hello + puts format_name(@name) + end + end + + def self.top + helper + end + end + """) + ) + return f + + +class TestRubyParser: + def test_class_and_module_extracted(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + kinds = {c.name: c.kind for c in idx.classes} + assert kinds.get("Greeter") == "class" + assert kinds.get("Greetings") == "module" + + def test_superclass_recorded(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + greeter = next(c for c in idx.classes if c.name == "Greeter") + assert "Base" in greeter.bases + + def test_methods_attached_to_class(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + hello = next( + f for f in idx.functions if f.name == "hello" and f.class_name == "Greeter" + ) + assert hello.kind == "method" + init = next(f for f in idx.functions if f.name == "initialize") + assert init.class_name == "Greeter" + + def test_singleton_method(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + top = next(f for f in idx.functions if f.name == "top") + assert top.kind == "singleton_method" + assert top.class_name == "Greetings" + + def test_requires_as_imports(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + modules = [imp.source_module for imp in idx.imports] + assert "json" in modules + assert "helper" in modules + + def test_calls_collected(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + hello = next(f for f in idx.functions if f.name == "hello") + assert "puts" in hello.calls + assert "format_name" in hello.calls + + def test_malformed_does_not_raise(self, tmp_path): + f = tmp_path / "broken.rb" + f.write_text("class def end module (((") + idx = RubyParser().parse(f) + assert idx.lang == "ruby" + + def test_lang(self, sample_ruby): + idx = RubyParser().parse(sample_ruby) + assert idx.lang == "ruby" + + +class TestRubyRoundTrip: + @pytest.fixture(autouse=True) + def clean_db(self): + reset_connection() + yield + reset_connection() + + def test_index_file_lands_symbols(self, sample_ruby, tmp_path): + ok = index_file(sample_ruby, tmp_path) + assert ok is True + + conn = get_connection(tmp_path) + files = conn.find_nodes("File", return_fields=["path", "lang"]) + assert any(fi["lang"] == "ruby" for fi in files) + + cls_names = [ + c["name"] for c in conn.find_nodes("Class", return_fields=["name"]) + ] + assert "Greeter" in cls_names + + fn_names = [ + f["name"] for f in conn.find_nodes("Function", return_fields=["name"]) + ] + assert "hello" in fn_names diff --git a/uv.lock b/uv.lock index 35ab187..22f814e 100644 --- a/uv.lock +++ b/uv.lock @@ -212,6 +212,10 @@ dependencies = [ kuzu = [ { name = "kuzu" }, ] +langs = [ + { name = "tree-sitter-c-sharp" }, + { name = "tree-sitter-ruby" }, +] [package.metadata] requires-dist = [ @@ -222,14 +226,16 @@ requires-dist = [ { name = "rank-bm25", specifier = ">=0.2" }, { name = "rich", specifier = ">=13.0" }, { name = "tree-sitter", specifier = ">=0.23" }, + { name = "tree-sitter-c-sharp", marker = "extra == 'langs'", specifier = ">=0.23" }, { name = "tree-sitter-go", specifier = ">=0.23" }, { name = "tree-sitter-java", specifier = ">=0.23" }, { name = "tree-sitter-python", specifier = ">=0.23" }, + { name = "tree-sitter-ruby", marker = "extra == 'langs'", specifier = ">=0.23" }, { name = "tree-sitter-rust", specifier = ">=0.23" }, { name = "tree-sitter-typescript", specifier = ">=0.23" }, { name = "watchdog", specifier = ">=4.0" }, ] -provides-extras = ["kuzu"] +provides-extras = ["kuzu", "langs"] [[package]] name = "click" @@ -1476,6 +1482,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" }, ] +[[package]] +name = "tree-sitter-c-sharp" +version = "0.23.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/7e2962bc1901daf264e7ce263b168e0139304a5f8f66c9b2baf20e550f87/tree_sitter_c_sharp-0.23.5.tar.gz", hash = "sha256:2635c7d5ec93e59f2e831b571bed99c4cc68a5d183a0994020aa769e1b990a71", size = 1147914, upload-time = "2026-04-14T16:11:22.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/c4/86d8d469400a856757a464a6ac01af97d8cdacbb595e62bdb98bf1e9db90/tree_sitter_c_sharp-0.23.5-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:61e1981cf21b09ee547b9c4c68e64fb4394325f8fc8d5f6d50d41471eba923ea", size = 333658, upload-time = "2026-04-14T16:11:11.288Z" }, + { url = "https://files.pythonhosted.org/packages/c8/13/593c8603f834eaf15082b81e079289fc9f062b4c0ab5b9489134084eec06/tree_sitter_c_sharp-0.23.5-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:a75994a11f6fed3f5b8c36ad6a00e5dc43205bd912c43af3a2a54fdf649664eb", size = 376296, upload-time = "2026-04-14T16:11:12.972Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/a8855cbb5bbab28adb29c2c7f0e7be5a9f1d21450c13b3c3e613190d9b8c/tree_sitter_c_sharp-0.23.5-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aa88a780204cd153c4c1ae2d59c654cee1402212fa0d069823d6d34301587438", size = 358333, upload-time = "2026-04-14T16:11:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c8/e0f391e343f5424d0627e3b6886c77baeb1249a3f10986be00b0b64ecdab/tree_sitter_c_sharp-0.23.5-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea38fb095d85d360dc5a0bec2fa605e496228876f798c9e089d5f0e72bcef46", size = 359448, upload-time = "2026-04-14T16:11:15.419Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fc/10f807ac79f928241c5e0d827fdaf91e97dfba662fc7e07d7bd664140ec1/tree_sitter_c_sharp-0.23.5-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:05a9256415e7f24d4f133133794a9c224c60d19f677a04e2f6a94c25090b6d65", size = 358144, upload-time = "2026-04-14T16:11:17.087Z" }, + { url = "https://files.pythonhosted.org/packages/de/2a/6c3e12ef0cf09138717fcc02e1de8b76a3928d1bed65c7e3c2bd3172bcef/tree_sitter_c_sharp-0.23.5-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8636dc70b5a373c35c1036ed5de98e801f2e4d105ae41e2e20b6804c36e3bf33", size = 357525, upload-time = "2026-04-14T16:11:18.214Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/bd287b092d611df95a9149117fd27b5947ce75527113d6898a4b4e2c8858/tree_sitter_c_sharp-0.23.5-cp310-abi3-win_amd64.whl", hash = "sha256:41a28cfa3d9ea50f5629e44550a03188c8fbd5079803dfc03554b6fd594b33fa", size = 338756, upload-time = "2026-04-14T16:11:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fb/114ff43fdd256d0befed32f77c1dadee9517867181c70794571f718ed05c/tree_sitter_c_sharp-0.23.5-cp310-abi3-win_arm64.whl", hash = "sha256:2de4ebf95ddc2e92cd3105c8a8e0e7ec646bc82f52bfaf2f3acec0fa2401ec09", size = 337260, upload-time = "2026-04-14T16:11:20.849Z" }, +] + [[package]] name = "tree-sitter-go" version = "0.25.0" @@ -1523,6 +1545,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/19/4b5569d9b1ebebb5907d11554a96ef3fa09364a30fcfabeff587495b512f/tree_sitter_python-0.25.0-cp310-abi3-win_arm64.whl", hash = "sha256:0fbf6a3774ad7e89ee891851204c2e2c47e12b63a5edbe2e9156997731c128bb", size = 74169, upload-time = "2025-09-11T06:47:56.747Z" }, ] +[[package]] +name = "tree-sitter-ruby" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/5b/6d24be4fde4743481bd8e3fd24b434870cb6612238c8544b71fe129ed850/tree_sitter_ruby-0.23.1.tar.gz", hash = "sha256:886ed200bfd1f3ca7628bf1c9fefd42421bbdba70c627363abda67f662caa21e", size = 489602, upload-time = "2024-11-11T04:51:30.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/2e/2717b9451c712b60f833827a696baf29d8e50a0f7dccbf22a8d7006cc19e/tree_sitter_ruby-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:39f391322d2210843f07081182dbf00f8f69cfbfa4687b9575cac6d324bae443", size = 177959, upload-time = "2024-11-11T04:51:19.958Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/c41ecf7692b8ecccd26861d3293a88150a4a52fc081abe60f837030d7315/tree_sitter_ruby-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:aa4ee7433bd42fac22e2dad4a3c0f332292ecf482e610316828c711a0bb7f794", size = 195069, upload-time = "2024-11-11T04:51:21.82Z" }, + { url = "https://files.pythonhosted.org/packages/d8/01/14ef2d5107e6f42b64a400c3bbc3dd3b8fd24c3cef5306004ae03668f231/tree_sitter_ruby-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62b36813a56006b7569db7868f6b762caa3f4e419bd0f8cf9ccbb4abb1b6254c", size = 226761, upload-time = "2024-11-11T04:51:23.021Z" }, + { url = "https://files.pythonhosted.org/packages/23/dd/1171b5dd25da10f768732a20fb62d2e3ae66e3b42329351f2ce5bf723abb/tree_sitter_ruby-0.23.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7bcd93972b4ca2803856d4fe0fbd04123ff29c4592bbb9f12a27528bd252341", size = 214427, upload-time = "2024-11-11T04:51:24.854Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/de76c877a90fd8a62cd60f496d7832efddc1b18a148593d9aa9b4a9ce5e0/tree_sitter_ruby-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66c65d6c2a629783ca4ab2bab539bd6f271ce6f77cacb62845831e11665b5bd3", size = 210409, upload-time = "2024-11-11T04:51:26.093Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/f5bcca350b84cdf75a53e918b8efa06c46ed650d99d3ef22195e9d8020cc/tree_sitter_ruby-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:02e2c19ebefe29226c14aa63e11e291d990f5b5c20a99940ab6e7eda44e744e5", size = 179843, upload-time = "2024-11-11T04:51:27.265Z" }, + { url = "https://files.pythonhosted.org/packages/71/5c/a2e068ad4b2c4ba9b774a88b24149168d3bcd94f58b964e49dcabfe5fd24/tree_sitter_ruby-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:ed042007e89f2cceeb1cbdd8b0caa68af1e2ce54c7eb2053ace760f90657ac9f", size = 178025, upload-time = "2024-11-11T04:51:29.051Z" }, +] + [[package]] name = "tree-sitter-rust" version = "0.24.2"