Skip to content

feat(dynamic-calls): Phase 6 Rust parity — add GetMethod/Invoke/apply detection to Rust extractors (C#, Swift, Elixir, Lua)#1670

Merged
carlos-alm merged 1 commit into
mainfrom
fix/issue-1656
Jun 21, 2026
Merged

feat(dynamic-calls): Phase 6 Rust parity — add GetMethod/Invoke/apply detection to Rust extractors (C#, Swift, Elixir, Lua)#1670
carlos-alm merged 1 commit into
mainfrom
fix/issue-1656

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

  • Add GetMethod/GetRuntimeMethod/GetDeclaredMethodreflection and Invokeunresolved-dynamic detection to csharp.rs, matching the TS handleCsInvocationExpr implementation
  • Add NSSelectorFromString (literal → reflection, computed → unresolved-dynamic) and performSelectorunresolved-dynamic to swift.rs, matching the TS handleSwiftCallExpression implementation
  • Add apply(mod, :atom, args)reflection and apply(mod, var, args)computed-key to elixir.rs, matching the TS apply case in handleElixirCall
  • Add load/loadstring/dofileeval and t[k]() (bracket_index_expression) → computed-key to lua.rs, matching the TS handleLuaFunctionCall implementation

Test plan

  • All 419 Rust unit tests pass (cargo test -p codegraph-core --lib)
  • Parser tests pass for all four languages (tests/parsers/csharp.test.ts, swift.test.ts, elixir.test.ts, lua.test.ts)
  • Cross-engine parity tests pass (tests/engines/parity.test.ts)
  • No regressions in native analysis tests

Closes #1656

… detection to Rust extractors

C# (csharp.rs): GetMethod/GetRuntimeMethod/GetDeclaredMethod → reflection (literal) or
computed-key; Invoke → unresolved-dynamic. Mirrors TS handleCsInvocationExpr.

Swift (swift.rs): NSSelectorFromString(literal) → reflection; NSSelectorFromString(expr) →
unresolved-dynamic; performSelector → unresolved-dynamic. Mirrors TS handleSwiftCallExpression.

Elixir (elixir.rs): apply(mod, :atom, args) → reflection; apply(mod, var, args) →
computed-key. Mirrors TS handleElixirCall apply case.

Lua (lua.rs): load/loadstring/dofile → eval; t[k]() bracket_index_expression → computed-key
(or resolves to literal if string key). Mirrors TS handleLuaFunctionCall.

Closes #1656
@github-actions

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

25 functions changed31 callers affected across 15 files

  • PurgeStmts.dataflowByCallEdge in src/db/repository/build-stmts.ts:9 (0 transitive callers)
  • preparePurgeStmts in src/db/repository/build-stmts.ts:29 (8 transitive callers)
  • runPurge in src/db/repository/build-stmts.ts:99 (8 transitive callers)
  • shouldSkipEntry in src/domain/graph/builder/helpers.ts:59 (2 transitive callers)
  • CollectContext.ignoreSet in src/domain/graph/builder/helpers.ts:143 (0 transitive callers)
  • walkCollect in src/domain/graph/builder/helpers.ts:183 (1 transitive callers)
  • collectFiles in src/domain/graph/builder/helpers.ts:230 (0 transitive callers)
  • resolveFallbackTargets in src/domain/graph/builder/stages/build-edges.ts:1012 (3 transitive callers)
  • tryNativeOrchestrator in src/domain/graph/builder/stages/native-orchestrator.ts:2089 (5 transitive callers)
  • shouldIgnorePath in src/domain/graph/watcher.ts:13 (3 transitive callers)
  • collectTrackedFiles in src/domain/graph/watcher.ts:143 (3 transitive callers)
  • WatcherContext.ignoreSet in src/domain/graph/watcher.ts:173 (0 transitive callers)
  • setupWatcher in src/domain/graph/watcher.ts:177 (2 transitive callers)
  • startPollingWatcher in src/domain/graph/watcher.ts:232 (2 transitive callers)
  • startNativeWatcher in src/domain/graph/watcher.ts:280 (2 transitive callers)
  • runInterproceduralStitch in src/features/dataflow.ts:488 (7 transitive callers)
  • buildInterproceduralStitch in src/features/dataflow.ts:604 (1 transitive callers)
  • buildDataflowVerticesFromMap in src/features/dataflow.ts:845 (0 transitive callers)
  • buildDataflowEdges in src/features/dataflow.ts:951 (4 transitive callers)
  • buildIgnoreSet in src/shared/constants.ts:41 (4 transitive callers)

@greptile-apps

greptile-apps Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR ports the Phase 6 dynamic-call detection patterns from the TypeScript WASM extractors into their Rust counterparts for C#, Swift, Elixir, and Lua. All four extractors gain new classification paths (reflection, unresolved-dynamic, computed-key, eval) that were previously missing in the native engine.

  • C#: GetMethod/GetRuntimeMethod/GetDeclaredMethod are resolved to reflection when the first argument is a string literal, otherwise computed-key; .Invoke() on any member expression maps to unresolved-dynamic.
  • Swift: NSSelectorFromString with a literal string maps to reflection; with a computed expression or performSelector maps to unresolved-dynamic.
  • Elixir: apply/3 with an atom second argument maps to reflection; with a variable maps to computed-key. The 2-arg form apply/2 is also dispatched here and will be classified as computed-key with the args-list text stored in key_expr, which is semantically incorrect metadata for that rare form.
  • Lua: load/loadstring/dofile emit eval sinks; t[k]() bracket-index calls are classified as computed-key (variable key) or resolved to a normal call (string literal key).

Confidence Score: 5/5

Safe to merge; all primary use-cases are handled correctly and no call data is silently dropped.

The changes are additive detection logic with no mutation of existing call-extraction paths. Each new branch is guarded by a specific name check and falls back gracefully. The one edge case (Elixir apply/2 storing the args-list text as key_expr) produces misleading metadata but does not lose the call or misidentify it as static.

elixir.rs — the apply/2 form (apply(fun, args)) is dispatched into the same handler as apply/3 and its second argument (the args list) ends up stored as key_expr.

Important Files Changed

Filename Overview
crates/codegraph-core/src/extractors/csharp.rs Adds GetMethod/GetRuntimeMethod/GetDeclaredMethod → reflection and Invoke → unresolved-dynamic detection; logic is correct. Refactored member_access_expression arm for clarity with no behavioral regressions.
crates/codegraph-core/src/extractors/swift.rs Adds NSSelectorFromString (literal → reflection, computed → unresolved-dynamic) and performSelector → unresolved-dynamic; simple_identifier case correctly merged into the _ arm; is_empty guard is a reasonable safety check.
crates/codegraph-core/src/extractors/elixir.rs Adds apply/3 (atom → reflection, variable → computed-key) detection; apply/2 form will misclassify the args-list as a function-selector key_expr, producing semantically incorrect metadata for that rare call pattern.
crates/codegraph-core/src/extractors/lua.rs Adds load/loadstring/dofile → eval and bracket_index_expression → computed-key detection; key extraction logic is correct for standard Lua bracket calls.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph csharp ["C# — handle_invocation_expr"]
        CS1[member_access_expression] --> CSI{method name?}
        CSI -->|Invoke| CS_UNR[unresolved-dynamic]
        CSI -->|GetMethod / GetRuntimeMethod / GetDeclaredMethod| CS_LIT{first arg is string literal?}
        CS_LIT -->|yes| CS_REF[reflection + key_expr]
        CS_LIT -->|no| CS_COMP[computed-key]
        CSI -->|other| CS_NORM[normal call]
    end
    subgraph swift ["Swift — match_swift_node call_expression"]
        SW1[call_expression fn_node] --> SW_NAME{call_name}
        SW_NAME -->|performSelector| SW_UNR[unresolved-dynamic]
        SW_NAME -->|NSSelectorFromString| SW_LIT{string literal arg?}
        SW_LIT -->|yes| SW_REF[reflection + key_expr]
        SW_LIT -->|no| SW_UNR2[unresolved-dynamic]
        SW_NAME -->|other| SW_NORM[normal call]
    end
    subgraph elixir ["Elixir — handle_elixir_apply"]
        EX1[apply call] --> EX_ARGS{arguments node?}
        EX_ARGS -->|none| EX_PLAIN[plain apply call]
        EX_ARGS -->|found| EX_2ND{second arg?}
        EX_2ND -->|none| EX_PLAIN
        EX_2ND -->|atom / atom_literal| EX_REF[reflection + fn_name]
        EX_2ND -->|other incl. lists| EX_COMP[computed-key]
    end
    subgraph lua ["Lua — handle_lua_function_call"]
        LUA1[function_call] --> LUA_ID{identifier?}
        LUA_ID -->|load / loadstring / dofile| LUA_EVAL[eval]
        LUA_ID -->|require| LUA_IMP[import]
        LUA1 --> LUA_BRACK[bracket_index_expression]
        LUA_BRACK --> LUA_KEY{key kind?}
        LUA_KEY -->|string / string_literal| LUA_NORM[normal call with receiver]
        LUA_KEY -->|variable / other| LUA_COMP[computed-key + key_expr]
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph csharp ["C# — handle_invocation_expr"]
        CS1[member_access_expression] --> CSI{method name?}
        CSI -->|Invoke| CS_UNR[unresolved-dynamic]
        CSI -->|GetMethod / GetRuntimeMethod / GetDeclaredMethod| CS_LIT{first arg is string literal?}
        CS_LIT -->|yes| CS_REF[reflection + key_expr]
        CS_LIT -->|no| CS_COMP[computed-key]
        CSI -->|other| CS_NORM[normal call]
    end
    subgraph swift ["Swift — match_swift_node call_expression"]
        SW1[call_expression fn_node] --> SW_NAME{call_name}
        SW_NAME -->|performSelector| SW_UNR[unresolved-dynamic]
        SW_NAME -->|NSSelectorFromString| SW_LIT{string literal arg?}
        SW_LIT -->|yes| SW_REF[reflection + key_expr]
        SW_LIT -->|no| SW_UNR2[unresolved-dynamic]
        SW_NAME -->|other| SW_NORM[normal call]
    end
    subgraph elixir ["Elixir — handle_elixir_apply"]
        EX1[apply call] --> EX_ARGS{arguments node?}
        EX_ARGS -->|none| EX_PLAIN[plain apply call]
        EX_ARGS -->|found| EX_2ND{second arg?}
        EX_2ND -->|none| EX_PLAIN
        EX_2ND -->|atom / atom_literal| EX_REF[reflection + fn_name]
        EX_2ND -->|other incl. lists| EX_COMP[computed-key]
    end
    subgraph lua ["Lua — handle_lua_function_call"]
        LUA1[function_call] --> LUA_ID{identifier?}
        LUA_ID -->|load / loadstring / dofile| LUA_EVAL[eval]
        LUA_ID -->|require| LUA_IMP[import]
        LUA1 --> LUA_BRACK[bracket_index_expression]
        LUA_BRACK --> LUA_KEY{key kind?}
        LUA_KEY -->|string / string_literal| LUA_NORM[normal call with receiver]
        LUA_KEY -->|variable / other| LUA_COMP[computed-key + key_expr]
    end
Loading

Reviews (2): Last reviewed commit: "feat(dynamic-calls): phase 6 Rust parity..." | Re-trigger Greptile

Comment on lines 2116 to +2127
const resultJson = ctx.nativeDb.buildGraph(
ctx.rootDir,
JSON.stringify(ctx.config),
JSON.stringify(ctx.aliases),
JSON.stringify(ctx.opts),
);
// Restore FK enforcement for JS post-passes (CHA, dataflow, structure).
try {
ctx.nativeDb.exec('PRAGMA foreign_keys = ON');
} catch {
// exec may not exist on very old addon versions — safe to ignore
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 FK enforcement left OFF on buildGraph throw

If buildGraph() throws (e.g. OOM, parse error, or binary crash), the PRAGMA foreign_keys = ON restoration is never reached. Every JS post-pass (CHA, dataflow, structure) that runs on the same ctx.nativeDb connection afterward would then execute without FK enforcement, potentially allowing inconsistent rows that should have been rejected. The fix is to wrap buildGraph() in a try/finally so the PRAGMA foreign_keys = ON executes unconditionally.

Suggested change
const resultJson = ctx.nativeDb.buildGraph(
ctx.rootDir,
JSON.stringify(ctx.config),
JSON.stringify(ctx.aliases),
JSON.stringify(ctx.opts),
);
// Restore FK enforcement for JS post-passes (CHA, dataflow, structure).
try {
ctx.nativeDb.exec('PRAGMA foreign_keys = ON');
} catch {
// exec may not exist on very old addon versions — safe to ignore
}
let resultJson: string;
try {
resultJson = ctx.nativeDb.buildGraph(
ctx.rootDir,
JSON.stringify(ctx.config),
JSON.stringify(ctx.aliases),
JSON.stringify(ctx.opts),
);
} finally {
// Restore FK enforcement for JS post-passes (CHA, dataflow, structure).
try {
ctx.nativeDb.exec('PRAGMA foreign_keys = ON');
} catch {
// exec may not exist on very old addon versions — safe to ignore
}
}

Fix in Claude Code

Comment on lines +267 to 277
if method_name == "Invoke" {
symbols.calls.push(Call {
name: node_text(&name, source).to_string(),
line: start_line(node),
dynamic: None,
name: "<dynamic:unresolved>".to_string(),
line: call_line,
dynamic: Some(true),
dynamic_kind: Some("unresolved-dynamic".to_string()),
receiver,
..Default::default()
});
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 All .Invoke() calls classified as unresolved-dynamic

The current check catches every receiver.Invoke(...) call, not just MethodInfo.Invoke. Standard delegate and lambda invocations (myFunc.Invoke(arg), Action.Invoke(), LINQ delegate chains) are all statically knowable but will now be flagged as unresolved-dynamic and emit sink edges. This is parity with the TS extractor, but worth verifying that C# code heavy on delegate .Invoke() patterns doesn't accumulate noisy sink edges that inflate unresolved-dynamic counts.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@carlos-alm carlos-alm merged commit e183376 into main Jun 21, 2026
29 checks passed
@carlos-alm carlos-alm deleted the fix/issue-1656 branch June 21, 2026 09:28
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(dynamic-calls): Phase 6 Rust parity — add GetMethod/Invoke/apply detection to Rust extractors (C#, Swift, Elixir, Lua)

1 participant