|
|
@@ -115,6 +115,38 @@ function extractName(node: SyntaxNode, source: string, extractor: LanguageExtrac
|
|
|
return '<anonymous>';
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Resolve a Scala type node to its base type NAME for name-matching — unwrapping
|
|
|
+ * `generic_type` (`Monoid[Int]` → `Monoid`), taking the last segment of a
|
|
|
+ * qualified `stable_type_identifier` (`cats.Functor` → `Functor`), and falling
|
|
|
+ * back to a descendant `type_identifier`. Returns null for non-type nodes.
|
|
|
+ * Shared by Scala inheritance and type-reference extraction.
|
|
|
+ */
|
|
|
+function scalaBaseTypeName(node: SyntaxNode | null, source: string): string | null {
|
|
|
+ if (!node) return null;
|
|
|
+ switch (node.type) {
|
|
|
+ case 'type_identifier':
|
|
|
+ case 'identifier':
|
|
|
+ return getNodeText(node, source);
|
|
|
+ case 'generic_type':
|
|
|
+ // `<base> type_arguments` — the base type is the first named child.
|
|
|
+ return scalaBaseTypeName(node.namedChild(0), source);
|
|
|
+ case 'stable_type_identifier':
|
|
|
+ case 'stable_identifier': {
|
|
|
+ // Qualified `a.b.C` — match on the simple (last) segment.
|
|
|
+ const ids = node.namedChildren.filter(
|
|
|
+ (c: SyntaxNode) => c.type === 'type_identifier' || c.type === 'identifier'
|
|
|
+ );
|
|
|
+ const last = ids[ids.length - 1];
|
|
|
+ return last ? getNodeText(last, source) : null;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ const id = node.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier');
|
|
|
+ return id ? getNodeText(id, source) : null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Tree-sitter node kinds that represent constructor invocations
|
|
|
* (`new Foo()` and friends). Used by extractInstantiation to emit
|
|
|
@@ -126,6 +158,7 @@ const INSTANTIATION_KINDS: ReadonlySet<string> = new Set([
|
|
|
'instance_creation_expression', // some grammars
|
|
|
'composite_literal', // go — `Widget{...}` / `pkga.Widget{...}`
|
|
|
'struct_expression', // rust — `Widget { n: 1 }` / `m::Widget { .. }`
|
|
|
+ 'instance_expression', // scala — `new Monoid[Int] { ... }`
|
|
|
]);
|
|
|
|
|
|
/**
|
|
|
@@ -2223,6 +2256,23 @@ export class TreeSitterExtractor {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
+ // Scala: `new Monoid[Int] { ... }` — the constructor is a `generic_type`
|
|
|
+ // (or qualified `stable_type_identifier`) using `[...]` type args, which the
|
|
|
+ // generic `<...>` strip below misses. Unwrap to the base type name.
|
|
|
+ if (node.type === 'instance_expression') {
|
|
|
+ const name = scalaBaseTypeName(ctor, this.source);
|
|
|
+ if (name) {
|
|
|
+ this.unresolvedReferences.push({
|
|
|
+ fromNodeId: fromId,
|
|
|
+ referenceName: name,
|
|
|
+ referenceKind: 'instantiates',
|
|
|
+ line: node.startPosition.row + 1,
|
|
|
+ column: node.startPosition.column,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
let className = getNodeText(ctor, this.source);
|
|
|
// Strip type-argument suffix first: `new Map<K, V>()` would
|
|
|
// otherwise produce className 'Map<K, V>' (the constructor
|
|
|
@@ -2612,6 +2662,28 @@ export class TreeSitterExtractor {
|
|
|
child.type === 'base_clause' || // PHP class extends
|
|
|
child.type === 'extends_interfaces' // Java interface extends
|
|
|
) {
|
|
|
+ // Scala: `extends A[X] with B with C` packs EVERY supertype into the
|
|
|
+ // one extends_clause (separated by `with`), each a `generic_type` /
|
|
|
+ // `type_identifier` / `stable_type_identifier`. The generic path below
|
|
|
+ // takes only namedChild(0) and keeps the full text (`A[X]`), so a
|
|
|
+ // parameterized supertype — every typeclass in cats/algebra — never
|
|
|
+ // matched and `with`-mixed traits past the first were dropped. Iterate
|
|
|
+ // all supertypes and unwrap each to its base type name.
|
|
|
+ if (this.language === 'scala') {
|
|
|
+ for (const target of child.namedChildren) {
|
|
|
+ const name = scalaBaseTypeName(target, this.source);
|
|
|
+ if (name) {
|
|
|
+ this.unresolvedReferences.push({
|
|
|
+ fromNodeId: classId,
|
|
|
+ referenceName: name,
|
|
|
+ referenceKind: 'extends',
|
|
|
+ line: target.startPosition.row + 1,
|
|
|
+ column: target.startPosition.column,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
// Extract parent class/interface names
|
|
|
// Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier
|
|
|
const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list');
|
|
|
@@ -2912,7 +2984,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', 'scala',
|
|
|
]);
|
|
|
|
|
|
/**
|
|
|
@@ -2929,6 +3001,9 @@ export class TreeSitterExtractor {
|
|
|
// Go
|
|
|
'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64',
|
|
|
'float32', 'float64', 'complex64', 'complex128', 'rune', 'error',
|
|
|
+ // Scala (capitalized primitives + ubiquitous stdlib aliases)
|
|
|
+ 'Int', 'Long', 'Short', 'Byte', 'Float', 'Double', 'Boolean', 'Char', 'Unit',
|
|
|
+ 'String', 'Any', 'AnyRef', 'AnyVal', 'Nothing', 'Null',
|
|
|
]);
|
|
|
|
|
|
/**
|
|
|
@@ -2950,10 +3025,19 @@ export class TreeSitterExtractor {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // Extract parameter type annotations
|
|
|
- const params = getChildByField(node, this.extractor.paramsField || 'parameters');
|
|
|
- if (params) {
|
|
|
- this.extractTypeRefsFromSubtree(params, nodeId);
|
|
|
+ // Extract parameter type annotations. Scala curries — `def f(a)(implicit
|
|
|
+ // M: TC)` has MULTIPLE `parameters` siblings, and the typeclass is almost
|
|
|
+ // always in the trailing implicit list — so walk every parameter list, not
|
|
|
+ // just getChildByField's first match.
|
|
|
+ if (this.language === 'scala') {
|
|
|
+ for (const pc of node.namedChildren) {
|
|
|
+ if (pc.type === 'parameters') this.extractTypeRefsFromSubtree(pc, nodeId);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const params = getChildByField(node, this.extractor.paramsField || 'parameters');
|
|
|
+ if (params) {
|
|
|
+ this.extractTypeRefsFromSubtree(params, nodeId);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Extract return type annotation
|
|
|
@@ -2962,6 +3046,22 @@ export class TreeSitterExtractor {
|
|
|
this.extractTypeRefsFromSubtree(returnType, nodeId);
|
|
|
}
|
|
|
|
|
|
+ // Scala context bounds / type-parameter bounds: `def f[A: Monoid]`,
|
|
|
+ // `[F[_]: Monad]`, `[A <: Foo]` carry the bound type inside `type_parameters`.
|
|
|
+ // This is THE pervasive way a typeclass is required in Scala, yet the bound
|
|
|
+ // never appears in the value parameters. Param NAMES are `identifier` (not
|
|
|
+ // `type_identifier`), so only the bound types surface. Scala-only: in other
|
|
|
+ // languages a `type_parameters` child holds declaration names as
|
|
|
+ // `type_identifier` (TS `<T>`), which would wrongly surface as refs.
|
|
|
+ if (this.language === 'scala') {
|
|
|
+ const typeParams = node.namedChildren.find(
|
|
|
+ (c: SyntaxNode) => c.type === 'type_parameters'
|
|
|
+ );
|
|
|
+ if (typeParams) {
|
|
|
+ this.extractTypeRefsFromSubtree(typeParams, nodeId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// Extract direct type annotation (for class fields like `model: ITextModel`)
|
|
|
const typeAnnotation = node.namedChildren.find(
|
|
|
(c: SyntaxNode) => c.type === 'type_annotation'
|