-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Expand file tree
/
Copy pathCompletion.scala
More file actions
774 lines (670 loc) · 35.1 KB
/
Completion.scala
File metadata and controls
774 lines (670 loc) · 35.1 KB
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
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
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
import java.util.logging.Logger
/**
* 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:
private val logger = Logger.getLogger(this.getClass.getName)
def scopeContext(pos: SourcePosition)(using Context): CompletionResult =
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 rawPrefix = completionPrefix(untpdPath, pos)
// Lazy mode is to avoid too many checks as it's mostly for printing types
val completer = new Completer(Mode.Lazy, pos, untpdPath, _ => true)
completer.scopeCompletions
/** 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
// Ignore `package foo@@` and `package foo.bar@@`
case ((_: tpd.Select) | (_: tpd.Ident)):: (_ : tpd.PackageDef) :: _ => Mode.None
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.InterpolatedString(_, untpd.Literal(Constants.Constant(_: String)) :: _) :: _ =>
Mode.Term | Mode.Scope
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 =>
val nameStart = tree.span.point
val start = if pos.source.content().lift(nameStart).contains('`') then nameStart + 1 else nameStart
tree.name.toString.take(pos.span.point - start)
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
private object StringContextApplication:
def unapply(path: List[tpd.Tree]): Option[tpd.Apply] =
path match
case tpd.Select(qual @ tpd.Apply(tpd.Select(tpd.Select(_, StdNames.nme.StringContext), _), _), _) :: _ =>
Some(qual)
case _ => None
private object NamedTupleSelection:
def unapply(path: List[tpd.Tree])(using Context): Option[tpd.Tree] =
path match
case (tpd.Apply(tpd.Apply(tpd.TypeApply(fun, _), List(qual)), _)) :: _
if fun.symbol.exists && fun.symbol.name == nme.apply &&
fun.symbol.owner.exists && fun.symbol.owner == defn.NamedTupleModule.moduleClass =>
Some(qual)
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 construct 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.names
case StringContextApplication(qual) =>
completer.scopeCompletions.names ++ completer.selectionCompletions(qual)
case tpd.Select(qual, _) :: _ => completer.selectionCompletions(qual)
case (tree: tpd.ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr)
case NamedTupleSelection(qual) => completer.selectionCompletions(qual)
case _ => completer.scopeCompletions.names
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 =
try
if denot.isType then denot.symbol.showFullName
else denot.info.widenTermRefExpr.show
catch case NonFatal(_) => denot.symbol.name.toString
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 = try
lazy val isEnum = sym.is(Enum) ||
(sym.companionClass.exists && sym.companionClass.is(Enum))
sym.exists &&
!sym.isAbsent(canForce = false) &&
!sym.isPrimaryConstructor &&
// running sourceSymbol on ExportedTerm will force a lot of computation from collectSubTrees
(sym.is(ExportedTerm) || 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)))
)
catch
case NonFatal(ex) =>
false
end isValidCompletionSymbol
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)(using Context):
/** 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
*/
lazy val scopeCompletions: CompletionResult =
/** 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)
val renames = collection.mutable.Map.empty[Symbol, Name]
def addMapping(name: Name, denots: ScopedDenotations) =
mappings(name) = mappings(name) :+ denots
ctx.outersIterator.foreach { case ctx @ given Context =>
if ctx.isImportContext then
val imported = importedCompletions
imported.names.foreach { (name, denots) =>
addMapping(name, ScopedDenotations(denots, ctx, include(_, name)))
}
imported.renames.foreach { (name, newName) =>
renames(name) = newName
}
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 _ =>
}
}
CompletionResult(resultMappings, renames.toMap)
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)
if qual.symbol.is(Package) then
directMemberCompletions(adjustedQual)
else if qual.typeOpt.hasSimpleKind then
implicitConversionMemberCompletions(adjustedQual) ++
extensionCompletions(adjustedQual) ++
directMemberCompletions(adjustedQual) ++
namedTupleCompletions(adjustedQual)
else
Map.empty
/** 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): CompletionResult =
val imp = ctx.importInfo
val renames = collection.mutable.Map.empty[Symbol, Name]
if imp == null then
CompletionResult(Map.empty, Map.empty)
else
def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] =
imp.site.member(name).alternatives
.collect { case denot if include(denot, nameInScope) =>
if name != nameInScope then
renames(denot.symbol) = 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
val results = givenImports ++ wildcardMembers ++ explicitMembers
CompletionResult(results, renames.toMap)
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(true)
.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)
// Try added due to https://github.com/scalameta/metals/issues/7872
def tryApplyingReceiverToExtension(termRef: TermRef): Option[SingleDenotation] =
try
ctx.typer.tryApplyingExtensionMethod(termRef, qual)
.map { tree =>
val tpe = asDefLikeType(tree.typeOpt.dealias)
termRef.denot.asSingleDenotation.mapInfo(_ => tpe)
}
catch case NonFatal(ex) =>
logger.warning(
s"Exception when trying to apply extension method:\n ${ex.getMessage()}\n${ex.getStackTrace().mkString("\n")}"
)
None
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 extMethodsInScope = scopeCompletions.names.toList.flatMap:
case (name, denots) =>
denots.collect:
case d if d.isTerm && d.symbol.is(Extension) => (d.symbol.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) &&
(mode.is(Mode.Lazy) || 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)
&& (mode.is(Mode.Lazy) || 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] = try {
val typer = ctx.typer
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span, Set.empty).allImplicits
interactiv.println(i"implicit conversion targets considered: ${conversions.toList}%, %")
conversions
} catch case NonFatal(ex) =>
logger.warning(
s"Exception when searching for implicit conversions:\n ${ex.getMessage()}\n${ex.getStackTrace().mkString("\n")}"
)
Set.empty
/** 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]]
case class CompletionResult(names: Map[Name, Seq[SingleDenotation]], renames: Map[Symbol, Name])
/**
* 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)
val Lazy: Mode = new Mode(32)