import type { Node as SyntaxNode } from 'web-tree-sitter'; import { getNodeText, getChildByField } from '../tree-sitter-helpers'; import type { LanguageExtractor } from '../tree-sitter-types'; /** Check if a node matches the `fun interface` misparse pattern */ function isFunInterfaceNode(node: SyntaxNode): boolean { let hasFun = false; let hasInterfaceType = false; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; if (child.type === 'fun' && !child.isNamed) hasFun = true; if (child.type === 'user_type') { const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier'); if (typeId && typeId.text === 'interface') hasInterfaceType = true; } } return hasFun && hasInterfaceType; } export const kotlinExtractor: LanguageExtractor = { functionTypes: ['function_declaration'], classTypes: ['class_declaration'], methodTypes: ['function_declaration'], // Methods are functions inside classes interfaceTypes: [], // Handled via classifyClassNode structTypes: [], // Kotlin uses data classes enumTypes: [], // Handled via classifyClassNode enumMemberTypes: ['enum_entry'], typeAliasTypes: ['type_alias'], importTypes: ['import_header'], callTypes: ['call_expression'], variableTypes: ['property_declaration'], fieldTypes: ['property_declaration'], extraClassNodeTypes: ['object_declaration'], nameField: 'simple_identifier', bodyField: 'function_body', visitNode: (node, ctx) => { // Handle Kotlin `fun interface` declarations. // Tree-sitter-kotlin doesn't support `fun interface` syntax (Kotlin 1.4+). // It produces two different misparse patterns: // Pattern 1 (simple): ERROR node + sibling lambda_literal for body // Pattern 2 (complex): function_declaration misparse with ERROR child // Skip lambda_literal bodies that were already consumed by a fun interface ERROR node if (node.type === 'lambda_literal') { const prev = node.previousSibling; if (prev && prev.type === 'ERROR' && isFunInterfaceNode(prev)) return true; return false; } if (node.type !== 'ERROR' && node.type !== 'function_declaration') return false; if (!isFunInterfaceNode(node)) return false; // Extract the interface name from simple_identifier child let nameText: string | null = null; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child && child.type === 'simple_identifier') { nameText = child.text; break; } } if (!nameText) return false; // Create the interface node const ifaceNode = ctx.createNode('interface', nameText, node); if (!ifaceNode) return false; ctx.pushScope(ifaceNode.id); if (node.type === 'ERROR') { // Pattern 1: body is in the next sibling lambda_literal const nextSibling = node.nextSibling; if (nextSibling && nextSibling.type === 'lambda_literal') { for (let i = 0; i < nextSibling.namedChildCount; i++) { const child = nextSibling.namedChild(i); if (child && child.type === 'statements') { for (let j = 0; j < child.namedChildCount; j++) { const stmt = child.namedChild(j); if (stmt) ctx.visitNode(stmt); } } } } } // Pattern 2 (function_declaration): nested classes are siblings at source_file level, // already visited by the normal traversal. The single abstract method is misparsed // and cannot be reliably recovered, but the interface node itself is the key value. ctx.popScope(); return true; }, paramsField: 'function_value_parameters', returnField: 'type', resolveBody: (node, _bodyField) => { // Kotlin's tree-sitter grammar doesn't use field names, so getChildByField fails. // Find body by type: function_body for functions/methods, class_body for classes, // enum_class_body for enums. for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child && (child.type === 'function_body' || child.type === 'class_body' || child.type === 'enum_class_body')) { return child; } } return null; }, classifyClassNode: (node) => { // Kotlin reuses class_declaration for classes, interfaces, and enums. // Detect by checking for keyword children: // interface Foo { } → has 'interface' keyword child // enum class Level { } → has 'enum' keyword child // class / data class / abstract class → default 'class' for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; if (child.type === 'interface') return 'interface'; if (child.type === 'enum') return 'enum'; } return 'class'; }, getReceiverType: (node, source) => { // Kotlin extension functions: fun Type.method() { } // AST: function_declaration > user_type, ".", simple_identifier // The user_type before the dot is the receiver type. let foundUserType: SyntaxNode | null = null; for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (!child) continue; if (child.type === 'user_type') { foundUserType = child; } else if (child.type === '.' && foundUserType) { // The user_type before the dot is the receiver type const typeId = foundUserType.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier'); return typeId ? getNodeText(typeId, source) : getNodeText(foundUserType, source); } else if (child.type === 'simple_identifier' || child.type === 'function_value_parameters') { // Past the function name — no receiver break; } } return undefined; }, getSignature: (node, source) => { // Kotlin function signature: fun name(params): ReturnType const params = getChildByField(node, 'function_value_parameters'); const returnType = getChildByField(node, 'type'); if (!params) return undefined; let sig = getNodeText(params, source); if (returnType) { sig += ': ' + getNodeText(returnType, source); } return sig; }, getVisibility: (node) => { // Check for visibility modifiers in Kotlin for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'modifiers') { const text = child.text; if (text.includes('public')) return 'public'; if (text.includes('private')) return 'private'; if (text.includes('protected')) return 'protected'; if (text.includes('internal')) return 'internal'; } } return 'public'; // Kotlin defaults to public }, isStatic: (_node) => { // Kotlin doesn't have static, uses companion objects return false; }, isAsync: (node) => { // Kotlin uses suspend keyword for coroutines for (let i = 0; i < node.childCount; i++) { const child = node.child(i); if (child?.type === 'modifiers' && child.text.includes('suspend')) { return true; } } return false; }, extractImport: (node, source) => { const importText = source.substring(node.startIndex, node.endIndex).trim(); const identifier = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier'); if (identifier) { return { moduleName: source.substring(identifier.startIndex, identifier.endIndex), signature: importText }; } return null; }, };