diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef5f5aa..c8b99e984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 1a9800ee3..e0aee08d0 100644 --- a/README.md +++ b/README.md @@ -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 | @@ -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 diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index d29fa11b3..8b227362f 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -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 = ` diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 576845e20..b248a6357 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -38,6 +38,7 @@ const WASM_GRAMMAR_FILES: Record = { lua: 'tree-sitter-lua.wasm', luau: 'tree-sitter-luau.wasm', objc: 'tree-sitter-objc.wasm', + glsl: 'tree-sitter-glsl.wasm', }; /** @@ -108,6 +109,13 @@ export const EXTENSION_MAP: Record = { // 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', }; /** @@ -185,7 +193,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise { + 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; +} diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 543598b8e..d23015e0c 100644 --- a/src/extraction/languages/index.ts +++ b/src/extraction/languages/index.ts @@ -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> = { typescript: typescriptExtractor, @@ -49,4 +50,5 @@ export const EXTRACTORS: Partial> = { lua: luaExtractor, luau: luauExtractor, objc: objcExtractor, + glsl: glslExtractor, }; diff --git a/src/extraction/wasm/tree-sitter-glsl.wasm b/src/extraction/wasm/tree-sitter-glsl.wasm new file mode 100755 index 000000000..a1aa03be7 Binary files /dev/null and b/src/extraction/wasm/tree-sitter-glsl.wasm differ diff --git a/src/types.ts b/src/types.ts index e710e31a1..608df7427 100644 --- a/src/types.ts +++ b/src/types.ts @@ -92,6 +92,7 @@ export const LANGUAGES = [ 'twig', 'xml', 'properties', + 'glsl', 'unknown', ] as const;