feat(elixir): index Elixir with Phoenix, Plug and Ecto flow coverage - #1648
Open
ferrine wants to merge 3 commits into
Open
feat(elixir): index Elixir with Phoenix, Plug and Ecto flow coverage#1648ferrine wants to merge 3 commits into
ferrine wants to merge 3 commits into
Conversation
Elixir's grammar has no declaration node types — `defmodule`, `def`,
`alias`, an Ecto `schema` and a Phoenix route are all the same `call`
node, told apart only by the text of the target identifier. So the
generic node-type ladder has nothing to match and every construct is
dispatched through the visitNode hook, which also owns call extraction
(visitFunctionBody does not invoke the hook, and Elixir call sites live
inside bodies).
Three things beyond plain symbol extraction:
- Alias expansion — `alias Foo.{A, B}`, `as:`, `__MODULE__` and the
implicit nested-module alias — so a call written `Repo.insert(...)`
carries `MyApp.Repo::insert` and resolves by exact qualified name
instead of by bare name.
- Clause merging by (module, name, arity). The GenServer idiom spells a
multi-clause function as repeated `def`s, which otherwise index as one
identical node per clause and scatter every caller edge.
- Macro-argument dispatch, closed end to end rather than half-bridged:
Phoenix routes become `route` nodes linked to the controller action
they dispatch to (nested `scope` path + alias composition, `resources`
expansion, `forward`, `live`); `plug :atom` / `plug Module` link to the
function or `call/2` that runs; Ecto schema fields and association
target modules are extracted.
Kernel special forms (`case`, `if`, `quote`, `raise`, …) are suppressed
as call refs — they are syntax, and on plug they were ~1,900 refs that
could only ever resolve wrongly.
Validated on plug (S), phoenix (M) and firezone (L, 2,111 files):
extraction PASS on all three, 137/144 route→action edges resolved at
100% precision on firezone, node/edge counts stable across re-index and
incremental sync. Agent A/B (sonnet/high, 2 runs/arm) takes Read to ~0
and cuts tool calls 4-8x on every repo; wall-clock only improves on the
large repo, which is recorded honestly in the coverage playbook.
Grammar is the tree-sitter-wasms tree-sitter-elixir build (ABI 14),
health-checked — no vendored wasm needed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Bsi9EH64kMisnik4E1gf7
…macros A protobuf generator writes a module's shape as bare macro calls in the module body — `field :observed_at, 3, type: :string`, `oneof :kind, 0`, `rpc :GetRun, Req, Resp` — with no enclosing block. Ecto's `schema do … end` gives its fields a block to be recognised by; these have nothing but the `use Protobuf` / `use GRPC.Service` marker at the top of the module, so they were falling through to ordinary call handling and doing two kinds of damage at once. The declarations went missing, leaving every generated message as a module with zero members, so anything matching a wire declaration against its generated peer could only ever reach the enclosing module. And the macro calls were then resolved by name: on a codebase that defines its own `field/2`, every generated field in the repo landed on that one unrelated private helper, making it the third most-called symbol there. Reading the `use` marker fixes both. Fields carry their tag the way the declaration side records it, an enum's values become enum members rather than fields, and an rpc links to the request and response messages it names — including through a `stream(...)` wrapper, which is the only place that binding is written. Gated on the marker, so a hand-written `field(...)` call in an ordinary module still resolves as the call it is, and the marker stops applying at the end of the module that carried it (generators put every message from one file in one file). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bsi9EH64kMisnik4E1gf7
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013Bsi9EH64kMisnik4E1gf7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch:
ferrine:feat/elixir-support→mainSize: 10 files, +1806/−1 · Tests: 26 in
__tests__/extraction.test.tsGrammar:
tree-sitter-elixirfromtree-sitter-wasms(already vendored, ABIhealth-checked — no new dependency, no new
.wasm).Why
Elixir was unsupported. Added following
.claude/skills/add-lang.What changed
src/extraction/languages/elixir.ts, plus the four standard wiring edits.Elixir's grammar is homoiconic — every construct is a
callnode, includingdefmoduleanddef— so there are no declaration node types to map andeverything dispatches through the
visitNodehook. Two grammar details worthflagging for review, because both produced silent, total extraction failures
before they were found:
argumentsis a plain named child of acall, not a field (onlytargetis), so
childForFieldName('arguments')never finds it.keywordnode's text spans trailing whitespace ("for: "), so it must betrimmed before comparison.
Indexed: modules including nested ones with
@moduledoc; public/privatefunctions with
@docand@spec; macros, guards, operator definitions,defdelegatetargets;defstruct/defexceptionfields;@type/@opaque;module attributes as constants; protocols and their
defimpl;@behaviourlinks;
alias/import/require/usewithalias/as:/{A, B}expansion soremote calls resolve to the real module;
&fun/1captures;%Struct{}instantiation.
Multi-clause functions are merged into one symbol per arity rather than one
node per clause — the GenServer idiom otherwise indexes a 6-clause
handle_callas six identical nodes with caller edges landing on an arbitraryone. Adjacency is a safe merge key because Elixir warns on any non-adjacent
redefinition. Same-name different-arity stays separate.
Kernel.SpecialFormsand the Kernel macros that are language syntax aresuppressed as call targets — without that list every
if/case/quotemints acallsref, and a project that legitimately definesdef send/2collectshundreds of wrong caller edges.
Framework coverage: Phoenix routes as symbols linked to the controller
action they dispatch to (nested
scopepaths and aliases,resourcesexpansion,
forward,live); Plug pipeline entries linked to the functionor plug that runs; Ecto schema fields and association edges.
Generated-protobuf modules
A protobuf generator writes a module's shape as bare macro calls in the module
body —
field :observed_at, 3, type: :string,oneof,rpc— with noenclosing block. Ecto's
schema do … endgives its fields a block to berecognised by; these have nothing but the
use Protobuf/use GRPC.Servicemarker at the top of the module, so they were doing damage twice over: the
declarations went missing entirely, and the macro calls resolved by name, so a
project that defines its own
field/2collected every generated field in therepo as a caller of that one unrelated helper.
Reading the
usemarker fixes both. Fields carry their tag, an enum's valuesbecome
enum_member, and an rpc links to the request and response messages itnames — including through a
stream(...)wrapper. Gated on the marker, so ahand-written
field(...)call in an ordinary module still resolves as a call,and the marker stops applying at the end of its own module.
Notes for review
assets/languages/elixir.svguses the published simple-icons path plus awordmark recomposed from published glyph metrics — not hand-drawn.