Skip to content
Merged
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
1 change: 1 addition & 0 deletions ast_rag/dto/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class Language(str, Enum):
PYTHON = "python"
TYPESCRIPT = "typescript"
TSX = "tsx"
GO = "go"


class BlockType(str, Enum):
Expand Down
3 changes: 3 additions & 0 deletions ast_rag/services/parsing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
from ast_rag.services.parsing.rust import RUST_QUERIES
from ast_rag.services.parsing.python import PYTHON_QUERIES
from ast_rag.services.parsing.typescript import TYPESCRIPT_QUERIES
from ast_rag.services.parsing.go import GO_QUERIES

LANGUAGE_QUERIES: dict[str, dict[str, str]] = {
"java": JAVA_QUERIES,
"cpp": CPP_QUERIES,
"rust": RUST_QUERIES,
"python": PYTHON_QUERIES,
"typescript": TYPESCRIPT_QUERIES,
"go": GO_QUERIES,
# TSX grammar is a superset of TypeScript's (adds JSX nodes), so the
# existing TypeScript queries apply unchanged.
"tsx": TYPESCRIPT_QUERIES,
Expand All @@ -35,6 +37,7 @@
"RUST_QUERIES",
"PYTHON_QUERIES",
"TYPESCRIPT_QUERIES",
"GO_QUERIES",
"BlockExtractor",
"ParserManager",
"NodeExtractor",
Expand Down
64 changes: 64 additions & 0 deletions ast_rag/services/parsing/go.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
go.py - Tree-sitter S-expression queries for Go.

BASIC extraction: structs, interfaces, functions, methods, imports, calls.

Go models types through `type_declaration -> type_spec`, with the concrete
shape (`struct_type` / `interface_type`) hanging off the `type` field, so
structs and interfaces are matched on the type_spec rather than on a
dedicated node. Methods are `method_declaration` (they carry a `receiver`);
plain functions are `function_declaration`.
"""

from __future__ import annotations

GO_QUERIES: dict[str, str] = {
"struct_defs": """
(type_spec
name: (type_identifier) @name
type: (struct_type) @body
) @node
""",
"interface_defs": """
(type_spec
name: (type_identifier) @name
type: (interface_type) @body
) @node
""",
"function_defs": """
(function_declaration
name: (identifier) @name
parameters: (parameter_list) @params
) @node
""",
"method_defs": """
(method_declaration
receiver: (parameter_list) @receiver
name: (field_identifier) @name
parameters: (parameter_list) @params
) @node
""",
"field_defs": """
(field_declaration
name: (field_identifier) @name
type: (_) @field_type
) @node
""",
"imports": """
(import_spec
path: (interpreted_string_literal) @path
) @node
""",
# `callee_name` is what EdgeExtractor._extract_call_edges reads. Go calls
# are either a bare identifier (`helper()`) or a selector
# (`fmt.Println()`); for the latter the method name is the useful half.
"calls": """
[
(call_expression
function: (identifier) @callee_name)
(call_expression
function: (selector_expression
field: (field_identifier) @callee_name))
] @node
""",
}
3 changes: 3 additions & 0 deletions ast_rag/services/parsing/parser_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import tree_sitter_cpp as tscpp
import tree_sitter_java as tsjava
import tree_sitter_rust as tsrust
import tree_sitter_go as tsgo
import tree_sitter_python as tspython
import tree_sitter_typescript as tsts
import tree_sitter as ts
Expand Down Expand Up @@ -50,6 +51,7 @@
".h": "cpp",
".java": "java",
".rs": "rust",
".go": "go",
".py": "python",
".ts": "typescript",
".tsx": "tsx",
Expand Down Expand Up @@ -139,6 +141,7 @@ def _init_languages(self) -> None:
"cpp": ts.Language(tscpp.language()),
"java": ts.Language(tsjava.language()),
"rust": ts.Language(tsrust.language()),
"go": ts.Language(tsgo.language()),
"python": ts.Language(tspython.language()),
"typescript": ts.Language(tsts.language_typescript()),
"tsx": ts.Language(tsts.language_tsx()),
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
"tree-sitter-rust>=0.23",
"tree-sitter-python>=0.23",
"tree-sitter-typescript>=0.23",
"tree-sitter-go>=0.23",
# Graph database
"neo4j>=5.14",
# Vector store (Qdrant — Python 3.14 compatible)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ tree-sitter-java>=0.23
tree-sitter-rust>=0.23
tree-sitter-python>=0.23
tree-sitter-typescript>=0.23
tree-sitter-go>=0.23

# Graph database driver
neo4j>=5.14
Expand Down
138 changes: 138 additions & 0 deletions tests/test_go_parsing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Basic Go extraction (issue #17).

Go models types as `type_declaration -> type_spec`, with the concrete shape on
the `type` field, so structs and interfaces are matched on the type_spec rather
than on a dedicated node. Methods carry a `receiver` and are a distinct node
type from plain functions.
"""

from __future__ import annotations

from pathlib import Path

import pytest
from tree_sitter import Query, QueryCursor

from ast_rag.models import NodeKind
from ast_rag.services.parsing import LANGUAGE_QUERIES
from ast_rag.services.parsing.go import GO_QUERIES
from ast_rag.services.parsing.parser_manager import EXT_TO_LANG, ParserManager

GO_SRC = b"""
package main

import "fmt"

type Shape interface {
Area() float64
}

type Rect struct {
W float64
H float64
}

func (r Rect) Area() float64 {
return r.W * r.H
}

func describe(s Shape) string {
return fmt.Sprintf("area=%v", s.Area())
}

func main() {
fmt.Println(describe(Rect{W: 2, H: 3}))
}
"""


@pytest.fixture(scope="module")
def pm() -> ParserManager:
return ParserManager()


@pytest.fixture()
def parsed(pm: ParserManager, tmp_path: Path):
path = tmp_path / "main.go"
path.write_bytes(GO_SRC)
tree = pm.parse_file(str(path), source=GO_SRC)
assert tree is not None, "Go source failed to parse"
nodes = pm.extract_nodes(tree, str(path), "go")
edges = pm.extract_edges(tree, nodes, str(path), "go", source=GO_SRC)
return nodes, edges


def _named(nodes, kind: NodeKind):
return {n.name for n in nodes if n.kind == kind}


def test_go_is_registered():
assert EXT_TO_LANG[".go"] == "go"
assert "go" in LANGUAGE_QUERIES


def test_language_detected_from_extension(pm: ParserManager, tmp_path: Path):
path = tmp_path / "x.go"
path.write_bytes(b"package main\n")
assert pm.detect_language(str(path)) == "go"


def test_structs_and_interfaces_extracted(parsed):
nodes, _ = parsed
assert "Rect" in _named(nodes, NodeKind.STRUCT)
assert "Shape" in _named(nodes, NodeKind.INTERFACE)


def test_functions_and_methods_distinguished(parsed):
nodes, _ = parsed
functions = _named(nodes, NodeKind.FUNCTION)
methods = _named(nodes, NodeKind.METHOD)
assert {"describe", "main"} <= functions
# Area has a receiver, so it is a method rather than a function
assert "Area" in methods
assert "Area" not in functions


def test_struct_fields_extracted(parsed):
nodes, _ = parsed
assert {"W", "H"} <= _named(nodes, NodeKind.FIELD)


def test_imports_extracted(parsed):
_, edges = parsed
kinds = {str(e.kind) for e in edges}
assert any("IMPORTS" in k for k in kinds)


def test_calls_query_captures_bare_and_selector_calls():
"""Both `helper()` and `pkg.Helper()` must yield a callee_name.

Asserted at the query level: turning these matches into CALLS edges also
requires the _extract_call_edges fix, which is a separate change.
"""
import tree_sitter as ts
import tree_sitter_go as tsgo

lang = ts.Language(tsgo.language())
tree = ts.Parser(lang).parse(GO_SRC)
matches = list(QueryCursor(Query(lang, GO_QUERIES["calls"])).matches(tree.root_node))

names = set()
for _, md in matches:
cap = md.get("callee_name")
if cap is None:
continue
node = cap[0] if isinstance(cap, list) else cap
names.add(node.text.decode())

assert "describe" in names, "bare identifier call not captured"
assert "Println" in names, "selector call not captured"
assert "Area" in names


@pytest.mark.parametrize("query_name", sorted(GO_QUERIES))
def test_every_go_query_compiles(query_name: str):
import tree_sitter as ts
import tree_sitter_go as tsgo

Query(ts.Language(tsgo.language()), GO_QUERIES[query_name])
12 changes: 6 additions & 6 deletions tests/test_unsupported_language.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,12 @@ def test_format_lists_every_language(self) -> None:

class TestParserManagerUnsupported:
def test_returns_none_and_warns(self, pm: ParserManager, tmp_path: Path, caplog) -> None:
path = tmp_path / "main.go"
path.write_text("package main\n", encoding="utf-8")
path = tmp_path / "main.rb"
path.write_text("puts 1\n", encoding="utf-8")
with caplog.at_level("WARNING"):
assert pm.parse_file(str(path)) is None
messages = [rec.message for rec in caplog.records]
assert any(".go" in m and "Supported languages" in m for m in messages)
assert any(".rb" in m and "Supported languages" in m for m in messages)

def test_extension_less_file_warns(self, pm: ParserManager, tmp_path: Path, caplog) -> None:
path = tmp_path / "Makefile"
Expand All @@ -95,12 +95,12 @@ def test_supported_file_does_not_warn(self, pm: ParserManager, tmp_path: Path, c
class TestParsingServiceUnsupported:
def test_value_error_lists_supported_languages(self, tmp_path: Path) -> None:
service = ParsingService()
path = tmp_path / "main.go"
path.write_text("package main\n", encoding="utf-8")
path = tmp_path / "main.rb"
path.write_text("puts 1\n", encoding="utf-8")
with pytest.raises(ValueError) as exc_info:
service.parse_file(str(path))
message = str(exc_info.value)
assert "'.go'" in message
assert "'.rb'" in message
assert "Supported languages" in message
assert "java" in message

Expand Down
Loading