From 6511722250546eac730f972378c006e046aa9193 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Fri, 3 Jul 2026 14:32:20 -0500 Subject: [PATCH] feat(extraction): add Erlang language support (.erl/.hrl) (#635, #648) (#1165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vendored WhatsApp/tree-sitter-erlang 0.19 (the ELP grammar, ABI 14) with an Erlang-shaped extractor: multi-clause/multi-arity functions merged into one symbol, -spec signatures, records with fields, -type/-opaque aliases, -define macros, -include/-include_lib file edges, and -export-driven visibility. Modules wrap in a namespace so remote mod:fn(...) calls resolve through the existing qualified-name matcher as mod::fn with zero resolver changes. -behaviour declarations link to the behaviour module — gated to namespace targets only (bare-name fallthrough linked -behaviour(supervisor) to an unrelated macro constant on emqx). OTP indirection with static targets is followed: spawn/apply/proc_lib/timer/rpc MFA-argument callees, and gen_server:call/cast(?MODULE | ?SERVER) to the module's own handle_call/handle_cast. Var-module dispatch and message sends stay deliberately unlinked. codegraph_explore also normalizes Erlang-native query spelling (mod:fn/3, init/2) so named symbols resolve as typed. Benchmarked on cowboy (189 files), ejabberd (414), emqx (2,447): extraction PASS on all three; with-codegraph arms reached 2/2/0 file Reads vs 10/5+/19 without, fastest on the largest repo. Co-authored-by: Claude Fable 5 --- .claude/skills/agent-eval/corpus.json | 25 +- CHANGELOG.md | 1 + README.md | 3 +- __tests__/explore-output-budget.test.ts | 26 +- __tests__/extraction.test.ts | 384 ++++++++++++++++++++ __tests__/resolution.test.ts | 61 ++++ src/extraction/grammars.ts | 8 +- src/extraction/languages/erlang.ts | 276 ++++++++++++++ src/extraction/languages/index.ts | 2 + src/extraction/tree-sitter-helpers.ts | 1 + src/extraction/tree-sitter.ts | 209 +++++++++++ src/extraction/wasm/tree-sitter-erlang.wasm | Bin 0 -> 421639 bytes src/mcp/tools.ts | 41 ++- src/resolution/name-matcher.ts | 20 + src/types.ts | 1 + 15 files changed, 1050 insertions(+), 8 deletions(-) create mode 100644 src/extraction/languages/erlang.ts create mode 100755 src/extraction/wasm/tree-sitter-erlang.wasm diff --git a/.claude/skills/agent-eval/corpus.json b/.claude/skills/agent-eval/corpus.json index ce5ae3b..93e9f24 100644 --- a/.claude/skills/agent-eval/corpus.json +++ b/.claude/skills/agent-eval/corpus.json @@ -469,5 +469,28 @@ "files": "~145", "question": "When a job finishes video encoding, how does staxrip decide which muxer runs and how does the muxer command line get built and executed? Trace from job processing to the mkvmerge invocation." } + ], + "Erlang": [ + { + "name": "cowboy", + "repo": "https://github.com/ninenines/cowboy", + "size": "Small", + "files": "~190", + "question": "How does an incoming HTTP request travel from cowboy's connection process to a user-defined handler's init/2 callback? Trace the path through the stream handler and middleware chain." + }, + { + "name": "ejabberd", + "repo": "https://github.com/processone/ejabberd", + "size": "Medium", + "files": "~410", + "question": "When a client sends a chat message, how does the stanza get from the receiving C2S process to the recipient's session on the same node? Trace the path through the router and session manager." + }, + { + "name": "emqx", + "repo": "https://github.com/emqx/emqx", + "size": "Large", + "files": "~2450", + "question": "How does a PUBLISH packet from an MQTT client reach the sessions of matching subscribers? Trace the flow from the connection/channel layer through the broker's routing to session delivery." + } ] -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index faf97c8..144ec92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### New Features +- CodeGraph now indexes **Erlang** (`.erl`, `.hrl`) — functions, with clauses and arities of the same name grouped as one symbol spanning all of them, plus records with their fields, `-type`/`-opaque` aliases, `-define` macros, and `-spec` signatures attached to every function. Cross-module `mod:fn(...)` calls resolve to the target module's function, `fun name/arity` values are captured as references (so callback registrations like `lists:foreach(fun submit/1, ...)` link up), `-include`/`-include_lib` connect to the header files they pull in, `-behaviour` declarations link a callback module to its behaviour (and only ever to a module — a same-named macro or function elsewhere in the repo is never mistaken for one), and `-export` lists (plus `-compile(export_all)`) drive each function's public/private flag. OTP's indirection idioms are followed where the target is static: `spawn`/`apply`/`proc_lib`/`timer`/`rpc` calls that name their target as `(Module, Function, Args)` arguments produce call edges, and a module's public API wrappers connect to its own `handle_call`/`handle_cast` when `gen_server:call`/`cast` targets `?MODULE` (including the `-define(SERVER, ?MODULE)` idiom). Truly dynamic dispatch (`Mod:handle(...)`, message sends, var-module spawns) is deliberately left unlinked rather than guessed. `codegraph_explore` also understands Erlang-native symbol spelling in queries — `mod:fn/3` and `init/2` find the symbols they name. (#635, #648) - CodeGraph now indexes **Visual Basic .NET** (`.vb`) — classes, Modules, interfaces, structures, enums, properties, events, `MustOverride` abstract members, and `Declare` P/Invoke signatures, with `Inherits`/`Implements` hierarchy edges, call edges (resolved through VB's ambiguous call-vs-index parentheses), and `New`/`As New` instantiation links. Real-world VB styles parse cleanly: WinForms designer files, interpolated and multi-line strings, XML literals (embedded `<%= %>` expressions included), single-line and multi-line LINQ queries, multi-line lambdas, `Handles`/`WithEvents` event wiring, Custom Events, date literals, classic type-character identifiers (`i%`, `name$`), and non-English (Unicode) identifiers. (#648, #639, #170) - CodeGraph now indexes **COBOL** (`.cbl`, `.cob`, `.cpy`) — programs, sections and paragraphs with `PERFORM`/`GO TO` call edges, `CALL` cross-program calls, `COPY` copybook imports (standalone copybooks included), and DATA DIVISION records with 88-level condition names, in both fixed and free source format. Impact queries work on data items: every `MOVE`/`ADD`/`COMPUTE`/`SUBTRACT` write-site links back to the field it changes, so "what touches this copybook field" answers across programs. CICS flows connect too: `EXEC CICS LINK`/`XCTL` program targets, `EXEC SQL INCLUDE` copybooks, and pseudo-conversational `RETURN TRANSID(...)` hops resolve to the program owning the transaction id. (#590, #648) - CodeGraph now indexes **CFML** (`.cfc`, `.cfm`, `.cfs`) — both the classic tag-based style (``/``) and modern bare-script `component { ... }` syntax, including `extends`/`implements`, embedded `` blocks (at any nesting depth, including inside ``/``/``), call edges, and calls embedded in `#hash#` expressions inside `` SQL bodies. Files saved with a UTF-8 byte-order mark and tags with unquoted attribute values — both common in long-lived CFML codebases — are handled too. Thanks @ghedwards. (#1118) diff --git a/README.md b/README.md index a9522c8..3ae0732 100644 --- a/README.md +++ b/README.md @@ -244,7 +244,7 @@ The reliable, universal payoff is **surgical context and speed**: CodeGraph coll | **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 | | **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes | | **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config | -| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, CFML, COBOL, Svelte, Vue, Astro, Liquid, Pascal/Delphi | +| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Erlang, CFML, COBOL, Svelte, Vue, Astro, Liquid, Pascal/Delphi | | **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks | | **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules | | **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only | @@ -718,6 +718,7 @@ is written): | CFML | `.cfc`, `.cfm`, `.cfs` | Full support (tag-based ``/`` and bare-script `component { ... }` styles, `extends`/`implements`, embedded `` delegation, call edges) | | COBOL | `.cbl`, `.cob`, `.cpy` | Full support (programs, sections/paragraphs with PERFORM/GO TO call edges, CALL 'literal' cross-program calls, COPY copybook imports — including standalone `.cpy` files — DATA DIVISION records/fields/88-levels, EXEC CICS LINK/XCTL and EXEC SQL INCLUDE targets; fixed and free format) | | Visual Basic .NET | `.vb` | Full support (classes, Modules, interfaces, structures, enums, properties, events, `Declare` P/Invoke, `Handles`/`WithEvents`, `Inherits`/`Implements` edges, call edges through VB's call/index paren ambiguity, `As New` instantiation, interpolated strings, LINQ, Unicode identifiers) | +| Erlang | `.erl`, `.hrl` | Full support (functions with multi-clause/multi-arity grouping, `-spec` signatures, records with fields, `-type`/`-opaque` aliases, `-define` macros, `-include`/`-include_lib`/`-import` edges, local and `mod:fn` remote call edges, `fun name/arity` references, `spawn`/`apply`/`proc_lib`/`timer`/`rpc` MFA-argument call edges, `gen_server:call/cast(?MODULE)` → own `handle_call`/`handle_cast` links, `-behaviour` links, `-export`-based visibility) | ## Measured cross-file coverage diff --git a/__tests__/explore-output-budget.test.ts b/__tests__/explore-output-budget.test.ts index 5e14647..9d9a0b3 100644 --- a/__tests__/explore-output-budget.test.ts +++ b/__tests__/explore-output-budget.test.ts @@ -10,7 +10,7 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; -import { getExploreOutputBudget, getExploreBudget, ToolHandler } from '../src/mcp/tools'; +import { getExploreOutputBudget, getExploreBudget, normalizeQuerySpelling, ToolHandler } from '../src/mcp/tools'; import CodeGraph from '../src/index'; describe('getExploreOutputBudget', () => { @@ -254,3 +254,27 @@ describe('codegraph_explore output respects the adaptive budget', () => { expect(hasMethodBody).toBe(true); }); }); + +describe('normalizeQuerySpelling (Erlang mod:fn/arity)', () => { + it('rewrites Erlang-native symbol spellings to pipeline shapes', () => { + expect(normalizeQuerySpelling('cowboy_stream_h:request_process/3')) + .toBe('cowboy_stream_h.request_process'); + expect(normalizeQuerySpelling('ejabberd_router:route/1 do_route/1 session')) + .toBe('ejabberd_router.route do_route session'); + expect(normalizeQuerySpelling('init/2 handle_call/3')).toBe('init handle_call'); + }); + + it('leaves query-language field prefixes and other spellings alone', () => { + expect(normalizeQuerySpelling('kind:function lang:erlang route')) + .toBe('kind:function lang:erlang route'); + expect(normalizeQuerySpelling('path:src/api name:auth')).toBe('path:src/api name:auth'); + expect(normalizeQuerySpelling('Foo::bar baz')).toBe('Foo::bar baz'); + expect(normalizeQuerySpelling('https://example.com/docs')).toBe('https://example.com/docs'); + expect(normalizeQuerySpelling('meeting at 12:30')).toBe('meeting at 12:30'); + expect(normalizeQuerySpelling('src/2fa/handler.ts')).toBe('src/2fa/handler.ts'); + }); + + it('maps Lua colon-method spelling onto the qualified form', () => { + expect(normalizeQuerySpelling('logger:log message')).toBe('logger.log message'); + }); +}); diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 4441e2b..08fd356 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -107,6 +107,11 @@ describe('Language Detection', () => { expect(isSourceFile('Renderer/Shaders.metal')).toBe(true); }); + it('should detect Erlang files', () => { + expect(detectLanguage('src/my_server.erl')).toBe('erlang'); + expect(detectLanguage('include/records.hrl')).toBe('erlang'); + }); + it('should return unknown for unsupported extensions', () => { expect(detectLanguage('styles.css')).toBe('unknown'); expect(detectLanguage('data.json')).toBe('unknown'); @@ -8753,3 +8758,382 @@ End Class expect(calls).toContain('Parser.GetSteamNameAndID'); }); }); + +// ============================================================================= +// Erlang (vendored WhatsApp/tree-sitter-erlang grammar — the ELP grammar) +// ============================================================================= + +describe('Erlang Extraction', () => { + describe('Language detection', () => { + it('should report Erlang as supported', () => { + expect(isLanguageSupported('erlang')).toBe(true); + expect(getSupportedLanguages()).toContain('erlang'); + expect(isSourceFile('apps/app/src/foo.erl')).toBe(true); + expect(isSourceFile('include/foo.hrl')).toBe(true); + }); + }); + + describe('Function extraction', () => { + it('should merge multi-clause functions into one node spanning all clauses', () => { + const code = `-module(m). +-export([classify/1]). + +classify(X) when is_atom(X) -> + atom; +classify(X) when is_binary(X) -> + binary; +classify(_X) -> + other. +`; + const result = extractFromSource('src/m.erl', code); + const fns = result.nodes.filter((n) => n.kind === 'function' && n.name === 'classify'); + expect(fns).toHaveLength(1); + expect(fns[0]!.startLine).toBe(4); + expect(fns[0]!.endLine).toBe(9); + expect(fns[0]!.language).toBe('erlang'); + }); + + it('should qualify functions with the module namespace', () => { + const code = `-module(my_server). +-export([start/0]). + +start() -> ok. +helper() -> ok. +`; + const result = extractFromSource('src/my_server.erl', code); + const ns = result.nodes.find((n) => n.kind === 'namespace'); + expect(ns?.name).toBe('my_server'); + const start = result.nodes.find((n) => n.kind === 'function' && n.name === 'start'); + expect(start?.qualifiedName).toBe('my_server::start'); + }); + + it('should flag exported functions and honor -compile(export_all)', () => { + const code = `-module(m). +-export([api/0]). + +api() -> internal(). +internal() -> ok. +`; + const result = extractFromSource('src/m.erl', code); + const api = result.nodes.find((n) => n.name === 'api'); + const internal = result.nodes.find((n) => n.name === 'internal'); + expect(api?.isExported).toBe(true); + expect(internal?.isExported).toBe(false); + + const all = extractFromSource('src/all.erl', `-module(all). +-compile(export_all). + +anything() -> ok. +`); + expect(all.nodes.find((n) => n.name === 'anything')?.isExported).toBe(true); + }); + + it('should use the preceding -spec as the signature and capture doc comments', () => { + const code = `-module(m). + +%% Fetches a value by key. +-spec fetch(binary()) -> {ok, term()} | not_found. +fetch(Key) -> + lookup(Key). + +lookup(_K) -> not_found. +`; + const result = extractFromSource('src/m.erl', code); + const fetch = result.nodes.find((n) => n.name === 'fetch'); + expect(fetch?.signature).toBe('-spec fetch(binary()) -> {ok, term()} | not_found.'); + expect(fetch?.docstring).toBe('Fetches a value by key.'); + }); + + it('should fall back to the clause header as the signature', () => { + const code = `-module(m). + +resize(W, H) when W > 0, H > 0 -> + {W, H}. +`; + const result = extractFromSource('src/m.erl', code); + const resize = result.nodes.find((n) => n.name === 'resize'); + expect(resize?.signature).toBe('resize(W, H) when W > 0, H > 0'); + }); + }); + + describe('Record and type extraction', () => { + it('should extract records as structs with fields', () => { + const code = `-module(m). + +-record(state, { + store = #{} :: map(), + counter = 0 :: non_neg_integer() +}). +`; + const result = extractFromSource('src/m.erl', code); + const rec = result.nodes.find((n) => n.kind === 'struct'); + expect(rec?.name).toBe('state'); + const fields = result.nodes.filter((n) => n.kind === 'field').map((n) => n.name); + expect(fields).toContain('store'); + expect(fields).toContain('counter'); + }); + + it('should extract -type and -opaque as type aliases, without bogus type-call refs', () => { + const code = `-module(m). + +-type key() :: atom() | binary(). +-opaque handle() :: reference(). +-spec noop(key()) -> ok. +noop(_K) -> ok. +`; + const result = extractFromSource('src/m.erl', code); + const aliases = result.nodes.filter((n) => n.kind === 'type_alias').map((n) => n.name); + expect(aliases).toContain('key'); + expect(aliases).toContain('handle'); + // Type-position expressions parse as `call` nodes — the spec/type subtrees + // must not leak `calls` refs to type names like atom()/binary(). + const bogus = result.unresolvedReferences.filter( + (r) => r.referenceKind === 'calls' && ['atom', 'binary', 'reference', 'key'].includes(r.referenceName) + ); + expect(bogus).toHaveLength(0); + }); + + it('should extract -define macros as constants', () => { + const code = `-module(m). + +-define(TIMEOUT, 5000). +-define(WRAP(X), {ok, X}). +`; + const result = extractFromSource('src/m.erl', code); + const consts = result.nodes.filter((n) => n.kind === 'constant').map((n) => n.name); + expect(consts).toContain('TIMEOUT'); + expect(consts).toContain('WRAP'); + }); + }); + + describe('Import extraction', () => { + it('should extract -include/-include_lib and -import', () => { + const code = `-module(m). + +-include("records.hrl"). +-include_lib("kernel/include/logger.hrl"). +-import(lists, [map/2]). +`; + const result = extractFromSource('src/m.erl', code); + const imports = result.nodes.filter((n) => n.kind === 'import').map((n) => n.name); + expect(imports).toContain('records.hrl'); + expect(imports).toContain('kernel/include/logger.hrl'); + expect(imports).toContain('lists'); + const ref = result.unresolvedReferences.find( + (r) => r.referenceKind === 'imports' && r.referenceName === 'records.hrl' + ); + expect(ref).toBeDefined(); + }); + }); + + describe('Call extraction', () => { + it('should record local calls bare and remote calls module-qualified', () => { + const code = `-module(m). +-export([run/1]). + +run(X) -> + Y = prepare(X), + other_mod:process(Y). + +prepare(X) -> X. +`; + const result = extractFromSource('src/m.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + expect(calls).toContain('prepare'); + // `mod:fn(...)` is emitted as `mod::fn` — the same shape the module + // namespace gives every function's qualifiedName, so it resolves via + // the qualified-name matcher. + expect(calls).toContain('other_mod::process'); + }); + + it('should not emit calls for dynamic dispatch (var module / var fun)', () => { + const code = `-module(m). +-export([run/2]). + +run(Mod, F) -> + Mod:handle(x), + F(y). +`; + const result = extractFromSource('src/m.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + expect(calls).not.toContain('handle'); + expect(calls).not.toContain('Mod::handle'); + expect(calls).not.toContain('F'); + }); + + it('should connect gen_server self-calls to the module handlers', () => { + const code = `-module(kv_store). +-behaviour(gen_server). +-export([get/1, put/2, drop/1]). +-export([init/1, handle_call/3, handle_cast/2]). + +-define(SERVER, ?MODULE). + +get(Key) -> + gen_server:call(?SERVER, {get, Key}). + +put(Key, Value) -> + gen_server:cast(?MODULE, {put, Key, Value}). + +drop(Key) -> + gen_server:call(kv_store, {drop, Key}). + +init(_) -> {ok, #{}}. +handle_call({get, K}, _From, S) -> {reply, maps:find(K, S), S}; +handle_call({drop, K}, _From, S) -> {reply, ok, maps:remove(K, S)}. +handle_cast({put, K, V}, S) -> {noreply, maps:put(K, V, S)}. +`; + const result = extractFromSource('src/kv_store.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + // ?SERVER (defined as ?MODULE), ?MODULE, and the module's own atom all + // count as self — public API wrappers connect to their handlers. + expect(calls.filter((c) => c === 'kv_store::handle_call')).toHaveLength(2); + expect(calls).toContain('kv_store::handle_cast'); + }); + + it('should not connect gen_server calls to other processes', () => { + const code = `-module(m). +-export([go/2]). + +go(Pid, Msg) -> + gen_server:call(Pid, Msg), + gen_server:call(other_registered_name, Msg), + gen_server:cast({global, some_name}, Msg). +`; + const result = extractFromSource('src/m.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + expect(calls.filter((c) => c.includes('handle_call') || c.includes('handle_cast'))).toHaveLength(0); + }); + + it('should lift static MFA arguments of the spawn/apply family into call refs', () => { + const code = `-module(m). +-export([boot/2]). + +boot(Req, Env) -> + Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env]), + spawn(?MODULE, monitor_loop, [Pid]), + apply(other_mod, handle, [Req]), + timer:apply_after(500, other_mod, tick, []), + Pid. + +request_process(_R, _E) -> ok. +monitor_loop(_P) -> ok. +`; + const result = extractFromSource('src/m.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + expect(calls).toContain('request_process'); // ?MODULE → bare, same-file resolution + expect(calls).toContain('monitor_loop'); + expect(calls).toContain('other_mod::handle'); + expect(calls).toContain('other_mod::tick'); + }); + + it('should stay silent on dynamic spawn/apply (var module, fun value, or plain fun)', () => { + const code = `-module(m). +-export([go/3]). + +go(M, F, A) -> + spawn(M, F, A), + spawn(fun() -> helper() end), + apply(M, F, A). + +helper() -> ok. +`; + const result = extractFromSource('src/m.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + // The fun body's call is still walked; no phantom MFA targets appear. + expect(calls).toContain('helper'); + expect(calls.filter((c) => c !== 'spawn' && c !== 'apply' && c !== 'helper')).toHaveLength(0); + }); + + it('should treat ?MODULE:fn calls as local calls', () => { + const code = `-module(m). +-export([kick/0]). + +kick() -> + ?MODULE:work(). + +work() -> ok. +`; + const result = extractFromSource('src/m.erl', code); + const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName); + expect(calls).toContain('work'); + }); + + it('should capture fun name/arity values as function references', () => { + const code = `-module(m). +-export([wire/1]). + +wire(Pids) -> + lists:foreach(fun notify/1, Pids), + lists:map(fun m:notify/1, Pids). + +notify(_P) -> ok. +`; + const result = extractFromSource('src/m.erl', code); + const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references').map((r) => r.referenceName); + expect(refs).toContain('notify'); + expect(refs).toContain('m::notify'); + }); + + it('should reference records used in bodies and argument patterns', () => { + const code = `-module(m). +-export([mk/1, get_id/1]). + +-record(req, {id, payload}). + +mk(Id) -> #req{id = Id}. +get_id(#req{id = Id}) -> Id. +`; + const result = extractFromSource('src/m.erl', code); + const refs = result.unresolvedReferences.filter( + (r) => r.referenceKind === 'references' && r.referenceName === 'req' + ); + expect(refs.length).toBeGreaterThanOrEqual(2); + }); + + it('should attribute calls from every clause of a multi-clause function', () => { + const code = `-module(m). +-export([handle/1]). + +handle({a, X}) -> + first(X); +handle({b, X}) -> + second(X). + +first(X) -> X. +second(X) -> X. +`; + const result = extractFromSource('src/m.erl', code); + const handle = result.nodes.find((n) => n.kind === 'function' && n.name === 'handle'); + const calls = result.unresolvedReferences.filter( + (r) => r.referenceKind === 'calls' && r.fromNodeId === handle?.id + ).map((r) => r.referenceName); + expect(calls).toContain('first'); + expect(calls).toContain('second'); + }); + }); + + describe('Behaviour extraction', () => { + it('should emit an implements reference for -behaviour', () => { + const code = `-module(m). +-behaviour(gen_server). + +init(_) -> {ok, #{}}. +`; + const result = extractFromSource('src/m.erl', code); + const impl = result.unresolvedReferences.find((r) => r.referenceKind === 'implements'); + expect(impl?.referenceName).toBe('gen_server'); + }); + + it('should not create symbols from -callback declarations', () => { + const code = `-module(b). + +-callback handle_thing(term()) -> ok. +-callback init(list()) -> {ok, term()}. +`; + const result = extractFromSource('src/b.erl', code); + const fns = result.nodes.filter((n) => n.kind === 'function'); + expect(fns).toHaveLength(0); + }); + }); +}); diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index 1351442..a9dd2a3 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -83,6 +83,67 @@ describe('Resolution Module', () => { expect(result?.resolvedBy).toBe('exact-match'); }); + it('should resolve Erlang -behaviour refs only to module namespaces', () => { + // On emqx, `-behaviour(supervisor)` (OTP behaviour, not in the repo) + // fell through to bare-name matching and resolved to a + // `-define(supervisor, ...)` macro constant in an unrelated app. + const macroConstant: Node = { + id: 'constant:apps/bridge/src/impl.erl:supervisor:61', + kind: 'constant', + name: 'supervisor', + qualifiedName: 'impl::supervisor', + filePath: 'apps/bridge/src/impl.erl', + language: 'erlang', + startLine: 61, + endLine: 61, + startColumn: 0, + endColumn: 0, + updatedAt: Date.now(), + }; + const behaviourModule: Node = { + id: 'namespace:src/my_behaviour.erl:my_behaviour:1', + kind: 'namespace', + name: 'my_behaviour', + qualifiedName: 'my_behaviour', + filePath: 'src/my_behaviour.erl', + language: 'erlang', + startLine: 1, + endLine: 1, + startColumn: 0, + endColumn: 0, + updatedAt: Date.now(), + }; + const nodes = [macroConstant, behaviourModule]; + const context: ResolutionContext = { + getNodesInFile: () => [], + getNodesByName: (name) => nodes.filter((n) => n.name === name), + getNodesByQualifiedName: () => [], + getNodesByKind: () => [], + fileExists: () => false, + readFile: () => null, + getProjectRoot: () => '/test', + getAllFiles: () => [], + getNodesByLowerName: () => [], + getImportMappings: () => [], + }; + const mkRef = (name: string) => ({ + fromNodeId: 'namespace:src/worker.erl:worker:1', + referenceName: name, + referenceKind: 'implements' as const, + line: 2, + column: 0, + filePath: 'src/worker.erl', + language: 'erlang' as const, + }); + + // Out-of-repo behaviour whose name collides with a macro constant: + // stays unresolved instead of linking the constant. + expect(matchReference(mkRef('supervisor'), context)).toBeNull(); + // In-repo behaviour module resolves to its namespace. + const resolved = matchReference(mkRef('my_behaviour'), context); + expect(resolved?.targetNodeId).toBe(behaviourModule.id); + }); + it('should prefer same-module candidates over cross-module matches', () => { // Simulates a Python monorepo where multiple apps define navigate() const candidateA: Node = { diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 52360c6..d139fd8 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -44,6 +44,7 @@ const WASM_GRAMMAR_FILES: Record = { cfquery: 'tree-sitter-cfquery.wasm', cobol: 'tree-sitter-cobol.wasm', vbnet: 'tree-sitter-vbnet.wasm', + erlang: 'tree-sitter-erlang.wasm', }; /** @@ -135,6 +136,10 @@ export const EXTENSION_MAP: Record = { // VB.NET: vendored grammar (patched govindbanura/tree-sitter-vbnet) — classes, // modules, interfaces, structures, properties, events, Handles clauses, LINQ. '.vb': 'vbnet', + // Erlang: modules (.erl) and header files (.hrl). Vendored WhatsApp/ + // tree-sitter-erlang grammar (the ELP grammar). + '.erl': 'erlang', + '.hrl': 'erlang', // Spring config: `application.properties` / `application-*.properties`. Same // shape as the `.yml` variants — the YAML/properties extractor emits one node // per leaf key, and the Spring resolver links `@Value("${k}")` references. @@ -253,7 +258,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise | 'all' = new Set(); + +/** + * Clause-merge state: the previous fun_decl's name and node id. A fun_decl + * whose clause repeats that name is a continuation clause (or a same-name + * different-arity definition — deliberately grouped under one node, the way + * overloads are elsewhere) and attaches to the existing node instead of + * creating a duplicate. + */ +let lastFnFile = ''; +let lastFnName = ''; +let lastFnId = ''; + +function moduleExports(node: SyntaxNode, source: string, filePath: string): Set | 'all' { + if (filePath === exportsFile) return exportsMemo; + let root: SyntaxNode = node; + while (root.parent) root = root.parent; + let result: Set | 'all' = new Set(); + for (let i = 0; i < root.namedChildCount; i++) { + const form = root.namedChild(i); + if (!form) continue; + if ( + form.type === 'compile_options_attribute' && + getNodeText(form, source).includes('export_all') + ) { + result = 'all'; + break; + } + if (form.type === 'export_attribute') { + for (const fa of form.namedChildren) { + if (fa.type !== 'fa') continue; + const fun = getChildByField(fa, 'fun'); + if (fun) result.add(atomText(fun, source)); + } + } + } + exportsFile = filePath; + exportsMemo = result; + return result; +} + +/** The -spec directly above a function (comments may sit between), if it names it. */ +function precedingSpec(node: SyntaxNode, name: string, source: string): SyntaxNode | null { + let prev = node.previousNamedSibling; + while (prev && prev.type === 'comment') prev = prev.previousNamedSibling; + if (prev?.type === 'spec') { + const specFun = getChildByField(prev, 'fun'); + if (specFun && atomText(specFun, source) === name) return prev; + } + return null; +} + +/** `name(Args) when Guard` — the clause text up to the `->`. */ +function clauseHeader(clause: SyntaxNode, source: string): string | undefined { + const body = getChildByField(clause, 'body'); + const end = body ? body.startIndex : clause.endIndex; + return collapseWs(source.substring(clause.startIndex, end)) || undefined; +} + +function handleFunDecl(node: SyntaxNode, ctx: ExtractorContext): boolean { + const clauses = node.namedChildren.filter((c) => c.type === 'function_clause'); + const first = clauses[0]; + if (!first) return true; // macro-templated clause (`?M(...) -> ...`) — no static name + const nameNode = getChildByField(first, 'name'); + if (!nameNode) return true; + const name = atomText(nameNode, ctx.source); + if (!name) return true; + + // Continuation clause: extend the existing node's span and attribute this + // clause's calls to it. + if (ctx.filePath === lastFnFile && name === lastFnName && lastFnId) { + for (let i = ctx.nodes.length - 1; i >= 0; i--) { + const n = ctx.nodes[i]; + if (n && n.id === lastFnId) { + if (node.endPosition.row + 1 > n.endLine) n.endLine = node.endPosition.row + 1; + break; + } + } + ctx.pushScope(lastFnId); + for (const clause of clauses) ctx.visitFunctionBody(clause, lastFnId); + ctx.popScope(); + return true; + } + + const spec = precedingSpec(node, name, ctx.source); + const exports = moduleExports(node, ctx.source, ctx.filePath); + const fn = ctx.createNode('function', name, node, { + docstring: getPrecedingDocstring(spec ?? node, ctx.source), + signature: spec + ? collapseWs(getNodeText(spec, ctx.source)).slice(0, 300) + : clauseHeader(first, ctx.source), + isExported: exports === 'all' || exports.has(name), + }); + if (!fn) return true; + ctx.pushScope(fn.id); + // The whole clause is walked (not just the body) so record patterns in the + // arguments and guard calls contribute references too. + for (const clause of clauses) ctx.visitFunctionBody(clause, fn.id); + ctx.popScope(); + lastFnFile = ctx.filePath; + lastFnName = name; + lastFnId = fn.id; + return true; +} + +function handleRecordDecl(node: SyntaxNode, ctx: ExtractorContext): boolean { + const nameNode = getChildByField(node, 'name'); + if (!nameNode) return true; + const rec = ctx.createNode('struct', atomText(nameNode, ctx.source), node, { + docstring: getPrecedingDocstring(node, ctx.source), + signature: collapseWs(getNodeText(node, ctx.source)).slice(0, 300), + }); + if (rec) { + ctx.pushScope(rec.id); + for (const field of node.namedChildren) { + if (field.type !== 'record_field') continue; + const fieldName = getChildByField(field, 'name'); + if (fieldName) ctx.createNode('field', atomText(fieldName, ctx.source), field); + } + ctx.popScope(); + } + return true; // field types/defaults are type-position exprs — don't descend +} + +function handleTypeAlias(node: SyntaxNode, ctx: ExtractorContext): boolean { + const typeName = getChildByField(node, 'name'); // type_name wrapper + const nameNode = typeName ? getChildByField(typeName, 'name') : null; + if (nameNode) { + ctx.createNode('type_alias', atomText(nameNode, ctx.source), node, { + signature: collapseWs(getNodeText(node, ctx.source)).slice(0, 200), + }); + } + return true; +} + +function handlePpDefine(node: SyntaxNode, ctx: ExtractorContext): boolean { + const lhs = getChildByField(node, 'lhs'); + const nameNode = lhs ? getChildByField(lhs, 'name') : null; + if (nameNode) { + ctx.createNode('constant', getNodeText(nameNode, ctx.source), node, { + signature: collapseWs(getNodeText(node, ctx.source)).slice(0, 200), + }); + } + return true; // the replacement's calls only exist at expansion sites +} + +function handleBehaviour(node: SyntaxNode, ctx: ExtractorContext): boolean { + const nameNode = getChildByField(node, 'name'); + const parentId = ctx.nodeStack[ctx.nodeStack.length - 1]; + if (nameNode && parentId) { + // `-behaviour(x)` implements x's callback contract. Resolves when the + // behaviour module is in the repo; OTP behaviours (gen_server, …) simply + // stay unresolved. + ctx.addUnresolvedReference({ + fromNodeId: parentId, + referenceName: atomText(nameNode, ctx.source), + referenceKind: 'implements', + line: node.startPosition.row + 1, + column: node.startPosition.column, + }); + } + return true; +} + +export const erlangExtractor: LanguageExtractor = { + functionTypes: ['fun_decl'], // dispatched via visitNode (name lives on the clause) + classTypes: [], + methodTypes: [], + interfaceTypes: [], + structTypes: ['record_decl'], // dispatched via visitNode (fields are direct children) + enumTypes: [], + typeAliasTypes: ['type_alias', 'opaque'], // dispatched via visitNode + importTypes: ['import_attribute', 'pp_include', 'pp_include_lib'], + callTypes: [ + 'call', + 'internal_fun', // fun f/1 + 'external_fun', // fun mod:f/1 + 'record_expr', // #rec{...} construction + 'record_update_expr', // X#rec{...} + 'record_index_expr', // #rec.field + 'record_field_expr', // X#rec.field + ], + variableTypes: [], + nameField: 'name', + bodyField: 'body', + paramsField: 'args', + + // `-module(m)` wraps the file's declarations in a namespace so every + // function's qualifiedName is `m::f` — which is exactly the reference shape + // the extractCall erlang branch emits for remote calls, so `mod:f(...)` + // resolves through matchByQualifiedName with no resolver changes. + packageTypes: ['module_attribute'], + extractPackage: (node, source) => { + const name = getChildByField(node, 'name'); + return name ? atomText(name, source) : null; + }, + + extractImport: (node, source) => { + if (node.type === 'import_attribute') { + const mod = getChildByField(node, 'module'); + if (!mod) return null; + return { + moduleName: atomText(mod, source), + signature: collapseWs(getNodeText(node, source)).slice(0, 200), + }; + } + // pp_include / pp_include_lib — a C-include-style file dependency on a .hrl. + const file = getChildByField(node, 'file'); + if (!file) return null; + const headerPath = getNodeText(file, source).replace(/^"/, '').replace(/"$/, ''); + if (!headerPath) return null; + return { moduleName: headerPath, signature: getNodeText(node, source).trim() }; + }, + + visitNode: (node, ctx) => { + switch (node.type) { + case 'fun_decl': + return handleFunDecl(node, ctx); + case 'record_decl': + return handleRecordDecl(node, ctx); + case 'type_alias': + case 'opaque': + return handleTypeAlias(node, ctx); + case 'pp_define': + return handlePpDefine(node, ctx); + case 'behaviour_attribute': + return handleBehaviour(node, ctx); + // -spec / -callback: their type expressions parse as `call` nodes; + // consume the subtree so the walker doesn't mint bogus call refs. + case 'spec': + case 'callback': + return true; + default: + return false; + } + }, +}; diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 6e16766..a257713 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -31,6 +31,7 @@ import { cfscriptExtractor } from './cfscript'; import { cfqueryExtractor } from './cfquery'; import { cobolExtractor } from './cobol'; import { vbnetExtractor } from './vbnet'; +import { erlangExtractor } from './erlang'; export const EXTRACTORS: Partial> = { typescript: typescriptExtractor, @@ -59,4 +60,5 @@ export const EXTRACTORS: Partial> = { cfquery: cfqueryExtractor, cobol: cobolExtractor, vbnet: vbnetExtractor, + erlang: erlangExtractor, }; diff --git a/src/extraction/tree-sitter-helpers.ts b/src/extraction/tree-sitter-helpers.ts index 4ffa595..a6438e1 100644 --- a/src/extraction/tree-sitter-helpers.ts +++ b/src/extraction/tree-sitter-helpers.ts @@ -84,6 +84,7 @@ function cleanCommentMarkers(comment: string): string { .replace(/^\/\/[/!]?\s?/gm, '') // // , and Rust/Swift doc lines /// //! .replace(/^--\s?/gm, '') // Lua/Luau line comments .replace(/^#\s?/gm, '') // Python/Ruby/shell line comments + .replace(/^%+\s?/gm, '') // Erlang line comments (% / %% / %%%) .replace(/^\s*\*\s?/gm, '') // block-comment continuation (* foo) .trim(); } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index b16a63c..c162159 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -60,6 +60,22 @@ const VUE_STORE_FACTORY_CALLEES = new Set(['defineStore', 'createStore']); * `const actions = {…}` as a store collection — see looksLikeVueStoreFile). */ const VUE_STORE_FILE_SIGNAL = /\bdefineStore\b|\bcreateStore\b|\bVuex\b|\bmutations\b|\bactions\b|\bgetters\b|\bnamespaced\b/g; +/** + * Erlang calls that take their real callee as (Module, Function, Args) + * ARGUMENTS — the spawn/apply family. Keys are the callee as the call site + * spells it: bare for auto-imported BIFs, `module:function` for remote calls. + * Used by the erlang branch of extractCall to lift a static MFA pair into a + * call edge (the spawned/applied function is otherwise invisible to the graph). + */ +const ERLANG_MFA_CALLS = new Set([ + 'spawn', 'spawn_link', 'spawn_monitor', 'spawn_opt', 'apply', + 'erlang:spawn', 'erlang:spawn_link', 'erlang:spawn_monitor', 'erlang:spawn_opt', 'erlang:apply', + 'proc_lib:spawn', 'proc_lib:spawn_link', 'proc_lib:spawn_opt', 'proc_lib:start', 'proc_lib:start_link', + 'timer:apply_after', 'timer:apply_interval', + 'rpc:call', 'rpc:cast', 'rpc:async_call', + 'erpc:call', 'erpc:cast', +]); + /** * Extract the name from a node based on language */ @@ -3492,6 +3508,50 @@ export class TreeSitterExtractor { /** * Extract a function call */ + /** + * Whether an Erlang gen_server target expression statically refers to the + * module it appears in: `?MODULE`, a macro the file defines as `?MODULE` + * (`-define(SERVER, ?MODULE)` — the standard idiom), or the module's own + * name as an atom. The self-macro set is memoized per file (single entry — + * extraction is file-sequential). + */ + private erlangSelfMacroFile = ''; + private erlangSelfMacros = new Set(); + + private isErlangSelfReference(target: SyntaxNode): boolean { + const ownModule = (this.filePath.split('/').pop() ?? '').replace(/\.erl$/, ''); + if (target.type === 'atom') { + return getNodeText(target, this.source) === ownModule; + } + if (target.type !== 'macro_call_expr') return false; + const nameNode = getChildByField(target, 'name'); + if (!nameNode) return false; + const macroName = getNodeText(nameNode, this.source); + if (macroName === 'MODULE') return true; + if (this.erlangSelfMacroFile !== this.filePath) { + this.erlangSelfMacroFile = this.filePath; + this.erlangSelfMacros = new Set(); + let root: SyntaxNode = target; + while (root.parent) root = root.parent; + for (let i = 0; i < root.namedChildCount; i++) { + const form = root.namedChild(i); + if (form?.type !== 'pp_define') continue; + const lhs = getChildByField(form, 'lhs'); + const defName = lhs ? getChildByField(lhs, 'name') : null; + const replacement = getChildByField(form, 'replacement'); + if ( + defName && + replacement?.type === 'macro_call_expr' && + getChildByField(replacement, 'name') && + getNodeText(getChildByField(replacement, 'name')!, this.source) === 'MODULE' + ) { + this.erlangSelfMacros.add(getNodeText(defName, this.source)); + } + } + } + return this.erlangSelfMacros.has(macroName); + } + private extractCall(node: SyntaxNode): void { if (this.nodeStack.length === 0) return; @@ -3543,6 +3603,155 @@ export class TreeSitterExtractor { return; } + // Erlang: a local call is `call(expr: atom, args)`; a remote call nests it + // under `remote(module: remote_module, fun: call)` — the module qualifier + // lives on the PARENT. Remote calls are emitted as `mod::fn`, which is + // byte-identical to the qualifiedName the module namespace gives every + // function (see packageTypes in languages/erlang.ts), so they resolve via + // matchByQualifiedName. A var/macro callee or module (`F(X)`, `?M(X)`, + // `Mod:handle(X)`) has no static target — except `?MODULE:fn(X)`, which the + // bare name + same-file preference resolves correctly. `fun name/1` / + // `fun mod:name/1` values are function REFERENCES (callback registration), + // and record construction/update/index/field-access are `references` to the + // record's struct node. + if (this.language === 'erlang') { + const line = node.startPosition.row + 1; + const column = node.startPosition.column; + const erlAtom = (n: SyntaxNode): string => getNodeText(n, this.source).replace(/^'([\s\S]*)'$/, '$1'); + if (node.type === 'call') { + let callee = getChildByField(node, 'expr'); + let moduleNode: SyntaxNode | null = null; + // remote(module, fun: call) — the shape the grammar produces today; the + // node-types also permit call(expr: remote), so handle both nestings. + if (node.parent?.type === 'remote') { + moduleNode = getChildByField(node.parent, 'module'); + } else if (callee?.type === 'remote') { + moduleNode = getChildByField(callee, 'module'); + callee = getChildByField(callee, 'fun'); + } + if (callee?.type === 'atom') { + const fnBare = erlAtom(callee); + let calleeName = fnBare; + const moduleExpr = moduleNode ? getChildByField(moduleNode, 'module') : null; + if (moduleExpr?.type === 'atom') { + calleeName = `${erlAtom(moduleExpr)}::${calleeName}`; + } else if (moduleExpr) { + // Non-atom module qualifier. `?MODULE:f(X)` targets THIS module — + // keep the bare name so same-file preference resolves it. Anything + // else (`Mod:f(X)`) is behaviour-style dynamic dispatch with no + // static target: emitting the bare name would link an arbitrary + // same-named function, so stay silent instead. + const macroName = + moduleExpr.type === 'macro_call_expr' ? getChildByField(moduleExpr, 'name') : null; + if (!macroName || getNodeText(macroName, this.source) !== 'MODULE') return; + } + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: calleeName, + referenceKind: 'calls', + line, + column, + }); + // gen_server self-dispatch: `gen_server:call(?SERVER, Msg)` / + // `gen_server:cast(?MODULE, Msg)` — the OTP API-wrapper idiom (a + // module's public functions wrap gen_server requests to itself, and + // the real work happens in its own handle_call/handle_cast). The + // target is static when the first argument is ?MODULE, a macro the + // file defines as ?MODULE (the standard `-define(SERVER, ?MODULE)`), + // or the module's own name as an atom — emit the qualified callback + // ref so the module's public API connects to its handlers. Any other + // target (pid/var/registered name of another process) stays silent. + if ( + moduleExpr?.type === 'atom' && + erlAtom(moduleExpr) === 'gen_server' && + (fnBare === 'call' || fnBare === 'cast' || fnBare === 'send_request') + ) { + const argsNode = getChildByField(node, 'args'); + const target = argsNode?.namedChild(0) ?? null; + if (target && this.isErlangSelfReference(target)) { + const ownModule = (this.filePath.split('/').pop() ?? '').replace(/\.erl$/, ''); + if (ownModule) { + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: `${ownModule}::${fnBare === 'cast' ? 'handle_cast' : 'handle_call'}`, + referenceKind: 'calls', + line, + column, + }); + } + } + } + // MFA-in-argument dispatch: the spawn/apply family names its real + // callee in ARGUMENT position — `proc_lib:spawn_link(?MODULE, + // request_process, [Req, Env, Middlewares])` — so the walker above + // sees only the spawn itself and the spawned function ends up with + // zero callers (measured on cowboy: request_process had no incoming + // edges and the agent Read the file to find it). When the (Module, + // Function) pair is static, lift it as a call edge. The pair is + // found positionally-agnostically (first adjacent module-atom/ + // ?MODULE + atom pair) so every arity variant works: spawn/3, + // spawn(Node,M,F,A)/4, timer:apply_after(Time,M,F,A), + // rpc:call(Node,M,F,A). A var module or fun stays silent. + const familyKey = moduleExpr?.type === 'atom' ? `${erlAtom(moduleExpr)}:${fnBare}` : fnBare; + if (ERLANG_MFA_CALLS.has(familyKey)) { + const argsNode = getChildByField(node, 'args'); + const argExprs = argsNode ? argsNode.namedChildren : []; + for (let i = 0; i + 1 < argExprs.length; i++) { + const m = argExprs[i]!; + const f = argExprs[i + 1]!; + if (f.type !== 'atom') continue; + const isLocalModule = + m.type === 'macro_call_expr' && + getChildByField(m, 'name') !== null && + getNodeText(getChildByField(m, 'name')!, this.source) === 'MODULE'; + if (m.type !== 'atom' && !isLocalModule) continue; + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: isLocalModule ? erlAtom(f) : `${erlAtom(m)}::${erlAtom(f)}`, + referenceKind: 'calls', + line: f.startPosition.row + 1, + column: f.startPosition.column, + }); + break; + } + } + } + return; + } + if (node.type === 'internal_fun' || node.type === 'external_fun') { + const funNode = getChildByField(node, 'fun'); + if (funNode?.type !== 'atom') return; // fun Mod:F/A with var parts — dynamic + let refName = erlAtom(funNode); + if (node.type === 'external_fun') { + const moduleWrapper = getChildByField(node, 'module'); + const moduleAtom = moduleWrapper ? getChildByField(moduleWrapper, 'name') : null; + if (moduleAtom?.type !== 'atom') return; + refName = `${erlAtom(moduleAtom)}::${refName}`; + } + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: refName, + referenceKind: 'references', + line, + column, + }); + return; + } + // record_expr / record_update_expr / record_index_expr / record_field_expr + const recordName = getChildByField(node, 'name'); + const recordAtom = recordName?.type === 'record_name' ? getChildByField(recordName, 'name') : null; + if (recordAtom?.type === 'atom') { + this.unresolvedReferences.push({ + fromNodeId: callerId, + referenceName: erlAtom(recordAtom), + referenceKind: 'references', + line, + column, + }); + } + return; + } + // Ruby `call` nodes use `receiver` + `method` fields (tree-sitter-ruby), not // the `object`/`name`/`function` fields the branches below expect — so // without this they fell through to the generic path, which took the diff --git a/src/extraction/wasm/tree-sitter-erlang.wasm b/src/extraction/wasm/tree-sitter-erlang.wasm new file mode 100755 index 0000000000000000000000000000000000000000..e0e8dd79289629ce26990ac935b4228a1751ce18 GIT binary patch literal 421639 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%0At(rcX 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 t.replace(FILE_EXT, '').trim()) @@ -2457,8 +2487,11 @@ export class ToolHandler { * tax on small projects while earning its keep on large ones. */ private async handleExplore(args: Record): Promise { - const query = this.validateString(args.query, 'query'); - if (typeof query !== 'string') return query; + const rawQuery = this.validateString(args.query, 'query'); + if (typeof rawQuery !== 'string') return rawQuery; + // One normalization point so the flow-builder, relevance search, and + // ranking all see the same canonical spelling (Erlang `mod:fn/arity`). + const query = normalizeQuerySpelling(rawQuery); const cg = this.getCodeGraph(args.projectPath as string | undefined); const projectRoot = cg.getProjectRoot(); @@ -2539,7 +2572,7 @@ export class ToolHandler { // overloads (the query also named the type) all earn it. (#1064) const tierSeedIds = new Set(); { - const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro)$/i; + const FILE_EXT = /\.(?:java|kt|kts|ts|tsx|js|jsx|mjs|cjs|cs|py|go|rb|php|swift|rs|cpp|cc|cxx|c|h|hpp|scala|lua|dart|vue|svelte|astro|erl|hrl)$/i; const CALLABLE = new Set(['method', 'function', 'component', 'constructor']); const isTestPath = (p: string) => /(^|\/)(tests?|specs?|__tests__|testdata|mocks?|fixtures?)\//i.test(p) || /\.(test|spec)\.[a-z]+$/i.test(p); const bodyLines = (n: Node) => Math.max(0, (n.endLine ?? n.startLine) - n.startLine); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index feb3e8e..f47f771 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1753,6 +1753,26 @@ export function matchReference( return matchFunctionRef(ref, context); } + // Erlang `-behaviour(m)` refs target a MODULE. Letting them fall through to + // bare-name matching grabs any same-named symbol — on emqx, + // `-behaviour(supervisor)` resolved to a `-define(supervisor, …)` macro + // constant in an unrelated app. Resolve only to the behaviour module's + // namespace; an out-of-repo behaviour (OTP's gen_server/supervisor) stays + // unresolved rather than guessed. + if (ref.language === 'erlang' && ref.referenceKind === 'implements') { + const modules = context + .getNodesByName(ref.referenceName) + .filter((n) => n.language === 'erlang' && n.kind === 'namespace'); + const chosen = preferCallSiteFile(modules, ref.filePath)[0]; + if (!chosen) return null; + return { + original: ref, + targetNodeId: chosen.id, + confidence: 0.9, + resolvedBy: 'exact-match', + }; + } + // Try strategies in order of confidence let result: ResolvedRef | null; diff --git a/src/types.ts b/src/types.ts index d58e7b2..8d82959 100644 --- a/src/types.ts +++ b/src/types.ts @@ -100,6 +100,7 @@ export const LANGUAGES = [ 'cfquery', 'cobol', 'vbnet', + 'erlang', 'unknown', ] as const;