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
67 changes: 67 additions & 0 deletions gitgalaxy/core/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2555,6 +2555,73 @@ def _dart_scan_terminator(
end_idx = semi_after_arrow + 1
else:
continue
# #1629: typescript/javascript idiomatically use brace-less,
# expression-bodied arrow functions (`const swap = (x) => x + 1`,
# curried FP chains with no `{` anywhere in the definition --
# fp-ts's primary export shape). The generic brace-only fallback
# below drops every one of them; at least 88 of the corpus's 159
# func recall misses are this shape. Mirror #1266's scala
# approach: when no `{` shows up in the window, find the first
# un-nested `=>` after the signature and bound the expression
# body by the next func_start match (TS/JS arrow bodies have no
# reliable `;` terminator either, so the next-match bound is the
# closer analogy than csharp's trailing-semicolon scan).
elif lang_id in ("typescript", "javascript"):
# A brace-less assignment match that is itself in expression
# position (preceding non-whitespace char is `>`/`)`/`,`) is a
# return type, not a name -- `=> M = (M) => ...` in fp-ts's
# foldMap reports a phantom `M`. Only declaration-position
# matches (`const swap = ...`, line-start object members) are
# real functions.
if start_idx > 0:
p = start_idx - 1
while p >= 0 and safe_code[p] in " \t":
p -= 1
if p >= 0 and safe_code[p] in ">),":
continue
brace_idx = safe_code.find(opener, start_idx, search_limit)
if brace_idx != -1:
end_idx = self._find_balanced_end(safe_code, brace_idx, opener, closer)
else:
# Only assignment-shaped matches (`const foo = ... => ...`
# or `foo: Type = ... => ...`) are real runtime functions.
# An interface/type member's function-type annotation
# (`readonly alt: <A>(...) => ...`, no `=` anywhere before
# its `=>`) is pure type-level syntax -- #1631's remaining
# phantom shape -- so require an un-nested `=` before the
# first `=>`.
depth_paren = depth_bracket = depth_angle = 0
pos = match.end()
saw_assignment = False
arrow_idx = -1
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 ch == "<":
depth_angle += 1
elif ch == ">":
depth_angle = max(0, depth_angle - 1)
elif depth_paren == 0 and depth_bracket == 0 and depth_angle == 0:
if ch == "=" and pos + 1 < search_limit and safe_code[pos + 1] == ">":
if saw_assignment:
arrow_idx = pos
break
break # the `=>` belongs to an annotation, not an assignment
if ch == "=":
saw_assignment = True
elif ch in ";{":
break # the statement ended without an arrow -- not a function body
pos += 1
if arrow_idx == -1:
continue
end_idx = next_match_start
else:
brace_idx = safe_code.find(opener, start_idx, search_limit)
if brace_idx == -1:
Expand Down
45 changes: 34 additions & 11 deletions tests/core_engine/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2118,23 +2118,46 @@ def test_detector_csharp_lambda_default_parameter_arrow_not_mistaken_for_body():
assert "f(1)" in code[: code.index(";") + 1]


def test_detector_csharp_expression_body_fallback_gated_to_csharp_only():
def test_detector_ts_js_braceless_arrow_capture():
"""
#1629: typescript/javascript brace-less arrow functions
(`const double = (x) => x * 2;`) are real functions and must be
captured by _slice_by_braces, not dropped by the generic brace-only
fallback. This replaced the old expectation (tested pre-#1629 in
test_detector_csharp_expression_body_fallback_gated_to_csharp_only)
that no non-csharp language may use an arrow fallback -- TS/JS now
have their own next-match-bounded version of that handling, because
brace-less expression bodies are their dominant export shape
(88 of 159 corpus recall misses were this shape).
"""
The `=>`-then-`;` fallback in _slice_by_braces is explicitly gated to
`lang_id == "csharp"` -- proves it doesn't change behavior for other
Mode-B (brace-slicing) languages that also use `=>` for lambdas
(e.g. javascript/typescript arrow functions assigned to a const,
which are not real func_start matches and must still produce zero
satellites, not a hallucinated one via the new fallback).
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

for lang in ("typescript", "javascript"):
detector = StructuralExtractor(lang, LANGUAGE_DEFINITIONS)
rules = LANGUAGE_DEFINITIONS[lang]["rules"]
code = "const double = (x) => x * 2;\n"

satellites, _ = detector._slice_by_braces(code, lang, rules, 0, {})
names = [s["name"] for s in satellites]
assert names == ["double"], f"[{lang}] brace-less arrow not captured: {names}"


def test_detector_ts_js_braceless_arrow_capture_gated_from_other_mode_b_languages():
"""
The #1629 brace-less arrow capture is deliberately gated to
`lang_id in ("typescript", "javascript")` -- other Mode-B languages
keep the generic brace-only fallback. Proves a Mode-B language without
the gate still drops a brace-less arrow-shaped const rather than
hallucinating a function from it.
"""
from gitgalaxy.standards.language_standards import LANGUAGE_DEFINITIONS

js_detector = StructuralExtractor("javascript", LANGUAGE_DEFINITIONS)
js_rules = LANGUAGE_DEFINITIONS["javascript"]["rules"]
php_detector = StructuralExtractor("php", LANGUAGE_DEFINITIONS)
php_rules = LANGUAGE_DEFINITIONS["php"]["rules"]
code = "const double = (x) => x * 2;\n"

satellites, _ = js_detector._slice_by_braces(code, "javascript", js_rules, 0, {})
assert satellites == [], "the csharp-only arrow fallback must not fire for other languages"
satellites, _ = php_detector._slice_by_braces(code, "php", php_rules, 0, {})
assert satellites == [], "the ts/js brace-less arrow capture must not fire for other Mode-B languages"


# ==============================================================================
Expand Down
Loading
Loading