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

### New Features

- GLSL shader files (`.glsl`, `.vert`, `.frag`, `.comp`, `.geom`, `.tesc`, `.tese`) are now indexed — functions, structs, and intra-file calls are extracted so you can navigate and query shader code alongside the rest of the project. (#685)
- `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)

### Fixes
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,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, Dart, Lua, Luau, Svelte, Liquid, Pascal/Delphi |
| **20+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Dart, Lua, Luau, GLSL, Svelte, Liquid, Pascal/Delphi |
| **Framework-aware Routes** | Recognizes web-framework routing files and links URL patterns to their handlers across 14 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 @@ -627,6 +627,7 @@ is written):
| Pascal / Delphi | `.pas`, `.dpr`, `.dpk`, `.lpr` | Full support (classes, records, interfaces, enums, DFM/FMX form files) |
| Lua | `.lua` | Full support (functions, methods with receivers, local variables, `require` imports, call edges) |
| Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) |
| GLSL | `.glsl`, `.vert`, `.frag`, `.comp`, `.geom`, `.tesc`, `.tese` | Full support (functions, structs, intra-file calls) |

## Troubleshooting

Expand Down
68 changes: 68 additions & 0 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4414,6 +4414,74 @@ void helperFunction(int count) {
});
});

describe('GLSL Extraction', () => {
describe('Language detection', () => {
it('should detect GLSL files by all shader extensions', () => {
expect(detectLanguage('shader.glsl')).toBe('glsl');
expect(detectLanguage('main.vert')).toBe('glsl');
expect(detectLanguage('main.frag')).toBe('glsl');
expect(detectLanguage('compute.comp')).toBe('glsl');
expect(detectLanguage('geometry.geom')).toBe('glsl');
expect(detectLanguage('tess.tesc')).toBe('glsl');
expect(detectLanguage('tess.tese')).toBe('glsl');
});

it('should report GLSL as supported', () => {
expect(isLanguageSupported('glsl')).toBe(true);
expect(getSupportedLanguages()).toContain('glsl');
});
});

describe('Function extraction', () => {
it('should extract void and typed functions', () => {
const code = `
void main() {
gl_Position = vec4(0.0);
}

vec3 computeNormal(vec3 a, vec3 b) {
return normalize(cross(a, b));
}
`;
const result = extractFromSource('shader.vert', code);
const fns = result.nodes.filter((n) => n.kind === 'function').map((n) => n.name);
expect(fns).toContain('main');
expect(fns).toContain('computeNormal');
const main = result.nodes.find((n) => n.name === 'main');
expect(main?.language).toBe('glsl');
});
});

describe('Struct extraction', () => {
it('should extract struct definitions', () => {
const code = `
struct Light {
vec3 position;
vec3 color;
float intensity;
};
`;
const result = extractFromSource('lighting.glsl', code);
const structs = result.nodes.filter((n) => n.kind === 'struct').map((n) => n.name);
expect(structs).toContain('Light');
});
});

describe('Call extraction', () => {
it('should record intra-file function calls', () => {
const code = `
float square(float x) { return x * x; }
void main() { float s = square(2.0); gl_Position = vec4(s); }
`;
const result = extractFromSource('shader.frag', code);
const call = result.unresolvedReferences.find(
(r) => r.referenceKind === 'calls' && r.referenceName === 'square'
);
expect(call).toBeDefined();
});
});
});

describe('Regression: issue-specific extraction fixes', () => {
it('indexes inner functions of an anonymous AMD/CommonJS module wrapper (#528)', () => {
const code = `
Expand Down
11 changes: 10 additions & 1 deletion 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',
glsl: 'tree-sitter-glsl.wasm',
};

