Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3151d5c
fix(vapor): closure, webSocket and on(…) routes are routes (#2157)
colbymchenry Sep 30, 2026
9269f92
fix(aspnet): FastEndpoints routes, and minimal API paths get a leadin…
colbymchenry Sep 30, 2026
5ba3674
fix(resolution): a framework-resolved edge names its resolver (#2159)
colbymchenry Sep 30, 2026
7aa124c
fix(scala): a bare call reached by name alone must be in reach (#2160)
colbymchenry Sep 30, 2026
2d1b644
fix(extraction): Flow-typed JavaScript is read as TSX (#2161)
colbymchenry Sep 30, 2026
d03e4db
fix(resolution): an unexported ES module binding is not reachable cro…
colbymchenry Sep 30, 2026
cf449fd
fix(resolution): a name destructured from a composable reaches what i…
colbymchenry Sep 30, 2026
37bcd5c
fix(react-router): nested, lazy and constant routes; per-app tsconfig…
colbymchenry Sep 30, 2026
05740b5
fix(resolution): a call on an outside type is not a project method (#…
colbymchenry Sep 30, 2026
715c62b
fix(rust,go): read a call's shape before guessing its method (#2167)
colbymchenry Sep 30, 2026
b7b2a44
fix(kotlin): split primary constructors keep their class; std chain n…
colbymchenry Sep 30, 2026
791ae39
fix(resolution): a receiver declared with an outside type calls that …
colbymchenry Sep 30, 2026
a39fd14
fix(swift): read a call's shape before reaching a member by name (#2170)
colbymchenry Sep 30, 2026
f9530d7
fix(resolution): a call through a declared member calls that member's…
colbymchenry Sep 30, 2026
2e2b98c
fix(kotlin): a bare call reaches only a member in reach (#2172)
colbymchenry Sep 30, 2026
ef6262f
merge(upstream): reconcile #2157 through #2172 in the native resolver
bompus Sep 30, 2026
57a6c3d
fix(kotlin): retain DSL receivers and companion imports during reconc…
bompus Sep 30, 2026
2e0b2a0
fix(resolution): preserve declared targets and route boundaries
bompus Sep 30, 2026
4870612
fix(resolution): cap uncertain Kotlin chains and retain return positions
bompus Sep 30, 2026
ce9f281
fix(kotlin): bind return chains to their AST call sites
bompus Sep 30, 2026
630321d
fix(kotlin): retain scoped receiver types through fluent calls
bompus Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Follow [@getcodegraph](https://x.com/getcodegraph) on X for updates.

## About this fork

This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains all of upstream `main` (last merged: [`a5b0160`](https://github.com/colbymchenry/codegraph/commit/a5b0160e), after v1.6.1, 2026-09-30) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.
This is **bompus/codegraph**, a fork of [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph). Its default branch, `fork/consolidated`, contains all of upstream `main` (last merged: [`2e2b98c`](https://github.com/colbymchenry/codegraph/commit/2e2b98c9), after v1.6.1, 2026-09-30) plus the fork's own work, and it takes upstream changes as they land. Changes that suit upstream are also offered there as pull requests.

The fork publishes no releases. The install scripts, npm package, badges and `codegraph upgrade` further down this page install **upstream's** releases. To run the fork, build it from source (below).

Expand Down Expand Up @@ -114,6 +114,8 @@ Dispatch and framework coverage the fork adds, by kind:
| React Native `NativeModules[key]` | Computed native-module calls to the native method |
| `window.postMessage` | Posted messages to their listeners |

Flow-annotated JavaScript is parsed through the TSX grammar. Java and C# calls through declared fields or properties use their declared types; unresolved external types remain unresolved. Rust, Go, Scala, Swift and Kotlin calls also use the receiver and lexical scope at the call site. Kotlin receiver inference follows bounded chains of declared returns and verified receiver-preserving methods. Properties initialized by typed factory calls retain compatible imported extensions. Kotlin chains use the callee position and declared return type, including nested and multiline calls; imported return-type hypotheses for standard method names keep confidence at most 0.7 when the receiver type is unknown.

**Server endpoints**

| Addition | What it links |
Expand Down Expand Up @@ -500,8 +502,8 @@ CodeGraph detects web-framework routing files and emits `route` nodes linked by
| **Play** | `GET`/`POST`/… verb routes in `conf/routes` → `Controller.method` actions (Scala + Java), including projects kept in subdirectories |
| **Gin / chi / gorilla / mux** | `r.GET(...)`, `router.HandleFunc(...)` |
| **Axum / actix / Rocket** | `.route("/x", get(handler))` |
| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods |
| **Vapor** | `app.get("x", use: handler)` |
| **ASP.NET** | `[HttpGet("/x")]` attributes on action methods and FastEndpoints `Configure()` verb calls |
| **Vapor** | `app.get("x", use: handler)` and closure handlers |
| **Analog** | `src/app/pages/**/*.page.ts` files (`index`, dot segments, `[param]`, `[...rest]` and `(group)` names) bound to the page's default component class; a page with a same-named folder is a layout, not a route | — |
| **Astro** | `src/pages/` file-based routes (`.astro` pages + `.ts` endpoints, `[param]`/`[...rest]` syntax); each page links to its component, exported `GET`/`POST`/… endpoint methods link to their handlers, and `<a href>` / `Astro.redirect` link to the page they name |
| **RedwoodSDK** | Literal `defineApp([...])` trees with `route`, `index`, `render`, `layout` and `prefix`, plus `{ get, post, … }` method tables; each route links to its final handler and becomes a page once that handler is shown to return JSX | — |
Expand All @@ -514,7 +516,7 @@ These frameworks additionally emit **`navigates`** edges: the function that send
|---|---|---|
| **Expo Router** | Every screen file under `app/` (`app/item/[id].tsx` → `/item/[id]`, groups stripped), bound to its default-export component; `+api` files are endpoints (`GET /hello`) bound to their handlers | `router.push` / `replace` / `navigate`, template hrefs, `{ pathname }` objects, and a helper's returned href |
| **Next.js** | App Router `app/**/page.tsx` and Pages Router pages (`(group)` stripped, `[slug]` → `:slug`); `app/api/**/route.ts` exports and `pages/api/*` are endpoints, not screens | `router.push` / `replace` / `prefetch`, `redirect()` / `permanentRedirect()` in a server action or page, `NextResponse.redirect(new URL(…))` in middleware, `<Link href>` and internal `<a href>` |
| **React Router** | `<Route path component/element>` (v5 and v6), `createBrowserRouter([{ path, element }])`, framework mode's `app/routes.ts` (`route`, `index`, `layout`, `prefix`), and the default file convention under `app/routes/` (Remix, or React Router with `flatRoutes()`: dot nesting, index and pathless segments, `$param`, optional `($segment)` and `$` splats), each bound to its module's default component | `history.push` / `replace`, `useNavigate`'s `navigate`, a loader's `redirect`, `<Link to>` / `<NavLink to>` / `<Navigate to>` / v5's `<Redirect to>` / react-router-bootstrap's `<LinkContainer to>`, and a `styled(Link)` wrapper |
| **React Router** | `<Route path component/element>` (v5 and v6), `createBrowserRouter` / `createHashRouter` / `createMemoryRouter` arrays with nested `children`, constant paths, `Component` and lazy module exports, framework mode's `app/routes.ts` (`route`, `index`, `layout`, `prefix`), and the default file convention under `app/routes/` (Remix, or React Router with `flatRoutes()`: dot nesting, index and pathless segments, `$param`, optional `($segment)` and `$` splats), each bound to its module's default component | `history.push` / `replace`, `useNavigate`'s `navigate`, a loader's `redirect`, `<Link to>` / `<NavLink to>` / `<Navigate to>` / v5's `<Redirect to>` / react-router-bootstrap's `<LinkContainer to>`, and a `styled(Link)` wrapper |
| **TanStack Router** | `createFileRoute('/posts/$postId')` (file-based) and `createRoute({ path, getParentRoute })` composed up its parent chain (code-based); `_pathless` segments, `(group)` folders, `__root` and `<Outlet/>` layouts are not addresses; TanStack Start `server.handlers` (and `createHandlers`) in those files become method-qualified endpoints (`GET /api/users`) | `navigate({ to })`, a thrown `redirect({ to })`, `<Link to>` / `<Navigate to>` — where `to` is the route PATTERN and the values ride beside it in `params` |
| **Vue Router** / **Nuxt** | `createRouter({ routes: [...] })` / `new Router(...)` and the route tables it's given (`export const constantRoutes = [...]`, per-module route files), with the view each entry names — a lazy `() => import(…)` bound to its file — and `children` joined onto their parent's path, the parent being the layout around them; plus, in a Nuxt app, `pages/` file-based routes, each linked to its page component (`index` folders, root index pages, Nuxt 4 route groups), `server/api/` and `server/routes/` endpoints (method suffixes such as `.get.ts`, catch-alls) and route middleware | `router.push` / `replace`, `$router.push` / `this.$router.push`, Nuxt's `navigateTo`, `<router-link>` / `<RouterLink>` / `<NuxtLink>` — **by route name** (`push({ name: 'profile' })`) as well as by path |
| **Solid Router** | Imported `Router`/`Route` JSX and route-config arrays (`path`, `component`, `children`) with static `lazy(() => import(...))` components. A table exported from another file as `RouteDefinition[]` (the official template's `routes.ts`) is read too, prefixed by where it is registered | — |
Expand Down Expand Up @@ -554,7 +556,7 @@ Real iOS and React Native codebases live across multiple languages — a Swift c
| Expo Modules | expo-haptics | expo-camera | expo SDK sweep (7 packages) |
| Fabric / Paper views | [react-native-segmented-control](https://github.com/react-native-segmented-control/segmented-control) | [react-native-screens](https://github.com/software-mansion/react-native-screens) | [react-native-skia](https://github.com/Shopify/react-native-skia) |

Each bridge emits edges tagged `provenance:'heuristic'` with `metadata.synthesizedBy:` set to a stable channel name (e.g. `swift-objc-bridge`, `rn-event-channel`, `fabric-native-impl`, `expo-module-extract`), so the agent can tell at a glance how a hop got into the graph.
Every bridge hop says how it got into the graph. A hop matched by a bridge resolver carries `metadata.resolvedBy: 'framework'` and `metadata.framework` naming the resolver (`swift-objc-bridge`, `react-native-bridge`, `expo-modules-js`, `fabric-view`). A synthesized channel is tagged `provenance:'heuristic'` with `metadata.synthesizedBy` (`rn-event-channel`, `fabric-native-impl`).

---
## Quick Start
Expand Down
2 changes: 1 addition & 1 deletion __tests__/cross-language-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('cross-language name matches', () => {

it('keeps a Kotlin call onto a Java method (one JVM)', async () => {
project({
'src/Main.kt': 'fun main() {\n shout()\n}\n',
'src/Main.kt': 'import Loud.shout\n\nfun main() {\n shout()\n}\n',
'src/Loud.java': 'public class Loud {\n public static int shout() { return 1; }\n}\n',
});
expect(await targetsOf('main')).toContain('src/Loud.java:shout');
Expand Down
217 changes: 217 additions & 0 deletions __tests__/declared-member-receiver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* A C# / Java call through a field, property or parameter is a call on the
* type it is declared with — read from the class's own member declarations,
* those it inherits (with the type arguments the subclass gives), a using
* alias, or a generic or enhanced-for declaration — never a guess at a
* same-named method of some project class:
*
* - Newtonsoft's `_innerWriter.WriteValue(…)` inside TraceJsonWriter went to
* TraceJsonWriter's own `WriteValue`, and `_textWriter.Write(…)` on a
* `TextWriter` to a test's `ThrowingWriter.Write`;
* - a C# interface's members are public, so a call on an interface-typed
* field reaches the interface's method (eShop's `_fixUriService.Fix…()`);
* - `using Assert = …XUnitAssert;` makes `Assert.AreEqual` XUnitAssert's;
* - a standard .NET name on an untyped receiver (`table.Columns.Add(…)`) is
* not a project class's same-named method unless the receiver names it.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-member-receiver-'));
const files: Record<string, string> = {
'src/KnownClinic.java': `package app;
public class KnownClinic {
private app.Repository owners;
public void check() { this.owners.find(); }
}
`,
'src/ExternalClinic.java': `package app;
public class ExternalClinic {
private outside.Repository owners;
public void check() { this.owners.find(); }
}
`,
'src/Repository.java': `package app;
public interface Repository {
java.util.List<String> find();
}
`,
'src/Clinic.java': `package app;
public class Clinic {
protected Repository owners;
public void check() {
java.util.List<String> owners = this.owners.find();
}
}
`,
'src/Json/JsonWriter.cs': `namespace App.Json;

public abstract class JsonWriter
{
public virtual void WriteValue(string value) { }
}
`,
'src/Json/TraceJsonWriter.cs': `using System.IO;

namespace App.Json;

internal class TraceJsonWriter : JsonWriter
{
private readonly JsonWriter _innerWriter;
/* a block comment naming the _textWriter field */
private readonly TextWriter _textWriter;

public override void WriteValue(string value)
{
_innerWriter.WriteValue(value);
_textWriter.Write(value);
}
}
`,
'tests/ThrowingWriter.cs': `using System.IO;

namespace App.Tests;

public class ThrowingWriter : TextWriter
{
public override void Write(string value) { }
}
`,
'tests/XUnitAssert.cs': `namespace App.Tests;

public static class XUnitAssert
{
public static void AreEqual(object a, object b) { }
}
`,
'src/DefaultJsonNameTable.cs': `namespace App;

public class DefaultJsonNameTable
{
public void Add(string key) { }
}
`,
'src/Services/IFixUriService.cs': `namespace App.Services;

public interface IFixUriService
{
void FixBasketItemPictureUri(string uri);
}
`,
'src/Services/FixUriService.cs': `namespace App.Services;

public class FixUriService : IFixUriService
{
public void FixBasketItemPictureUri(string uri) { }
}
`,
'src/Services/BasketService.cs': `namespace App.Services;

public class BasketService
{
private readonly IFixUriService _fixUriService;

public void Load(string uri)
{
_fixUriService.FixBasketItemPictureUri(uri);
}
}
`,
'tests/Integration/IntegrationTest.cs': `namespace App.Tests.Integration;

public abstract class IntegrationTest<TFixture> where TFixture : new()
{
protected TFixture Fixture { get; private set; }
}

public class DropCreateDatabaseAlways
{
public object CreateContext() => null;
}
`,
'tests/Integration/QueryTests.cs': `using Assert = App.Tests.XUnitAssert;

namespace App.Tests.Integration;

public class QueryTests : IntegrationTest<QueryTests.DatabaseInitializer>
{
public class DatabaseInitializer : DropCreateDatabaseAlways { }

public void Runs()
{
var context = Fixture.CreateContext();
Assert.AreEqual(1, 1);
var table = MakeTable();
table.Columns.Add("price");
}
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

/** `Owner::member` of every call edge out of a file. */
function callsFrom(file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg
.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls')
.map((e) => cg.getNode(e.target)!.qualifiedName)
.sort();
}

describe('C# calls through declared members', () => {
it('reach the field’s declared type, and nothing for an outside one', () => {
expect(callsFrom('src/Json/TraceJsonWriter.cs')).toEqual(['App.Json::JsonWriter::WriteValue']);
});

it('reach an interface’s method: interface members are public', () => {
expect(callsFrom('src/Services/BasketService.cs')).toEqual([
'App.Services::IFixUriService::FixBasketItemPictureUri',
]);
});

it('follow an inherited property typed by the type argument, a using alias, and leave a .NET name alone', () => {
expect(callsFrom('tests/Integration/QueryTests.cs')).toEqual([
'App.Tests.Integration::DropCreateDatabaseAlways::CreateContext',
'App.Tests::XUnitAssert::AreEqual',
]);
});
});

describe('C# default visibility', () => {
it('is public in an interface and internal for a type in a namespace', () => {
const method = cg.getNodesInFile('src/Services/IFixUriService.cs').find((n) => n.kind === 'method')!;
expect(method.visibility).toBe('public');
const cls = cg.getNodesInFile('src/Json/TraceJsonWriter.cs').find((n) => n.kind === 'class')!;
expect(cls.visibility).toBe('internal');
});
});

it('keeps an explicit Java field receiver when a local shares its name', () => {
expect(callsFrom('src/Clinic.java')).toContain('app::Repository::find');
});

it('leaves a qualified external field type outside the project', () => {
expect(callsFrom('src/ExternalClinic.java')).not.toContain('app::Repository::find');
});

it('keeps the exact qualified project field type', () => {
expect(callsFrom('src/KnownClinic.java')).toContain('app::Repository::find');
});
88 changes: 88 additions & 0 deletions __tests__/destructured-call-result.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* A call through a name destructured from a call's result — a composable or
* a custom hook — reaches the function that callee returns under that key:
* `const { getDefaultActivityRoute } = useDefaultActivity()` (mealie, where the
* function is a module-level one the composable returns), `const { t } =
* useI18n()`, `const { login } = useAuth()` (declared in the hook's body).
* The local binding used to rule out every cross-file candidate.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

let root = '';
let cg: CodeGraph;

beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-destructured-'));
const files: Record<string, string> = {
'src/hooks/useNested.ts': `export function useNested() {
function hiddenAction() { }
function nested() { return { hiddenAction }; }
return undefined;
}
`,
'src/pages/nested.ts': `import { useNested } from '../hooks/useNested';
export function runNested() {
const { hiddenAction } = useNested();
hiddenAction();
}
`,
'package.json': JSON.stringify({ name: 'app', dependencies: { vue: '^3' } }),
'src/composables/use-default-activity.ts': `function getDefaultActivityRoute(key?: string): string {
return key ?? '/';
}
export default function useDefaultActivity() {
return { getDefaultActivityRoute };
}
`,
'src/hooks/useAuth.ts': `export function useAuth() {
function login(user: string) {
return user;
}
const logout = () => null;
return { login, signOut: logout };
}
`,
'src/pages/index.ts': `import useDefaultActivity from '../composables/use-default-activity';
import { useAuth } from '../hooks/useAuth';
export function go() {
const { getDefaultActivityRoute } = useDefaultActivity();
const { login, signOut: leave } = useAuth();
login('ada');
leave();
return getDefaultActivityRoute('x');
}
`,
};
for (const [rel, content] of Object.entries(files)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});

afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});

describe('names destructured from a call', () => {
it('reach what the callee returns under that key', () => {
const go = cg.getNodesInFile('src/pages/index.ts').find((n) => n.name === 'go')!;
const targets = cg
.getOutgoingEdges(go.id)
.filter((e) => e.kind === 'calls')
.map((e) => `${cg.getNode(e.target)!.filePath}:${cg.getNode(e.target)!.name}`);
expect(targets).toContain('src/composables/use-default-activity.ts:getDefaultActivityRoute');
expect(targets).toContain('src/hooks/useAuth.ts:login');
});
});

it('ignores actions returned only by a nested function', () => {
const run = cg.getNodesInFile('src/pages/nested.ts').find((n) => n.name === 'runNested')!;
const targets = cg.getOutgoingEdges(run.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name);
expect(targets).not.toContain('hiddenAction');
});
Loading
Loading