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
38 changes: 36 additions & 2 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5181,8 +5181,12 @@ def _slice_by_keywords(
if stack_depth == 0:
if net_change > 0:
satellite_name = self._extract_semantic_name(safe_line, lang_key)
# #2758: index of the line the name was recovered FROM, when
# that is not the opener line itself.
decl_line_idx = None
if satellite_name == "Anonymous_Block":
for past_safe in reversed(past_safe_lines):
for past_idx in range(len(past_safe_lines) - 1, -1, -1):
past_safe = past_safe_lines[past_idx]
if past_safe.strip():
# #2438: skip a self-contained one-liner
# declaration -- its name belongs to its own node.
Expand All @@ -5195,13 +5199,43 @@ def _slice_by_keywords(
fallback = self._extract_semantic_name(past_safe, lang_key)
if fallback != "Anonymous_Block":
satellite_name = fallback
decl_line_idx = past_idx
break
current_satellite = [orig_line]
is_current_satellite_class = (
bool(class_opener_pattern.search(safe_line)) if class_opener_pattern else False
)
stack_depth += net_change
sat_start_line = current_line_offset + 1
# #2758: anchor the node where it is DECLARED, not where its
# body happens to open. Mode D's openers include the bare
# brace, so a K&R-style declaration (`f_create_role()` on one
# line, `{` on the next -- how curl's own initscript.sh is
# written throughout) reported start_line on the brace. The
# lookback above already had to walk back to find the name;
# it recovered the name and left the position behind.
#
# This moves only the ANCHOR, never the satellite's text:
# `current_satellite` still begins at the opener, so `loc`,
# `args` and the per-function hit_vector are untouched, and
# the declaration line's own keywords stay where they are.
# Safe here specifically because Mode D computes
# `sat_end_line` independently (see below) rather than as
# `start_line + loc - 1` the way the brace/label modes do --
# so the span EXTENDS backwards over the declaration instead
# of sliding one line earlier.
sat_start_line = (
offset + decl_line_idx + 1 if decl_line_idx is not None else current_line_offset + 1
)
# `sat_start_char` deliberately does NOT follow it. The char
# pair is not metadata: `_calculate_block_metrics` slices the
# file's `spatial_map` with `bisect(indices, start_char)`, so
# moving it re-attributes every signal between the
# declaration and the opener into this function. Measured
# when this fix first moved both: `versioned_copy` in
# curl/initscript.sh went from 0 to 36 control-flow branches.
# The line anchor is what the recorders and SARIF report and
# what `spatial_correlation` scopes on; the char pair stays
# the block's own coordinates.
sat_start_char = current_char_offset
else:
global_dust.append(orig_line)
Expand Down
49 changes: 49 additions & 0 deletions tests/core_engine/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -4103,3 +4103,52 @@ def test_keyword_buckets_are_neither_duplicates_nor_orphans():
eq = docker.splice(code, "")["equations"]
assert not eq.get("duplicate_logic"), "identical RUN instructions are not duplicated functions"
assert not eq.get("orphaned_logic"), "a RUN bucket is a keyword, and a keyword cannot be orphaned"


def test_mode_d_anchors_a_function_at_its_declaration_not_its_opening_brace():
"""#2758: Mode D's openers include the bare `{`, so a K&R-style shell
function -- declaration on one line, brace on the next, which is how curl's
own `initscript.sh` is written throughout -- reported `start_line` on the
BRACE. The lookback at `_slice_by_keywords` already had to walk back a line
to find the name; it recovered the name and left the position behind.

`start_line` is not diagnostic-only: `sarif_recorder.py` uses it as the
`locations` line of every SARIF result, and `spatial_correlation.py` builds
each function's dampener window as `(start_line, end_line + 1)`, so a window
starting one line late can miss a signal sitting on the declaration itself.

Only the ANCHOR moves. The satellite's text still begins at the opener, so
`loc` is unchanged -- asserted below, because that is what keeps the span
extending backwards over the declaration rather than sliding a line earlier.
Mode D can do that safely only because it computes `sat_end_line`
independently, unlike the brace/label modes' `start_line + loc - 1`.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

shell = StructuralExtractor("shell", LANGUAGE_DEFINITIONS)
code = 'f_create_role()\n{\n : "$1"\n}\n\nf_inline() {\n : "$1"\n}\n'
by_name = {f["name"]: f for f in shell.splice(code, "").get("functions", [])}

kr = by_name["f_create_role"]
assert kr["start_line"] == 1, "the K&R declaration is on line 1, not the brace on line 2"
assert kr["end_line"] == 4, "the end must not move with the start -- the span extends, not slides"
assert kr["loc"] == 3, "the satellite's own text is untouched; only the anchor moved"

same_line = by_name["f_inline"]
assert same_line["start_line"] == 6, "the same-line brace form was already correct and must not move"
assert same_line["end_line"] == 8
assert same_line["loc"] == 3


def test_mode_d_anchor_does_not_move_when_the_name_is_on_the_opener():
"""#2758's guard: the anchor only follows a name the lookback had to recover
from an earlier line. When `_extract_semantic_name` finds the name on the
opener line itself -- the common case in every Mode D language -- nothing
about the position changes.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

shell = StructuralExtractor("shell", LANGUAGE_DEFINITIONS)
code = 'function f_named {\n : "$1"\n}\n'
funcs = [f for f in shell.splice(code, "").get("functions", []) if f["name"] == "f_named"]
assert funcs and funcs[0]["start_line"] == 1
Loading
Loading