# Reference Resolution & Framework Routes

> How unresolved references become graph edges through import resolution, name matching, path aliases, framework detectors, and route resolvers for web frameworks across JavaScript, Python, Ruby, Java, Go, Rust, C#, Swift, PHP, Svelte, and Vue.

- Repository: colbymchenry/codegraph
- GitHub: https://github.com/colbymchenry/codegraph
- Human wiki: https://grok-wiki.com/public/wiki/colbymchenry-codegraph-89e8b2c4d43a
- Complete Markdown: https://grok-wiki.com/public/wiki/colbymchenry-codegraph-89e8b2c4d43a/llms-full.txt

## Source Files

- `src/resolution/index.ts`
- `src/resolution/types.ts`
- `src/resolution/import-resolver.ts`
- `src/resolution/name-matcher.ts`
- `src/resolution/path-aliases.ts`
- `src/resolution/frameworks/index.ts`
- `src/resolution/frameworks/express.ts`
- `__tests__/frameworks.test.ts`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [src/resolution/index.ts](src/resolution/index.ts)
- [src/resolution/types.ts](src/resolution/types.ts)
- [src/resolution/import-resolver.ts](src/resolution/import-resolver.ts)
- [src/resolution/name-matcher.ts](src/resolution/name-matcher.ts)
- [src/resolution/path-aliases.ts](src/resolution/path-aliases.ts)
- [src/resolution/frameworks/index.ts](src/resolution/frameworks/index.ts)
- [src/resolution/frameworks/express.ts](src/resolution/frameworks/express.ts)
- [src/resolution/frameworks/nestjs.ts](src/resolution/frameworks/nestjs.ts)
- [src/resolution/frameworks/python.ts](src/resolution/frameworks/python.ts)
- [src/resolution/frameworks/laravel.ts](src/resolution/frameworks/laravel.ts)
- [src/resolution/frameworks/ruby.ts](src/resolution/frameworks/ruby.ts)
- [src/resolution/frameworks/java.ts](src/resolution/frameworks/java.ts)
- [src/resolution/frameworks/go.ts](src/resolution/frameworks/go.ts)
- [src/resolution/frameworks/rust.ts](src/resolution/frameworks/rust.ts)
- [src/resolution/frameworks/csharp.ts](src/resolution/frameworks/csharp.ts)
- [src/resolution/frameworks/swift.ts](src/resolution/frameworks/swift.ts)
- [src/resolution/frameworks/svelte.ts](src/resolution/frameworks/svelte.ts)
- [src/resolution/frameworks/vue.ts](src/resolution/frameworks/vue.ts)
- [src/extraction/tree-sitter.ts](src/extraction/tree-sitter.ts)
- [src/extraction/index.ts](src/extraction/index.ts)
- [__tests__/frameworks.test.ts](__tests__/frameworks.test.ts)
- [__tests__/frameworks-integration.test.ts](__tests__/frameworks-integration.test.ts)
- [__tests__/resolution.test.ts](__tests__/resolution.test.ts)
- [docs/plans/2026-04-24-framework-resolver-extract.md](docs/plans/2026-04-24-framework-resolver-extract.md)
</details>

# Reference Resolution & Framework Routes

This page explains how CodeGraph turns unresolved references produced during extraction into graph edges. The core path is intentionally local and provider-neutral: source files are parsed, unresolved references are persisted, and `ReferenceResolver` later resolves them using framework-specific rules, import resolution, name matching, path aliases, and re-export chasing.

The route side of the system uses the same pipeline. Framework extractors create `route` nodes plus unresolved references from routes to handlers; those references then flow through the normal resolver, so a Django `path("users/", UserListView.as_view())`, Express `app.get('/users', listUsers)`, or Axum `.route("/users", get(list_users))` can become a graph edge instead of remaining text in a route file.

## Mental Model

