Skip to content

fix(typescript/javascript): stop dropping bodyless overload signatures - #2284

Merged
squid-protocol merged 4 commits into
mainfrom
fix/typescript-2278-overload-collapse
Aug 26, 2026
Merged

fix(typescript/javascript): stop dropping bodyless overload signatures#2284
squid-protocol merged 4 commits into
mainfrom
fix/typescript-2278-overload-collapse

Conversation

@squid-protocol

Copy link
Copy Markdown
Owner

Summary

Fixes #2278. TypeScript allows multiple bodyless function-overload signatures sharing a name, followed by one real body-bearing implementation. func_start's regex correctly matched every overload (confirmed via struct_func_start's raw signal count), but detector.py's terminator-scanning loop (the #1629 brace-less/expression-bodied arrow scanner, gated to typescript/javascript only) silently dropped any match that never found a real terminator in its search window -- exactly the shape of a bodyless overload signature, whose own return-type expression can contain literal {}/=> tokens that aren't a real body or terminator at all.

Fix

Three changes, all scoped to the typescript/javascript branch of _slice_by_braces:

  1. When the scan reaches a {, check whether it's preceded (skipping whitespace) by a type-level operator (&|:?=) -- if so it's a type literal's own brace (e.g. fp-ts's : {}) & empty-object-type intersection members), not a real body opener; skip past its matching close and keep scanning.
  2. When no terminator is ever found, give the match a real function_data row bounded by the next match -- but only when a genuine parameter list immediately follows the name (optionally after <generics>). This guard is required: an earlier draft without it turned real Flow type-level fields (callback: A => T,, const task: Task = ...) into phantom function matches in react/ReactFlightServer.js -- caught and fixed during this PR's own verification.
  3. Loosen the min-2-lines-of-body filter to also allow single-line bodies for typescript/javascript, since a recovered overload's body (bounded by the next match) can be very short.

Verification

  • fp-ts/pipeable.ts: struct_func_start/function_count now match exactly at 224/224 (was 224/217) -- all 8 pipeable overloads recovered.
  • Full corpus diff (typescript): missing-function count 131 → 123 (this fix's own isolated contribution).
  • Full corpus diff (javascript): zero missing, zero new false-positive/phantom matches -- every new "extra" entry traced to a real function in an already Flow-broken file tree-sitter can't parse (same class as the pre-existing 198-entry baseline), not a regression. Confirmed via direct source reading of every new/changed entry, not assumed from the diff alone.
  • pytest on typescript/javascript/detector suites: 463 passed. Cross-language sanity spot-check (java/csharp/python/rust, since this touches a nominally shared file even though the changed branch itself is language-gated): 262 passed.
  • tests/ruff_audit.py --ci / tests/mypy_audit.py --ci: no new findings beyond baseline.
  • ReDoS pathological-input timing probe: no slowdown.

This is the highest-risk of a batch of 4 TypeScript fixes (the only one touching shared detector.py rather than an isolated per-language regex). A first draft was dispatched to a Gemini/agy subagent per this repo's tree-sitter-accuracy-sweep-style workflow; independent verification in the main session caught a real precision regression that draft's own narrow testing missed, root-caused it, and landed a scoped, evidenced fix directly.

Test plan

  • struct_func_start/function_count before/after (pipeable.ts)
  • Full-corpus tri-comparison diff, both typescript and javascript (before/after)
  • Every new "extra" entry manually traced to real source, not assumed
  • test_typescript.py / test_typescript_strict.py / test_javascript.py / test_javascript_strict.py / test_detector.py
  • Cross-language sanity spot-check (java/csharp/python/rust)
  • ruff/mypy baseline audits
  • ReDoS sanity check

TypeScript allows multiple bodyless function-overload SIGNATURES
sharing a name, followed by one real body-bearing implementation.
func_start's regex correctly matched every overload (confirmed via
struct_func_start's raw signal count), but detector.py's terminator-
scanning loop (the #1629 brace-less/expression-bodied arrow scanner,
gated to typescript/javascript only) silently dropped any match that
never found a real terminator in its search window -- exactly the
shape of a bodyless overload signature, whose own return-type
expression can contain literal `{}`/`=>` tokens that aren't a real
body or terminator at all (fp-ts's pipeable.ts: `: {}) &` empty-
object-type intersection members).

Three changes, all scoped to the typescript/javascript branch:
1. When the scan reaches a `{`, check whether it's preceded (skipping
   whitespace) by a type-level operator (`&|:?=`) -- if so, it's a
   type literal's own brace, not a real body opener; skip past its
   matching close and keep scanning for the real terminator.
