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
79 changes: 79 additions & 0 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2460,6 +2460,85 @@ def _slice_by_braces(
end_idx = term_idx + 1
else:
continue # neither a body nor a bodyless `;` terminator ever showed up in the window
# #1756: Go's bodyless function declarations (assembly-backed
# implementations and //go:linkname targets -- e.g. "func
# memmove(to, from unsafe.Pointer, n uintptr)" with no { body,
# legal and common in the stdlib) were silently dropped by the
# generic brace-only fallback below: Go's automatic-semicolon-
# insertion rule means a bodyless declaration ends at the end of
# its signature line without a literal ";", so the brace search
# either found nothing in the bounded window (brace_idx == -1,
# match discarded) or -- when a struct/interface literal
# happened to appear later -- attributed an unrelated block as
# the function's body. Mirrors #1319's rust bodyless
# trait-method handling, with the declaration bound taken from
# Go's own ASI rule: after the parameter list closes, the first
# top-level { is the body; a literal ";" or (far more common)
# the end of the line means the declaration is bodyless. "func"
# at line start is unambiguous in Go (never a call or bare
# statement), so a bodyless terminator is never a false match.
#
# One Go-specific wrinkle: a return type may itself contain a
# brace group ("func f() struct{ X int } { ... }",
# "interface{ ... }"), which sits at top level after the
# parameter list and would be mistaken for the body. Such a
# group is always closed on the same line, and a real body {
# always follows on that same line -- so a top-level { whose
# balanced close is followed by another { before the line ends
# is a type literal, not the body; skip past it and keep
# scanning.
elif lang_id == "go":
params_end_idx = self._find_balanced_end(safe_code, match.end() - 1, "(", ")")
# Go has no angle-bracket grouping: generics use square brackets
# ([T any]), so < and > only ever appear as operators -- most
# notably the channel-direction operator (chan<- / <-chan),
# whose lone < would poison an angle-depth counter and stall the
# scan below. Track parens and brackets only.
depth_paren = depth_bracket = 0
pos = params_end_idx
term_idx, term_kind = -1, None
while pos < search_limit:
ch = safe_code[pos]
if ch == "(":
depth_paren += 1
elif ch == ")":
depth_paren = max(0, depth_paren - 1)
elif ch == "[":
depth_bracket += 1
elif ch == "]":
depth_bracket = max(0, depth_bracket - 1)
elif depth_paren == 0 and depth_bracket == 0:
if ch == opener:
# A brace group that is a type literal (struct{
# ... } / interface{ ... } in the return type)
# closes before the line ends and is followed by
# the real body's { on that same line -- or, for
# a bodyless declaration, by the end of the
# line. Only a { whose balanced close is NOT
# followed by another { before the next newline
# is the function's own body.
group_end = self._find_balanced_end(safe_code, pos, opener, closer)
line_end = safe_code.find("\n", group_end + 1, search_limit)
if line_end == -1:
line_end = search_limit
if safe_code.find(opener, group_end + 1, line_end) != -1:
pos = group_end + 1
continue
term_idx, term_kind = pos, "brace"
break
elif ch == ";":
term_idx, term_kind = pos, "semi"
break
elif ch == "\n":
term_idx, term_kind = pos, "eol"
break
pos += 1
if term_kind == "brace":
end_idx = self._find_balanced_end(safe_code, term_idx, opener, closer)
elif term_kind in ("semi", "eol"):
end_idx = term_idx + 1
else:
continue # neither a body nor a bodyless declaration bound showed up in the window
elif lang_id == "kotlin":
paren_idx = safe_code.find("(", match.end(), search_limit)
brace_idx = safe_code.find(opener, match.end(), search_limit)
Expand Down
151 changes: 151 additions & 0 deletions tests/core_engine/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2761,6 +2761,157 @@ def test_objectivec_c_style_real_definition_still_extracted():
assert found["c_style_func"] == 2, f"expected args=2, got args={found['c_style_func']}"


def test_go_bodyless_function_declarations_extracted():
"""
#1756: Go's bodyless function declarations (assembly-backed
implementations and //go:linkname targets -- func memmove(to, from
unsafe.Pointer, n uintptr) with no { body) have no brace group, and
Go's automatic-semicolon-insertion rule means the declaration ends at
the end of its signature line without a literal ;. The generic
Mode-B brace-only fallback in _slice_by_braces (detector.py)
required a { within the search window and silently dropped every
one of these. func_start's own regex always matched them -- the gap
was purely in detector.py's downstream body-boundary search, not the
regex (the same shape as #1314/#1319's rust/objc bodyless handling).
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

code = (
"package runtime\n"
"\n"
"func memmove(to, from unsafe.Pointer, n uintptr)\n"
"\n"
"func add(a, b int) int {\n"
"\treturn a + b\n"
"}\n"
"\n"
"//go:linkname gogo runtime.gogo\n"
"func gogo()\n"
"\n"
"func sub(a, b int) int {\n"
"\treturn a - b\n"
"}\n"
)
detector = StructuralExtractor("go", LANGUAGE_DEFINITIONS)
result = detector.splice(code, "", raw_content=code)

found = {fn["name"]: fn for fn in result.get("functions", [])}
expected = {"memmove", "gogo", "add", "sub"}
missing = expected - set(found)
assert not missing, f"Go function declaration(s) not extracted: {missing}"
assert found["memmove"]["args"] == 3, f"expected memmove args=3, got args={found['memmove']['args']}"
# Bodyless declarations span their signature line only -- no phantom body.
assert found["memmove"]["start_line"] == found["memmove"]["end_line"], (
"bodyless memmove should span just its signature line"
)
assert found["gogo"]["start_line"] == found["gogo"]["end_line"], "bodyless gogo should span just its signature line"


def test_go_bodyless_declaration_not_misattributed_following_block():
"""
#1756 companion: when a bodyless declaration is followed by an
unrelated brace block (a struct literal later in the file), the
pre-fix generic brace search grabbed that block as the phantom
function's body. A bodyless declaration must end at its own
signature line instead.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

code = (
"package main\n"
"\n"
"func flushICache(begin, end uintptr)\n"
"\n"
"type Foo struct {\n"
"\tX int\n"
"}\n"
"\n"
"func bar() int {\n"
"\treturn 1\n"
"}\n"
)
detector = StructuralExtractor("go", LANGUAGE_DEFINITIONS)
result = detector.splice(code, "", raw_content=code)

found = {fn["name"]: fn for fn in result.get("functions", [])}
assert "flushICache" in found, "bodyless flushICache should be extracted"
assert found["flushICache"]["end_line"] == 3, (
f"flushICache must end at its own signature line, got end_line={found['flushICache']['end_line']}"
)
assert "bar" in found, "ordinary braced function after the struct must still be extracted"


def test_go_channel_direction_operator_does_not_poison_body_scan():
"""
#1760 review follow-up: Go's channel-direction operator (chan<- / <-chan)
contains a lone < that an angle-bracket depth counter would never balance,
stalling the body-boundary scan and silently dropping every following
function. Go has no angle-bracket grouping (generics are [T any]), so the
Go branch must track parens and brackets only.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

code = (
"package main\n"
"\n"
"func makeSendChan() chan<- int {\n"
"\tch := make(chan int)\n"
"\treturn ch\n"
"}\n"
"\n"
"func makeRecvChan() <-chan int {\n"
"\tch := make(chan int)\n"
"\treturn ch\n"
"}\n"
"\n"
"func add(a, b int) int {\n"
"\treturn a + b\n"
"}\n"
)
detector = StructuralExtractor("go", LANGUAGE_DEFINITIONS)
result = detector.splice(code, "", raw_content=code)

found = {fn["name"]: fn for fn in result.get("functions", [])}
expected = {"makeSendChan", "makeRecvChan", "add"}
missing = expected - set(found)
assert not missing, f"Go function(s) dropped by channel operator: {missing}"
assert found["makeSendChan"]["args"] == 0, "makeSendChan should take no args"
assert found["makeRecvChan"]["args"] == 0, "makeRecvChan should take no args"


def test_go_struct_return_type_not_truncated_at_type_literal_brace():
"""
#1756 wrinkle: a return type that itself contains a brace group
(func f() struct{ X int } { ... }) puts a top-level { before the real
body -- the generic brace search stopped at the struct literal's {,
truncating the function's span to the type. The real body must be
included.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

code = (
"package main\n"
"\n"
"func makePoint() struct{ X, Y int } {\n"
"\treturn struct{ X, Y int }{1, 2}\n"
"}\n"
"\n"
"func other() int {\n"
"\treturn 2\n"
"}\n"
)
detector = StructuralExtractor("go", LANGUAGE_DEFINITIONS)
result = detector.splice(code, "", raw_content=code)

found = {fn["name"]: fn for fn in result.get("functions", [])}
assert "makePoint" in found, "makePoint should be extracted"
assert found["makePoint"]["start_line"] == 3
assert found["makePoint"]["end_line"] == 5, (
f"makePoint's span must include its real body, got end_line={found['makePoint']['end_line']}"
)
assert "other" in found, "ordinary braced function after it must still be extracted"


def test_objectivec_args_body_lookalikes_excluded_by_signature_bound():
"""
#1335: `_slice_by_braces`'s objc branches now bound `args_pattern.search`
Expand Down
Loading
Loading