```text
source file
  |
  | tree-sitter or custom extractor
  v
nodes + unresolvedReferences
  |
  | framework extractors may add route nodes + route->handler unresolved refs
  v
ReferenceResolver.resolveAll()
  |
  | 1. framework resolve()
  | 2. import resolve()
  | 3. name matcher
  v
ResolvedRef[]
  |
  | createEdges()
  v
graph edges with confidence + resolvedBy metadata
```

`FrameworkResolver.extract()` is the bridge between web framework syntax and ordinary graph resolution. It returns `{ nodes, references }`, where route nodes enter the node set and handler references enter the unresolved-reference set. Later, `CodeGraph.resolveReferences()` loads unresolved references from the database and persists resolved edges.

Sources: [src/resolution/types.ts:112-143](), [src/extraction/tree-sitter.ts:2480-2547](), [src/index.ts:565-578]()

## Resolver Context and Strategy Order

`ReferenceResolver` owns the resolution context used by all strategies. The context wraps database queries and filesystem reads with small caches for file nodes, file contents, import mappings, re-exports, symbol names, lower-case names, qualified names, known files, and project aliases. It warms lightweight file/name caches before resolving and clears them when the resolver is reinitialized.

The central `resolveOne()` order is:

| Order | Strategy | What it handles | Early return |
|---:|---|---|---|
| 0 | built-in/external skip | JS, React hooks, Python, Go, Pascal built-ins and similar external names | returns `null` |
| 1 | framework resolver | framework conventions such as services, controllers, routes, compiler macros | returns immediately at confidence `>= 0.9` |
| 2 | import resolver | local imports, aliases, namespace imports, default/named imports, barrels | returns immediately at confidence `>= 0.9` |
| 3 | name matcher | file-path, qualified-name, method-call, exact-name, fuzzy matches | participates in best-candidate selection |

The resolver also uses a pre-filter: if a reference name is not in the known symbol-name set and does not match a local import, it skips the expensive strategies. The import escape matters because barrel re-export chains can expose a name that has no declaration under that exact imported name.

Sources: [src/resolution/index.ts:119-151](), [src/resolution/index.ts:159-185](), [src/resolution/index.ts:190-320](), [src/resolution/index.ts:390-496]()

## From Resolved References to Graph Edges

A successful resolution produces a `ResolvedRef` with a target node id, confidence, and `resolvedBy` method. `createEdges()` then converts those into graph edges and keeps the original reference line and column. Two semantic promotions happen at this final step:

| Original edge kind | Target shape | Final edge kind | Why |
|---|---|---|---|
| `extends` | interface or protocol | `implements` | class/struct inheritance syntax often targets an interface-like symbol |
| `calls` | class or struct | `instantiates` | languages such as Python and Ruby use `Foo()` for object construction |

The output edge metadata keeps `confidence` and `resolvedBy`, which lets downstream callers understand whether an edge came from imports, a framework rule, an exact name match, fuzzy matching, and so on.

Sources: [src/resolution/types.ts:31-60](), [src/resolution/index.ts:498-540](), [__tests__/resolution.test.ts:610-647](), [__tests__/resolution.test.ts:664-689]()

## Import Resolution

`resolveImportPath()` first classifies external imports, then resolves relative imports or aliased/absolute imports. For JS/TS, external classification checks Node built-ins but consults project aliases before treating bare specifiers as npm packages, so prefixes like `@utils/*` can still resolve locally.

Supported extension resolution is language-specific. JS/TS tries source files and index files, Python tries `.py` and `__init__.py`, Rust tries `.rs` and `mod.rs`, and Java, C#, PHP, Ruby, and Go have their expected source extensions.

Import mapping extraction is regex-based and currently covers JS/TS `import` and `require`, Python `from/import`, Go import declarations, and PHP `use` statements. `resolveViaImport()` matches the local reference name against those mappings, resolves the mapped source path, and then looks for the exported symbol in the resolved file.

Sources: [src/resolution/import-resolver.ts:12-56](), [src/resolution/import-resolver.ts:66-112](), [src/resolution/import-resolver.ts:146-202](), [src/resolution/import-resolver.ts:204-225](), [src/resolution/import-resolver.ts:230-328](), [src/resolution/import-resolver.ts:331-452](), [src/resolution/import-resolver.ts:585-636]()

