/
KtFirReferenceShortener.kt
1590 lines (1333 loc) · 69.6 KB
/
KtFirReferenceShortener.kt
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
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.analysis.api.fir.components
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.psi.SmartPsiElementPointer
import org.jetbrains.kotlin.KtFakeSourceElementKind
import org.jetbrains.kotlin.analysis.api.components.*
import org.jetbrains.kotlin.analysis.api.fir.KtFirAnalysisSession
import org.jetbrains.kotlin.analysis.api.fir.components.ElementsToShortenCollector.PartialOrderOfScope.Companion.toPartialOrder
import org.jetbrains.kotlin.analysis.api.fir.isImplicitDispatchReceiver
import org.jetbrains.kotlin.analysis.api.fir.references.KDocReferenceResolver
import org.jetbrains.kotlin.analysis.api.fir.utils.computeImportableName
import org.jetbrains.kotlin.analysis.api.fir.utils.firSymbol
import org.jetbrains.kotlin.analysis.api.lifetime.KtLifetimeToken
import org.jetbrains.kotlin.analysis.api.symbols.KtCallableSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtClassLikeSymbol
import org.jetbrains.kotlin.analysis.api.symbols.KtSymbol
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LLFirResolveSession
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFir
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.getOrBuildFirFile
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.resolveToFirSymbol
import org.jetbrains.kotlin.analysis.low.level.api.fir.resolver.AllCandidatesResolver
import org.jetbrains.kotlin.analysis.low.level.api.fir.util.ContextCollector
import org.jetbrains.kotlin.analysis.utils.printer.parentOfType
import org.jetbrains.kotlin.analysis.utils.printer.parentsOfType
import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.declarations.*
import org.jetbrains.kotlin.fir.declarations.builder.buildImport
import org.jetbrains.kotlin.fir.declarations.builder.buildResolvedImport
import org.jetbrains.kotlin.fir.declarations.utils.classId
import org.jetbrains.kotlin.fir.expressions.*
import org.jetbrains.kotlin.fir.expressions.builder.buildFunctionCall
import org.jetbrains.kotlin.fir.expressions.builder.buildPropertyAccessExpression
import org.jetbrains.kotlin.fir.references.FirErrorNamedReference
import org.jetbrains.kotlin.fir.references.FirNamedReference
import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference
import org.jetbrains.kotlin.fir.references.FirThisReference
import org.jetbrains.kotlin.fir.references.builder.buildSimpleNamedReference
import org.jetbrains.kotlin.fir.resolve.FirSamResolver
import org.jetbrains.kotlin.fir.resolve.ResolutionMode
import org.jetbrains.kotlin.fir.resolve.SessionHolderImpl
import org.jetbrains.kotlin.fir.resolve.diagnostics.ConeAmbiguityError
import org.jetbrains.kotlin.fir.resolve.diagnostics.ConeUnmatchedTypeArgumentsError
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.resolve.transformers.PackageResolutionResult
import org.jetbrains.kotlin.fir.resolve.transformers.resolveToPackageOrClass
import org.jetbrains.kotlin.fir.scopes.*
import org.jetbrains.kotlin.fir.scopes.impl.*
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.impl.*
import org.jetbrains.kotlin.fir.types.*
import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid
import org.jetbrains.kotlin.kdoc.psi.impl.KDocName
import org.jetbrains.kotlin.name.*
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.*
import org.jetbrains.kotlin.resolve.ROOT_PREFIX_FOR_IDE_RESOLUTION_MODE
import org.jetbrains.kotlin.resolve.calls.tower.CandidateApplicability
import org.jetbrains.kotlin.utils.addIfNotNull
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import org.jetbrains.kotlin.utils.exceptions.errorWithAttachment
internal class KtFirReferenceShortener(
override val analysisSession: KtFirAnalysisSession,
override val token: KtLifetimeToken,
override val firResolveSession: LLFirResolveSession,
) : KtReferenceShortener(), KtFirAnalysisSessionComponent {
private val context = FirShorteningContext(analysisSession)
override fun collectShortenings(
file: KtFile,
selection: TextRange,
shortenOptions: ShortenOptions,
classShortenStrategy: (KtClassLikeSymbol) -> ShortenStrategy,
callableShortenStrategy: (KtCallableSymbol) -> ShortenStrategy
): ShortenCommand {
require(!file.isCompiled) { "No sense to collect references for shortening in compiled file $file" }
val declarationToVisit = file.findSmallestElementOfTypeContainingSelection<KtDeclaration>(selection)
?: file
val firDeclaration = declarationToVisit.getCorrespondingFirElement() ?: return ShortenCommandImpl(
@Suppress("DEPRECATION")
file.createSmartPointer(),
importsToAdd = emptySet(),
starImportsToAdd = emptySet(),
listOfTypeToShortenInfo = emptyList(),
listOfQualifierToShortenInfo = emptyList(),
thisLabelsToShorten = emptyList(),
kDocQualifiersToShorten = emptyList(),
)
val towerContext = FirTowerDataContextProvider.create(firResolveSession, declarationToVisit)
//TODO: collect all usages of available symbols in the file and prevent importing symbols that could introduce name clashes, which
// may alter the meaning of existing code.
val collector = ElementsToShortenCollector(
shortenOptions,
context,
towerContext,
file,
selection,
classShortenStrategy = { classShortenStrategy(buildSymbol(it) as KtClassLikeSymbol) },
callableShortenStrategy = { callableShortenStrategy(buildSymbol(it) as KtCallableSymbol) },
firResolveSession,
)
firDeclaration.accept(CollectingVisitor(collector))
val additionalImports = AdditionalImports(
collector.getNamesToImport(starImport = false).toSet(),
collector.getNamesToImport(starImport = true).toSet(),
)
val kDocCollector = KDocQualifiersToShortenCollector(
analysisSession,
selection,
additionalImports,
classShortenStrategy = {
minOf(classShortenStrategy(buildSymbol(it) as KtClassLikeSymbol), ShortenStrategy.SHORTEN_IF_ALREADY_IMPORTED)
},
callableShortenStrategy = {
minOf(callableShortenStrategy(buildSymbol(it) as KtCallableSymbol), ShortenStrategy.SHORTEN_IF_ALREADY_IMPORTED)
},
)
kDocCollector.visitElement(declarationToVisit)
@Suppress("DEPRECATION")
return ShortenCommandImpl(
file.createSmartPointer(),
additionalImports.simpleImports,
additionalImports.starImports,
collector.typesToShorten.distinctBy { it.element }.map { TypeToShortenInfo(it.element.createSmartPointer(), it.shortenedRef) },
collector.qualifiersToShorten.distinctBy { it.element }.map { QualifierToShortenInfo(it.element.createSmartPointer(), it.shortenedRef) },
collector.labelsToShorten.distinctBy { it.element }.map { ThisLabelToShortenInfo(it.element.createSmartPointer()) },
kDocCollector.kDocQualifiersToShorten.distinctBy { it.element }.map { it.element.createSmartPointer() },
)
}
private fun KtElement.getCorrespondingFirElement(): FirElement? {
require(this is KtFile || this is KtDeclaration)
val firElement = getOrBuildFir(firResolveSession)
return when (firElement) {
is FirDeclaration -> firElement
is FirAnonymousFunctionExpression -> firElement.anonymousFunction
is FirFunctionTypeParameter -> firElement
else -> null
}
}
private fun buildSymbol(firSymbol: FirBasedSymbol<*>): KtSymbol = analysisSession.firSymbolBuilder.buildSymbol(firSymbol)
}
private class FirTowerDataContextProvider private constructor(
private val contextProvider: ContextCollector.ContextProvider
) {
companion object {
fun create(firResolveSession: LLFirResolveSession, targetElement: KtElement): FirTowerDataContextProvider {
val firFile = targetElement.containingKtFile.getOrBuildFirFile(firResolveSession)
val sessionHolder = run {
val firSession = firResolveSession.useSiteFirSession
val scopeSession = firResolveSession.getScopeSessionFor(firSession)
SessionHolderImpl(firSession, scopeSession)
}
val designation = ContextCollector.computeDesignation(firFile, targetElement)
val contextProvider = ContextCollector.process(
firFile,
sessionHolder,
designation,
shouldCollectBodyContext = false, // we only query SELF context
filter = { ContextCollector.FilterResponse.CONTINUE }
)
return FirTowerDataContextProvider(contextProvider)
}
}
fun getClosestAvailableParentContext(ktElement: KtElement): FirTowerDataContext? {
for (parent in ktElement.parentsWithSelf) {
val context = contextProvider[parent, ContextCollector.ContextKind.SELF]
if (context != null) {
return context.towerDataContext
}
}
return null
}
}
private fun FqName.dropFakeRootPrefixIfPresent(): FqName =
tail(FqName(ROOT_PREFIX_FOR_IDE_RESOLUTION_MODE))
private data class AdditionalImports(val simpleImports: Set<FqName>, val starImports: Set<FqName>)
private inline fun <reified T : KtElement> KtFile.findSmallestElementOfTypeContainingSelection(selection: TextRange): T? =
findElementAt(selection.startOffset)
?.parentsOfType<T>(withSelf = true)
?.firstOrNull { selection in it.textRange }
/**
* How a symbol is imported. The order of the enum entry represents the priority of imports. If a symbol is available from multiple kinds of
* imports, the symbol from "smaller" kind is used. For example, an explicitly imported symbol can overwrite a star-imported symbol.
*/
private enum class ImportKind {
/** The symbol is available from the local scope and hence cannot be imported or overwritten. */
LOCAL,
/** Explicitly imported by user. */
EXPLICIT,
/** Implicitly imported from package. */
PACKAGE,
/** Explicitly imported by Kotlin default. For example, `kotlin.String`. */
DEFAULT_EXPLICIT,
/** Star imported (star import) by user. */
STAR,
/** Star imported (star import) by Kotlin default. */
DEFAULT_STAR;
fun hasHigherPriorityThan(that: ImportKind): Boolean = this < that
companion object {
fun fromScope(scope: FirScope): ImportKind {
return when (scope) {
is FirDefaultStarImportingScope -> DEFAULT_STAR
is FirAbstractStarImportingScope -> STAR
is FirPackageMemberScope -> PACKAGE
is FirDefaultSimpleImportingScope -> DEFAULT_EXPLICIT
is FirAbstractSimpleImportingScope -> EXPLICIT
else -> LOCAL
}
}
fun fromShortenOption(option: ShortenStrategy): ImportKind? = when (option) {
ShortenStrategy.SHORTEN_AND_IMPORT -> EXPLICIT
ShortenStrategy.SHORTEN_AND_STAR_IMPORT -> STAR
else -> null
}
}
}
private data class AvailableSymbol<out T>(
val symbol: T,
val importKind: ImportKind,
)
private class FirShorteningContext(val analysisSession: KtFirAnalysisSession) {
private val firResolveSession = analysisSession.firResolveSession
private val firSession: FirSession
get() = firResolveSession.useSiteFirSession
class ClassifierCandidate(val scope: FirScope, val availableSymbol: AvailableSymbol<FirClassifierSymbol<*>>)
fun findFirstClassifierInScopesByName(positionScopes: List<FirScope>, targetClassName: Name): AvailableSymbol<FirClassifierSymbol<*>>? =
positionScopes.firstNotNullOfOrNull { scope -> findFirstClassifierSymbolByName(scope, targetClassName) }
fun findClassifiersInScopesByName(scopes: List<FirScope>, targetClassName: Name): List<ClassifierCandidate> =
scopes.mapNotNull { scope ->
val classifierSymbol = findFirstClassifierSymbolByName(scope, targetClassName) ?: return@mapNotNull null
ClassifierCandidate(scope, classifierSymbol)
}
private fun findFirstClassifierSymbolByName(scope: FirScope, targetClassName: Name): AvailableSymbol<FirClassifierSymbol<*>>? {
val classifierSymbol = scope.findFirstClassifierByName(targetClassName) ?: return null
return AvailableSymbol(classifierSymbol, ImportKind.fromScope(scope))
}
private fun FirClassLikeSymbol<*>.getSamConstructor(): FirNamedFunctionSymbol? {
val samResolver = FirSamResolver(firSession, analysisSession.getScopeSessionFor(firSession))
return samResolver.getSamConstructor(fir)?.symbol
}
/**
* Finds constructors with a given [targetClassName] available within the [scope], including SAM constructors
* (which are not explicitly declared in the class).
*
* Includes type-aliased constructors too if typealias confirms to the [targetClassName].
*
* Do not confuse with constructors **declared** in the scope (see [FirScope.processDeclaredConstructors]).
*/
private fun findAvailableConstructors(scope: FirScope, targetClassName: Name): List<FirFunctionSymbol<*>> {
val classLikeSymbol = scope.findFirstClassifierByName(targetClassName) as? FirClassLikeSymbol
?: return emptyList()
val constructors = (classLikeSymbol as? FirClassSymbol)?.declarationSymbols?.filterIsInstance<FirConstructorSymbol>().orEmpty()
val samConstructor = classLikeSymbol.getSamConstructor()
return constructors + listOfNotNull(samConstructor)
}
fun findFunctionsInScopes(scopes: List<FirScope>, name: Name): List<AvailableSymbol<FirFunctionSymbol<*>>> {
return scopes.flatMap { scope ->
val importKind = ImportKind.fromScope(scope)
buildList {
// Collect constructors
findAvailableConstructors(scope, name).mapTo(this) { AvailableSymbol(it, importKind) }
// Collect functions
scope.getFunctions(name).mapTo(this) { AvailableSymbol(it, importKind) }
}
}
}
fun findPropertiesInScopes(scopes: List<FirScope>, name: Name): List<AvailableSymbol<FirVariableSymbol<*>>> {
return scopes.flatMap { scope ->
val importKind = ImportKind.fromScope(scope)
scope.getProperties(name).map {
AvailableSymbol(it, importKind)
}
}
}
private fun FirScope.findFirstClassifierByName(name: Name): FirClassifierSymbol<*>? {
var element: FirClassifierSymbol<*>? = null
processClassifiersByName(name) {
if (element == null) {
element = it
}
}
return element
}
fun findScopesAtPosition(
position: KtElement,
newImports: Sequence<FqName>,
towerContextProvider: FirTowerDataContextProvider,
withImplicitReceivers: Boolean = true,
): List<FirScope>? {
val towerDataContext = towerContextProvider.getClosestAvailableParentContext(position) ?: return null
val nonLocalScopes = towerDataContext.nonLocalTowerDataElements
.asSequence()
.filter { withImplicitReceivers || it.implicitReceiver == null }
.flatMap {
// We must use `it.getAvailableScopes()` instead of `it.scope` to check scopes of companion objects
// and context receivers as well.
it.getAvailableScopes()
}
val result = buildList {
addAll(nonLocalScopes)
addIfNotNull(createFakeImportingScope(newImports))
addAll(towerDataContext.localScopes)
}
return result.asReversed()
}
private fun createFakeImportingScope(newImports: Sequence<FqName>): FirScope? {
val resolvedNewImports = newImports.mapNotNull { createFakeResolvedImport(it) }.toList()
if (resolvedNewImports.isEmpty()) return null
return FirExplicitSimpleImportingScope(
resolvedNewImports,
firSession,
analysisSession.getScopeSessionFor(firSession),
)
}
private fun createFakeResolvedImport(fqNameToImport: FqName): FirResolvedImport? {
val packageOrClass =
(resolveToPackageOrClass(firSession.symbolProvider, fqNameToImport) as? PackageResolutionResult.PackageOrClass) ?: return null
val delegateImport = buildImport {
importedFqName = fqNameToImport
isAllUnder = false
}
return buildResolvedImport {
delegate = delegateImport
packageFqName = packageOrClass.packageFqName
}
}
fun getRegularClass(type: ConeKotlinType?): FirRegularClass? {
return type?.toRegularClassSymbol(firSession)?.fir
}
fun toClassSymbol(classId: ClassId) =
firSession.symbolProvider.getClassLikeSymbolByClassId(classId)
fun convertToImportableName(callableSymbol: FirCallableSymbol<*>): FqName? =
callableSymbol.computeImportableName(firSession)
}
private sealed class ElementToShorten {
abstract val element: KtElement
abstract val nameToImport: FqName?
abstract val importAllInParent: Boolean
}
private class ShortenType(
override val element: KtUserType,
val shortenedRef: String? = null,
override val nameToImport: FqName? = null,
override val importAllInParent: Boolean = false,
) : ElementToShorten()
private class ShortenQualifier(
override val element: KtDotQualifiedExpression,
val shortenedRef: String? = null,
override val nameToImport: FqName? = null,
override val importAllInParent: Boolean = false
) : ElementToShorten()
private class ShortenThisLabel(
override val element: KtThisExpression,
) : ElementToShorten() {
override val nameToImport: FqName? = null
override val importAllInParent: Boolean = false
}
/**
* N.B. Does not subclass [ElementToShorten] because currently
* there's no reason to do that.
*/
private class ShortenKDocQualifier(
val element: KDocName,
)
private class CollectingVisitor(private val collector: ElementsToShortenCollector) : FirVisitorVoid() {
private val visitedProperty = mutableSetOf<FirProperty>()
override fun visitValueParameter(valueParameter: FirValueParameter) {
super.visitValueParameter(valueParameter)
valueParameter.correspondingProperty?.let { visitProperty(it) }
}
override fun visitProperty(property: FirProperty) {
if (visitedProperty.add(property)) {
super.visitProperty(property)
}
}
override fun visitScript(script: FirScript) {
script.declarations.forEach {
it.accept(this)
}
}
override fun visitElement(element: FirElement) {
element.acceptChildren(this)
}
override fun visitErrorTypeRef(errorTypeRef: FirErrorTypeRef) {
visitResolvedTypeRef(errorTypeRef)
errorTypeRef.partiallyResolvedTypeRef?.accept(this)
}
override fun visitResolvedTypeRef(resolvedTypeRef: FirResolvedTypeRef) {
collector.processTypeRef(resolvedTypeRef)
resolvedTypeRef.acceptChildren(this)
resolvedTypeRef.delegatedTypeRef?.accept(this)
}
override fun visitResolvedQualifier(resolvedQualifier: FirResolvedQualifier) {
super.visitResolvedQualifier(resolvedQualifier)
collector.processTypeQualifier(resolvedQualifier)
}
override fun visitErrorResolvedQualifier(errorResolvedQualifier: FirErrorResolvedQualifier) {
super.visitErrorResolvedQualifier(errorResolvedQualifier)
collector.processTypeQualifier(errorResolvedQualifier)
}
override fun visitFunctionCall(functionCall: FirFunctionCall) {
super.visitFunctionCall(functionCall)
collector.processFunctionCall(functionCall)
}
override fun visitPropertyAccessExpression(propertyAccessExpression: FirPropertyAccessExpression) {
super.visitPropertyAccessExpression(propertyAccessExpression)
collector.processPropertyAccess(propertyAccessExpression)
}
override fun visitThisReference(thisReference: FirThisReference) {
super.visitThisReference(thisReference)
collector.processThisReference(thisReference)
}
}
private class ElementsToShortenCollector(
private val shortenOptions: ShortenOptions,
private val shorteningContext: FirShorteningContext,
private val towerContextProvider: FirTowerDataContextProvider,
private val containingFile: KtFile,
private val selection: TextRange,
private val classShortenStrategy: (FirClassLikeSymbol<*>) -> ShortenStrategy,
private val callableShortenStrategy: (FirCallableSymbol<*>) -> ShortenStrategy,
private val firResolveSession: LLFirResolveSession,
) {
val typesToShorten: MutableList<ShortenType> = mutableListOf()
val qualifiersToShorten: MutableList<ShortenQualifier> = mutableListOf()
val labelsToShorten: MutableList<ShortenThisLabel> = mutableListOf()
fun processTypeRef(resolvedTypeRef: FirResolvedTypeRef) {
val typeElement = resolvedTypeRef.correspondingTypePsi ?: return
if (typeElement.qualifier == null) return
val classifierId = resolvedTypeRef.type.lowerBoundIfFlexible().candidateClassId ?: return
findClassifierQualifierToShorten(classifierId, typeElement)?.let(::addElementToShorten)
}
/**
* Retrieves the corresponding [KtUserType] PSI the given [FirResolvedTypeRef].
*
* This code handles some quirks of FIR sources and PSI:
* - in `vararg args: String` declaration, `String` type reference has fake source, but `Array<String>` has real source
* (see [KtFakeSourceElementKind.ArrayTypeFromVarargParameter]).
* - if FIR reference points to the type with generic parameters (like `Foo<Bar>`), its source is not [KtTypeReference], but
* [KtNameReferenceExpression].
*/
private val FirResolvedTypeRef.correspondingTypePsi: KtUserType?
get() {
val sourcePsi = when {
// array type for vararg parameters is not present in the code, so no need to handle it
delegatedTypeRef?.source?.kind == KtFakeSourceElementKind.ArrayTypeFromVarargParameter -> null
// but the array's underlying type is present with a fake source, and needs to be handled
source?.kind == KtFakeSourceElementKind.ArrayTypeFromVarargParameter -> psi
else -> realPsi
}
val outerTypeElement = when (sourcePsi) {
is KtTypeReference -> sourcePsi.typeElement
is KtNameReferenceExpression -> sourcePsi.parent as? KtTypeElement
else -> null
}
return outerTypeElement?.unwrapNullability() as? KtUserType
}
val ConeKotlinType.candidateClassId: ClassId?
get() {
return when (this) {
is ConeErrorType -> when (val diagnostic = this.diagnostic) {
// Tolerate code that misses type parameters while shortening it.
is ConeUnmatchedTypeArgumentsError -> diagnostic.symbol.classId
else -> null
}
is ConeClassLikeType -> lookupTag.classId
else -> null
}
}
fun getNamesToImport(starImport: Boolean = false): Sequence<FqName> = sequence {
yieldAll(typesToShorten)
yieldAll(qualifiersToShorten)
}.filter { starImport == it.importAllInParent }.mapNotNull { it.nameToImport }.distinct()
private fun findFakePackageToShorten(typeElement: KtUserType): ElementToShorten? {
val deepestTypeWithQualifier = typeElement.qualifiedTypesWithSelf.last()
return if (deepestTypeWithQualifier.hasFakeRootPrefix()) createElementToShorten(deepestTypeWithQualifier) else null
}
fun processTypeQualifier(resolvedQualifier: FirResolvedQualifier) {
if (resolvedQualifier.isImplicitDispatchReceiver) return
val wholeClassQualifier = resolvedQualifier.classId ?: return
val qualifierPsi = resolvedQualifier.psi ?: return
val wholeQualifierElement = when (qualifierPsi) {
is KtDotQualifiedExpression -> qualifierPsi
is KtNameReferenceExpression -> qualifierPsi.getDotQualifiedExpressionForSelector() ?: return
else -> return
}
findClassifierQualifierToShorten(wholeClassQualifier, wholeQualifierElement)?.let(::addElementToShorten)
}
private val FirClassifierSymbol<*>.classIdIfExists: ClassId?
get() = (this as? FirClassLikeSymbol<*>)?.classId
private val FirConstructorSymbol.classIdIfExists: ClassId?
get() = this.containingClassLookupTag()?.classId
/**
* Returns true if the class symbol has a type parameter that is supposed to be provided for its parent class.
*
* Example:
* class Outer<T> {
* inner class Inner // Inner has an implicit type parameter `T`.
* }
*/
private fun FirClassLikeSymbol<*>.hasTypeParameterFromParent(): Boolean = typeParameterSymbols.orEmpty().any {
it.containingDeclarationSymbol != this
}
private fun FirScope.correspondingClassIdIfExists(): ClassId = when (this) {
is FirNestedClassifierScope -> klass.classId
is FirNestedClassifierScopeWithSubstitution -> originalScope.correspondingClassIdIfExists()
is FirClassUseSiteMemberScope -> ownerClassLookupTag.classId
else -> errorWithAttachment("FirScope ${this::class}` is expected to be one of FirNestedClassifierScope and FirClassUseSiteMemberScope to get ClassId") {
withEntry("firScope", this@correspondingClassIdIfExists) { it.toString() }
}
}
private fun ClassId.idWithoutCompanion() = if (shortClassName == SpecialNames.DEFAULT_NAME_FOR_COMPANION_OBJECT) outerClassId else this
private fun FirScope.isScopeForClass(): Boolean = when {
this is FirNestedClassifierScope -> true
this is FirNestedClassifierScopeWithSubstitution -> originalScope.isScopeForClass()
this is FirClassUseSiteMemberScope -> true
else -> false
}
/**
* Assuming that both this [FirScope] and [another] are [FirNestedClassifierScope] or [FirClassUseSiteMemberScope] and both of them
* are surrounding [from], returns whether this [FirScope] is closer than [another] based on the distance from [from].
*
* If one of this [FirScope] and [another] is not [FirNestedClassifierScope] or [FirClassUseSiteMemberScope], it returns false.
*
* Example:
* class Outer { // scope1 ClassId = Other
* class Inner { // scope2 ClassId = Other.Inner
* fun foo() {
* // Distance to scopes for classes from <element> in the order from the closest:
* // scope2 -> scope3 -> scope1
* <element>
* }
* companion object { // scope3 ClassId = Other.Inner.Companion
* }
* }
* }
*
* This function determines the distance based on [ClassId].
*/
private fun FirScope.isScopeForClassCloserThanAnotherScopeForClass(another: FirScope, from: KtClassOrObject): Boolean {
// Make sure both are scopes for classes
if (!isScopeForClass() || !another.isScopeForClass()) return false
if (this == another) return false
val classId = correspondingClassIdIfExists()
val classIdOfAnother = another.correspondingClassIdIfExists()
if (classId == classIdOfAnother) return false
// Find the first ClassId matching inner class. If the first matching one is this scope's ClassId, it means this scope is closer
// than `another`.
val candidates = setOfNotNull(classId, classIdOfAnother, classId.idWithoutCompanion(), classIdOfAnother.idWithoutCompanion())
val closestClassId = findMostInnerClassMatchingId(from, candidates)
return closestClassId == classId || (closestClassId != classIdOfAnother && closestClassId == classId.idWithoutCompanion())
}
/**
* Travels all containing classes of [innerClass] and finds the one matching ClassId with one of [candidates]. Returns the matching
* ClassId. If it does not have a matching ClassId, it returns null.
*/
private fun findMostInnerClassMatchingId(innerClass: KtClassOrObject, candidates: Set<ClassId>): ClassId? {
var classInNestedClass: KtClassOrObject? = innerClass
while (classInNestedClass != null) {
val containingClassId = classInNestedClass.getClassId()
if (containingClassId in candidates) return containingClassId
classInNestedClass = classInNestedClass.findClassOrObjectParent()
}
return null
}
/**
* An enum class to specify the distance of scopes from an element as a partial order
*
* Example:
* import ... // scope1 FirExplicitSimpleImportingScope - enum entry: ExplicitSimpleImporting
* // scope2 FirPackageMemberScope - enum entry: PackageMember
* class Outer { // scope3 FirClassUseSiteMemberScope - enum entry: ClassUseSite
* class Inner { // scope4 FirClassUseSiteMemberScope or FirNestedClassifierScope - enum entry: ClassUseSite/NestedClassifier
* fun foo() { // scope5 FirLocalScope - enum entry: Local
*
* // Distance to scopes from <element> in the order from the closest:
* // scope5 -> scope4 -> scope6 -> scope3 -> scope1 -> scope2
* <element>
*
* }
* companion object {
* // scope6 FirClassUseSiteMemberScope or FirNestedClassifierScope - enum entry: ClassUseSite/NestedClassifier
* }
* }
* }
*/
private enum class PartialOrderOfScope(
val scopeDistanceLevel: Int // Note: Don't use the built-in ordinal since there are some scopes that are at the same level.
) {
Local(1),
ScriptDeclarations(2),
ClassUseSite(2),
NestedClassifier(2),
TypeParameter(2),
ExplicitSimpleImporting(3),
PackageMember(4),
Unclassified(5),
;
companion object {
fun FirScope.toPartialOrder(): PartialOrderOfScope {
return when (this) {
is FirLocalScope -> Local
is FirScriptDeclarationsScope -> ScriptDeclarations
is FirClassUseSiteMemberScope -> ClassUseSite
is FirNestedClassifierScope -> NestedClassifier
is FirTypeParameterScope -> TypeParameter
is FirNestedClassifierScopeWithSubstitution -> originalScope.toPartialOrder()
is FirExplicitSimpleImportingScope -> ExplicitSimpleImporting
is FirPackageMemberScope -> PackageMember
else -> Unclassified
}
}
}
}
/**
* Returns whether this [FirScope] is a scope wider than [another] based on the above [PartialOrderOfScope] or not.
*/
private fun FirScope.isWiderThan(another: FirScope): Boolean =
toPartialOrder().scopeDistanceLevel > another.toPartialOrder().scopeDistanceLevel
/**
* Assuming that all scopes in this List<FirScope> and [base] are surrounding [from], returns whether an element of
* this List<FirScope> is closer than [base] based on the distance from [from].
*/
private fun List<FirScope>.hasScopeCloserThan(base: FirScope, from: KtElement) = any { scope ->
if (scope.isScopeForClass() && base.isScopeForClass()) {
val classContainingFrom = from.findClassOrObjectParent() ?: return@any false
return@any scope.isScopeForClassCloserThanAnotherScopeForClass(base, classContainingFrom)
}
base.isWiderThan(scope)
}
/**
* Returns true if [containingFile] has a [KtImportDirective] whose imported FqName is the same as [classId] but references a different
* symbol.
*/
private fun importDirectiveForDifferentSymbolWithSameNameIsPresent(classId: ClassId): Boolean {
val importDirectivesWithSameImportedFqName = containingFile.collectDescendantsOfType { importedDirective: KtImportDirective ->
importedDirective.importedFqName?.shortName() == classId.shortClassName
}
return importDirectivesWithSameImportedFqName.isNotEmpty() &&
importDirectivesWithSameImportedFqName.all { it.importedFqName != classId.asSingleFqName() }
}
private fun shortenClassifierIfAlreadyImported(
classId: ClassId,
element: KtElement,
classSymbol: FirClassLikeSymbol<*>,
scopes: List<FirScope>,
): Boolean {
val name = classId.shortClassName
val availableClassifiers = shorteningContext.findClassifiersInScopesByName(scopes, name)
val matchingAvailableSymbol = availableClassifiers.firstOrNull { it.availableSymbol.symbol.classIdIfExists == classId }
val scopeForClass = matchingAvailableSymbol?.scope ?: return false
if (availableClassifiers.map { it.scope }.hasScopeCloserThan(scopeForClass, element)) return false
/**
* If we have a property with the same name, avoid dropping qualifiers makes it reference a property with the same name e.g.,
* package my.component
* class foo { .. } // A
* ..
* fun test() {
* val foo = .. // B
* my.component.foo::class.java // If we drop `my.component`, it will reference `B` instead of `A`
* }
*/
if (shorteningContext.findPropertiesInScopes(scopes, name).isNotEmpty()) {
val firForElement = element.getOrBuildFir(firResolveSession) as? FirQualifiedAccessExpression
val typeArguments = firForElement?.typeArguments ?: emptyList()
val qualifiedAccessCandidates = findCandidatesForPropertyAccess(classSymbol.annotations, typeArguments, name, element)
if (qualifiedAccessCandidates.mapNotNull { it.candidate.originScope }.hasScopeCloserThan(scopeForClass, element)) return false
}
return !importDirectiveForDifferentSymbolWithSameNameIsPresent(classId)
}
private fun shortenIfAlreadyImportedAsAlias(referenceExpression: KtElement, referencedSymbolFqName: FqName): ElementToShorten? {
val importDirectiveForReferencedSymbol = containingFile.importDirectives.firstOrNull {
it.importedFqName == referencedSymbolFqName && it.alias != null
} ?: return null
val aliasedName = importDirectiveForReferencedSymbol.alias?.name
return createElementToShorten(referenceExpression, shortenedRef = aliasedName)
}
private fun shortenClassifierQualifier(
positionScopes: List<FirScope>,
qualifierClassId: ClassId,
qualifierElement: KtElement,
): ElementToShorten? {
val classSymbol = shorteningContext.toClassSymbol(qualifierClassId) ?: return null
val option = classShortenStrategy(classSymbol)
if (option == ShortenStrategy.DO_NOT_SHORTEN) return null
// If its parent has a type parameter, we do not shorten it ATM because it will lose its type parameter. See KTIJ-26072
if (classSymbol.hasTypeParameterFromParent()) return null
shortenIfAlreadyImportedAsAlias(qualifierElement, qualifierClassId.asSingleFqName())?.let { return it }
if (shortenClassifierIfAlreadyImported(qualifierClassId, qualifierElement, classSymbol, positionScopes)) {
return createElementToShorten(qualifierElement)
}
if (option == ShortenStrategy.SHORTEN_IF_ALREADY_IMPORTED) return null
val importAllInParent = option == ShortenStrategy.SHORTEN_AND_STAR_IMPORT
if (importBreaksExistingReferences(qualifierClassId, importAllInParent)) return null
// Find class with the same name that's already available in this file.
val availableClassifier = shorteningContext.findFirstClassifierInScopesByName(positionScopes, qualifierClassId.shortClassName)
when {
// No class with name `classId.shortClassName` is present in the scope. Hence, we can safely import the name and shorten
// the reference.
availableClassifier == null -> {
return createElementToShorten(
qualifierElement,
qualifierClassId.asSingleFqName(),
importAllInParent
)
}
// The class with name `classId.shortClassName` happens to be the same class referenced by this qualified access.
availableClassifier.symbol.classIdIfExists == qualifierClassId -> {
// Respect caller's request to use star import, if it's not already star-imported.
return when {
availableClassifier.importKind == ImportKind.EXPLICIT && importAllInParent -> {
createElementToShorten(qualifierElement, qualifierClassId.asSingleFqName(), importAllInParent)
}
// Otherwise, just shorten it and don't alter import statements
else -> createElementToShorten(qualifierElement)
}
}
importedClassifierOverwritesAvailableClassifier(availableClassifier, importAllInParent) -> {
return createElementToShorten(qualifierElement, qualifierClassId.asSingleFqName(), importAllInParent)
}
}
return null
}
/**
* Finds the longest qualifier in [wholeQualifierElement] which can be safely shortened in the [positionScopes].
* [wholeQualifierClassId] is supposed to reflect the class which is referenced by the [wholeQualifierElement].
*
* N.B. Even if the [wholeQualifierElement] is not strictly in the [selection],
* some outer part of it might be, and we want to shorten that.
* So we have to check all the outer qualifiers.
*/
private fun findClassifierQualifierToShorten(
wholeQualifierClassId: ClassId,
wholeQualifierElement: KtElement,
): ElementToShorten? {
val positionScopes = shorteningContext.findScopesAtPosition(
wholeQualifierElement,
getNamesToImport(),
towerContextProvider,
withImplicitReceivers = false,
) ?: return null
val allClassIds = wholeQualifierClassId.outerClassesWithSelf
val allQualifiedElements = wholeQualifierElement.qualifiedElementsWithSelf
for ((classId, element) in allClassIds.zip(allQualifiedElements)) {
if (!element.inSelection) continue
shortenClassifierQualifier(positionScopes, classId, element)?.let { return it }
}
val lastQualifier = allQualifiedElements.last()
if (!lastQualifier.inSelection) return null
return findFakePackageToShorten(lastQualifier)
}
private fun createElementToShorten(
element: KtElement,
referencedSymbol: FqName? = null,
importAllInParent: Boolean = false,
shortenedRef: String? = null,
): ElementToShorten {
var nameToImport = if (importAllInParent) {
referencedSymbol?.parentOrNull() ?: error("Provided FqName '$referencedSymbol' cannot be imported with a star")
} else {
referencedSymbol
}
return when (element) {
is KtUserType -> ShortenType(element, shortenedRef, nameToImport, importAllInParent)
is KtDotQualifiedExpression -> ShortenQualifier(element, shortenedRef, nameToImport, importAllInParent)
is KtThisExpression -> ShortenThisLabel(element)
else -> error("Unexpected ${element::class}")
}
}
private fun findFakePackageToShorten(element: KtElement): ElementToShorten? {
return when (element) {
is KtUserType -> findFakePackageToShorten(element)
is KtDotQualifiedExpression -> findFakePackageToShorten(element)
else -> error("Unexpected ${element::class}")
}
}
/**
* Returns `true` if adding [classToImport] import to the [file] might alter or break the
* resolve of existing references in the file.
*
* N.B.: At the moment it might have both false positives and false negatives, since it does not
* check all possible references.
*/
private fun importBreaksExistingReferences(classToImport: ClassId, importAllInParent: Boolean): Boolean {
return importAffectsUsagesOfClassesWithSameName(classToImport, importAllInParent)
}
/**
* Same as above, but for more general callable symbols.
*
* Currently only checks constructor calls, assuming `true` for everything else.
*/
private fun importBreaksExistingReferences(callableToImport: FirCallableSymbol<*>, importAllInParent: Boolean): Boolean {
if (callableToImport is FirConstructorSymbol) {
val classToImport = callableToImport.classIdIfExists
if (classToImport != null) {
return importAffectsUsagesOfClassesWithSameName(classToImport, importAllInParent)
}
}
return false
}
private fun importedClassifierOverwritesAvailableClassifier(
availableClassifier: AvailableSymbol<FirClassifierSymbol<*>>,
importAllInParent: Boolean
): Boolean {
val importKindFromOption = if (importAllInParent) ImportKind.STAR else ImportKind.EXPLICIT
return importKindFromOption.hasHigherPriorityThan(availableClassifier.importKind)
}
private fun importAffectsUsagesOfClassesWithSameName(classToImport: ClassId, importAllInParent: Boolean): Boolean {
var importAffectsUsages = false
containingFile.accept(object : KtVisitorVoid() {
override fun visitElement(element: PsiElement) {
element.acceptChildren(this)
}
override fun visitImportList(importList: KtImportList) {}
override fun visitPackageDirective(directive: KtPackageDirective) {}
override fun visitSimpleNameExpression(expression: KtSimpleNameExpression) {
if (importAffectsUsages) return
if (KtPsiUtil.isSelectorInQualified(expression)) return
val shortClassName = classToImport.shortClassName
if (expression.getReferencedNameAsName() != shortClassName) return
val contextProvider = FirTowerDataContextProvider.create(firResolveSession, expression)
val positionScopes = shorteningContext.findScopesAtPosition(expression, getNamesToImport(), contextProvider) ?: return
val availableClassifier = shorteningContext.findFirstClassifierInScopesByName(positionScopes, shortClassName) ?: return
when {
availableClassifier.symbol.classIdIfExists == classToImport -> return
importedClassifierOverwritesAvailableClassifier(availableClassifier, importAllInParent) -> {
importAffectsUsages = true
}
}
}
})
return importAffectsUsages
}
private fun resolveUnqualifiedAccess(
fullyQualifiedAccess: FirQualifiedAccessExpression,
name: Name,