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
74 changes: 60 additions & 14 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2114,10 +2114,29 @@ def _find_slash_terminator(text: str, content_start: int) -> int:
# Macro Shields (Strictly Gated to C-Family)
if lang_id in ("c", "cpp", "objective-c", "cs", "swift"):
lines = safe_code.splitlines(keepends=True)
in_dead_branch = False
dead_nesting_depth = 0
# Per-open-#if branch policy. Each stack entry is a (policy, side)
# pair where policy is the #if condition's static truth value and
# side is which branch of that #if we are currently in:
# policy True (#if 1 / #if true) -> first branch alive, #else dead
# policy False (#if 0 / #if false) -> first branch dead, #else alive
# policy None (unknown, e.g. #if FOO / #ifdef FOO)
# -> scan BOTH branches. This is the
# #1720 fix: tree-sitter ground truth
# parses both, and implementations
# living in #else were being blanked.
# any() over the stack: an inner #if inside an outer dead region stays
# dead even if its own condition would flip it; #endif pops restore it.
branch_stack: list[tuple[Optional[bool], str]] = []
in_multiline_macro = False

def _branch_dead(entry: tuple[Optional[bool], str]) -> bool:
policy, side = entry
if policy is True:
return side == "else"
if policy is False:
return side == "first"
return False

for i in range(len(lines)):
line = lines[i]
stripped = line.lstrip()
Expand All @@ -2129,31 +2148,58 @@ def _find_slash_terminator(text: str, content_start: int) -> int:
continue

if stripped.startswith("#"):
if stripped.startswith("#if"):
if in_dead_branch:
dead_nesting_depth += 1
elif stripped.startswith(("#else", "#elif")):
if not in_dead_branch and dead_nesting_depth == 0:
in_dead_branch = True
elif stripped.startswith("#endif") and in_dead_branch:
if dead_nesting_depth > 0:
dead_nesting_depth -= 1
else:
in_dead_branch = False
if re.match(r"#if\b", stripped):
branch_stack.append((self._classify_preproc_condition(stripped[3:].strip()), "first"))
elif stripped.startswith("#ifdef ") or stripped.startswith("#ifndef "):
branch_stack.append((None, "first"))
elif re.match(r"#elif\b", stripped) and branch_stack:
# an #elif starts a fresh condition on the else side
branch_stack[-1] = (self._classify_preproc_condition(stripped[5:].strip()), "first")
elif stripped.startswith("#else") and branch_stack:
policy, _ = branch_stack[-1]
branch_stack[-1] = (policy, "else")
elif stripped.startswith("#endif") and branch_stack:
branch_stack.pop()

if stripped.startswith("#define") and stripped.rstrip(" \t\r\n").endswith("\\"):
in_multiline_macro = True

lines[i] = " " * (len(line) - 1) + "\n" if line.endswith("\n") else " " * len(line)
continue

if in_dead_branch:
if any(_branch_dead(e) for e in branch_stack):
lines[i] = " " * (len(line) - 1) + "\n" if line.endswith("\n") else " " * len(line)

safe_code = "".join(lines)

return safe_code

@staticmethod
def _classify_preproc_condition(condition: str) -> Optional[bool]:
"""
Returns the static truth value of a C-family preprocessor #if condition,
or None when it cannot be evaluated without a macro table.

Recognized constants (after stripping C comments and whitespace):
True -- "1", "true", "TRUE"
False -- "0", "false", "FALSE"
None -- everything else (macro names, defined(X), expressions)

Used by _build_brace_safe_stream's macro shield so #else branches that
genuinely contain implementations are scanned instead of blindly blanked
(#1720): only a statically-true #if (#if 1) makes its #else branch dead,
and only a statically-false #if (#if 0) makes its first branch dead.
"""
if condition is None:
return None
# strip C-style comments and surrounding whitespace
cond = re.sub(r"/\*.*?\*/|//.*$", "", condition, flags=re.S).strip()
if cond in ("1", "true", "TRUE", "True"):
return True
if cond in ("0", "false", "FALSE", "False"):
return False
return None