### Path Aliases

Path aliases are loaded from root `tsconfig.json` or `jsconfig.json`. The loader parses JSON-with-comments, reads `compilerOptions.baseUrl` and `compilerOptions.paths`, supports TypeScript-style `*` wildcards, sorts patterns by specificity, and deliberately does not follow `extends` chains or bundler configs yet.

When an alias matches, `applyAliases()` returns candidate project-relative paths in replacement priority order. The import resolver then applies the language extension list to each candidate. If no project alias applies, the resolver still falls back to conventional aliases such as `@/`, `~/`, `@src/`, `src/`, `@app/`, and `app/`.

Sources: [src/resolution/path-aliases.ts:1-24](), [src/resolution/path-aliases.ts:30-55](), [src/resolution/path-aliases.ts:57-122](), [src/resolution/path-aliases.ts:138-200](), [src/resolution/path-aliases.ts:202-242](), [src/resolution/import-resolver.ts:173-202](), [__tests__/resolution.test.ts:715-783]()

### Re-export Chains

Barrel files are resolved by `findExportedSymbol()`. It first checks direct exported declarations in the resolved file. If none match, it asks the context for JS/TS re-exports and follows named or wildcard exports recursively, with a depth cap and a visited set to avoid cycles. Named re-export aliases are handled by chasing the original upstream name.

```typescript
// src/resolution/import-resolver.ts
export { signIn as login } from './auth';
// an import of "login" is chased upstream as "signIn"
```

Tests cover both wildcard barrel chains and renamed re-exports, proving that imports through `all.ts` or `index.ts` can still attach callers to the original declaration.

Sources: [src/resolution/import-resolver.ts:515-583](), [src/resolution/import-resolver.ts:638-730](), [__tests__/resolution.test.ts:786-847]()

## Name Matching

The name matcher is the general fallback after framework and import strategies. It tries:

| Strategy | Example | Confidence behavior |
|---|---|---|
| file path | `snippets/drawer-menu.liquid` | exact path, suffix path, or only-file fallback |
| qualified name | `User.save`, `User::save` | exact qualified name first, then suffix-style qualified match |
| method call | `obj.method`, `Class::method` | class-local method, capitalized receiver, then receiver/class word overlap |
| exact name | `navigate` | unique match, or best scored candidate among duplicates |
| fuzzy | case-insensitive callable lookup | last resort, lower confidence |

When duplicate names exist, `findBestMatch()` scores candidates by same file, directory proximity, same language, reference kind bias, export status, and line proximity. This is why a Python monorepo can prefer `apps/app_a/src/server.py::navigate` when the caller is also in `apps/app_a`.

Sources: [src/resolution/name-matcher.ts:10-63](), [src/resolution/name-matcher.ts:65-147](), [src/resolution/name-matcher.ts:149-278](), [src/resolution/name-matcher.ts:290-397](), [src/resolution/name-matcher.ts:399-463](), [__tests__/resolution.test.ts:38-144]()

## Framework Registry and Detection

All framework resolvers are registered in one array. The registry includes PHP Laravel; JS/TS Express, NestJS, React, Svelte, and Vue; Python Django, Flask, and FastAPI; Ruby Rails; Java Spring; Go; Rust; C# ASP.NET; and SwiftUI, UIKit, and Vapor. `detectFrameworks()` runs each resolver’s project-level `detect()` safely and ignores resolver exceptions. `getApplicableFrameworks()` filters detected resolvers by file language, treating resolvers without a `languages` list as universal.

Framework detection happens once per indexing run in `ExtractionOrchestrator`. The orchestrator builds a filesystem-backed detection context before graph nodes exist, stores detected framework names, and passes them into each parse call. Worker-thread parsing receives those names too, so framework extraction is not limited to the in-process fallback path.