2. When "no terminator was ever found" (previously: drop the match
   entirely), give it a real function_data row bounded by the next
   match instead -- but ONLY when a genuine parameter list (`(`,
   optionally after `<generics>`) immediately follows the name. This
   guard is required: without it, a parameter's own function-type
   annotation or a type-annotated variable declaration (Flow's
   `callback: A => T,`, `const task: Task = ...`) also lacks a
   findable terminator and would otherwise be misattributed a bogus
   function_data row instead of correctly finding nothing -- confirmed
   via a real regression in react/ReactFlightServer.js during
   verification of an earlier draft of this fix (Component/Task/
   callback/getAsyncIterator, all real Flow type-level fields, briefly
   became phantom matches without this guard).
3. Loosen the min-2-lines-of-body filter to also allow single-line
   bodies for typescript/javascript, since a recovered bodyless
   overload's "body" (bounded by the next match) can be very short.

Verified via the full 23-file typescript + javascript corpus diff
against tree-sitter:
- fp-ts/pipeable.ts: struct_func_start and function_count now match
  exactly (224/224, was 224/217) -- all 8 `pipeable` overloads recovered.
- typescript missing-function count: 131 -> 123 (this fix's own
  isolated contribution; the sibling #2276/#2277/#2279 PRs cover the
  rest of the corpus's gaps).
- javascript: zero missing, zero new false-positive/phantom matches --
  every new "extra" entry (vs. tree-sitter) traced to a real function
  in an already Flow-broken file tree-sitter simply can't parse (same
  documented class as the pre-existing 198-entry baseline), not a
  regression.
- Full typescript/javascript/detector pytest suite (463 tests) plus a
  cross-language sanity spot-check (java/csharp/python/rust, 262
  tests) all pass.
- ReDoS pathological-input timing probe -- no slowdown.
- ruff/mypy -- no new findings beyond baseline.

Fixes #2278.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🐦‍⬛ Muninn Security Scan

✅ No security issues found.

🐦‍⬛ Powered by Muninn · Skald Lab

Two follow-up fixes to the same-day #2278 diff, found during its own
golden-master/tree-sitter-accuracy verification pass (not shipped in
the first commit):

1. Reverted the second `break` -> `pos += 2; continue` change (the
   "belongs to an annotation, not an assignment" case) back to its
   original `break`. Keeping it let a curried arrow function's
   anonymous returned closure (fp-ts/TaskEither.ts's `tryCatch =
   <E, A>(...) => async () => { ... }`) scan past its own `=>` and
   reach the closure's REAL closing brace, misattributing "async" (the
   modifier keyword, mistaken for the method's own name since nothing
   else follows it here) a genuine function_data row with a real body
   -- a phantom this exact regex+detector combination already produced
   on main too, just previously always dropped via the "no terminator
   found" fallback this same PR's first commit only just started
   giving matches a home. This specific `pos+=2` variant wasn't
   actually needed by any of pipeable's/createInstance's overloads
   (confirmed: both still fully recovered after reverting it), so
   reverting is a pure fix with no lost coverage. The first `pos += 2`
   change (the `outer_container in (paren,angle,bracket)` case) is
   unaffected and stays as shipped.
2. Added a defense-in-depth guard to the "give it a home" fallback:
   never accept a captured name that's one of TS/JS's own reserved
   modifier keywords (async/static/public/private/protected/abstract/
   readonly/override/get/set) immediately followed by a real parameter
   list -- none of these can ever legitimately be a real function's
   own bare name in this position, only a modifier the regex mistook
   for one.
3. Golden masters + the typescript tree-sitter-accuracy baseline
   re-blessed against the corrected code (the first bless, run before
   this fix, had baked the "async" phantom in).

Verified: fp-ts/pipeable.ts still fully recovered (struct_func_start/
function_count 224/224). tree_sitter_accuracy_audit --lang typescript:
extra_functions back to the 12-baseline (was transiently 13 with the
async phantom). --all --ci: all 31 languages OK. Full pytest (463),
cross-language spot-check (262), ReDoS probe, ruff/mypy/audit_check:
all clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@squid-protocol
squid-protocol enabled auto-merge (squash) August 26, 2026 20:07
squid-protocol and others added 2 commits August 26, 2026 16:29
Resolves trivial summary-table conflict with #2276/#2277/#2279 (now
on main) -- detector.py itself merged cleanly, no other PR touches it.
Golden masters/baseline taken from origin/main wholesale, will be
regenerated fresh against the merged code before pushing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@squid-protocol
squid-protocol merged commit 07ef873 into main Aug 26, 2026
27 of 28 checks passed
@squid-protocol
squid-protocol deleted the fix/typescript-2278-overload-collapse branch August 26, 2026 20:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TypeScript] Same-name overload signatures collapse to one — detector.py drops 6 of 7 bodyless pipeable overloads

1 participant