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
35 changes: 35 additions & 0 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3678,6 +3678,28 @@ def _dart_scan_terminator(

args_search_text = code[start_idx:args_sig_end] if args_sig_end is not None else None

# #2012: Pattern 2 - constructors with member-initializer-lists overcount.
# Truncate at the first top-level `:` before the brace to exclude the list.
if lang_id in ("c", "cpp") and args_search_text is not None:
depth_paren = depth_angle = 0
for i_ch, ch in enumerate(args_search_text):
if ch == "(":
depth_paren += 1
elif ch == ")":
depth_paren = max(0, depth_paren - 1)
elif ch == "<":
depth_angle += 1
elif ch == ">":
depth_angle = max(0, depth_angle - 1)
elif ch == ":" and depth_paren == 0 and depth_angle == 0:
# Exclude `::`
if (i_ch + 1 < len(args_search_text) and args_search_text[i_ch + 1] == ":") or (
i_ch > 0 and args_search_text[i_ch - 1] == ":"
):
continue
args_search_text = args_search_text[:i_ch]
break

# #1837: a c/cpp signature is sometimes duplicated across an
# #if/#else preprocessor conditional (e.g. micropython/gc.c's
# gc_mark_subtree, gated on MICROPY_GC_SPLIT_HEAP), so
Expand Down Expand Up @@ -4652,6 +4674,13 @@ def _count_top_level_args(self, args_str: str, treat_as_body: bool = False) -> i
):
open_idx = inner_open
wrapper_end = self._matching_paren_end(args_str, open_idx)
# #2012: Pattern 1 - `operator()`'s first `()` is its name, not the parameter list.
# If we matched `operator()` in C/C++, skip the first empty paren pair.
elif self.primary_lang_id in ("c", "cpp") and args_str[max(0, open_idx - 8) : open_idx] == "operator":
inner_open = args_str.find("(", open_idx + 1)
if inner_open != -1:
open_idx = inner_open
wrapper_end = self._matching_paren_end(args_str, open_idx)
body = args_str[open_idx + 1 : wrapper_end]

