Skip to content

fix(erlang): give same-name different-arity functions separate arity-qualified nodes (#1610) - #1615

Merged
colbymchenry merged 2 commits into
mainfrom
fix/1610-erlang-arity-nodes
Aug 26, 2026
Merged

fix(erlang): give same-name different-arity functions separate arity-qualified nodes (#1610)#1615
colbymchenry merged 2 commits into
mainfrom
fix/1610-erlang-arity-nodes

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Fixes #1610. Also fixes #1358 (the <<binary>> arity miscount in behaviour dispatch, reported separately and hit by the same code path).

Problem

Arity is part of an Erlang function's identity — f/1 and f/2 are unrelated top-level definitions — but the extractor merged consecutive same-name fun_decls regardless of arity. Reproduced on main exactly as reported:

  • adjacent f(X) -> …. f(X, Y) -> ….one node spanning both, with the first definition's signature;
  • interleaved f/1, g/0, f/2 → two nodes with identical qualified_name;
  • cowboy_req's header(Name, Req) -> header(Name, Req, undefined). → a self-loop header → header, with the -spec for /3 swallowed by the merged span;
  • -export([f/1]) marked every arity exported.

Fix

  • One node per (name, arity). Clauses of the same name+arity still merge (that part of the old behavior was correct); a different arity starts a new node. qualifiedName carries the canonical spelling — mod::f/1 — while the node name stays bare so search and bare-name matching are unchanged.
  • -export and -spec are per-arity. -export([f/1]) exports exactly f/1; a spec sitting between two arities attaches to the arity its signature names.
  • Refs carry the call-site arity wherever it's statically known: local f/1, remote mod::f/2, fun f/1 / fun mod:f/1 values, gen_server dispatch (handle_call/3, handle_cast/2), and spawn/apply MFA lists (spawn_link(?MODULE, work, [A, B])work/2).
  • The matcher resolves only to the named arity — same file first (a local call targets its own module) — and when no definition of that arity exists it resolves to nothing rather than a sibling arity: silent beats wrong. An arity-less dynamic-MFA ref resolves only when the module defines exactly one arity of that name.
  • Behaviour dispatch selects the implementer node of the site's arity, and the arity counter now skips <<1,2,3>> binary-literal commas per its own docstring (Erlang: erlangArityAt doesn't skip <<binary>> literals (contrary to its docstring), so binary-arg dispatch sites get an inflated arity and the behaviour-callback edge is dropped or mis-linked — verified #1358) — Mod:decode(<<1,2,3>>, Opts) counts 2, not 4.
  • codegraph_explore / codegraph_node accept the written mod:fn/3 spelling against the new arity-qualified names (the issue's measured cowboy_stream_h:request_process/3 shape).

Validation

Minimal fixtures (all three reported shapes) now index as gap::f/1 + gap::f/2, distinct inter::f/1/inter::f/2, and a real deleg::header/2 → deleg::header/3 edge with no self-loop.

Cowboy (fresh --depth 1 clone, this build vs unmodified main build):

main this PR
nodes 3,668 3,748 (+80 — the arity splits; no explosion)
erlang function nodes 2,850 2,930
behaviour dispatch edges 38 44
cowboy_req::header one node, span 420–425, /3's spec lost header/2 (420–421, its own spec) + header/3 (424–425, its spec)
delegation self-loop header/2 → header/3

calls edges drop 6,059 → 5,656: a sample of every removed pair shows the false-positive class the issue predicted — out-of-repo/BIF calls (length/1, error/1, quicer:*) that previously name-matched onto unrelated same-named in-repo functions now stay unresolved.

Tests: new arity coverage in extraction + a new arity-resolution integration suite + a #1358 binary-literal behaviour test; updated existing Erlang expectations to the arity-carrying spellings. Full suite: 3,018 passed, 0 failed.

No migration: an existing Erlang index picks the new shape up on its next re-index (codegraph sync / re-init).

Erlang is wasm-only (not in the native kernel), so there is no kernel-parity surface.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK

…qualified nodes (#1610)

Arity is part of an Erlang function's identity: f/1 and f/2 are unrelated
definitions. The extractor merged consecutive same-name fun_decls regardless
of arity, so both landed in one node (or two nodes with colliding qualified
names when interleaved), the wrong -spec/signature/span was attributed, the
everyday f/N -> f/N+1 delegation became a self-loop, and -export([f/1])
marked every arity exported.

- Each (name, arity) group now gets its own node; qualifiedName carries the
  canonical spelling (mod::f/1). Node names stay bare for search.
- -export and preceding -spec attribution are per-arity.
- Call/fun refs carry the call-site arity (f/1, mod::f/2, fun mod:f/1,
  gen_server handle_call/3 + handle_cast/2, static MFA lists); the matcher
  resolves them only to a def of exactly that arity, same-file first, and
  refuses to guess a sibling arity (silent beats wrong). Arity-less dynamic
  MFA refs resolve only when a single arity exists.
- The behaviour dispatch synthesizer selects the implementer node of the
  site's arity, and erlangArityAt now skips <<binary>> literal commas per its
  own docstring (#1358) - dispatch sites passing binaries no longer miscount.
- Explore/node symbol lookups accept the written mod:fn/3 spelling against
  the new arity-qualified names.

Cowboy: nodes 3668 -> 3748 (+80 arity splits, no explosion), behaviour
dispatch edges 38 -> 44, cowboy_req header/2 and header/3 split with the
right specs and a real /2 -> /3 delegation edge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LxZj6W6Y1SHXwvpT3uwJpK
@danusha2345

Copy link
Copy Markdown
Contributor

I reviewed and reproduced two remaining correctness gaps in this patch:

  1. A bare Erlang f/N with no same-module definition still falls back to a project-wide candidate. In a minimal repro, the BIF call length/1 incorrectly resolved to an unrelated in-project other::length/1.
  2. -import(Module, [f/N]) is not represented in extractImportMappings, so when multiple modules define the same f/N, path proximity can select the wrong module. My repro with -import(imported, [pick/1]) resolved to a nearby wrong::pick/1.

The node/edge shape also needs an EXTRACTION_VERSION bump; otherwise existing indexes are not marked stale and unchanged Erlang files are skipped by codegraph sync.

I pushed a focused follow-up commit that fixes all three points and adds permanent end-to-end regressions:

Validation after the final change: npm run build:kernel, npm run build, and the full suite — 190 test files, 3,190 passed, 9 skipped. The local index was then force-rebuilt and reports extraction version 26 with zero pending changes/refs.

@Dshuishui

Dshuishui commented Aug 26, 2026

Copy link
Copy Markdown

Verified this branch end-to-end — the three shapes are fixed, and the calls edges that disappear on cowboy are all genuine false positives (quicer:* is out of repo, error/1 and length/1 are BIFs).

One small thing: arity is counted with namedChildCount, and tree-sitter counts comment as a named child, so a comment at the top level of an argument list inflates it:

f(A, % why
  B)        % read as f/3 — the edge is silently dropped

Only rebar3 is affected in the projects I checked (2 real edges). I put a patch and a regression test for it in #1619, targeting this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants