diff --git a/src/extraction/grammars.ts b/src/extraction/grammars.ts index 1b15996c0..2a96d5913 100644 --- a/src/extraction/grammars.ts +++ b/src/extraction/grammars.ts @@ -39,6 +39,7 @@ const WASM_GRAMMAR_FILES: Record = { r: 'tree-sitter-r.wasm', luau: 'tree-sitter-luau.wasm', objc: 'tree-sitter-objc.wasm', + odin: 'tree-sitter-odin.wasm', }; /** @@ -115,6 +116,7 @@ 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', + '.odin': 'odin', }; /** @@ -221,7 +223,7 @@ export async function loadGrammarsForLanguages(languages: Language[]): Promise> = { typescript: typescriptExtractor, @@ -51,4 +52,5 @@ export const EXTRACTORS: Partial> = { r: rExtractor, luau: luauExtractor, objc: objcExtractor, + odin: odinExtractor, }; diff --git a/src/extraction/languages/odin.ts b/src/extraction/languages/odin.ts new file mode 100644 index 000000000..d79f2596b --- /dev/null +++ b/src/extraction/languages/odin.ts @@ -0,0 +1,77 @@ +import type { Node as SyntaxNode } from 'web-tree-sitter'; +import type { LanguageExtractor } from '../tree-sitter-types'; + +export const odinExtractor: LanguageExtractor = { + functionTypes: ['procedure_declaration', 'overloaded_procedure_declaration'], + classTypes: [], // Odin has no classes + methodTypes: [], // Procedures are not attached to classes/objects + interfaceTypes: [], // Odin has no interfaces/traits + structTypes: ['struct_declaration', 'union_declaration', 'bit_field_declaration'], + enumTypes: ['enum_declaration'], + enumMemberTypes: ['identifier'], // Enum values are identifiers + typeAliasTypes: [], + importTypes: ['import_declaration'], + callTypes: ['call_expression', 'selector_call_expression'], + variableTypes: ['variable_declaration', 'var_declaration', 'const_declaration', 'const_type_declaration'], + fieldTypes: ['field'], // Struct fields + + nameField: 'name', + bodyField: 'body', + paramsField: 'parameters', + + resolveName: (node: SyntaxNode, source: string) => { + // In Odin, declarations are structured as: name :: definition or name : type := definition + // The LHS name (an identifier or expression) is the first named child. + const first = node.firstNamedChild; + if (first) { + return source.substring(first.startIndex, first.endIndex).trim(); + } + return undefined; + }, + + resolveBody: (node: SyntaxNode, _bodyField: string) => { + if (node.type === 'procedure_declaration') { + const procNode = node.namedChildren.find(c => c.type === 'procedure'); + if (procNode) { + const block = procNode.namedChildren.find(c => c.type === 'block'); + if (block) return block; + } + } else if ( + node.type === 'struct_declaration' || + node.type === 'union_declaration' || + node.type === 'bit_field_declaration' || + node.type === 'enum_declaration' + ) { + // The struct/enum fields are direct children of the declaration node. + // Returning the node itself allows the core extractor to visit its children. + return node; + } + return null; + }, + + getSignature: (node: SyntaxNode, source: string) => { + const procNode = node.namedChildren.find(c => c.type === 'procedure' || c.type === 'overloaded_procedure'); + if (procNode) { + return source.substring(procNode.startIndex, procNode.endIndex).trim(); + } + return undefined; + }, + + extractImport: (node: SyntaxNode, source: string) => { + const pathNode = node.childForFieldName('path'); + if (!pathNode) return null; + + let modulePath = source.substring(pathNode.startIndex, pathNode.endIndex).trim(); + // Strip string quotes + if ((modulePath.startsWith('"') && modulePath.endsWith('"')) || + (modulePath.startsWith('`') && modulePath.endsWith('`'))) { + modulePath = modulePath.slice(1, -1); + } + + const signature = source.substring(node.startIndex, node.endIndex).trim(); + return { + moduleName: modulePath, + signature: signature, + }; + }, +}; diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 1761ef2ba..933dc8713 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -1682,7 +1682,8 @@ export class TreeSitterExtractor { // Skip forward declarations and type references (no body = not a definition) // — EXCEPT C# positional records (`record struct M(decimal Amount);`), // complete definitions with no body block. (#831) - const body = getChildByField(node, this.extractor.bodyField); + const body = this.extractor.resolveBody?.(node, this.extractor.bodyField) + ?? getChildByField(node, this.extractor.bodyField); if (!body && node.type !== 'record_declaration') return; const name = extractName(node, this.source, this.extractor); diff --git a/src/extraction/wasm/tree-sitter-odin.wasm b/src/extraction/wasm/tree-sitter-odin.wasm new file mode 100755 index 000000000..7879285fb Binary files /dev/null and b/src/extraction/wasm/tree-sitter-odin.wasm differ diff --git a/src/types.ts b/src/types.ts index 656bb1090..49d8b12d4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,6 +95,7 @@ export const LANGUAGES = [ 'twig', 'xml', 'properties', + 'odin', 'unknown', ] as const;