Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .claude/skills/agent-eval/corpus.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@
{ "name": "expo-haptics", "repo": "https://github.com/expo/expo/tree/main/packages/expo-haptics", "size": "Small", "files": "~15", "question": "How does `Haptics.notificationAsync(...)` in JS reach `UINotificationFeedbackGenerator` in the Swift Module?" },
{ "name": "expo-camera", "repo": "https://github.com/expo/expo/tree/main/packages/expo-camera", "size": "Medium", "files": "~70", "question": "How does a JS `CameraView.takePictureAsync(options)` reach the native AVCaptureSession / CameraDevice call?" }
],
"ReScript": [
{ "name": "rescript-core", "repo": "https://github.com/rescript-lang/rescript-core", "size": "Small", "files": "~100", "question": "How does rescript-core implement the Array module's map and reduce functions?" },
{ "name": "rescript-relay", "repo": "https://github.com/zth/rescript-relay", "size": "Medium", "files": "~250", "question": "How does rescript-relay transform a GraphQL query into typed ReScript modules at build time?" },
{ "name": "rescript", "repo": "https://github.com/rescript-lang/rescript", "size": "Large", "files": "~3500", "question": "How does the ReScript standard library implement the Belt.Array utility functions?" }
],
"React Native Fabric (view components)": [
{ "name": "react-native-segmented-control", "repo": "https://github.com/react-native-segmented-control/segmented-control", "size": "Small", "files": "~25", "question": "How does JSX `<SegmentedControl onChange={cb}/>` reach the native onChange handler on iOS/Android?" },
{ "name": "react-native-screens", "repo": "https://github.com/software-mansion/react-native-screens", "size": "Medium", "files": "~1200", "question": "How does JSX `<ScreenStack>` reach the native RNSScreenStackView component?" },
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- Cross-file impact and blast-radius coverage now spans **all 22 supported languages and 14 web frameworks**, each validated on a real-world repo — see the new coverage table in the README. This release ships the cross-file resolution behind it, including Lua and Luau `require`, Shopify OS 2.0 Liquid section templates, Delphi form code-behind, Rust cross-module calls and Rocket route macros, Swift Fluent relationships, and the SvelteKit / Nuxt / Vapor / Axum route conventions. The residual everywhere is genuine static-analysis frontiers (runtime dispatch, reflection / DI, framework-convention entry points), never hidden.
- C# types are now tracked by their namespace-qualified name. Same-named types in different namespaces — a domain entity and a DTO both called `CatalogBrand`, say — are told apart instead of collapsing into one arbitrary match, so a reference resolves to the right one and impact no longer conflates them. (C#)
- ASP.NET Razor (`.cshtml`) and Blazor (`.razor`) markup are now parsed for code relationships. A `@model` / `@inherits` / `@inject` directive links the view to the C# view-model, base type, or service it names; a Blazor `<MyComponent/>` tag (plus `@typeof(...)` and generic `TItem="..."` arguments) links to the component class; and the C# inside `@code { }` / `@functions { }` / `@{ }` blocks is analyzed too, so services and types used in component logic are linked. A view-model, component, or service referenced only from markup is no longer reported as having no dependents, and editing it surfaces the views that use it. (ASP.NET, Blazor)
- CodeGraph now indexes **ReScript** (`.res`) — functions, modules, records, variants, imports, and call edges. Tested on the ReScript compiler, rescript-core, and rescript-relay.
- A Razor/Blazor type reference now resolves through the component's `@using` namespaces — including the folder's cascading `_Imports.razor` — so a simple name that exists in several namespaces lands on the right one. A `@model` / `<MyComponent>` / `@code` reference to `CatalogBrand` resolves to the `@using`'d DTO (`BlazorShared.Models.CatalogBrand`) rather than a same-named domain entity. (ASP.NET, Blazor)
- `codegraph status --json` now also reports the running CLI `version`, the index directory (`indexPath`), and a `lastIndexed` timestamp (ISO-8601, or null when nothing's indexed yet), so CI and scripts can pin the CLI version and check index freshness from a single command. A matching `CodeGraph.getLastIndexedAt()` library method exposes the same freshness check without shelling out. Thanks @12122J and @eddieran. (#329)
- TypeScript service/RPC contracts defined as a tuple of generic types — `type MyServiceList = [Service<'query_apply_record', …>, Service<'apply_confirm', …>]` — now index each entry's string-literal name as a searchable symbol. Previously these names existed only as type arguments, so `codegraph query query_apply_record` found nothing even though the names are the app's primary API surface. The pattern is common in typed RPC / BFF clients and mock servers where the types are the source of truth for a runtime proxy object. Utility types (`Pick`, `Omit`, `Record`) and route paths are deliberately left out to avoid noise. Thanks @jiezhiyong. (#634) (TypeScript)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ CodeGraph cuts **tokens, tool calls, and wall-clock time on every repo** — acr
| **Full-Text Search** | Find code by name instantly across your entire codebase, powered by FTS5 |
| **Impact Analysis** | Trace callers, callees, and the full impact radius of any symbol before making changes |
| **Always Fresh** | File watcher uses native OS events (FSEvents/inotify/ReadDirectoryChangesW) with debounced auto-sync — the graph stays current as you code, zero config |
| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Scala, Dart, Lua, Luau, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **21+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Scala, Dart, ReScript, Lua, Luau, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 17 frameworks |
| **Mixed iOS / React Native / Expo** | Closes cross-language flows that static parsing misses: Swift ↔ ObjC bridging, React Native legacy bridge + TurboModules + Fabric view components, native → JS event emitters, Expo Modules |
| **100% Local** | No data leaves your machine. No API keys. No external services. SQLite database only |
Expand Down
267 changes: 267 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6976,3 +6976,270 @@ describe('Swift property wrappers / attributes (blast-radius recall)', () => {
} finally { cleanupTempDir(dir); }
});
});

