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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### New Features

- GDScript (`.gd`) is now a supported language: functions and typed signatures, `_init` constructors, inner classes with methods, the full `var`/`const`/`@export`/`@onready` variable family, signals (extracted as properties with their parameter lists), enums with members, `static func` detection, and call edges — including calls inside initializers like `preload(...)`. Grammar: PrestonKnopp/tree-sitter-gdscript v6.1.0, vendored as an ABI-15 wasm rebuilt from upstream source.

- Anonymous usage telemetry is now stored entirely on CodeGraph's own first-party infrastructure — no third-party analytics vendor receives any of it, and the endpoint that receives it makes no outbound requests at all. Individual events are deleted after 90 days, leaving only anonymous daily totals. Nothing about what is collected changed, your IP address is still never read or stored, and every off-switch works exactly as before (`codegraph telemetry off`, `CODEGRAPH_TELEMETRY=0`, `DO_NOT_TRACK=1`). `TELEMETRY.md` remains the complete field-by-field list.

- `codegraph_explore` no longer re-sends source it already returned earlier in the same conversation. A file it has already shown you comes back as a short pointer — the path, the symbols and the exact line range, with confirmation that the file hasn't changed since — and the space that frees is spent on code you haven't seen yet, so a follow-up call covers new ground instead of repeating the last one. If a file was edited in between, its source is always shown again in full. Set `CODEGRAPH_EXPLORE_DEDUP=0` to turn this off.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ CodeGraph's parsing engine is a **native Rust kernel**: 20 languages — TypeScr
| **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, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, Svelte, Vue, Astro, Liquid, Pascal/Delphi |
| **20+ Languages** | TypeScript, JavaScript, ArkTS, Python, Go, Rust, Java, C#, VB.NET, PHP, Ruby, C, C++, CUDA, Objective-C, Metal, Swift, Kotlin, Scala, Dart, Lua, Luau, GDScript, R, Nix, Erlang, CFML, COBOL, Solidity, Terraform/OpenTofu, 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 Expand Up @@ -796,6 +796,7 @@ is written):
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| R | `.R` `.r` | Full support (functions in every assignment form, S4/R5/R6 classes with methods, `library`/`require` imports, `source()` file references, call edges) |
| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) |
| GDScript | `.gd` | Full support (functions with typed signatures, `_init` constructors, inner classes, `var`/`const`/`@export`/`@onready` variables, signals, enums with members, static detection, call edges — including calls inside initializers like `preload()`) |
| CFML | `.cfc`, `.cfm`, `.cfs` | Full support (tag-based `<cfcomponent>`/`<cffunction>` and bare-script `component { ... }` styles, `extends`/`implements`, embedded `<cfscript>` delegation, call edges) |
| COBOL | `.cbl`, `.cob`, `.cpy` | Full support (programs, sections/paragraphs with PERFORM/GO TO call edges, CALL 'literal' cross-program calls, COPY copybook imports — including standalone `.cpy` files — DATA DIVISION records/fields/88-levels, EXEC CICS LINK/XCTL and EXEC SQL INCLUDE targets; fixed and free format) |
| Visual Basic .NET | `.vb` | Full support (classes, Modules, interfaces, structures, enums, properties, events, `Declare` P/Invoke, `Handles`/`WithEvents`, `Inherits`/`Implements` edges, call edges through VB's call/index paren ambiguity, `As New` instantiation, interpolated strings, LINQ, Unicode identifiers) |
Expand Down
113 changes: 113 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ describe('Language Detection', () => {
expect(detectLanguage('entry/src/main/ets/common/utils.ts')).toBe('typescript');
});

it('should detect GDScript files', () => {
expect(detectLanguage('player.gd')).toBe('gdscript');
expect(detectLanguage('scripts/enemies/boss_ai.gd')).toBe('gdscript');
expect(isSourceFile('player.gd')).toBe(true);
});

it('should detect Nix files', () => {
expect(detectLanguage('default.nix')).toBe('nix');
expect(detectLanguage('pkgs/development/tools/misc/codegraph/default.nix')).toBe('nix');
Expand Down Expand Up @@ -205,6 +211,7 @@ describe('Language Support', () => {
expect(languages).toContain('dart');
expect(languages).toContain('solidity');
expect(languages).toContain('nix');
expect(languages).toContain('gdscript');
});
});

Expand Down Expand Up @@ -347,6 +354,112 @@ in
});
});

