feat: Adds Elixir Language Support#320
Conversation
|
Someone is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
xkonjin
left a comment
There was a problem hiding this comment.
Code Review: Elixir Language Support
Thorough, well-structured addition. The approach mirrors the existing Ruby/Swift patterns nicely. A few items worth addressing:
Potential Issues
-
tree-sitter-swiftaccidentally removed frompackage-lock.json(line 39 diff) — the-"tree-sitter-swift": "^0.6.0",removal from the top-leveldependenciesblock looks unintentional. It's still inoptionalDependenciesinpackage.json, but the lockfile change could cause issues on clean installs. Please verify this was deliberate. -
@ladybugdb/corereordering — the package.json diff moves this dependency from one position to another (alphabetical sort fix). Harmless but confirm it wasn't an accidental addition or version bump. -
toSnakeCaseedge case — the regex inelixir.tsresolver handlesHTTPClient→http_clientcorrectly, but Elixir modules with numbers (e.g.OAuth2) would produceo_auth2which may not match filesystem conventions. Consider whether number-adjacent casing matters for your indexed repos. -
Heritage query fragility — the
@behaviourheritage query assumes it's a direct child of thedo_block:(do_block (unary_operator (call target: (identifier) @_attr ...)))If there are intervening nodes (e.g.,
@moduledocbefore@behaviour), this pattern may not match. The query should use wildcard/sibling matching rather than requiring direct child position. Worth verifying with a fixture that has other attributes betweendefmoduleand@behaviour.
Test Coverage
-
Missing test for
toSnakeCase— the resolver has a non-trivial regex-based snake_case converter but no unit test for edge cases (single-char modules, all-caps likeHTTP, mixed likeOAuth2Client). -
Pipe operator test asserts
e.source === 'process'— this string comparison against function names could be brittle if the graph stores qualified names (e.g.,Pipeline.process). Other language tests seem to use.includes()or partial matching.
Nits
- The
RubyHeritageItemdeprecation alias is a nice touch for backward compat. - Good decision making tree-sitter-elixir an optionalDependency with graceful fallback.
- The test fixtures are comprehensive — alias, multi-alias, pipe, Phoenix, behaviour, and visibility all covered.
Overall this is solid work. The heritage query fragility (#4) is the most likely source of production issues.
|
@claude use ultra-thinking and review this PR if it fits into our architecture and is correctly implementing type resolution for supporting member based CALLS edges. We only accept a PR if it is fully supports the given langauge features and handles type resolutions properly! |
|
Claude finished @magyargergo's task in 4m 32s —— View job PR Review: Elixir Language Support — Architecture & Type Resolution Analysis
Architecture FitnessThe PR correctly mirrors the Ruby/Swift patterns and integrates cleanly into the ingestion pipeline: it registers in However, there is a critical gap in how member-based CALLS edges work for Elixir — the most important ask in this review. 🚨 Critical Issue: Member-Based CALLS Edges Are BrokenThis is the most significant finding and a blocker for acceptance. Root CauseIn Elixir, qualified calls like The call-processor uses
Result: All What Works vs. What Doesn't
What Needs to Be FixedFix 1 — const MEMBER_ACCESS_NODE_TYPES = new Set([
// ... existing entries ...
'dot', // Elixir: Module.func() — nameNode sits inside dot node
]);Fix 2 — // Elixir dot node: (dot left: (alias "Module") right: (identifier "func"))
if (!receiver && parent.type === 'dot') {
receiver = parent.childForFieldName('left');
}Without both fixes, Related Structural Gap: No
|
CI Report❌ Some checks failed Pipeline Status
Test Results
❌ 1 failed / 2422 passed 17 test(s) skipped — expand for detailsIntegration:
Code CoverageCombined (Unit + Integration)
Coverage breakdown by test suiteUnit Tests
Integration Tests
📋 View full run · Generated by CI |
|
@claude review the written integration tests for each cases for the given language we required comprehensive integration tests in our fixtures datasets. |
|
Claude finished @magyargergo's task in 3m 20s —— View job Integration Test Review: Elixir Language Support
Assessment: Tests Are Smoke-Level Only — Not ComprehensiveCompared against the Ruby integration tests (the gold standard in this repo — 12 describe blocks, 40+ assertions covering member calls, disambiguation, type inference, self/parent resolution, return type inference, chained calls), the Elixir tests are in an early "does it crash" state. They verify symbol discovery and basic IMPORTS edges, but leave the core pipeline features entirely untested. 🚨 Critical: The Primary Feature Has No TestsCross-module member CALLS edges ( The // elixir.test.ts:58-65
it('processes call edges without errors', () => {
// The elixir-alias fixture has mostly qualified calls (Repo.insert) which
// may not produce CALLS edges due to symbol resolution filtering.
const calls = getRelationships(result, 'CALLS');
expect(Array.isArray(calls)).toBe(true); // ← this always passes, proves nothing
});
A required missing fixture: With tests asserting:
🔴 Gap 1: Multi-Alias Expansion Not Validated End-to-EndFixture: expect(serviceImports.length).toBeGreaterThanOrEqual(1); // only requires ONEThe comment says "At least one of User or Admin should resolve." This accepts partial failure. The feature being tested is Required: assert 🔴 Gap 2: Phoenix Framework Detection Not TestedThe phoenix fixture has
Nothing tests EXTENDS/IMPLEMENTS edges from it('emits EXTENDS edge from use Phoenix.Controller', () => {
const extends_ = getRelationships(result, 'EXTENDS');
const controllerEdge = extends_.find(e =>
e.source === 'MyAppWeb.UserController' && e.target.includes('Phoenix.Controller')
);
expect(controllerEdge).toBeDefined();
});Similarly, the LiveView Also missing: no test that 🔴 Gap 3:
|
| Location | Current assertion | Should be |
|---|---|---|
elixir.test.ts:55 |
userToRepo.length >= 1 |
exact count + verify targetFilePath basename |
elixir.test.ts:97 |
serviceImports.length >= 1 |
=== 2 + both user.ex and admin.ex present |
elixir.test.ts:131 |
e.source === 'process' |
check qualified name or e.sourceFilePath.includes('pipeline.ex') |
elixir.test.ts:208-213 |
targets.toContain(...) |
check source module name + verify edge points to real graph node |
No test anywhere checks rel.reason on EXTENDS/IMPORTS edges (Ruby tests verify 'include', 'extend', 'prepend', 'import-resolved'). No test checks sourceLabel/targetLabel on CALLS edges.
🟡 describe.skipIf Pattern Not Used
The Elixir tests use an ad-hoc if (skipIfNoElixir()) return; inside each describe block. The Swift tests use the cleaner describe.skipIf(!swiftAvailable)(...) pattern, which integrates with the test reporter and marks tests as properly skipped rather than silently empty. Fix this →
🟡 simple.ex Sample Fixture Is Unused by Integration Tests
test/fixtures/sample-code/simple.ex covers defmodule, @behaviour, alias, use, defstruct, defprotocol, defimpl, @spec, def/defp, defmacro — a comprehensive fixture. But it's only consumed by the tree-sitter-languages.test.ts unit test for parsing. The integration resolver tests have no equivalent that exercises all these constructs together end-to-end through the full pipeline.
Required Fixtures to Add
| Fixture | What it tests |
|---|---|
elixir-member-calls |
Module.func() → CALLS edge (the critical gap) |
elixir-struct-type-inference |
%User{} struct construction → type binding → CALLS |
elixir-spec-type |
@spec func() :: User.t() return type → type binding |
elixir-defprotocol |
defprotocol/defimpl → IMPLEMENTS edges |
elixir-defstruct-props |
defstruct → Property nodes / HAS_FIELD edges |
The pipe and visibility fixtures are the only ones where the tests are definitive and actually validate what they claim to.
|
Thanks for the reviews! I'll make appropriate updates to the PR today. Let me know if you have any other guidance or insights. |
1863f12 to
4ebff21
Compare
4ebff21 to
3ceee02
Compare
|
@claude ultra-think while identifying Elixir-language gaps. Perfom this analysis on this pull request and verify those findings. Act as a senior compiler font-end engineer and expert in static analysis tools and review this change from that perpective and review the architectural fit. Also check out previous comments and reason with their requests. You must also cross-reference it with our type-resolution-system.md. |
|
Claude finished @magyargergo's task in 6m 6s —— View job Deep Elixir PR Analysis — Compiler Front-End & Static Analysis Perspective
Executive SummaryThis review is conducted against the two-commit version of the PR (after the second commit Resolved Issues (Verified in Current Code)The second commit resolved the central blocker identified in the prior review. I confirmed all fixes in the actual source:
The core CALLS pipeline now correctly classifies 🔴 Critical Remaining Issue:
|
| Feature | Elixir Implementation | Quality |
|---|---|---|
| Declarations | extractDeclaration is a no-op |
|
| Parameters | extractParameter is a no-op |
|
| Initializer / constructor inference | scanConstructorBinding handles %User{} |
✅ |
| Constructor binding scan | Present via scanConstructorBinding |
✅ |
| For-loop element types | Not implemented | 🔴 Missing |
| Pattern binding | Not implemented (function head %User{} = u) |
🟡 Missing |
| Assignment chains | Inherited from base engine | ✅ |
| Comment-based types | Not implemented (@doc not parsed for types) |
@spec not doc comments |
| Return type extraction | extractReturnType implemented |
✅ |
The absence of for-loop element types is notable since Elixir's Enum.each(users, fn user -> user.name end) is common. The anonymous function parameter user has no inline type — it would need @spec inference from the Enum.each context, which is outside the scope of the current tier system. This is an acceptable limitation but should be documented.
Pattern binding in function heads is a real gap for Elixir compared to other OO languages. Elixir's primary dispatch mechanism is pattern-matched function heads:
def process(%User{} = user), do: user.nameThe variable user is type-bound to User via pattern match, but the current scanConstructorBinding only handles user = %User{} (RHS struct literal), not LHS destructuring patterns.
🟡 CLASS_CONTAINER_TYPES Gap: No ownerId on Elixir Symbols
findEnclosingClassId in utils.ts:310 walks up the AST looking for CLASS_CONTAINER_TYPES. Since Elixir defmodule is a call node (not in this set), all Elixir function SymbolDefinitions have ownerId = null.
This means the D4 ownerId-based filtering in call-processor.ts:502 will never activate for Elixir. The system falls back to D3 (file-based narrowing), which is adequate for the common case where one file has one module. However, multi-module files (two defmodule blocks in the same .ex file) would experience CALLS edge ambiguity between methods of the two modules, since file-based narrowing alone cannot distinguish them.
🟢 What's Well Done
Plumbing is clean: The registration chain (supported-languages → parser-loader → parse-worker → LANGUAGE_QUERIES → callRouters → typeConfigs → resolvers/index) is complete and follows the existing pattern exactly.
isElixirFunctionDef (utils.ts:376-394) — well-considered: it correctly identifies call nodes as function definitions by inspecting the target identifier. Used correctly in both call-processor.ts (enclosing function lookup) and type-env.ts (scope key generation). The scope key format (funcName@startIndex) is consistent with the shared format documented in type-resolution-system.md.
routeElixirCall — correctly returns SKIP_RESULT for definition keywords and CALL_RESULT for everything else. The noRouting approach (queries handle all import/heritage dispatch instead of runtime routing) is architecturally consistent.
toSnakeCase — the double-regex approach (([A-Z]+)([A-Z][a-z]) then ([a-z\d])([A-Z])) is correct for standard CamelCase → snake_case conversion and handles HTTPClient → http_client properly.
Optional dependency pattern — try { ... } catch { ... } in both parser-loader.ts and parse-worker.ts is the correct pattern, matching how tree-sitter-swift is handled.
countCallArguments pipe support (utils.ts:853-859) — correctly adds 1 for the piped value when a call is the RHS of |>. This is a proper treatment of the implicit first argument in pipe chains.
@spec backward-walk — the corrected extractReturnType (from commit 2) properly handles @doc, @moduledoc, @deprecated, and @impl interleaved between @spec and def, maintaining the conservative "stop at real def/other call" logic.
Final Verdict
The second commit brought this PR close to the bar for member-based CALLS edge support. The core pipeline — query capture → inferCallForm('member') → receiver extraction (alias node as receiver) → D3 file-based narrowing — is now correctly wired. The elixir-alias and elixir-member-calls fixtures demonstrate this.
Remaining blockers before merge:
- 🔴
__MODULE__self-resolution broken due toCLASS_CONTAINER_TYPESnot includingdefmodule callnodes —lookupInEnvspecial-case is registered but the underlyingfindEnclosingClassNamewalker cannot reach it - 🔴
defimpl ... for: Moduleproduces zero IMPLEMENTS edges — protocol implementation relationships are not captured - 🔴
@behaviour :gen_serveratom-form not matched by heritage query
Acceptable for merge with documented limitations:
- 🟡
ownerIdalways null on Elixir functions (D4 filtering inactive; D3 covers most cases) - 🟡
t()return type resolves to string"t"instead of enclosing module name - 🟡 Pattern binding in function heads (
%User{} = userparams) not supported - 🟡
defguardwithwhenclause may not be captured (needs grammar verification) - 🟡 No
defstruct→ Property node emission
Core Registration & Parsing: - supported-languages.ts: Added Elixir = 'elixir' enum value - parser-loader.ts + parse-worker.ts: Registered tree-sitter-elixir as optional dependency with try/catch import - package.json: Moved tree-sitter-elixir to optionalDependencies - utils.ts: Added .ex/.exs extension mapping, Elixir built-in names, isElixirFunctionDef() helper for enclosing function detection - tree-sitter-queries.ts: Added ELIXIR_QUERIES with #eq? predicates for defmodule, def/defp, defmacro/defmacrop, defstruct, defprotocol, defimpl, alias/import/require/use imports, qualified calls, and heritage (use/@behaviour) - export-detection.ts: Added elixirExportChecker — walks up to enclosing call target to check def (public) vs defp (private) - call-routing.ts: Extended HeritageItem to support 'use'/'behaviour' heritage kinds; Elixir uses noRouting since queries handle all dispatch - type-extractors/elixir.ts (new): Stub type config with @SPEC return type extraction and struct construction scanning - resolvers/elixir.ts (new): CamelCase-to-snake_case module path resolution with multi-alias expansion Import Resolution & Call Routing: - import-processor.ts: Added Elixir branch calling resolveElixirImport() - resolvers/utils.ts: Added .ex/.exs to EXTENSIONS - call-processor.ts + type-env.ts: Added isElixirFunctionDef() checks for enclosing function/scope detection (Elixir's def/defp are call nodes, not in FUNCTION_NODE_TYPES) Type Extraction: - Basic @SPEC return type extraction from preceding unary_operator nodes - Struct construction inference (%User{} patterns) Phoenix Framework Detection: - Path-based: *_controller.ex, *_live.ex, *_channel.ex, router.ex, endpoint.ex, application.ex, mix.exs - AST-based: use Phoenix.Router/Controller/LiveView/Channel, use GenServer/Supervisor/Agent Tests: - simple.ex fixture: Module, def/defp, defmacro, defstruct, defprotocol, defimpl - 6 resolution fixtures: alias, multi-alias, pipe, phoenix, behaviour, visibility - 58 tests all passing (38 language + 20 Elixir resolver tests) - No regressions in existing tests (1425 unit tests pass, all other resolver tests pass) Fix quote rewriting Upgrades for additional PR comments
58f5965 to
072f928
Compare
|
@claude looking at the latest commit to address previous issues in this PR, can you ultra-think while identifying Elixir-language gaps. Perfom this analysis on this pull request and verify those findings. Act as a senior compiler font-end engineer and expert in static analysis tools and review this change from that perpective and review the architectural fit. Also check out previous comments and reason with their requests. You must also cross-reference it with our type-resolution-system.md. |
|
Claude finished @zander-raycraft's task in 10m 24s —— View job Deep Elixir PR Analysis — Compiler Front-End & Static Analysis Perspective
Verification of Previous FindingsAll critical issues from the previous two reviews are now confirmed resolved in the current commit (
🔬 New Deep Analysis — Compiler Front-End Perspective✅ Core Pipeline: Member CALLS Fully WiredThe complete call resolution path for The conservative fallthrough (receiver not in TypeEnv → D3 path) is architecturally sound. ✅
|
| Feature | Ruby | Swift | Elixir (this PR) |
|---|---|---|---|
| Assignment chains | No | Yes | No |
| Call-result variable binding | Yes¶ | No | No |
| Method-call-result binding | Yes¶ | No | No |
Elixir's omission here is consistent with Ruby's partial support. Elixir's immutable binding model (x = f() is pattern match, not assignment) makes this architecturally different from OO languages. Acceptable for initial support.
2. No ForLoopExtractor — Enum.each Lambda Vars Untyped
Enum.each(users, fn user -> user.name end) — user's type is not inferred. Ruby has a ForLoopExtractor (for users.each do |user|). Elixir's idiom (anonymous function argument) is structurally different from Ruby's block parameter — the variable is inside a stab_clause (->) which is not in FUNCTION_NODE_TYPES, so findEnclosingScopeKey correctly bubbles up to the enclosing def. But the variable user has no scope binding. This would need a new ForLoopExtractor that scans Enum.* call arguments and binds the anonymous function's parameter to the element type.
3. use MyAppWeb, :controller Macro Indirection
The use heritage query captures the alias child of arguments, which works for use Phoenix.Controller. For use MyAppWeb, :controller, @heritage.extends = "MyAppWeb" — producing an EXTENDS edge to MyAppWeb rather than Phoenix.Controller. This is a fundamental macro-expansion gap: without evaluating __using__/1, the actual behaviour injection is invisible to static analysis. The fixtures use the expanded use Phoenix.Controller form to sidestep this. This limitation should be documented in the fixture but is otherwise architecturally unavoidable without macro evaluation.
4. Duplicate Node IDs for Same-Name Functions in defprotocol + defimpl
In my_server.ex, both defprotocol Printable do def print(data) end and defimpl Printable, for: MyServer do def print(server) end define a function print in the same file. Both get nodeId = generateId('Function', 'path/my_server.ex:print') = the same ID. The second registration overwrites the first in the symbol table.
This is a pre-existing limitation of the name-based ID scheme (no module/class qualifier in the node ID), which exists across all languages. Ruby with two classes in the same file would hit the same issue. Acceptable for now but worth documenting.
5. Pattern Binding in Function Heads Not Supported
def process(%User{} = user), do: user.name — user is type-bound via pattern match in the function head, but scanConstructorBinding only handles RHS patterns (user = %User{}). This is consistent with the matrix (Python/PHP/Ruby/Swift all show "No" for pattern binding). Acceptable.
🔍 Compiler-Perspective: Query Correctness Deep Dive
isElixirFunctionDef and Function Arity Ambiguity
// utils.ts:415-421
const firstArg = args.children?.find((c: any) => c.type === 'call');
if (firstArg) {
const funcTarget = firstArg.childForFieldName?.('target')
?? firstArg.children?.find((c: any) => c.type === 'identifier');
if (funcTarget?.type === 'identifier') return funcTarget.text;
}
return null;Issue: This finds the FIRST call child of arguments, which is the function signature. But def foo, do: bar (no-arg function using , syntax instead of do ... end) doesn't wrap the name in arguments as a call — it would be (identifier "foo"). In tree-sitter-elixir, def foo without parens parses as (call target: (identifier "def") (arguments (identifier "foo"))). The firstArg search finds c.type === 'call' but the arg is an identifier, not a call. So isElixirFunctionDef would return null for zero-arity functions defined without parens.
This means def init, do: :ok would NOT be recognized as a function definition by isElixirFunctionDef, and scope keying / call-processor enclosure detection would miss it. The tree-sitter query @definition.function DOES capture it (matching (call target: "def" (arguments (identifier) @name))... wait, actually the definition query only matches (arguments (call target: (identifier) @name)) — a call inside arguments. Zero-arity def foo has (identifier "foo") not a (call ...) in arguments.
Actually I need to re-check the definition query:
(call
target: (identifier) @_kw
(arguments
(call
target: (identifier) @name))
(#eq? @_kw "def")) @definition.function
This requires (arguments (call ...)). For def foo (no parens), arguments would contain (identifier "foo"), not a call. So def foo is NOT captured by the definition query.
Heritage Query: (do_block (call ...)) Direct-Child Semantics
The heritage queries like:
(call ... (do_block (call target: (identifier) @_use (arguments (alias) @heritage.extends) ...)) ...)
In tree-sitter queries, (parent (child)) matches when child is a direct child of parent. This means if use is nested inside a with, if, or quote block within the do_block, the query won't match. In practice Elixir use is always at module body level, but this structural constraint is worth noting.
More importantly, the query requires the call to be a direct child of do_block. In tree-sitter-elixir's grammar, do_block children can include call, unary_operator, binary_operator — all as direct children. The use GenServer IS a direct child of the module's do_block. So this works for the common case.
However, if there are multiple use statements, each creates a separate query match (since the query anchors to defmodule and descends into do_block looking for any matching use child). Each match correctly produces a separate EXTENDS edge. ✅
defdelegate CALLS Gap
defdelegate foo(arg), to: OtherModule creates a @definition.function node for foo but does not produce a CALLS edge to OtherModule.foo. The to: clause is invisible to call resolution. Users relying on delegate call graphs would see foo defined but no outbound edges. This is an acceptable limitation.
✅ What's Architecturally Sound
- Pipeline registration is complete:
supported-languages → parser-loader → parse-worker → LANGUAGE_QUERIES → callRouters → typeConfigs → resolvers/index → type-extractors/index— all wired. - Optional dependency pattern with try/catch fallback is correctly applied in both
parser-loader.tsandparse-worker.ts. routeElixirCallcorrectly returnsSKIP_RESULTfor def keywords (no spurious CALLS edges ondef/defp/defmodule/defimpl/etc.).countCallArgumentspipe support (utils.ts:853-859) correctly adds 1 for the implicit piped argument — matches Elixir's formal semantics wherex |> f()desugars tof(x).toSnakeCasedouble-regex correctly handlesHTTPClient → http_client,UserService → user_service, multi-char caps sequences. The edge caseOAuth2 → o_auth2is a minor deviation fromoauth2but unlikely to cause file resolution failures in practice.- Scope key format (
funcName@startIndex) is consistent with the shared format documented intype-resolution-system.md, ensuring constructor-binding verification works correctly. isElixirFunctionDefintype-env.ts:847-848correctly gates scope key generation: Elixir function scopes arecallnodes (not inFUNCTION_NODE_TYPES), so without this special case, all Elixir code would run at file scope.
Verdict
Ready to merge with the following caveats documented:
The previous critical blockers are all resolved. The current implementation correctly handles:
- Member CALLS edges (
Repo.insert→insertinrepo.ex) ✅ - HAS_METHOD edges from
defmodule✅ - IMPLEMENTS edges from
@behaviour(both alias and atom forms) ✅ - IMPLEMENTS edges from
defimpl ... for: Module✅ - EXTENDS edges from
use Module✅ - Heritage item for
@behaviour :gen_server(OTP atom form) ✅ __MODULE__self-reference ✅
One newly-identified correctness issue worth fixing before merge:
🟡 Zero-arity def foo without parens: The definition query and isElixirFunctionDef both require the function signature to be wrapped in a call node inside arguments. def init, do: :ok (zero-arity, no parens) won't be captured as a function definition or recognized as a scope boundary. This is rare in production Elixir (parens are strongly preferred by mix format) but does appear in OTP-style code. Worth adding a fallback query pattern.
All other remaining gaps are consistent with the language feature matrix in type-resolution-system.md and are acceptable for initial Elixir support.
· PR Branch
|
|
I'll revisit this in a few weeks, probably on a fresh branch after the prettier changes land. |
Core Registration & Parsing:
supported-languages.ts: Added Elixir = 'elixir' enum valueparser-loader.ts+parse-worker.ts: Registered tree-sitter-elixir as optional dependency with try/catch importpackage.json: Moved tree-sitter-elixir to optionalDependenciesutils.ts: Added .ex/.exs extension mapping, Elixir built-in names, isElixirFunctionDef() helper for enclosing function detectiontree-sitter-queries.ts: Added ELIXIR_QUERIES with#eq?predicates for defmodule, def/defp, defmacro/defmacrop, defstruct, defprotocol, defimpl, alias/import/require/use imports, qualified calls, and heritage (use/@behaviour)export-detection.ts: Added elixirExportChecker — walks up to enclosing call target to check def (public) vs defp (private)call-routing.ts: Extended HeritageItem to support 'use'/'behaviour' heritage kinds; Elixir uses noRouting since queries handle all dispatchtype-extractors/elixir.ts(new): Stub type config with @SPEC return type extraction and struct construction scanningresolvers/elixir.ts(new): CamelCase-to-snake_case module path resolution with multi-alias expansionImport Resolution & Call Routing:
import-processor.ts: Added Elixir branch callingresolveElixirImport()resolvers/utils.ts: Added .ex/.exs to EXTENSIONScall-processor.ts+type-env.ts: AddedisElixirFunctionDef()checks for enclosing function/scope detection (Elixir's def/defp are call nodes, not in FUNCTION_NODE_TYPES)Type Extraction:
@specreturn type extraction from precedingunary_operatornodes%User{}patterns)Phoenix Framework Detection:
Tests:
simple.exfixture: Module, def/defp, defmacro, defstruct, defprotocol, defimpl