diff --git a/.claude/skills/agent-eval/corpus.json b/.claude/skills/agent-eval/corpus.json index 2cfedac4f..3d8adf340 100644 --- a/.claude/skills/agent-eval/corpus.json +++ b/.claude/skills/agent-eval/corpus.json @@ -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 `` 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 `` reach the native RNSScreenStackView component?" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ef5f5aa..f08d05e71 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 +- CodeGraph now indexes **ReScript** (`.res`) — functions, modules, records, variants, imports, and call edges. Tested on the ReScript compiler, rescript-core, and rescript-relay. - `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 250b507af..ec00a3880 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,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 | +| **21+ Languages** | TypeScript, JavaScript, Python, Go, Rust, Java, C#, PHP, Ruby, C, C++, Objective-C, Swift, Kotlin, Dart, ReScript, Lua, Luau, 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 | @@ -634,6 +634,7 @@ is written): | Liquid | `.liquid` | Full support | | 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) | +| ReScript | `.res` | Full support (functions, modules, records, variants, imports, call edges) | | Luau | `.luau` | Full support (everything in Lua, plus `type`/`export type` aliases, typed signatures, and Roblox instance-path `require`) | ## Troubleshooting diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index d29fa11b3..e7092d681 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -101,6 +101,10 @@ describe('Language Detection', () => { expect(detectLanguage('stdio.h', '#ifndef STDIO_H\nvoid printf();\n#endif\n')).toBe('c'); }); + it('should detect ReScript files', () => { + expect(detectLanguage('Main.res')).toBe('rescript'); + }); + it('should return unknown for unsupported extensions', () => { expect(detectLanguage('styles.css')).toBe('unknown'); expect(detectLanguage('data.json')).toBe('unknown'); @@ -129,6 +133,7 @@ describe('Language Support', () => { expect(languages).toContain('swift'); expect(languages).toContain('kotlin'); expect(languages).toContain('dart'); + expect(languages).toContain('rescript'); }); }); @@ -4459,3 +4464,270 @@ func (s Stack[T]) Len() int { return len(s.items) } expect(js.nodes.find((n) => n.name === 'handleRequest' && n.kind === 'function')).toBeDefined(); }); }); + +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): myResult => { + 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) => { +
{React.string(name)}
+} +`; + 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); + }); +}); diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 576845e20..e3a3907e9 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', + rescript: 'tree-sitter-rescript.wasm', }; /** @@ -101,6 +102,8 @@ export const EXTENSION_MAP: Record = { '.luau': 'luau', '.m': 'objc', '.mm': 'objc', + '.res': 'rescript', + '.resi': 'rescript', // XML: file-level tracking; the MyBatis extractor matches `` // shape and emits SQL-statement nodes (other XML returns empty). '.xml': 'xml', @@ -384,6 +387,7 @@ export function getLanguageDisplayName(language: Language): string { lua: 'Lua', luau: 'Luau', objc: 'Objective-C', + rescript: 'ReScript', yaml: 'YAML', twig: 'Twig', xml: 'XML', diff --git a/src/extraction/languages/index.ts b/src/extraction/languages/index.ts index 543598b8e..541ed929b 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 { rescriptExtractor } from './rescript'; export const EXTRACTORS: Partial> = { typescript: typescriptExtractor, @@ -49,4 +50,5 @@ export const EXTRACTORS: Partial> = { lua: luaExtractor, luau: luauExtractor, objc: objcExtractor, + rescript: rescriptExtractor, }; diff --git a/src/extraction/languages/rescript.ts b/src/extraction/languages/rescript.ts new file mode 100644 index 000000000..baf650339 --- /dev/null +++ b/src/extraction/languages/rescript.ts @@ -0,0 +1,265 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import { getNodeText, getChildByField } from '../tree-sitter-helpers'; +import type { LanguageExtractor, ExtractorContext } from '../tree-sitter-types'; + +function getLetBindingName(binding: SyntaxNode, source: string): string | null { + const pattern = getChildByField(binding, 'pattern'); + if (!pattern) return null; + if (pattern.type === 'value_identifier') return getNodeText(pattern, source); + // Destructuring patterns and other shapes — skip for now + return null; +} + +function getModuleBindingName(binding: SyntaxNode, source: string): string | null { + const name = getChildByField(binding, 'name'); + if (!name) return null; + if (name.type === 'module_identifier') return getNodeText(name, source); + return null; +} + +function getTypeBindingName(binding: SyntaxNode, source: string): string | null { + const name = getChildByField(binding, 'name'); + if (!name) return null; + if (name.type === 'type_identifier') return getNodeText(name, source); + return null; +} + +function getFunctionSignature(func: SyntaxNode, source: string): string | undefined { + const params = getChildByField(func, 'parameters'); + const returnType = getChildByField(func, 'return_type'); + if (!params) return undefined; + let sig = getNodeText(params, source); + if (returnType) { + sig += ': ' + getNodeText(returnType, source).replace(/^:\s*/, ''); + } + return sig; +} + +/** + * ReScript decorators sit as preceding siblings of declarations (let_declaration, + * type_declaration, module_declaration, external_declaration). Scan both direct + * children and preceding siblings, matching the orchestrator's extractDecoratorsFor + * pattern, and emit unresolved 'decorates' references. + */ +function extractReScriptDecorators( + declNode: SyntaxNode, + source: string, + ctx: ExtractorContext, + decoratedId: string +): void { + const consider = (n: SyntaxNode | null): void => { + if (!n || n.type !== 'decorator') return; + const idNode = n.namedChildren.find((c) => c.type === 'decorator_identifier'); + if (!idNode) return; + const name = getNodeText(idNode, source).replace(/^@/, ''); + if (!name) return; + ctx.addUnresolvedReference({ + fromNodeId: decoratedId, + referenceName: name, + referenceKind: 'decorates', + line: n.startPosition.row + 1, + column: n.startPosition.column, + }); + }; + + // 1. Decorators that are direct children of the declaration (some grammars) + for (let i = 0; i < declNode.namedChildCount; i++) { + consider(declNode.namedChild(i)); + } + + // 2. Decorators that are preceding siblings of the declaration + const parent = declNode.parent; + if (parent) { + const declStart = declNode.startIndex; + let declIdx = -1; + for (let i = 0; i < parent.namedChildCount; i++) { + const sibling = parent.namedChild(i); + if (sibling && sibling.startIndex === declStart) { + declIdx = i; + break; + } + } + if (declIdx > 0) { + for (let j = declIdx - 1; j >= 0; j--) { + const sibling = parent.namedChild(j); + if (!sibling) continue; + if (sibling.type !== 'decorator') break; + consider(sibling); + } + } + } +} + +export const rescriptExtractor: LanguageExtractor = { + functionTypes: [], // function nodes are always inside let_binding, handled in visitNode + classTypes: [], + methodTypes: [], + interfaceTypes: [], + structTypes: [], // record_type is inside type_binding, handled in visitNode + enumTypes: [], // variant_type is inside type_binding, handled in visitNode + enumMemberTypes: [], // variant_declaration is inside variant_type, handled in visitNode + typeAliasTypes: [], // type_declaration handled in visitNode (name is on type_binding, not directly) + importTypes: ['open_statement'], + callTypes: ['call_expression'], + variableTypes: [], // let_declaration handled in visitNode (name is on let_binding pattern, not directly) + nameField: 'name', + bodyField: 'body', + paramsField: 'parameters', + returnField: 'return_type', + + // The orchestrator only calls getSignature for nodes matched via functionTypes / + // methodTypes. Since ReScript handles those in visitNode, this hook is only + // reached if a future change adds function nodes to functionTypes. + getSignature: (node, source) => { + if (node.type === 'function') { + return getFunctionSignature(node, source); + } + return undefined; + }, + + extractImport: (node, source) => { + const mod = node.namedChildren.find((c) => c.type === 'module_identifier'); + if (mod) { + const moduleName = getNodeText(mod, source); + return { + moduleName, + signature: getNodeText(node, source).trim().slice(0, 100), + }; + } + return null; + }, + + visitNode: (node, ctx) => { + const source = ctx.source; + + // let_declaration → let_binding → function | constant + if (node.type === 'let_declaration') { + const binding = node.namedChildren.find((c) => c.type === 'let_binding'); + if (!binding) return false; + + const name = getLetBindingName(binding, source); + if (!name) return false; + + const body = getChildByField(binding, 'body'); + const isFunction = body?.type === 'function'; + + if (isFunction && body) { + const signature = getFunctionSignature(body, source); + const funcNode = ctx.createNode('function', name, node, { signature }); + if (funcNode) { + ctx.extractTypeAnnotations(body, funcNode.id); + extractReScriptDecorators(node, source, ctx, funcNode.id); + const funcBody = getChildByField(body, 'body'); + if (funcBody) { + ctx.pushScope(funcNode.id); + ctx.visitFunctionBody(funcBody, funcNode.id); + ctx.popScope(); + } + } + } else { + // ReScript let bindings are immutable by default — use 'constant' + const constNode = ctx.createNode('constant', name, node); + if (constNode) { + ctx.extractTypeAnnotations(binding, constNode.id); + extractReScriptDecorators(node, source, ctx, constNode.id); + } + if (body) { + ctx.visitNode(body); + } + } + return true; + } + + // module_declaration → module_binding → module + if (node.type === 'module_declaration') { + const binding = node.namedChildren.find((c) => c.type === 'module_binding'); + if (!binding) return false; + + const name = getModuleBindingName(binding, source); + if (!name) return false; + + const moduleNode = ctx.createNode('module', name, node); + if (moduleNode) { + extractReScriptDecorators(node, source, ctx, moduleNode.id); + const definition = getChildByField(binding, 'definition'); + if (definition) { + ctx.pushScope(moduleNode.id); + for (let i = 0; i < definition.namedChildCount; i++) { + const child = definition.namedChild(i); + if (child) ctx.visitNode(child); + } + ctx.popScope(); + } + } + return true; + } + + // type_declaration → type_binding → struct | enum | type_alias + if (node.type === 'type_declaration') { + const binding = node.namedChildren.find((c) => c.type === 'type_binding'); + if (!binding) return false; + + const name = getTypeBindingName(binding, source); + if (!name) return false; + + const body = getChildByField(binding, 'body'); + if (!body) { + const aliasNode = ctx.createNode('type_alias', name, node); + if (aliasNode) { + extractReScriptDecorators(node, source, ctx, aliasNode.id); + } + return true; + } + + if (body.type === 'record_type') { + const structNode = ctx.createNode('struct', name, node); + if (structNode) { + extractReScriptDecorators(node, source, ctx, structNode.id); + ctx.pushScope(structNode.id); + for (let i = 0; i < body.namedChildCount; i++) { + const child = body.namedChild(i); + if (child?.type === 'record_type_field') { + const prop = child.namedChildren.find((c) => c.type === 'property_identifier'); + if (prop) { + const fieldName = getNodeText(prop, source); + const typeAnno = child.namedChildren.find((c) => c.type === 'type_annotation'); + const sig = typeAnno + ? `${fieldName}: ${getNodeText(typeAnno, source).replace(/^:\s*/, '')}` + : fieldName; + const fieldNode = ctx.createNode('field', fieldName, child, { signature: sig }); + if (fieldNode) { + ctx.extractTypeAnnotations(child, fieldNode.id); + } + } + } + } + ctx.popScope(); + } + } else if (body.type === 'variant_type') { + const enumNode = ctx.createNode('enum', name, node); + if (enumNode) { + extractReScriptDecorators(node, source, ctx, enumNode.id); + ctx.pushScope(enumNode.id); + for (let i = 0; i < body.namedChildCount; i++) { + const child = body.namedChild(i); + if (child?.type === 'variant_declaration') { + const variant = child.namedChildren.find((c) => c.type === 'variant_identifier'); + if (variant) { + ctx.createNode('enum_member', getNodeText(variant, source), child); + } + } + } + ctx.popScope(); + } + } else { + const aliasNode = ctx.createNode('type_alias', name, node); + if (aliasNode) { + extractReScriptDecorators(node, source, ctx, aliasNode.id); + } + } + return true; + } + + return false; + }, +}; diff --git a/src/extraction/tree-sitter-types.ts b/src/extraction/tree-sitter-types.ts index 6c04fbaeb..94f604a45 100644 --- a/src/extraction/tree-sitter-types.ts +++ b/src/extraction/tree-sitter-types.ts @@ -56,6 +56,8 @@ export interface ExtractorContext { visitFunctionBody(body: SyntaxNode, functionId: string): void; /** Add an unresolved reference */ addUnresolvedReference(ref: UnresolvedReference): void; + /** Extract type references (type_identifier nodes) from an AST subtree and emit unresolved 'references' edges */ + extractTypeAnnotations(node: SyntaxNode, nodeId: string): void; /** Push a node ID onto the scope stack (for containment/qualified name building) */ pushScope(nodeId: string): void; /** Pop the last node ID from the scope stack */ diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index c6eb93ac9..e4f1eefa0 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -566,6 +566,7 @@ export class TreeSitterExtractor { visitNode: (node) => self.visitNode(node), visitFunctionBody: (body, functionId) => self.visitFunctionBody(body, functionId), addUnresolvedReference: (ref) => self.unresolvedReferences.push(ref), + extractTypeAnnotations: (node, nodeId) => self.extractTypeAnnotations(node, nodeId), pushScope: (nodeId) => self.nodeStack.push(nodeId), popScope: () => self.nodeStack.pop(), get filePath() { return self.filePath; }, @@ -2544,7 +2545,7 @@ export class TreeSitterExtractor { * Languages that support type annotations (TypeScript, etc.) */ private readonly TYPE_ANNOTATION_LANGUAGES = new Set([ - 'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp', + 'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp', 'rescript', ]); /** @@ -2561,6 +2562,8 @@ export class TreeSitterExtractor { // Go 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float32', 'float64', 'complex64', 'complex128', 'rune', 'error', + // ReScript + 'unit', 'option', 'list', 'result', 'promise', 'array', 'dict', 'map', 'set', 'date', ]); /** diff --git a/src/types.ts b/src/types.ts index e710e31a1..1c7f9f799 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,6 +88,7 @@ export const LANGUAGES = [ 'lua', 'luau', 'objc', + 'rescript', 'yaml', 'twig', 'xml',