scala.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import type { Node as SyntaxNode } from 'web-tree-sitter';
  2. import { getNodeText } from '../tree-sitter-helpers';
  3. import type { LanguageExtractor } from '../tree-sitter-types';
  4. function getValVarName(node: SyntaxNode, source: string): string | null {
  5. const patternNode = node.childForFieldName('pattern');
  6. if (!patternNode) return null;
  7. if (patternNode.type === 'identifier') return getNodeText(patternNode, source);
  8. const identChild = patternNode.namedChildren.find((c: SyntaxNode) => c.type === 'identifier');
  9. return identChild ? getNodeText(identChild, source) : null;
  10. }
  11. // Capitalized Scala primitives/ubiquitous aliases that shouldn't create refs.
  12. const SCALA_BUILTIN_TYPES = new Set([
  13. 'Int', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Boolean', 'Char', 'Unit',
  14. 'String', 'Any', 'AnyRef', 'AnyVal', 'Nothing', 'Null',
  15. ]);
  16. /**
  17. * Emit `references` edges for every type identifier in a Scala type subtree
  18. * (a `val`/`var` type annotation), unwrapping `generic_type` etc. Mirrors the
  19. * generic type-annotation extraction the core extractor runs for method
  20. * parameter/return types, but Scala `val`s are created here in visitNode so
  21. * their type is walked here too. A trait used only as a field type (the common
  22. * `implicit val x: Monoid[Int]` instance pattern) thus gains a dependent.
  23. */
  24. function emitScalaTypeRefs(typeNode: SyntaxNode, fromId: string, ctx: { addUnresolvedReference: (r: { fromNodeId: string; referenceName: string; referenceKind: 'references'; line: number; column: number }) => void }, source: string): void {
  25. if (typeNode.type === 'type_identifier') {
  26. const name = source.substring(typeNode.startIndex, typeNode.endIndex);
  27. if (name && !SCALA_BUILTIN_TYPES.has(name)) {
  28. ctx.addUnresolvedReference({
  29. fromNodeId: fromId,
  30. referenceName: name,
  31. referenceKind: 'references',
  32. line: typeNode.startPosition.row + 1,
  33. column: typeNode.startPosition.column,
  34. });
  35. }
  36. return;
  37. }
  38. for (let i = 0; i < typeNode.namedChildCount; i++) {
  39. const child = typeNode.namedChild(i);
  40. if (child) emitScalaTypeRefs(child, fromId, ctx, source);
  41. }
  42. }
  43. function extractVisibility(node: SyntaxNode): 'public' | 'private' | 'protected' {
  44. for (let i = 0; i < node.namedChildCount; i++) {
  45. const child = node.namedChild(i);
  46. if (!child) continue;
  47. if (child.type === 'modifiers' || child.type === 'access_modifier') {
  48. const text = child.text;
  49. if (text.includes('private')) return 'private';
  50. if (text.includes('protected')) return 'protected';
  51. }
  52. }
  53. return 'public';
  54. }
  55. export const scalaExtractor: LanguageExtractor = {
  56. // top-level function_definition is handled via methodTypes (same pattern as Kotlin)
  57. functionTypes: [],
  58. classTypes: ['class_definition', 'object_definition', 'trait_definition'],
  59. methodTypes: ['function_definition', 'function_declaration'],
  60. interfaceTypes: [],
  61. structTypes: [],
  62. enumTypes: ['enum_definition'],
  63. enumMemberTypes: [], // handled in visitNode — enum_case_definitions wraps the cases
  64. typeAliasTypes: ['type_definition'],
  65. importTypes: ['import_declaration'],
  66. callTypes: ['call_expression'],
  67. variableTypes: [], // val/var handled in visitNode (use `pattern` field, not `name`)
  68. fieldTypes: [],
  69. extraClassNodeTypes: [],
  70. nameField: 'name',
  71. bodyField: 'body',
  72. paramsField: 'parameters',
  73. returnField: 'return_type',
  74. interfaceKind: 'trait',
  75. classifyClassNode: (node: SyntaxNode) => {
  76. if (node.type === 'trait_definition') return 'trait';
  77. return 'class';
  78. },
  79. getSignature: (node: SyntaxNode, source: string) => {
  80. const params = node.childForFieldName('parameters');
  81. const returnType = node.childForFieldName('return_type');
  82. if (!params && !returnType) return undefined;
  83. let sig = params ? getNodeText(params, source) : '';
  84. if (returnType) sig += ': ' + getNodeText(returnType, source);
  85. return sig || undefined;
  86. },
  87. getVisibility: (node: SyntaxNode) => extractVisibility(node),
  88. isAsync: () => false,
  89. isStatic: (node: SyntaxNode) => {
  90. for (let i = 0; i < node.namedChildCount; i++) {
  91. const child = node.namedChild(i);
  92. if (child?.type === 'modifiers' && child.text.includes('static')) return true;
  93. }
  94. return false;
  95. },
  96. visitNode: (node: SyntaxNode, ctx) => {
  97. const t = node.type;
  98. // val/var: name is in `pattern` field (identifier), not `name`
  99. if (t === 'val_definition' || t === 'var_definition') {
  100. const name = getValVarName(node, ctx.source);
  101. if (!name) return false;
  102. const isInClass = ctx.nodeStack.length > 0 &&
  103. (() => {
  104. const parentId = ctx.nodeStack[ctx.nodeStack.length - 1];
  105. const parentNode = ctx.nodes.find((n) => n.id === parentId);
  106. return parentNode != null && (
  107. parentNode.kind === 'class' || parentNode.kind === 'trait' ||
  108. parentNode.kind === 'interface' || parentNode.kind === 'struct' ||
  109. parentNode.kind === 'enum' || parentNode.kind === 'module'
  110. );
  111. })();
  112. const kind = isInClass ? 'field' : (t === 'val_definition' ? 'constant' : 'variable');
  113. const typeNode = node.childForFieldName('type');
  114. const sig = typeNode
  115. ? `${t === 'val_definition' ? 'val' : 'var'} ${name}: ${getNodeText(typeNode, ctx.source)}`
  116. : undefined;
  117. const created = ctx.createNode(kind, name, node, { signature: sig, visibility: extractVisibility(node) });
  118. if (created && typeNode) emitScalaTypeRefs(typeNode, created.id, ctx, ctx.source);
  119. return true;
  120. }
  121. // enum_case_definitions wraps simple_enum_case / full_enum_case children
  122. if (t === 'enum_case_definitions') {
  123. for (let i = 0; i < node.namedChildCount; i++) {
  124. const child = node.namedChild(i);
  125. if (!child) continue;
  126. if (child.type === 'simple_enum_case' || child.type === 'full_enum_case') {
  127. const nameNode = child.childForFieldName('name');
  128. if (nameNode) ctx.createNode('enum_member', getNodeText(nameNode, ctx.source), child);
  129. }
  130. }
  131. return true;
  132. }
  133. // extension_definition: visit body children directly, no container node
  134. if (t === 'extension_definition') {
  135. const body = node.childForFieldName('body');
  136. if (body) {
  137. for (let i = 0; i < body.namedChildCount; i++) {
  138. const child = body.namedChild(i);
  139. if (child) ctx.visitNode(child);
  140. }
  141. }
  142. return true;
  143. }
  144. return false;
  145. },
  146. extractImport: (node: SyntaxNode, source: string) => {
  147. const importText = getNodeText(node, source).trim();
  148. const pathNode = node.childForFieldName('path');
  149. if (pathNode) return { moduleName: getNodeText(pathNode, source), signature: importText };
  150. for (let i = 0; i < node.namedChildCount; i++) {
  151. const child = node.namedChild(i);
  152. if (child?.type === 'identifier' || child?.type === 'stable_identifier') {
  153. return { moduleName: getNodeText(child, source), signature: importText };
  154. }
  155. }
  156. return null;
  157. },
  158. };