/**
Expand Down Expand Up @@ -108,6 +109,13 @@ export const EXTENSION_MAP: Record<string, Language> = {
// shape as the `.yml` variants — the YAML/properties extractor emits one node
// per leaf key, and the Spring resolver links `@Value("${k}")` references.
'.properties': 'properties',
'.glsl': 'glsl',
'.vert': 'glsl',
'.frag': 'glsl',
'.comp': 'glsl',
'.geom': 'glsl',
'.tesc': 'glsl',
'.tese': 'glsl',
};

/**
Expand Down Expand Up @@ -185,7 +193,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise<v
// ABI-13 build that corrupts the shared WASM heap under web-tree-sitter
// 0.25 (drops nested calls/imports on every file after the first); we
// vendor the upstream ABI-15 wasm instead.
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau')
const wasmPath = (lang === 'pascal' || lang === 'scala' || lang === 'lua' || lang === 'luau' || lang === 'glsl')
? path.join(__dirname, 'wasm', wasmFile)
: require.resolve(`tree-sitter-wasms/out/${wasmFile}`);
const language = await WasmLanguage.load(wasmPath);
Expand Down Expand Up @@ -388,6 +396,7 @@ export function getLanguageDisplayName(language: Language): string {
twig: 'Twig',
xml: 'XML',
properties: 'Java properties',
glsl: 'GLSL',
unknown: 'Unknown',
};
return names[language] || language;
Expand Down
93 changes: 93 additions & 0 deletions src/extraction/languages/glsl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { Node as SyntaxNode } from 'web-tree-sitter';
import { getChildByField, getNodeText } from '../tree-sitter-helpers';
import type { LanguageExtractor } from '../tree-sitter-types';

export const glslExtractor: LanguageExtractor = {
functionTypes: ['function_definition', 'preproc_function_def'],
classTypes: [],
methodTypes: [],
interfaceTypes: [],
structTypes: ['struct_specifier'],
enumTypes: [],
enumMemberTypes: [],
typeAliasTypes: [],
importTypes: [],
callTypes: ['function_call'],
variableTypes: ['declaration', 'local_declaration', 'parameter_declaration', 'field_declaration'],
nameField: 'name',
bodyField: 'body',
paramsField: 'parameters',
resolveName: (node, source) => {
if (node.type === 'function_definition' || node.type === 'preproc_function_def') {
const declarator = node.namedChildren.find((c: SyntaxNode) => c.type === 'function_declarator');
if (declarator) {
const nameNode = getChildByField(declarator, 'name') || declarator.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
if (nameNode) return getNodeText(nameNode, source);
}
} else if (node.type === 'struct_specifier') {
const nameNode = getChildByField(node, 'name') || node.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier' || c.type === 'identifier');
if (nameNode) return getNodeText(nameNode, source);
} else if (node.type === 'declaration' || node.type === 'local_declaration' || node.type === 'parameter_declaration' || node.type === 'field_declaration') {
const instanceName = getChildByField(node, 'instance_name');
if (instanceName) return getNodeText(instanceName, source);

const declaratorList = node.namedChildren.find((c: SyntaxNode) => c.type === 'declarator_list');
if (declaratorList) {
const declarator = declaratorList.namedChildren.find((c: SyntaxNode) => c.type === 'declarator');
if (declarator) {
const nameNode = getChildByField(declarator, 'name');
if (nameNode) return getNodeText(nameNode, source);
}
}

const initDeclarator = node.namedChildren.find((c: SyntaxNode) => c.type === 'init_declarator');
if (initDeclarator) {
const nameNode = getChildByField(initDeclarator, 'declarator') || initDeclarator.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
if (nameNode) return getNodeText(nameNode, source);
}

// Fallbacks
const nameNode = getChildByField(node, 'name');
if (nameNode) return getNodeText(nameNode, source);

const identifier = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier' || c.type === 'field_identifier');
if (identifier) return getNodeText(identifier, source);
}
return undefined;
},
// GLSL function_definition has no 'body' named field; the compound_statement
// is a plain named child — find it so visitFunctionBody gets called.
resolveBody: (node) => {
return node.namedChildren.find((c: SyntaxNode) => c.type === 'compound_statement') ?? null;
},

// Struct declarations are nested inside `declaration` nodes:
// declaration → declarator_list → declarator → type → type_specifier → struct_specifier
// The generic variable-extraction path sets skipChildren=true and never reaches
// struct_specifier. Intercept declaration nodes that carry a struct and extract
// the struct directly, then mark as handled so the variable path is skipped.
visitNode: (node, ctx) => {
if (node.type !== 'declaration') return false;
const structNode = findNestedStructSpecifier(node);
if (!structNode) return false;
const nameNode =
structNode.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier' || c.type === 'identifier') ??
structNode.childForFieldName('name');
if (!nameNode) return false;
const name = ctx.source.substring(nameNode.startIndex, nameNode.endIndex);
if (!name) return false;
ctx.createNode('struct', name, structNode, {});
return true;
},

extractImport: () => null,
};

function findNestedStructSpecifier(node: SyntaxNode): SyntaxNode | null {
if (node.type === 'struct_specifier') return node;
for (const child of node.namedChildren) {
const found = findNestedStructSpecifier(child);
if (found) return found;
}
return null;
}
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 { glslExtractor } from './glsl';

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,
glsl: glslExtractor,
};
Binary file added src/extraction/wasm/tree-sitter-glsl.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 @@ -92,6 +92,7 @@ export const LANGUAGES = [
'twig',
'xml',
'properties',
'glsl',
'unknown',
] as const;

Expand Down