Sources: [src/resolution/frameworks/index.ts:23-105](), [src/extraction/index.ts:437-507](), [src/extraction/index.ts:546-553](), [src/extraction/index.ts:692-731](), [src/extraction/parse-worker.ts:58-67]()

## Route Extraction by Framework

Framework route extractors generally follow the same pattern: strip comments, find route syntax, create a `route` node, then create an unresolved `references` or `imports` entry from the route node to the handler symbol. UI-file-routing frameworks may emit route nodes without handler references.

| Language | Resolver | Route shapes extracted | Handler reference behavior |
|---|---|---|---|
| JavaScript/TypeScript | Express | `app`/`router` HTTP methods and `use` with path | links the last handler argument, treating earlier handlers as middleware |
| JavaScript/TypeScript | NestJS | HTTP decorators, GraphQL ops, message/event patterns, WebSocket messages | links to the decorated method; joins controller prefix with method path |
| JavaScript/TypeScript | React/Next | pages/app default-export routes | emits route/component nodes; no route-handler references yet |
| Python | Django | `path`, `re_path`, `url`, `include` | links to view class/function or imports included module |
| Python | Flask/FastAPI | route decorators | links to decorated function |
| PHP | Laravel | `Route::method`, `resource`, `apiResource` | links to method or controller class depending on syntax |
| Ruby | Rails | `controller#action` route declarations | links to action name |
| Java | Spring | mapping annotations | links to the next Java method declaration |
| Go | Gin/Echo/Fiber/Chi/net/http-like route calls | links to tail identifier of handler expression |
| Rust | Actix/Rocket attributes and Axum `.route()` | links to following function or Axum handler |
| C# | ASP.NET attributes and Minimal APIs | links to next method or handler identifier |
| Swift | Vapor | `app/router/routes.method(..., use: handler)` | links to last handler segment |
| Svelte | SvelteKit | route files under `src/routes` | emits route nodes; also resolves runes, stores, and `$lib` imports |
| Vue | Nuxt | `pages/` and `server/api/` files | emits route nodes; also resolves macros, auto-imports, virtual modules, and aliases |

Sources: [src/resolution/frameworks/express.ts:101-146](), [src/resolution/frameworks/nestjs.ts:107-187](), [src/resolution/frameworks/python.ts:41-90](), [src/resolution/frameworks/python.ts:139-192](), [src/resolution/frameworks/laravel.ts:95-176](), [src/resolution/frameworks/ruby.ts:90-132](), [src/resolution/frameworks/java.ts:121-169](), [src/resolution/frameworks/go.ts:83-132](), [src/resolution/frameworks/rust.ts:92-172](), [src/resolution/frameworks/csharp.ts:119-202](), [src/resolution/frameworks/swift.ts:337-384](), [src/resolution/frameworks/react.ts:77-173](), [src/resolution/frameworks/svelte.ts:148-179](), [src/resolution/frameworks/vue.ts:190-220]()

## Framework-specific Reference Resolution

Framework `resolve()` methods handle conventions that are not explicit imports. Examples include Express middleware names, controller methods, service/helper method references, Laravel `Model::method()` and `Controller@method`, Rails model/controller/helper/service conventions, Spring service/repository/controller/entity naming, Go handler/service/middleware/model naming, Rust handler/service/struct/module naming, ASP.NET controller/service/repository/model/view-model naming, and Swift view/controller/model/middleware naming.

Some framework resolvers intentionally resolve framework-provided names back to the source node rather than user code. Svelte runes and SvelteKit `$app`/`$env` modules are compiler/framework-provided. Vue compiler macros, Nuxt auto-imports, and Nuxt virtual modules are handled similarly. This avoids wasting resolver work on symbols that should not have local declarations.

Sources: [src/resolution/frameworks/express.ts:54-99](), [src/resolution/frameworks/laravel.ts:47-93](), [src/resolution/frameworks/ruby.ts:34-88](), [src/resolution/frameworks/java.ts:52-119](), [src/resolution/frameworks/go.ts:27-81](), [src/resolution/frameworks/rust.ts:31-90](), [src/resolution/frameworks/csharp.ts:50-117](), [src/resolution/frameworks/swift.ts:37-78](), [src/resolution/frameworks/swift.ts:158-212](), [src/resolution/frameworks/swift.ts:294-335](), [src/resolution/frameworks/svelte.ts:69-146](), [src/resolution/frameworks/vue.ts:103-188]()