def _slice_by_braces(
self,
code: str,
Expand Down
103 changes: 97 additions & 6 deletions tests/core_engine/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,102 @@ def test_detector_c_macro_dead_branch_shield():
assert result["equations"]["high_risk_execution"] == 0, "Failed to scrub dead preprocessor branches!"


def test_detector_c_macro_else_branch_is_scanned_issue_1720():
"""
Regression test for #1720: the preprocessor shield used to assume the
first branch of every #if/#ifdef is the active one and blindly blanked
the #else branch, so real implementations living in #else were silently
dropped from extraction. With an unknown condition (e.g. #if FEATURE_FLAG)
the shield now scans BOTH branches; only statically-decidable conditions
(#if 0 / #if 1) prune a branch.
"""
opt_detector = StructuralExtractor("c", MOCK_LANG_DEFS)
code = (
"#if FEATURE_FLAG\n"
"int fastImplementation() {\n"
" return 1;\n"
"}\n"
"#else\n"
"int portableImplementation() {\n"
" return 2;\n"
"}\n"
"#endif\n"
)

result = opt_detector.splice(code, "")

names = [f["name"] for f in result["functions"]]
assert "fastImplementation" in names, "First branch of an unknown #if must still be scanned!"
assert "portableImplementation" in names, (
"#1720: implementation living in the #else branch was dropped from extraction!"
)


def test_detector_c_macro_static_truth_prunes_branches():
"""
Companion to the #1720 fix: statically-decidable #if conditions still
prune the dead branch. #if 0 => first branch dead, #else alive;
#if 1 => first branch alive, #else dead. Nested blocks must also honor
the outer branch's liveness (any() over the open-#if stack).
"""
opt_detector = StructuralExtractor("c", MOCK_LANG_DEFS)

code_if_zero = "#if 0\nint deadFast() {\n return 1;\n}\n#else\nint aliveFallback() {\n return 2;\n}\n#endif\n"
names_zero = [f["name"] for f in opt_detector.splice(code_if_zero, "")["functions"]]
assert "deadFast" not in names_zero, "#if 0 first branch must be pruned!"
assert "aliveFallback" in names_zero, "#if 0 #else branch must survive!"

code_if_one = "#if 1\nint aliveFast() {\n return 1;\n}\n#else\nint deadFallback() {\n return 2;\n}\n#endif\n"
names_one = [f["name"] for f in opt_detector.splice(code_if_one, "")["functions"]]
assert "aliveFast" in names_one, "#if 1 first branch must survive!"
assert "deadFallback" not in names_one, "#if 1 #else branch must be pruned!"


def test_detector_c_macro_no_space_boundaries_issue_1764():
"""
Regression test for a bug where `#if(1)` or `#elif(0)` (valid C preprocessor
syntax without a space) failed to push onto the branch stack because
`startswith("#if ")` was used. This led to premature `#endif` pops and
desynced branch nesting.
"""
opt_detector = StructuralExtractor("c", MOCK_LANG_DEFS)
code = (
"#if 0\n"
"int deadOne() { return 1; }\n"
"#if(1)\n"
"int deadTwo() { return 2; }\n"
"#endif\n"
"int deadThree() { return 3; }\n" # should stay dead -- still inside outer #if 0, before #else
"#else\n"
"int aliveOne() { return 4; }\n"
"#endif\n"
)

result = opt_detector.splice(code, "")
names = [f["name"] for f in result["functions"]]

assert "deadThree" not in names, "Premature pop caused dead code to be scanned as alive!"
assert "aliveOne" in names, "Valid #else branch was dropped due to stack desync!"

code_nested = (
"#if 0\n"
"int a() { return 1; }\n"
"#else\n"
"int b() { return 2; }\n"
"#if 1\n"
"int c() { return 3; }\n"
"#else\n"
"int d() { return 4; }\n"
"#endif\n"
"int e() { return 5; }\n"
"#endif\n"
)
names_nested = [f["name"] for f in opt_detector.splice(code_nested, "")["functions"]]
assert set(names_nested) == {"b", "c", "e"}, (
"Nested #if liveness was not honored: expected only b, c, e, got %r" % names_nested
)


def test_detector_nested_function_is_counted_as_own_node_braces():
"""
#1041: the brace-slicing guard used to skip any match whose start fell
Expand Down Expand Up @@ -2255,12 +2351,7 @@ def test_detector_ts_param_function_type_annotation_not_counted_as_function():

# The object-literal arrow property must still be counted: its line
# is preceded by `{`/`,`, never `(`.
object_code = (
"export const Either = {\n"
" URI,\n"
" ap: (fab, fa) => ({ fab, fa }),\n"
"}\n"
)
object_code = "export const Either = {\n URI,\n ap: (fab, fa) => ({ fab, fa }),\n}\n"
satellites2, _ = detector._slice_by_braces(object_code, lang, rules, 0, {})
names2 = [s["name"] for s in satellites2]
assert "ap" in names2, f"[{lang}] object-literal arrow property dropped: {names2}"
Expand Down
Loading
Loading