describe('GDScript Extraction', () => {
it('should extract functions, constructor, and signatures', () => {
const code = `extends Node2D

func _ready() -> void:
set_process(true)

func _init(width: int, height: int = 32):
pass

static func clamp_value(v: float, lo: float, hi: float) -> float:
return clampf(v, lo, hi)
`;

const result = extractFromSource('player.gd', code);

const ready = result.nodes.find((n) => n.kind === 'function' && n.name === '_ready');
expect(ready?.signature).toBe('() -> void');

// constructor_definition has no name field; resolveName supplies _init
const init = result.nodes.find((n) => n.name === '_init');
expect(init).toBeDefined();
expect(init?.signature).toBe('(width: int, height: int = 32)');

const clampValue = result.nodes.find((n) => n.name === 'clamp_value');
expect(clampValue?.signature).toBe('(v: float, lo: float, hi: float) -> float');
expect(clampValue?.isStatic).toBe(true);

const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
expect(calls).toContain('set_process');
expect(calls).toContain('clampf');
});

it('should extract the var/const family and walk initializers for calls', () => {
const code = `extends Node

const MAX_SPEED := 300.0
var health: int = 100
@export var display_name: String = "Player"
@onready var sprite = get_node("Sprite2D")
var scene = preload("res://enemy.tscn")
`;

const result = extractFromSource('stats.gd', code);

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

const health = result.nodes.find((n) => n.kind === 'variable' && n.name === 'health');
expect(health?.signature).toBe(': int = 100');

expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'display_name')).toBeDefined();
expect(result.nodes.find((n) => n.kind === 'variable' && n.name === 'sprite')).toBeDefined();

// Initializers are walked, so calls inside them are captured.
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
expect(calls).toContain('get_node');
expect(calls).toContain('preload');
});

it('should extract signals as properties with their parameter list', () => {
const code = `extends Node

signal died
signal health_changed(old_value, new_value)
`;

const result = extractFromSource('events.gd', code);

const died = result.nodes.find((n) => n.kind === 'property' && n.name === 'died');
expect(died).toBeDefined();

const healthChanged = result.nodes.find((n) => n.kind === 'property' && n.name === 'health_changed');
expect(healthChanged?.signature).toBe('(old_value, new_value)');
});

it('should extract enums with members and inner classes with methods', () => {
const code = `extends Node

enum State { IDLE, RUNNING = 10, DEAD }

class Inventory:
var items := []

func add(item) -> void:
items.append(item)
`;

const result = extractFromSource('game.gd', code);

expect(result.nodes.find((n) => n.kind === 'enum' && n.name === 'State')).toBeDefined();
const members = result.nodes.filter((n) => n.kind === 'enum_member').map((n) => n.name);
expect(members).toContain('IDLE');
expect(members).toContain('RUNNING');
expect(members).toContain('DEAD');

expect(result.nodes.find((n) => n.kind === 'class' && n.name === 'Inventory')).toBeDefined();
const add = result.nodes.find((n) => n.kind === 'method' && n.name === 'add');
expect(add?.signature).toBe('(item) -> void');

// obj.method(args) parses as attribute_call — the bare method name is emitted.
const calls = result.unresolvedReferences.filter((r) => r.referenceKind === 'calls').map((r) => r.referenceName);
expect(calls).toContain('append');
});
});