## Rust Workspace Module Resolution

Rust has one extra resolver helper for workspace crates. `getCargoWorkspaceCrateMap()` parses the root `Cargo.toml`, expands workspace members, reads member package names, and maps both hyphenated and underscore crate names to member directories. The Rust resolver checks local `src/name.rs` and `src/name/mod.rs`, then workspace member `src/lib.rs` or `src/main.rs`. Workspace hits get higher confidence so cross-crate imports can beat accidental same-file name matches.

Sources: [src/resolution/frameworks/cargo-workspace.ts:1-12](), [src/resolution/frameworks/cargo-workspace.ts:142-163](), [src/resolution/frameworks/cargo-workspace.ts:169-224](), [src/resolution/frameworks/rust.ts:71-87](), [src/resolution/frameworks/rust.ts:215-239](), [__tests__/frameworks.test.ts:544-610]()

## Tests Worth Reading First

For a first 30 minutes in this area, read the tests in this order:

1. `__tests__/frameworks.test.ts`: unit fixtures for `FrameworkResolver.extract()`, applicable-framework filtering, route extraction across frameworks, NestJS decorator edge cases, Laravel/Rails/Spring/Go/Rust examples, and Rust workspace crates.
2. `__tests__/frameworks-integration.test.ts`: end-to-end Django proof that a route node links to `UserListView`.
3. `__tests__/resolution.test.ts`: name matching, import path resolution, framework detection, edge kind promotion, path aliases, and re-export chains.

The planning doc is useful historical context because it states the intended architecture: replace a dead route-node-only hook with `extract()` returning both framework nodes and unresolved references, then let the existing resolver produce final edges. Treat the current code and tests as the source of truth where they differ.

Sources: [__tests__/frameworks.test.ts:5-39](), [__tests__/frameworks.test.ts:43-176](), [__tests__/frameworks.test.ts:178-457](), [__tests__/frameworks.test.ts:459-542](), [__tests__/frameworks-integration.test.ts:20-58](), [__tests__/resolution.test.ts:272-354](), [__tests__/resolution.test.ts:715-847](), [docs/plans/2026-04-24-framework-resolver-extract.md:5-9]()

## Extension Guidance

To add or change a resolver, keep the architecture portable: implement a local `FrameworkResolver` that depends on `ResolutionContext`, file contents, and indexed nodes rather than a hosted model, proprietary service, or vendor-specific connector. Register it in `src/resolution/frameworks/index.ts`, give it a `languages` list when possible, write small route extraction fixtures, and add at least one resolution fixture if the resolver has convention-based `resolve()` behavior.

For route support, prefer this contract:

```typescript
extract(filePath, content) {
  return {
    nodes: [routeNode],
    references: [{
      fromNodeId: routeNode.id,
      referenceName: handlerName,
      referenceKind: 'references',
      filePath,
      language,
      line,
      column: 0,
    }],
  };
}
```

That keeps framework knowledge at the edge of extraction while preserving the shared import/name/framework resolution pipeline for final graph edges.

Sources: [src/resolution/types.ts:123-143](), [src/resolution/frameworks/index.ts:96-105](), [src/extraction/tree-sitter.ts:2522-2545](), [docs/plans/2026-04-24-framework-resolver-extract.md:21-32]()

In short: CodeGraph resolves references by combining local project facts, framework conventions, import graphs, alias rules, and name matching. Framework routes are not a separate graph mechanism; they are source-derived `route` nodes plus ordinary unresolved references that become edges through the same resolver used for code symbols. Sources: [src/resolution/index.ts:450-496](), [src/extraction/tree-sitter.ts:2522-2547](), [__tests__/frameworks-integration.test.ts:40-55]()
