# How vitali87/code-graph-rag Resolves Function Calls Across the Knowledge Graph

> Discover how vitali87/code-graph-rag resolves function calls using a trie-backed registry and a multi-stage resolver for precise CALLS edges. Learn the techniques for accurate function lookup and disambiguation.

- Repository: [Vitali Avagyan/code-graph-rag](https://github.com/vitali87/code-graph-rag)
- Tags: deep-dive
- Published: 2026-08-16

---

**Code‑graph‑rag resolves function calls by building a trie‑backed function registry that maps qualified names (QNs) to graph nodes, then using a multi‑stage resolver that performs exact lookups, simple‑name disambiguation, prefix‑and‑suffix matching, and return‑type inference to emit precise CALLS edges.**

The `vitali87/code-graph-rag` project constructs a **knowledge graph** where every function, method, class, module, and package becomes a node. When the parser encounters a call expression, it must map that call to its target QN—a process that spans simple identifiers, fully qualified names, and complex factory patterns. This article examines how the resolution engine works, from trie construction to edge emission, using actual source paths from the repository.

---

## Building the Foundation: The FunctionRegistryTrie

Every definition in the parsed codebase enters a **trie‑backed registry** defined in [`codebase_rag/function_registry.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/function_registry.py). The `FunctionRegistryTrie` class provides hierarchical storage and multiple auxiliary indices for fast lookup.

### Core Data Structures

The trie stores QNs in a nested dictionary (`root`) while maintaining parallel indices:

- **`_entries`** — direct map of `QN → NodeType` (FUNCTION, CLASS, METHOD, etc.)
- **`_simple_name_lookup`** — maps bare names to sets of possible QNs
- **`_ending_with_cache`** — caches results for suffix‑based queries
- **`_duplicates`** and **`_variant_columns`** — handle same‑line duplicate definitions
- **`_properties`**, **`_abstracts`**, **`_callable_params`** — mark special callable attributes

During parsing, the `insert` method walks the QN parts (split by dots), creates missing trie nodes, and populates all indices. This upfront indexing is what makes cross‑reference resolution fast enough for large monorepos.

---

## The Resolution Pipeline: From Call Text to QN

When [`codebase_rag/parsers/call_processor.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/call_processor.py) encounters a call expression, it delegates to the resolver via:

```python
resolved = self._resolver.resolve_function_call(
    text, module_qn, local_var_types, class_context, caller_qn
)

```

The resolver implements a **fallback cascade**—each stage attempts resolution, and success short‑circuits the rest.

### Stage 1: Exact QN Lookup

If the call text is already a fully qualified name (e.g., `myproject.utils.do_work`), the resolver queries `registry.get(qn)` directly. This is the fastest path and handles explicit imports and fully specified calls.

### Stage 2: Simple‑Name Disambiguation

For bare identifiers like `process`, the resolver consults `_simple_name_lookup`. Two outcomes are possible:

- **Unique match** — the single QN is returned immediately
- **Multiple candidates** — the resolver uses `local_var_types` (built by `build_local_variable_type_map`) and `class_context` to filter based on the caller's scope

This local context is critical for languages with implicit `self` or closure captures.

### Stage 3: Prefix + Suffix Matching

Chained access patterns like `a.method()` where `a` is an instance of `pkg.subpkg.Class` require **trie navigation**. The resolver calls `find_with_prefix_and_suffix(prefix, suffix)`:

1. Navigate to the node matching the prefix (`pkg.subpkg.Class`)
2. Collect all QNs in that subtree ending with the suffix (`.method`)
3. Return the matching method's QN

This handles the common case where the left side is a module or package and the right side is a method name.

### Stage 4: Factory Inference

The call processor tracks factory patterns through `_factory_calls`. When it sees:

```python
wrapper = factory()
wrapper.do()

```

It records the factory binding and later resolves the factory's QN to the concrete type it produces. The resolver may invoke `_infer_free_function_return_type` to determine the return type when static analysis alone is insufficient.

### Stage 5: Bound Callable Peeling

For languages with explicit bound references (method groups in C#, `fn.bind(this)` in JavaScript), the resolver uses `_peel_bound_callable` to extract the underlying function QN before lookup.

### Output: CALLS Edge Creation

Successful resolution returns `(NodeLabel, resolved_qn)`. The `CallProcessor` then emits a **CALLS** relationship (defined in [`codebase_rag/types_defs.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/types_defs.py) as `RelationshipType.CALLS`) between `caller_qn` and the resolved callee, ingested into Neo4j by [`codebase_rag/graph_updater.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/graph_updater.py).

---

## Code Examples: Resolution in Practice

### Exact Qualified Name Resolution

```python

# Registration during parsing

caller_qn = "myproject.utils.helper"
callee_qn = "myproject.utils.do_work"
registry[callee_qn] = cs.NodeType.FUNCTION

# Resolution of fully qualified call

resolved = resolver.resolve_function_call(
    "myproject.utils.do_work",
    "myproject.utils",
    None,
    None,
    caller_qn
)

# Result: (NodeLabel.FUNCTION, "myproject.utils.do_work")

```

### Simple‑Name Disambiguation with Local Context

```python

# Registry contains one "process" method

registry["myproject.services.Service.process"] = cs.NodeType.FUNCTION

# Local variable map provides type context

local_vars = {"svc": "myproject.services.Service"}

resolved = resolver.resolve_function_call(
    "process",
    "myproject.services",
    local_vars,
    None,
    caller_qn
)

# Result: (NodeLabel.FUNCTION, "myproject.services.Service.process")

```

### Prefix + Suffix Method Resolution

```python

# Class and method registered

registry["pkg.subpkg.Class"] = cs.NodeType.CLASS
registry["pkg.subpkg.Class.method"] = cs.NodeType.METHOD

# Instance method call: a.method() where a: pkg.subpkg.Class

resolved = resolver.resolve_function_call(
    "method",
    "pkg.subpkg.Class",
    None,
    "pkg.subpkg.Class",
    caller_qn
)

# Trie finds prefix "pkg.subpkg.Class", suffix ".method"

# Result: (NodeLabel.METHOD, "pkg.subpkg.Class.method")

```

### Factory Return‑Type Inference

```python

# Factory function registered

registry["myproject.factories.factory"] = cs.NodeType.FUNCTION

# Call to factory—resolver infers concrete return type

resolved = resolver.resolve_function_call(
    "factory",
    "myproject.factories",
    None,
    None,
    caller_qn
)

# Result: QN of concrete type produced by factory, used for subsequent CALLS edges

```

---

## Cross‑Language Consistency

The resolver is **stateless** apart from the shared `FunctionRegistryTrie`. All language frontends—Python ([`codebase_rag/parsers/py/ast_analyzer.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/py/ast_analyzer.py)), TypeScript, Rust, Go, C++, and others—feed into the same registry and use the same resolution logic.

This design guarantees that **a CALLS edge from Python to a Rust FFI function** resolves identically to an edge within a single language, enabling accurate cross‑language dependency analysis in polyglot codebases.

---

## Key Files and Their Roles

| File | Purpose |
|------|---------|
| [`codebase_rag/function_registry.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/function_registry.py) | `FunctionRegistryTrie` with `find_with_prefix_and_suffix`, `find_ending_with`, and direct lookups |
| [`codebase_rag/parsers/call_processor.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/call_processor.py) | Orchestrates call‑site analysis and resolver invocation |
| [`codebase_rag/parsers/py/ast_analyzer.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/py/ast_analyzer.py) | Python‑specific class resolution, `self`/`cls` handling, return‑type inference |
| [`codebase_rag/parsers/frontends.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/parsers/frontends.py) | Supplies AST nodes and query cursors for tree‑sitter parsing |
| [`codebase_rag/types_defs.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/types_defs.py) | Schema constants: `NodeType`, `RelationshipType.CALLS` |
| [`codebase_rag/graph_updater.py`](https://github.com/vitali87/code-graph-rag/blob/main/codebase_rag/graph_updater.py) | Coordinates parsing, registry population, and Neo4j ingestion |

---

## Summary

- **Knowledge graph construction** relies on a trie‑backed registry in [`function_registry.py`](https://github.com/vitali87/code-graph-rag/blob/main/function_registry.py) that indexes all qualified names with multiple lookup strategies.
- **Function call resolution** follows a six‑stage cascade: exact QN, simple‑name with disambiguation, prefix‑suffix matching, factory inference, bound‑callable peeling, and return‑type fallback.
- **CALLS edge emission** connects caller and callee QNs in Neo4j, enabling graph traversal for impact analysis and code search.
- **Cross‑language support** emerges from sharing one stateless resolver across all parser frontends.

---

## Frequently Asked Questions

### How does code-graph-rag handle ambiguous simple names?

The resolver checks `_simple_name_lookup` for all candidates, then uses `local_var_types` (variable‑to‑type mappings from the caller's scope) and `class_context` to filter to the correct target. If ambiguity remains, it falls back to return‑type inference or flags the call for manual review.

### What happens when a factory function returns different types conditionally?

The resolver records factory bindings in `_factory_calls` and attempts `_infer_free_function_return_type`. For conditional returns, the engine may emit edges to multiple concrete types or to an abstract base class, depending on static analysis depth. The `CallProcessor` can be configured with heuristics for conservative or aggressive factory expansion.

### Can the trie registry handle very large codebases?

Yes—the hierarchical trie structure provides O(prefix length) navigation, and the auxiliary indices (`_simple_name_lookup`, `_ending_with_cache`) trade memory for speed. The implementation in [`function_registry.py`](https://github.com/vitali87/code-graph-rag/blob/main/function_registry.py) is designed for incremental updates, allowing the registry to grow as new files are parsed without rebuilding from scratch.

### How does prefix + suffix resolution differ from exact QN lookup?

**Exact QN lookup** requires the full qualified name as text—fastest but requires fully specified imports. **Prefix + suffix resolution** navigates to a known scope (the prefix, often a class or module) then searches for members matching the suffix. This handles the common pattern `instance.method()` where only the instance's type is known, not the method's full QN.