describe('ReScript Extraction', () => {
it('should extract function declarations', () => {
const code = `
let makeUser = (id: int, name: string): user => {
{id, name}
}
`;
const result = extractFromSource('User.res', code);

const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('makeUser');
expect(funcNode?.signature).toContain('(id: int, name: string)');
expect(funcNode?.language).toBe('rescript');
});

it('should extract constant declarations (immutable let bindings)', () => {
const code = `
let greeting = "Hello"
let count = 42
`;
const result = extractFromSource('Vars.res', code);

const constants = result.nodes.filter((n) => n.kind === 'constant');
expect(constants.length).toBe(2);
expect(constants.find((n) => n.name === 'greeting')).toBeDefined();
expect(constants.find((n) => n.name === 'count')).toBeDefined();
});

it('should extract module declarations', () => {
const code = `
module Utils = {
let add = (a: int, b: int): int => {
a + b
}
}
`;
const result = extractFromSource('Utils.res', code);

const moduleNode = result.nodes.find((n) => n.kind === 'module');
expect(moduleNode).toBeDefined();
expect(moduleNode?.name).toBe('Utils');

// The function inside the module should have a contains edge from the module
const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('add');
});

it('should extract record types as structs', () => {
const code = `
type user = {
id: int,
name: string,
}
`;
const result = extractFromSource('Types.res', code);

const structNode = result.nodes.find((n) => n.kind === 'struct');
expect(structNode).toBeDefined();
expect(structNode?.name).toBe('user');

const fields = result.nodes.filter((n) => n.kind === 'field');
expect(fields.length).toBe(2);
expect(fields.find((n) => n.name === 'id')).toBeDefined();
expect(fields.find((n) => n.name === 'name')).toBeDefined();
});

it('should extract variant types as enums', () => {
const code = `
type status = | Pending | Done | Error
`;
const result = extractFromSource('Status.res', code);

const enumNode = result.nodes.find((n) => n.kind === 'enum');
expect(enumNode).toBeDefined();
expect(enumNode?.name).toBe('status');

const members = result.nodes.filter((n) => n.kind === 'enum_member');
expect(members.length).toBe(3);
expect(members.find((n) => n.name === 'Pending')).toBeDefined();
expect(members.find((n) => n.name === 'Done')).toBeDefined();
expect(members.find((n) => n.name === 'Error')).toBeDefined();
});

it('should extract open statements as imports', () => {
const code = `
open Belt
`;
const result = extractFromSource('Imports.res', code);

const importNode = result.nodes.find((n) => n.kind === 'import');
expect(importNode).toBeDefined();
expect(importNode?.name).toBe('Belt');
});

it('should extract call expressions', () => {
const code = `
let greet = () => {
Js.log("hello")
Belt.Array.map([1, 2], x => x)
}
`;
const result = extractFromSource('Calls.res', code);

const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls');
expect(calls.length).toBeGreaterThanOrEqual(2);
expect(calls.find((r) => r.referenceName === 'Js.log')).toBeDefined();
expect(calls.find((r) => r.referenceName === 'Belt.Array.map')).toBeDefined();
});

it('should emit references edges for function parameter and return types', () => {
const code = `
let makeUser = (id: int, name: string): user => {
{id, name}
}
`;
const result = extractFromSource('TypeRefs.res', code);

const typeRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
expect(typeRefs.find((r) => r.referenceName === 'user')).toBeDefined();
// 'int' and 'string' are built-in primitives and should NOT create references
expect(typeRefs.find((r) => r.referenceName === 'int')).toBeUndefined();
expect(typeRefs.find((r) => r.referenceName === 'string')).toBeUndefined();
});

it('should emit references edges for record field types', () => {
const code = `
type user = {
id: int,
name: string,
role: role_type,
}
`;
const result = extractFromSource('RecordTypeRefs.res', code);

const typeRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
expect(typeRefs.find((r) => r.referenceName === 'role_type')).toBeDefined();
expect(typeRefs.find((r) => r.referenceName === 'int')).toBeUndefined();
expect(typeRefs.find((r) => r.referenceName === 'string')).toBeUndefined();
});

it('should emit references edges for generic types', () => {
const code = `
let process = (x: myOption<int>): myResult<int, string> => {
x
}
`;
const result = extractFromSource('GenericTypeRefs.res', code);

const typeRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
expect(typeRefs.find((r) => r.referenceName === 'myOption')).toBeDefined();
expect(typeRefs.find((r) => r.referenceName === 'myResult')).toBeDefined();
// Built-ins should be filtered out
expect(typeRefs.find((r) => r.referenceName === 'int')).toBeUndefined();
expect(typeRefs.find((r) => r.referenceName === 'string')).toBeUndefined();
});

it('should emit references edges for typed variable declarations', () => {
const code = `
let x: userId = 42
`;
const result = extractFromSource('TypedVar.res', code);

const typeRefs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references');
expect(typeRefs.find((r) => r.referenceName === 'userId')).toBeDefined();
});

it('should extract abstract type aliases', () => {
const code = `
type userId
`;
const result = extractFromSource('AbstractType.res', code);

const aliasNode = result.nodes.find((n) => n.kind === 'type_alias');
expect(aliasNode).toBeDefined();
expect(aliasNode?.name).toBe('userId');
});

it('should extract nested modules', () => {
const code = `
module Outer = {
module Inner = {
let value = 1
}
}
`;
const result = extractFromSource('NestedModules.res', code);

const modules = result.nodes.filter((n) => n.kind === 'module');
expect(modules.length).toBe(2);
expect(modules.find((n) => n.name === 'Outer')).toBeDefined();
expect(modules.find((n) => n.name === 'Inner')).toBeDefined();

const constant = result.nodes.find((n) => n.name === 'value' && n.kind === 'constant');
expect(constant).toBeDefined();
});

it('should gracefully skip destructuring patterns', () => {
const code = `
let {id, name} = user
`;
const result = extractFromSource('Destructuring.res', code);

// No node should be created for the destructuring binding itself
const namedNodes = result.nodes.filter((n) => n.name === 'id' || n.name === 'name');
expect(namedNodes.length).toBe(0);
// No errors should be emitted
expect(result.errors.length).toBe(0);
});

it('should create contains edges from module to its members', () => {
const code = `
module Utils = {
let add = (a: int, b: int): int => {
a + b
}
}
`;
const result = extractFromSource('Utils.res', code);

const moduleNode = result.nodes.find((n) => n.kind === 'module');
const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(moduleNode).toBeDefined();
expect(funcNode).toBeDefined();

const containsEdge = result.edges.find(
(e) => e.source === moduleNode?.id && e.target === funcNode?.id && e.kind === 'contains'
);
expect(containsEdge).toBeDefined();
});

it('should extract unit functions (no parameters)', () => {
const code = `
let init = () => {
Js.log("init")
}
`;
const result = extractFromSource('UnitFunc.res', code);

const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('init');
expect(funcNode?.signature).toBe('()');
});

it('should extract decorators as decorates references', () => {
const code = `
@react.component
let make = (~name: string) => {
<div> {React.string(name)} </div>
}
`;
const result = extractFromSource('Decorator.res', code);

const funcNode = result.nodes.find((n) => n.kind === 'function');
expect(funcNode).toBeDefined();
expect(funcNode?.name).toBe('make');

const decoratorRef = result.unresolvedReferences.find(
(r) => r.referenceKind === 'decorates' && r.referenceName === 'react.component'
);
expect(decoratorRef).toBeDefined();
expect(decoratorRef?.fromNodeId).toBe(funcNode?.id);
});
});
4 changes: 4 additions & 0 deletions src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
lua: 'tree-sitter-lua.wasm',
luau: 'tree-sitter-luau.wasm',
objc: 'tree-sitter-objc.wasm',
rescript: 'tree-sitter-rescript.wasm',
};

/**
Expand Down Expand Up @@ -106,6 +107,8 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.luau': 'luau',
'.m': 'objc',
'.mm': 'objc',
'.res': 'rescript',
'.resi': 'rescript',
// XML: file-level tracking; the MyBatis extractor matches `<mapper namespace="...">`
// shape and emits SQL-statement nodes (other XML returns empty).
'.xml': 'xml',
Expand Down Expand Up @@ -420,6 +423,7 @@ export function getLanguageDisplayName(language: Language): string {
lua: 'Lua',
luau: 'Luau',
objc: 'Objective-C',
rescript: 'ReScript',
yaml: 'YAML',
twig: 'Twig',
xml: 'XML',
Expand Down
2 changes: 2 additions & 0 deletions src/extraction/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { scalaExtractor } from './scala';
import { luaExtractor } from './lua';
import { luauExtractor } from './luau';
import { objcExtractor } from './objc';
import { rescriptExtractor } from './rescript';

export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
typescript: typescriptExtractor,
Expand All @@ -49,4 +50,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
lua: luaExtractor,
luau: luauExtractor,
objc: objcExtractor,
rescript: rescriptExtractor,
};
Loading