if not body.strip():
Expand Down Expand Up @@ -5233,6 +5262,12 @@ def _calculate_block_metrics(
else:
# Handle space-separated arguments (Lisp/Scheme/Shell)
args_count = len(args_str.strip().split())
elif args_search_text is not None and self.primary_lang_id in ("c", "cpp"):
# #2012: Pattern 1 - The cpp args regex rejects `operator()` syntax and
# out-of-class methods with non-whitelisted types (e.g. `mlir::ModuleOp`).
# Since func_start already validated this is a function, we can reliably
# fallback to the structural counter.
args_count = self._count_top_level_args(args_search_text)
except Exception as e:
self.logger.debug(f"Argument-count regex extraction failed, leaving args_count 0: {e}")

Expand Down
2 changes: 1 addition & 1 deletion gitgalaxy/standards/language_standards.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
| -------- | ----------- | -------------- | ------------ | --------------- |
| Apex | 100.0% | 100.0% | 100.0% | 100.0% |
| C | 99.0% | 99.5% | 100.0% | 100.0% |
| Cpp | 87.0% | 95.6% | 100.0% | 100.0% |
| Cpp | 87.0% | 95.0% | 100.0% | 100.0% |
| Csharp | 100.0% | 100.0% | 100.0% | 100.0% |
| Css | 100.0% | 100.0% | N/A | N/A |
| Dart | 99.4% | 99.3% | 100.0% | 100.0% |
Expand Down
106 changes: 69 additions & 37 deletions tests/core_engine/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,6 @@ def test_detector_mode_d_ruby_nested_methods_inside_class():
assert "Widget" in class_names, "The enclosing class's own satellite should still be reported."



# ==============================================================================
# TEST 6: MODE C (INDENTATION STRATIFICATION)
# ==============================================================================
Expand Down Expand Up @@ -912,11 +911,14 @@ def test_detector_catastrophic_fallbacks():
assert result["metadata"]["ownership"] == "Joe", "Fallback destroyed the Ghost Mass metadata!"

# 2. TimeoutError -> Hardware Guillotine drops cleanly
with patch.object(
opt_detector,
"_partition_segments",
side_effect=TimeoutError("Hardware thread timeout exceeded"),
), pytest.raises(TimeoutError):
with (
patch.object(
opt_detector,
"_partition_segments",
side_effect=TimeoutError("Hardware thread timeout exceeded"),
),
pytest.raises(TimeoutError),
):
opt_detector.splice("def foo(): pass", "")


Expand Down Expand Up @@ -2541,6 +2543,50 @@ def test_slice_by_braces_cpp_bounds_args_search_text_1836():
assert fn["args"] == 0, f"empty-arg cpp signature borrowed a call statement's args: {fn['args']}"


def test_slice_by_braces_cpp_args_counting_2012():
"""
#2012: Pattern 1 - Zero-undercount for `operator()` and out-of-class methods with non-whitelisted types.
Pattern 2 - Off-by-one overcount for constructors with member-initializer-lists.
Also verifies normal constructors and `operator()` call sites remain unaffected.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

code = """
// Pattern 1: operator() with real params
bool operator()(const Node *p_left, const Node *p_right) const { return true; }

// Pattern 1: out-of-class method with non-whitelisted type
void Translator::BuildBuffer(mlir::ModuleOp module) { return; }

// Pattern 2: zero-arg ctor with member-initializer list
VBufStorage_buffer_t::VBufStorage_buffer_t() : member1(x), member2(y) { }

// Normal: ctor with real params AND member-initializer list
FooBar::FooBar(int a, float b) : member1(a) { }

// Normal: operator() call-site (should not be extracted as a function definition)
void caller() {
bool res = my_obj(1, 2);
}
"""
detector = StructuralExtractor("cpp", LANGUAGE_DEFINITIONS)
result = detector.splice(code, "", raw_content=code)
funcs = {f["name"]: f for f in result["functions"]}

assert funcs["operator()"]["args"] == 2, f"Expected 2 args, got {funcs['operator()']['args']}"
assert funcs["Translator::BuildBuffer"]["args"] == 1, (
f"Expected 1 arg, got {funcs['Translator::BuildBuffer']['args']}"
)
assert funcs["VBufStorage_buffer_t::VBufStorage_buffer_t"]["args"] == 0, (
f"Expected 0 args, got {funcs['VBufStorage_buffer_t::VBufStorage_buffer_t']['args']}"
)
assert funcs["FooBar::FooBar"]["args"] == 2, f"Expected 2 args, got {funcs['FooBar::FooBar']['args']}"

# call-site should not be extracted, so caller() should be the only other func
assert "caller" in funcs
assert "my_obj" not in funcs


def test_slice_by_braces_c_preprocessor_conditional_signature_1837():
"""
#1837: a c/cpp signature duplicated across an #if/#else preprocessor
Expand Down Expand Up @@ -2588,33 +2634,15 @@ def test_slice_by_braces_c_preprocessor_conditional_edge_cases_1837_review():
# A single signature's OWN parameter list has a conditional param inside
# it -- the #else here is nested INSIDE the still-open "(", not preceded
# by a ")", so #1837's re-slice must not touch it.
interior_code = (
"void foo(int a\n"
"#ifdef X\n"
" , int b\n"
"#else\n"
" , int c\n"
"#endif\n"
") {\n"
" return;\n"
"}\n"
)
interior_code = "void foo(int a\n#ifdef X\n , int b\n#else\n , int c\n#endif\n) {\n return;\n}\n"
interior_result = detector.splice(interior_code, "", raw_content=interior_code)
interior_fn = next(f for f in interior_result["functions"] if f["name"] == "foo")
assert interior_fn["args"] == 3, f"interior conditional params: expected 3, got {interior_fn['args']}"

# An unrelated, later #ifdef/#elif block sits between an already-COMPLETE
# signature and the opening "{" -- must not be mistaken for a duplicate-
# signature branch fork and discard the real (already-resolved) signature.
later_code = (
"void foo(int a)\n"
"#ifdef SOME_FLAG\n"
"#elif OTHER_FLAG\n"
"#endif\n"
"{\n"
" return;\n"
"}\n"
)
later_code = "void foo(int a)\n#ifdef SOME_FLAG\n#elif OTHER_FLAG\n#endif\n{\n return;\n}\n"
later_result = detector.splice(later_code, "", raw_content=later_code)
later_fn = next(f for f in later_result["functions"] if f["name"] == "foo")
assert later_fn["args"] == 1, f"unrelated later conditional: expected 1, got {later_fn['args']}"
Expand Down Expand Up @@ -3328,9 +3356,10 @@ def test_detector_zig_single_quote_bound_prevents_cross_line_swallow():
wasi_cwd = next(s for s in satellites if s["name"] == "wasi_cwd")
assert wasi_cwd["loc"] <= 3, f"wasi_cwd's body must not swallow the rest of the file: loc={wasi_cwd['loc']}"


def test_detector_depth_aware_brace_idx():
"""
Proves that a brace inside a parameter list does not prematurely end the signature
Proves that a brace inside a parameter list does not prematurely end the signature
scan and cause argument count truncation for TypeScript.
"""
opt = StructuralExtractor("typescript", MOCK_LANG_DEFS)
Expand All @@ -3351,7 +3380,7 @@ def test_detector_depth_aware_brace_idx():

def test_detector_nested_parens_in_args():
"""
Proves that the top level argument counting correctly tracks braces and brackets
Proves that the top level argument counting correctly tracks braces and brackets
to prevent commas inside object literals from artificially inflating the argument count.
"""
opt = StructuralExtractor("typescript", MOCK_LANG_DEFS)
Expand All @@ -3360,14 +3389,16 @@ def test_detector_nested_parens_in_args():
assert opt._count_top_level_args("(options: { a: string, b: string }, cb: (x, y) => void)") == 2
assert opt._count_top_level_args("(arr: [1, 2, 3], nested: { x: [1, 2] })") == 2


def test_detector_nested_functions_in_signature_dropped():
"""
Proves that the signature_end logic correctly skips nested functions
Proves that the signature_end logic correctly skips nested functions
(like f: (a: A) => B inside flatMap's signature) rather than treating them as top-level.
"""
opt = StructuralExtractor("typescript", MOCK_LANG_DEFS)
opt.languages["typescript"]["rules"]["func_start"] = re.compile(
r"^[ \t]*([a-zA-Z_$][\w$]*)(?=[ \t\n]*:[ \t\n]*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?(?:\((?:[^()]|\([^()]*\))*\)[^=;{]*=>|[a-zA-Z_$][\w$]*[ \t\n]*=>))", re.M
r"^[ \t]*([a-zA-Z_$][\w$]*)(?=[ \t\n]*:[ \t\n]*(?:<(?:[^<>]|<[^<>]*>)*>\s*)?(?:\((?:[^()]|\([^()]*\))*\)[^=;{]*=>|[a-zA-Z_$][\w$]*[ \t\n]*=>))",
re.M,
)
opt.languages["typescript"]["rules"]["args"] = re.compile(r"")

Expand All @@ -3381,6 +3412,7 @@ def test_detector_nested_functions_in_signature_dropped():
satellites, _ = opt._slice_by_braces(code, "typescript", opt.languages["typescript"]["rules"], 0, {})
assert len(satellites) == 0, "Nested parameter function `f` should have been skipped by signature_end logic!"


def test_detector_m4_bracket_slicing_with_unbalanced_quotes():
"""
Issue #2204: m4 macro bodies are bounded by `(` and `)`, but they can contain
Expand All @@ -3390,8 +3422,8 @@ def test_detector_m4_bracket_slicing_with_unbalanced_quotes():
"""
from gitgalaxy.core.detector import StructuralExtractor
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS
code = '''

code = """
AC_DEFUN([AC_PROG_F77], [])

AC_DEFUN([MY_MACRO], [
Expand All @@ -3405,19 +3437,19 @@ def test_detector_m4_bracket_slicing_with_unbalanced_quotes():
[
echo "done"
])
'''
"""
extractor = StructuralExtractor("m4", LANGUAGE_DEFINITIONS)
rules = LANGUAGE_DEFINITIONS["m4"]["rules"]

sats, _ = extractor._slice_by_m4_brackets(code, rules, 0, {})

assert len(sats) == 3

# Check names
assert sats[0]["name"] == "AC_PROG_F77"
assert sats[1]["name"] == "MY_MACRO"
assert sats[2]["name"] == "ANOTHER"

# Check exact lengths
assert sats[0]["loc"] == 1
assert sats[1]["loc"] == 6
Expand Down
Loading
Loading