-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Completion.scala
711 lines (606 loc) · 31.8 KB
/
Completion.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
package dotty.tools.dotc.interactive
import dotty.tools.dotc.ast.untpd
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.ast.NavigateAST
import dotty.tools.dotc.config.Printers.interactiv
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Decorators.*
import dotty.tools.dotc.core.Denotations.SingleDenotation
import dotty.tools.dotc.core.Flags.*
import dotty.tools.dotc.core.Names.{Name, TermName}
import dotty.tools.dotc.core.NameKinds.SimpleNameKind
import dotty.tools.dotc.core.NameOps.*
import dotty.tools.dotc.core.Phases
import dotty.tools.dotc.core.Scopes.*
import dotty.tools.dotc.core.Symbols.{NoSymbol, Symbol, defn, newSymbol}
import dotty.tools.dotc.core.StdNames.nme
import dotty.tools.dotc.core.SymDenotations.SymDenotation
import dotty.tools.dotc.core.TypeError
import dotty.tools.dotc.core.Phases
import dotty.tools.dotc.core.Types.{AppliedType, ExprType, MethodOrPoly, NameFilter, NoType, RefinedType, TermRef, Type, TypeProxy}
import dotty.tools.dotc.parsing.Tokens
import dotty.tools.dotc.typer.Implicits.SearchSuccess
import dotty.tools.dotc.typer.Inferencing
import dotty.tools.dotc.util.Chars
import dotty.tools.dotc.util.SourcePosition
import scala.collection.mutable
import scala.util.control.NonFatal
import dotty.tools.dotc.core.ContextOps.localContext
import dotty.tools.dotc.core.Names
import dotty.tools.dotc.core.Types
import dotty.tools.dotc.core.Symbols
import dotty.tools.dotc.core.Constants
import dotty.tools.dotc.core.TypeOps
import dotty.tools.dotc.core.StdNames
/**
* One of the results of a completion query.
*
* @param label The label of this completion result, or the text that this completion result
* should insert in the scope where the completion request happened.
* @param description The description of this completion result: the fully qualified name for
* types, or the type for terms.
* @param symbols The symbols that are matched by this completion result.
*/
case class Completion(label: String, description: String, symbols: List[Symbol])
object Completion:
/** Get possible completions from tree at `pos`
*
* @return offset and list of symbols for possible completions
*/
def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) =
val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span)
val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase)
inContext(completionContext):
val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos)
val mode = completionMode(untpdPath, pos)
val rawPrefix = completionPrefix(untpdPath, pos)
val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath)
postProcessCompletions(untpdPath, completions, rawPrefix)
/** Get possible completions from tree at `pos`
* This method requires manually computing the mode, prefix and paths.
*
* @return completion map of name to list of denotations
*/
def rawCompletions(
pos: SourcePosition,
mode: Mode,
rawPrefix: String,
tpdPath: List[tpd.Tree],
untpdPath: List[untpd.Tree],
customMatcher: Option[Name => Boolean] = None
)(using Context): CompletionMap =
val adjustedPath = typeCheckExtensionConstructPath(untpdPath, tpdPath, pos)
computeCompletions(pos, mode, rawPrefix, adjustedPath, untpdPath, customMatcher)
/**
* Inspect `path` to determine what kinds of symbols should be considered.
*
* If the path starts with:
* - a `RefTree`, then accept symbols of the same kind as its name;
* - a renaming import, and the cursor is on the renamee, accept both terms and types;
* - an import, accept both terms and types;
*
* Otherwise, provide no completion suggestion.
*/
def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = path match
case GenericImportSelector(sel) =>
if sel.imported.span.contains(pos.span) then Mode.ImportOrExport // import scala.@@
else if sel.isGiven && sel.bound.span.contains(pos.span) then Mode.ImportOrExport
else Mode.None // import scala.{util => u@@}
case GenericImportOrExport(_) => Mode.ImportOrExport | Mode.Scope // import TrieMa@@
case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term | Mode.Scope // literal completions
case (ref: untpd.RefTree) :: _ =>
val maybeSelectMembers = if ref.isInstanceOf[untpd.Select] then Mode.Member else Mode.Scope
if (ref.name.isTermName) Mode.Term | maybeSelectMembers
else if (ref.name.isTypeName) Mode.Type | maybeSelectMembers
else Mode.None
case _ => Mode.None
/** When dealing with <errors> in varios palces we check to see if they are
* due to incomplete backticks. If so, we ensure we get the full prefix
* including the backtick.
*
* @param content The source content that we'll check the positions for the prefix
* @param start The start position we'll start to look for the prefix at
* @param end The end position we'll look for the prefix at
* @return Either the full prefix including the ` or an empty string
*/
private def checkBacktickPrefix(content: Array[Char], start: Int, end: Int): String =
content.lift(start) match
case Some(char) if char == '`' =>
content.slice(start, end).mkString
case _ =>
""
def naiveCompletionPrefix(text: String, offset: Int): String =
var i = offset - 1
while i >= 0 && text(i).isUnicodeIdentifierPart do i -= 1
i += 1 // move to first character
text.slice(i, offset)
/**
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
* returned prefix should be considered.
*/
def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String =
path match
case GenericImportSelector(sel) =>
if sel.isGiven then completionPrefix(sel.bound :: Nil, pos)
else if sel.isWildcard then pos.source.content()(pos.point - 1).toString
else completionPrefix(sel.imported :: Nil, pos)
// Foo.`se<TAB> will result in Select(Ident(Foo), <error>)
case (select: untpd.Select) :: _ if select.name == nme.ERROR =>
checkBacktickPrefix(select.source.content(), select.nameSpan.start, select.span.end)
// import scala.util.chaining.`s<TAB> will result in a Ident(<error>)
case (ident: untpd.Ident) :: _ if ident.name == nme.ERROR =>
checkBacktickPrefix(ident.source.content(), ident.span.start, ident.span.end)
case (tree: untpd.RefTree) :: _ if tree.name != nme.ERROR =>
tree.name.toString.take(pos.span.point - tree.span.point)
case _ => naiveCompletionPrefix(pos.source.content().mkString, pos.point)
end completionPrefix
private object GenericImportSelector:
def unapply(path: List[untpd.Tree]): Option[untpd.ImportSelector] =
path match
case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ => Some(sel)
case (sel: untpd.ImportSelector) :: _ => Some(sel)
case _ => None
private object GenericImportOrExport:
def unapply(path: List[untpd.Tree]): Option[untpd.ImportOrExport] =
path match
case untpd.Ident(_) :: (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport)
case (importOrExport: untpd.ImportOrExport) :: _ => Some(importOrExport)
case _ => None
/** Inspect `path` to determine the offset where the completion result should be inserted. */
def completionOffset(untpdPath: List[untpd.Tree]): Int =
untpdPath match
case (ref: untpd.RefTree) :: _ => ref.span.point
case _ => 0
/** Handle case when cursor position is inside extension method construct.
* The extension method construct is then desugared into methods, and consturct parameters
* are no longer a part of a typed tree, but instead are prepended to method parameters.
*
* @param untpdPath The typed or untyped path to the tree that is being completed
* @param tpdPath The typed path that will be returned if no extension method construct is found
* @param pos The cursor position
*
* @return Typed path to the parameter of the extension construct if found or tpdPath
*/
private def typeCheckExtensionConstructPath(
untpdPath: List[untpd.Tree], tpdPath: List[tpd.Tree], pos: SourcePosition
)(using Context): List[tpd.Tree] =
untpdPath.collectFirst:
case untpd.ExtMethods(paramss, _) =>
val enclosingParam = paramss.flatten
.find(_.span.contains(pos.span))
.flatMap:
case untpd.TypeDef(_, bounds: untpd.ContextBounds) => bounds.cxBounds.find(_.span.contains(pos.span))
case other => Some(other)
enclosingParam.map: param =>
ctx.typer.index(paramss.flatten)
val typedEnclosingParam = ctx.typer.typed(param)
Interactive.pathTo(typedEnclosingParam, pos.span)
.flatten.getOrElse(tpdPath)
private def computeCompletions(
pos: SourcePosition,
mode: Mode,
rawPrefix: String,
adjustedPath: List[tpd.Tree],
untpdPath: List[untpd.Tree],
matches: Option[Name => Boolean]
)(using Context): CompletionMap =
val hasBackTick = rawPrefix.headOption.contains('`')
val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix
val matches0 = matches.getOrElse(_.startsWith(prefix))
val completer = new Completer(mode, pos, untpdPath, matches0)
val result = adjustedPath match
// Ignore synthetic select from `This` because in code it was `Ident`
// See example in dotty.tools.languageserver.CompletionTest.syntheticThis
case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions
case tpd.Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual)
case tpd.Select(qual, _) :: _ => Map.empty
case (tree: tpd.ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr)
case _ => completer.scopeCompletions
interactiv.println(i"""completion info with pos = $pos,
| term = ${completer.mode.is(Mode.Term)},
| type = ${completer.mode.is(Mode.Type)},
| scope = ${completer.mode.is(Mode.Scope)},
| member = ${completer.mode.is(Mode.Member)}""")
result
def postProcessCompletions(path: List[untpd.Tree], completions: CompletionMap, rawPrefix: String)(using Context): (Int, List[Completion]) =
val describedCompletions = describeCompletions(completions)
val hasBackTick = rawPrefix.headOption.contains('`')
val backtickedCompletions =
describedCompletions.map(completion => backtickCompletions(completion, hasBackTick))
interactiv.println(i"""completion resutls = $backtickedCompletions%, %""")
val offset = completionOffset(path)
(offset, backtickedCompletions)
def backtickCompletions(completion: Completion, hasBackTick: Boolean) =
if hasBackTick || needsBacktick(completion.label) then
completion.copy(label = s"`${completion.label}`")
else
completion
// This borrows from Metals, which itself borrows from Ammonite. This uses
// the same approach, but some of the utils that already exist in Dotty.
// https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/mtags/KeywordWrapper.scala
// https://github.com/com-lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala
private def needsBacktick(s: String) =
val chunks = s.split("_", -1).nn
val validChunks = chunks.zipWithIndex.forall { case (chunk, index) =>
chunk.nn.forall(Chars.isIdentifierPart) ||
(chunk.nn.forall(Chars.isOperatorPart) &&
index == chunks.length - 1 &&
!(chunks.lift(index - 1).contains("") && index - 1 == 0))
}
val validStart =
Chars.isIdentifierStart(s(0)) || chunks(0).nn.forall(Chars.isOperatorPart)
val valid = validChunks && validStart && !keywords.contains(s)
!valid
end needsBacktick
private lazy val keywords = Tokens.keywords.map(Tokens.tokenString)
/**
* Return the list of code completions with descriptions based on a mapping from names to the denotations they refer to.
* If several denotations share the same name, each denotation will be transformed into a separate completion item.
*/
def describeCompletions(completions: CompletionMap)(using Context): List[Completion] =
for
(name, denots) <- completions.toList
denot <- denots
yield
Completion(name.show, description(denot), List(denot.symbol))
def description(denot: SingleDenotation)(using Context): String =
if denot.isType then denot.symbol.showFullName
else denot.info.widenTermRefExpr.show
def isInNewContext(untpdPath: List[untpd.Tree]): Boolean =
untpdPath match
case _ :: untpd.New(selectOrIdent: (untpd.Select | untpd.Ident)) :: _ => true
case _ => false
/** Include in completion sets only symbols that
* 1. is not absent (info is not NoType)
* 2. are not a primary constructor,
* 3. have an existing source symbol,
* 4. are the module class in case of packages,
* 5. are mutable accessors, to exclude setters for `var`,
* 6. symbol is not a package object
* 7. symbol is not an artifact of the compiler
* 8. symbol is not a constructor proxy module when in type completion mode
* 9. have same term/type kind as name prefix given so far
*/
def isValidCompletionSymbol(sym: Symbol, completionMode: Mode, isNew: Boolean)(using Context): Boolean =
lazy val isEnum = sym.is(Enum) ||
(sym.companionClass.exists && sym.companionClass.is(Enum))
sym.exists &&
!sym.isAbsent() &&
!sym.isPrimaryConstructor &&
sym.sourceSymbol.exists &&
(!sym.is(Package) || sym.is(ModuleClass)) &&
!sym.isAllOf(Mutable | Accessor) &&
!sym.isPackageObject &&
!sym.is(Artifact) &&
!(completionMode.is(Mode.Type) && sym.isAllOf(ConstructorProxyModule)) &&
!(isNew && isEnum) &&
(
(completionMode.is(Mode.Term) && (sym.isTerm || sym.is(ModuleClass))
|| (completionMode.is(Mode.Type) && (sym.isType || sym.isStableMember)))
)
given ScopeOrdering(using Context): Ordering[Seq[SingleDenotation]] with
val order =
List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass)
override def compare(x: Seq[SingleDenotation], y: Seq[SingleDenotation]): Int =
val owner0 = x.headOption.map(_.symbol.effectiveOwner).getOrElse(NoSymbol)
val owner1 = y.headOption.map(_.symbol.effectiveOwner).getOrElse(NoSymbol)
order.indexOf(owner0) - order.indexOf(owner1)
/** Computes code completions depending on the context in which completion is requested
* @param mode Should complete names of terms, types or both
* @param pos Cursor position where completion was requested
* @param matches Function taking name used to filter completions
*
* For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map
* and they never conflict with each other.
*/
class Completer(val mode: Mode, pos: SourcePosition, untpdPath: List[untpd.Tree], matches: Name => Boolean):
/** Completions for terms and types that are currently in scope:
* the members of the current class, local definitions and the symbols that have been imported,
* recursively adding completions from outer scopes.
* In case a name is ambiguous, no completions are returned for it.
* This mimics the logic for deciding what is ambiguous used by the compiler.
* In general in case of a name clash symbols introduced in more deeply nested scopes
* have higher priority and shadow previous definitions with the same name although:
* - imports with the same level of nesting cause an ambiguity if they are in the same name space
* - members and local definitions with the same level of nesting are allowed for overloading
* - an import is ignored if there is a local definition or a member introduced in the same scope
* (even if the import follows it syntactically)
* - a more deeply nested import shadowing a member or a local definition causes an ambiguity
*/
def scopeCompletions(using context: Context): CompletionMap =
/** Temporary data structure representing denotations with the same name introduced in a given scope
* as a member of a type, by a local definition or by an import clause
*/
case class ScopedDenotations private (denots: Seq[SingleDenotation], ctx: Context)
object ScopedDenotations:
def apply(denots: Seq[SingleDenotation], ctx: Context, includeFn: SingleDenotation => Boolean): ScopedDenotations =
ScopedDenotations(denots.filter(includeFn), ctx)
val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty)
def addMapping(name: Name, denots: ScopedDenotations) =
mappings(name) = mappings(name) :+ denots
ctx.outersIterator.foreach { case ctx @ given Context =>
if ctx.isImportContext then
importedCompletions.foreach { (name, denots) =>
addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
}
else if ctx.owner.isClass then
accessibleMembers(ctx.owner.thisType)
.groupByName.foreach { (name, denots) =>
addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
}
else if ctx.scope ne EmptyScope then
ctx.scope.toList.filter(symbol => include(symbol, symbol.name))
.flatMap(_.alternatives)
.groupByName.foreach { (name, denots) =>
addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
}
}
var resultMappings = Map.empty[Name, Seq[SingleDenotation]]
mappings.foreach { (name, denotss) =>
val first = denotss.head
// import a.c
def isSingleImport = denotss.length < 2
// import a.C
// locally { import b.C }
def isImportedInDifferentScope = first.ctx.scope ne denotss(1).ctx.scope
// import a.C
// import a.C
def isSameSymbolImportedDouble = denotss.forall(_.denots == first.denots)
// https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html
// import java.lang.*
// {
// import scala.*
// {
// import Predef.*
// { /* source */ }
// }
// }
def notConflictingWithDefaults = // is imported symbol
denotss.filterNot(_.denots.exists(denot => Interactive.isImportedByDefault(denot.symbol))).size <= 1
denotss.find(!_.ctx.isImportContext) match {
// most deeply nested member or local definition if not shadowed by an import
case Some(local) if local.ctx.scope == first.ctx.scope =>
resultMappings += name -> local.denots
case None if isSingleImport || isImportedInDifferentScope || isSameSymbolImportedDouble =>
resultMappings += name -> first.denots
case None if notConflictingWithDefaults =>
val ordered = denotss.map(_.denots).sorted
resultMappings += name -> ordered.head
case _ =>
}
}
resultMappings
end scopeCompletions
/** Widen only those types which are applied or are exactly nothing
*/
def widenQualifier(qual: tpd.Tree)(using Context): tpd.Tree =
qual.typeOpt.widenDealias match
case widenedType if widenedType.isExactlyNothing => qual.withType(widenedType)
case appliedType: AppliedType => qual.withType(appliedType)
case _ => qual
/** Completions for selections from a term.
* Direct members take priority over members from extensions
* and so do members from extensions over members from implicit conversions
*/
def selectionCompletions(qual: tpd.Tree)(using Context): CompletionMap =
val adjustedQual = widenQualifier(qual)
val implicitConversionMembers = implicitConversionMemberCompletions(adjustedQual)
val extensionMembers = extensionCompletions(adjustedQual)
val directMembers = directMemberCompletions(adjustedQual)
val namedTupleMembers = namedTupleCompletions(adjustedQual)
List(
implicitConversionMembers,
extensionMembers,
directMembers,
namedTupleMembers
).reduce(_ ++ _)
/** Completions for members of `qual`'s type.
* These include inherited definitions but not members added by extensions or implicit conversions
*/
def directMemberCompletions(qual: tpd.Tree)(using Context): CompletionMap =
if qual.typeOpt.isExactlyNothing then
Map.empty
else
accessibleMembers(qual.typeOpt).groupByName
/** Completions introduced by imports directly in this context.
* Completions from outer contexts are not included.
*/
private def importedCompletions(using Context): CompletionMap =
val imp = ctx.importInfo
if imp == null then
Map.empty
else
def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] =
imp.site.member(name).alternatives
.collect { case denot if include(denot, nameInScope) => nameInScope -> denot }
val givenImports = imp.importedImplicits
.map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) }
.filter((name, denot) => include(denot, name))
.groupByName
val wildcardMembers =
if imp.selectors.exists(_.imported.name == nme.WILDCARD) then
val denots = accessibleMembers(imp.site)
.filter(mbr => !mbr.symbol.is(Given) && !imp.excluded.contains(mbr.name.toTermName))
denots.groupByName
else
Map.empty
val explicitMembers =
val importNamesInScope = imp.forwardMapping.toList.map(_._2)
val duplicatedNames = importNamesInScope.diff(importNamesInScope.distinct)
val discardedNames = duplicatedNames ++ imp.excluded
imp.reverseMapping.toList
.filter { (nameInScope, _) => !discardedNames.contains(nameInScope) }
.flatMap { (nameInScope, original) =>
fromImport(original, nameInScope) ++
fromImport(original.toTypeName, nameInScope.toTypeName)
}.toSeq.groupByName
givenImports ++ wildcardMembers ++ explicitMembers
end importedCompletions
/** Completions from implicit conversions including old style extensions using implicit classes */
private def implicitConversionMemberCompletions(qual: tpd.Tree)(using Context): CompletionMap =
def tryToInstantiateTypeVars(conversionTarget: SearchSuccess): Type =
try
val typingCtx = ctx.fresh
inContext(typingCtx):
val methodRefTree = tpd.ref(conversionTarget.ref, needLoad = false)
val convertedTree = ctx.typer.typedAheadExpr(untpd.Apply(untpd.TypedSplice(methodRefTree), untpd.TypedSplice(qual) :: Nil))
Inferencing.fullyDefinedType(convertedTree.tpe, "", pos)
catch
case error => conversionTarget.tree.tpe // fallback to not fully defined type
if qual.typeOpt.isExactlyNothing || qual.typeOpt.isNullType then
Map.empty
else
implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState())
.flatMap { conversionTarget => accessibleMembers(tryToInstantiateTypeVars(conversionTarget)) }
.toSeq
.groupByName
/** Completions for named tuples */
private def namedTupleCompletions(qual: tpd.Tree)(using Context): CompletionMap =
def namedTupleCompletionsFromType(tpe: Type): CompletionMap =
val freshCtx = ctx.fresh.setExploreTyperState()
inContext(freshCtx):
tpe.namedTupleElementTypes
.map { (name, tpe) =>
val symbol = newSymbol(owner = NoSymbol, name, EmptyFlags, tpe)
val denot = SymDenotation(symbol, NoSymbol, name, EmptyFlags, tpe)
name -> denot
}
.toSeq
.filter((name, denot) => include(denot, name))
.groupByName
val qualTpe = qual.typeOpt
if qualTpe.isNamedTupleType then
namedTupleCompletionsFromType(qualTpe)
else if qualTpe.derivesFrom(defn.SelectableClass) then
val pre = if !TypeOps.isLegalPrefix(qualTpe) then Types.SkolemType(qualTpe) else qualTpe
val fieldsType = pre.select(StdNames.tpnme.Fields).dealias.simplified
namedTupleCompletionsFromType(fieldsType)
else Map.empty
/** Completions from extension methods */
private def extensionCompletions(qual: tpd.Tree)(using Context): CompletionMap =
def asDefLikeType(tpe: Type): Type = tpe match
case _: MethodOrPoly => tpe
case _ => ExprType(tpe)
def tryApplyingReceiverToExtension(termRef: TermRef): Option[SingleDenotation] =
ctx.typer.tryApplyingExtensionMethod(termRef, qual)
.map { tree =>
val tpe = asDefLikeType(tree.typeOpt.dealias)
termRef.denot.asSingleDenotation.mapInfo(_ => tpe)
}
def extractMemberExtensionMethods(types: Seq[Type]): Seq[(TermRef, TermName)] =
object DenotWithMatchingName:
def unapply(denot: SingleDenotation): Option[(SingleDenotation, TermName)] =
denot.name match
case name: TermName if include(denot, name) => Some((denot, name))
case _ => None
types.flatMap { tp =>
val tpe = tp.widenExpr
tpe.membersBasedOnFlags(required = ExtensionMethod, excluded = EmptyFlags)
.collect { case DenotWithMatchingName(denot, name) => TermRef(tpe, denot.symbol) -> name }
}
// There are four possible ways for an extension method to be applicable
// 1. The extension method is visible under a simple name, by being defined or inherited or imported in a scope enclosing the reference.
val termCompleter = new Completer(Mode.Term, pos, untpdPath, matches)
val extMethodsInScope = termCompleter.scopeCompletions.toList.flatMap:
case (name, denots) => denots.collect:
case d: SymDenotation if d.isTerm && d.termRef.symbol.is(Extension) => (d.termRef, name.asTermName)
// 2. The extension method is a member of some given instance that is visible at the point of the reference.
val givensInScope = ctx.implicits.eligible(defn.AnyType).map(_.implicitRef.underlyingRef)
val extMethodsFromGivensInScope = extractMemberExtensionMethods(givensInScope)
// 3. The reference is of the form r.m and the extension method is defined in the implicit scope of the type of r.
val implicitScopeCompanions = ctx.run.nn.implicitScope(qual.typeOpt).companionRefs.showAsList
val extMethodsFromImplicitScope = extractMemberExtensionMethods(implicitScopeCompanions)
// 4. The reference is of the form r.m and the extension method is defined in some given instance in the implicit scope of the type of r.
val givensInImplicitScope = implicitScopeCompanions.flatMap(_.membersBasedOnFlags(required = GivenVal, excluded = EmptyFlags)).map(_.info)
val extMethodsFromGivensInImplicitScope = extractMemberExtensionMethods(givensInImplicitScope)
val availableExtMethods = extMethodsFromGivensInImplicitScope ++ extMethodsFromImplicitScope ++ extMethodsFromGivensInScope ++ extMethodsInScope
val extMethodsWithAppliedReceiver = availableExtMethods.flatMap {
case (termRef, termName) =>
if termRef.symbol.is(ExtensionMethod) && !qual.typeOpt.isBottomType then
tryApplyingReceiverToExtension(termRef)
.map(denot => termName -> denot)
else None
}
extMethodsWithAppliedReceiver.groupByName
lazy val isNew: Boolean = isInNewContext(untpdPath)
/** Include in completion sets only symbols that
* 1. match the filter method,
* 2. satisfy [[Completion.isValidCompletionSymbol]]
*/
private def include(denot: SingleDenotation, nameInScope: Name)(using Context): Boolean =
matches(nameInScope) &&
completionsFilter(NoType, nameInScope) &&
isValidCompletionSymbol(denot.symbol, mode, isNew)
private def extractRefinements(site: Type)(using Context): Seq[SingleDenotation] =
site match
case RefinedType(parent, name, info) =>
val flags = info match
case _: (ExprType | MethodOrPoly) => Method
case _ => EmptyFlags
val symbol = newSymbol(owner = NoSymbol, name, flags, info)
val denot = SymDenotation(symbol, NoSymbol, name, flags, info)
denot +: extractRefinements(parent)
case tp: TypeProxy => extractRefinements(tp.superType)
case _ => List.empty
/** @param site The type to inspect.
* @return The members of `site` that are accessible and pass the include filter.
*/
private def accessibleMembers(site: Type)(using Context): Seq[SingleDenotation] = {
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
try
val member = site.member(name)
if member.symbol.is(ParamAccessor) && !member.symbol.isAccessibleFrom(site) then
buf ++= site.nonPrivateMember(name).alternatives
else
buf ++= member.alternatives
catch
case ex: TypeError =>
val members = site.memberDenots(completionsFilter, appendMemberSyms).collect {
case mbr if include(mbr, mbr.name)
&& mbr.symbol.isAccessibleFrom(site) => mbr
}
val refinements = extractRefinements(site).filter(mbr => include(mbr, mbr.name))
members ++ refinements
}
/**
* Given `qual` of type T, finds all the types S such that there exists an implicit conversion
* from T to S. It then applies conversion method for proper type parameter resolution.
*
* @param qual The argument to which the implicit conversion should be applied.
* @return The set of types after `qual` implicit conversion.
*/
private def implicitConversionTargets(qual: tpd.Tree)(using Context): Set[SearchSuccess] = {
val typer = ctx.typer
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits
interactiv.println(i"implicit conversion targets considered: ${conversions.toList}%, %")
conversions
}
/** Filter for names that should appear when looking for completions. */
private object completionsFilter extends NameFilter:
def apply(pre: Type, name: Name)(using Context): Boolean =
!name.isConstructorName && name.toTermName.info.kind == SimpleNameKind
def isStable = true
extension (denotations: Seq[SingleDenotation])
def groupByName(using Context): CompletionMap = denotations.groupBy(_.name)
extension [N <: Name](namedDenotations: Seq[(N, SingleDenotation)])
@annotation.targetName("groupByNameTupled")
def groupByName: CompletionMap = namedDenotations.groupMap((name, denot) => name)((name, denot) => denot)
private type CompletionMap = Map[Name, Seq[SingleDenotation]]
/**
* The completion mode: defines what kinds of symbols should be included in the completion
* results.
*/
class Mode(val bits: Int) extends AnyVal:
def is(other: Mode): Boolean = (bits & other.bits) == other.bits
def |(other: Mode): Mode = new Mode(bits | other.bits)
object Mode:
/** No symbol should be included */
val None: Mode = new Mode(0)
/** Term symbols are allowed */
val Term: Mode = new Mode(1)
/** Type and stable term symbols are allowed */
val Type: Mode = new Mode(2)
/** Both term and type symbols are allowed */
val ImportOrExport: Mode = new Mode(4) | Term | Type
val Scope: Mode = new Mode(8)
val Member: Mode = new Mode(16)