1
0

kotlin.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import type { Node as SyntaxNode } from 'web-tree-sitter';
  2. import { getNodeText, getChildByField } from '../tree-sitter-helpers';
  3. import type { LanguageExtractor } from '../tree-sitter-types';
  4. /** Check if a node matches the `fun interface` misparse pattern */
  5. function isFunInterfaceNode(node: SyntaxNode): boolean {
  6. let hasFun = false;
  7. let hasInterfaceType = false;
  8. for (let i = 0; i < node.childCount; i++) {
  9. const child = node.child(i);
  10. if (!child) continue;
  11. if (child.type === 'fun' && !child.isNamed) hasFun = true;
  12. if (child.type === 'user_type') {
  13. const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');
  14. if (typeId && typeId.text === 'interface') hasInterfaceType = true;
  15. }
  16. }
  17. return hasFun && hasInterfaceType;
  18. }
  19. export const kotlinExtractor: LanguageExtractor = {
  20. functionTypes: ['function_declaration'],
  21. classTypes: ['class_declaration'],
  22. methodTypes: ['function_declaration'], // Methods are functions inside classes
  23. interfaceTypes: [], // Handled via classifyClassNode
  24. structTypes: [], // Kotlin uses data classes
  25. enumTypes: [], // Handled via classifyClassNode
  26. enumMemberTypes: ['enum_entry'],
  27. typeAliasTypes: ['type_alias'],
  28. importTypes: ['import_header'],
  29. callTypes: ['call_expression'],
  30. variableTypes: ['property_declaration'],
  31. fieldTypes: ['property_declaration'],
  32. extraClassNodeTypes: ['object_declaration'],
  33. nameField: 'simple_identifier',
  34. bodyField: 'function_body',
  35. visitNode: (node, ctx) => {
  36. // Handle Kotlin `fun interface` declarations.
  37. // Tree-sitter-kotlin doesn't support `fun interface` syntax (Kotlin 1.4+).
  38. // It produces two different misparse patterns:
  39. // Pattern 1 (simple): ERROR node + sibling lambda_literal for body
  40. // Pattern 2 (complex): function_declaration misparse with ERROR child
  41. // Skip lambda_literal bodies that were already consumed by a fun interface ERROR node
  42. if (node.type === 'lambda_literal') {
  43. const prev = node.previousSibling;
  44. if (prev && prev.type === 'ERROR' && isFunInterfaceNode(prev)) return true;
  45. return false;
  46. }
  47. if (node.type !== 'ERROR' && node.type !== 'function_declaration') return false;
  48. if (!isFunInterfaceNode(node)) return false;
  49. // Extract the interface name from simple_identifier child
  50. let nameText: string | null = null;
  51. for (let i = 0; i < node.childCount; i++) {
  52. const child = node.child(i);
  53. if (child && child.type === 'simple_identifier') {
  54. nameText = child.text;
  55. break;
  56. }
  57. }
  58. if (!nameText) return false;
  59. // Create the interface node
  60. const ifaceNode = ctx.createNode('interface', nameText, node);
  61. if (!ifaceNode) return false;
  62. ctx.pushScope(ifaceNode.id);
  63. if (node.type === 'ERROR') {
  64. // Pattern 1: body is in the next sibling lambda_literal
  65. const nextSibling = node.nextSibling;
  66. if (nextSibling && nextSibling.type === 'lambda_literal') {
  67. for (let i = 0; i < nextSibling.namedChildCount; i++) {
  68. const child = nextSibling.namedChild(i);
  69. if (child && child.type === 'statements') {
  70. for (let j = 0; j < child.namedChildCount; j++) {
  71. const stmt = child.namedChild(j);
  72. if (stmt) ctx.visitNode(stmt);
  73. }
  74. }
  75. }
  76. }
  77. }
  78. // Pattern 2 (function_declaration): nested classes are siblings at source_file level,
  79. // already visited by the normal traversal. The single abstract method is misparsed
  80. // and cannot be reliably recovered, but the interface node itself is the key value.
  81. ctx.popScope();
  82. return true;
  83. },
  84. paramsField: 'function_value_parameters',
  85. returnField: 'type',
  86. resolveBody: (node, _bodyField) => {
  87. // Kotlin's tree-sitter grammar doesn't use field names, so getChildByField fails.
  88. // Find body by type: function_body for functions/methods, class_body for classes,
  89. // enum_class_body for enums.
  90. for (let i = 0; i < node.namedChildCount; i++) {
  91. const child = node.namedChild(i);
  92. if (child && (child.type === 'function_body' || child.type === 'class_body' || child.type === 'enum_class_body')) {
  93. return child;
  94. }
  95. }
  96. return null;
  97. },
  98. classifyClassNode: (node) => {
  99. // Kotlin reuses class_declaration for classes, interfaces, and enums.
  100. // Detect by checking for keyword children:
  101. // interface Foo { } → has 'interface' keyword child
  102. // enum class Level { } → has 'enum' keyword child
  103. // class / data class / abstract class → default 'class'
  104. for (let i = 0; i < node.childCount; i++) {
  105. const child = node.child(i);
  106. if (!child) continue;
  107. if (child.type === 'interface') return 'interface';
  108. if (child.type === 'enum') return 'enum';
  109. }
  110. return 'class';
  111. },
  112. getReceiverType: (node, source) => {
  113. // Kotlin extension functions: fun Type.method() { }
  114. // AST: function_declaration > user_type, ".", simple_identifier
  115. // The user_type before the dot is the receiver type.
  116. let foundUserType: SyntaxNode | null = null;
  117. for (let i = 0; i < node.childCount; i++) {
  118. const child = node.child(i);
  119. if (!child) continue;
  120. if (child.type === 'user_type') {
  121. foundUserType = child;
  122. } else if (child.type === '.' && foundUserType) {
  123. // The user_type before the dot is the receiver type
  124. const typeId = foundUserType.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');
  125. return typeId ? getNodeText(typeId, source) : getNodeText(foundUserType, source);
  126. } else if (child.type === 'simple_identifier' || child.type === 'function_value_parameters') {
  127. // Past the function name — no receiver
  128. break;
  129. }
  130. }
  131. return undefined;
  132. },
  133. getSignature: (node, source) => {
  134. // Kotlin function signature: fun name(params): ReturnType
  135. const params = getChildByField(node, 'function_value_parameters');
  136. const returnType = getChildByField(node, 'type');
  137. if (!params) return undefined;
  138. let sig = getNodeText(params, source);
  139. if (returnType) {
  140. sig += ': ' + getNodeText(returnType, source);
  141. }
  142. return sig;
  143. },
  144. getVisibility: (node) => {
  145. // Check for visibility modifiers in Kotlin
  146. for (let i = 0; i < node.childCount; i++) {
  147. const child = node.child(i);
  148. if (child?.type === 'modifiers') {
  149. const text = child.text;
  150. if (text.includes('public')) return 'public';
  151. if (text.includes('private')) return 'private';
  152. if (text.includes('protected')) return 'protected';
  153. if (text.includes('internal')) return 'internal';
  154. }
  155. }
  156. return 'public'; // Kotlin defaults to public
  157. },
  158. isStatic: (_node) => {
  159. // Kotlin doesn't have static, uses companion objects
  160. return false;
  161. },
  162. isAsync: (node) => {
  163. // Kotlin uses suspend keyword for coroutines
  164. for (let i = 0; i < node.childCount; i++) {
  165. const child = node.child(i);
  166. if (child?.type === 'modifiers' && child.text.includes('suspend')) {
  167. return true;
  168. }
  169. }
  170. return false;
  171. },
  172. extractImport: (node, source) => {
  173. const importText = source.substring(node.startIndex, node.endIndex).trim();
  174. const identifier = node.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
  175. if (identifier) {
  176. return { moduleName: source.substring(identifier.startIndex, identifier.endIndex), signature: importText };
  177. }
  178. return null;
  179. },
  180. };