describe('TypeScript Extraction', () => {
it('should extract function declarations', () => {
const code = `
Expand Down
5 changes: 4 additions & 1 deletion src/extraction/grammars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const WASM_GRAMMAR_FILES: Record<GrammarLanguage, string> = {
terraform: 'tree-sitter-terraform.wasm',
arkts: 'tree-sitter-arkts.wasm',
nix: 'tree-sitter-nix.wasm',
gdscript: 'tree-sitter-gdscript.wasm',
};

/**
Expand Down Expand Up @@ -121,6 +122,7 @@ export const EXTENSION_MAP: Record<string, Language> = {
'.sc': 'scala',
'.lua': 'lua',
'.luau': 'luau',
'.gd': 'gdscript',
'.m': 'objc',
'.mm': 'objc',
'.sol': 'solidity',
Expand Down Expand Up @@ -290,7 +292,7 @@ export async function initGrammars(): Promise<void> {
*/
const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery',
'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix',
'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix', 'gdscript',
'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go',
// R7a (C/C++ kernel port prep): tree-sitter-c v0.24.2 (b780e47) +
// tree-sitter-cpp v0.23.4 (f41e1a0), parser.c/scanner.c sha-matched against
Expand Down Expand Up @@ -640,6 +642,7 @@ export function getLanguageDisplayName(language: Language): string {
scala: 'Scala',
lua: 'Lua',
luau: 'Luau',
gdscript: 'GDScript',
objc: 'Objective-C',
solidity: 'Solidity',
nix: 'Nix',
Expand Down
102 changes: 102 additions & 0 deletions src/extraction/languages/gdscript.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { getNodeText, getChildByField } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';

// GDScript (Godot, tree-sitter-gdscript). A Python-like indentation grammar
// where every `.gd` file is an implicit class — top-level `func`s extract as
// functions, `class X:` inner classes as classes with methods.
//
// Grammar shapes that need care:
// - The var/const family (`variable_statement`, `export_variable_statement`,
// `onready_variable_statement`, `const_statement`) names its target via a
// `name`-typed child, NOT `identifier`, so the core's generic variable
// fallback (which looks for identifier children) can't read them. The
// visitNode hook creates variable/constant nodes itself, then walks the
// initializer so calls inside it (`preload(...)`, `Foo.new()`) are captured.
// - `func _init(...)` (constructor_definition) has no name field; resolveName
// supplies the conventional `_init`.
// - `enumerator` names its identifier via `left`, not `name`.
// - `obj.method(args)` parses as attribute(identifier, attribute_call(...)),
// so attribute_call joins callTypes; the core's namedChild(0) callee
// fallback then yields the bare method name (resolution is name-match only,
// matching how self/this receivers are emitted elsewhere).
// - `signal foo(a, b)` extracts as a property carrying the parameter list, so
// connect()-heavy scripts expose their signal surface in the graph.
const VARIABLE_NODE_TYPES = new Set([
'variable_statement',
'export_variable_statement',
'onready_variable_statement',
'const_statement',
]);

export const gdscriptExtractor: LanguageExtractor = {
functionTypes: ['function_definition', 'constructor_definition'],
classTypes: ['class_definition'],
methodTypes: ['function_definition', 'constructor_definition'],
interfaceTypes: [],
structTypes: [],
enumTypes: ['enum_definition'],
enumMemberTypes: ['enumerator'],
typeAliasTypes: [],
importTypes: [],
callTypes: ['call', 'attribute_call', 'base_call'],
variableTypes: [], // handled by the visitNode hook (see above)
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
returnField: 'return_type',

resolveName: (node, source) => {
if (node.type === 'constructor_definition') return '_init';
if (node.type === 'enumerator') {
const left = getChildByField(node, 'left');
return left ? getNodeText(left, source) : undefined;
}
return undefined;
},

// `static` is a named static_keyword CHILD (no field) on function_definition;
// the var statements carry it via a field, but a child scan covers both.
isStatic: (node) => node.namedChildren.some((c) => c?.type === 'static_keyword'),

getSignature: (node, source) => {
const params = getChildByField(node, 'parameters');
if (!params) return undefined;
let sig = getNodeText(params, source);
const ret = getChildByField(node, 'return_type');
if (ret) sig += ' -> ' + getNodeText(ret, source);
return sig;
},

visitNode: (node, ctx) => {
if (node.type === 'signal_statement') {
const nameNode = node.childForFieldName('name');
if (nameNode) {
const params = node.childForFieldName('parameters');
ctx.createNode('property', getNodeText(nameNode, ctx.source), node, {
signature: params ? getNodeText(params, ctx.source) : undefined,
});
}
return true;
}
if (VARIABLE_NODE_TYPES.has(node.type)) {
const nameNode = node.childForFieldName('name');
const valueNode = node.childForFieldName('value');
if (nameNode) {
const typeNode = node.childForFieldName('type');
const typeSig = typeNode ? `: ${getNodeText(typeNode, ctx.source)}` : '';
const initValue = valueNode ? getNodeText(valueNode, ctx.source).slice(0, 100) : '';
const initSig = initValue ? ` = ${initValue}${initValue.length >= 100 ? '...' : ''}` : '';
ctx.createNode(
node.type === 'const_statement' ? 'constant' : 'variable',
getNodeText(nameNode, ctx.source),
node,
{ signature: (typeSig + initSig).trim() || undefined },
);
}
// Walk the initializer so calls inside it (preload(), Foo.new()) are captured.
if (valueNode) ctx.visitNode(valueNode);
return true;
}
return false;
},
};
2 changes: 2 additions & 0 deletions src/extraction/languages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { solidityExtractor } from './solidity';
import { terraformExtractor } from './terraform';
import { arktsExtractor } from './arkts';
import { nixExtractor } from './nix';
import { gdscriptExtractor } from './gdscript';

export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
typescript: typescriptExtractor,
Expand Down Expand Up @@ -69,4 +70,5 @@ export const EXTRACTORS: Partial<Record<Language, LanguageExtractor>> = {
terraform: terraformExtractor,
arkts: arktsExtractor,
nix: nixExtractor,
gdscript: gdscriptExtractor,
};
Binary file added src/extraction/wasm/tree-sitter-gdscript.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export const LANGUAGES = [
'scala',
'lua',
'luau',
'gdscript',
'objc',
'r',
'solidity',
Expand Down