|
| 1 | +package com.itsaky.androidide.lsp.kotlin.utils |
| 2 | + |
| 3 | +import org.jetbrains.kotlin.analysis.api.KaSession |
| 4 | +import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull |
| 5 | +import org.jetbrains.kotlin.analysis.api.resolution.symbol |
| 6 | +import org.jetbrains.kotlin.analysis.api.symbols.KaCallableSymbol |
| 7 | +import org.jetbrains.kotlin.analysis.api.symbols.KaClassLikeSymbol |
| 8 | +import org.jetbrains.kotlin.analysis.api.symbols.KaConstructorSymbol |
| 9 | +import org.jetbrains.kotlin.analysis.api.symbols.KaSymbol |
| 10 | +import org.jetbrains.kotlin.idea.references.mainReference |
| 11 | +import org.jetbrains.kotlin.psi.KtArrayAccessExpression |
| 12 | +import org.jetbrains.kotlin.psi.KtCallExpression |
| 13 | +import org.jetbrains.kotlin.psi.KtDestructuringDeclaration |
| 14 | +import org.jetbrains.kotlin.psi.KtDestructuringDeclarationEntry |
| 15 | +import org.jetbrains.kotlin.psi.KtElement |
| 16 | +import org.jetbrains.kotlin.psi.KtFile |
| 17 | +import org.jetbrains.kotlin.psi.KtForExpression |
| 18 | +import org.jetbrains.kotlin.psi.KtImportList |
| 19 | +import org.jetbrains.kotlin.psi.KtNameReferenceExpression |
| 20 | +import org.jetbrains.kotlin.psi.KtOperationReferenceExpression |
| 21 | +import org.jetbrains.kotlin.psi.KtPropertyDelegate |
| 22 | +import org.jetbrains.kotlin.psi.psiUtil.collectDescendantsOfType |
| 23 | +import org.jetbrains.kotlin.psi.psiUtil.getParentOfType |
| 24 | + |
| 25 | +/** |
| 26 | + * Collects the importable fq-names (and their packages) referenced by [ktFile]'s body. MUST be |
| 27 | + * called inside [analyzeMaybeDangling]. Returns only plain strings, so nothing escapes the analyze |
| 28 | + * lifetime. A reference that fails to resolve doesn't join the used set; instead its short name is |
| 29 | + * recorded in [ImportUsage.unresolvedNames] so its import is kept. Both paths are safe: they lead to |
| 30 | + * keeping an import, never removing a used one. |
| 31 | + */ |
| 32 | +internal fun KaSession.collectImportUsage(ktFile: KtFile): ImportUsage { |
| 33 | + val usedFqNames = HashSet<String>() |
| 34 | + val usedPackages = HashSet<String>() |
| 35 | + val unresolvedNames = HashSet<String>() |
| 36 | + |
| 37 | + fun record(symbol: KaSymbol?) { |
| 38 | + val fq = symbol?.importableFqNameString() ?: return |
| 39 | + usedFqNames += fq |
| 40 | + val pkg = fq.substringBeforeLast('.', missingDelimiterValue = "") |
| 41 | + if (pkg.isNotEmpty()) usedPackages += pkg |
| 42 | + } |
| 43 | + |
| 44 | + fun recordAll(symbols: Collection<KaSymbol>?) { |
| 45 | + symbols?.forEach(::record) |
| 46 | + } |
| 47 | + |
| 48 | + // 1) Plain name / type references (excluding the import list itself). A null (or thrown) |
| 49 | + // resolution is treated as unresolved and its short name kept, so a used-but-unresolvable |
| 50 | + // reference never drops its import. A non-null, non-importable symbol (local, param) is a |
| 51 | + // clean resolve: it records nothing and is not unresolved. |
| 52 | + ktFile.collectDescendantsOfType<KtNameReferenceExpression>().forEach { ref -> |
| 53 | + if (ref.getParentOfType<KtImportList>(strict = false) != null) return@forEach |
| 54 | + val symbol = runCatching { ref.mainReference.resolveToSymbol() }.getOrNull() |
| 55 | + if (symbol != null) record(symbol) else unresolvedNames += ref.getReferencedName() |
| 56 | + } |
| 57 | + |
| 58 | + // 1b) Implicit-convention references that carry more than one resolution target and so don't |
| 59 | + // resolve through `resolveToSymbol()` (singular; returns null when ambiguous) but do resolve |
| 60 | + // through `resolveToSymbols()` (plural). Confirmed empirically: |
| 61 | + // - KtForExpression: resolves to [iterator(), hasNext(), next()] -- iterator is the |
| 62 | + // user-importable one for a `for (x in foo)` loop. |
| 63 | + // - KtDestructuringDeclarationEntry (one per destructured variable): resolves to that |
| 64 | + // variable's own componentN() symbol. |
| 65 | + // - KtPropertyDelegate: resolves to the delegate's getValue()/setValue() symbol(s). |
| 66 | + // Recording every returned symbol is safe: extra (e.g. stdlib Iterator.next) symbols only ever |
| 67 | + // keep an import, never drop a used one. |
| 68 | + ktFile.collectDescendantsOfType<KtForExpression>().forEach { forExpr -> |
| 69 | + runCatching { recordAll(forExpr.mainReference?.resolveToSymbols()) } |
| 70 | + } |
| 71 | + ktFile.collectDescendantsOfType<KtDestructuringDeclarationEntry>().forEach { entry -> |
| 72 | + runCatching { recordAll(entry.mainReference?.resolveToSymbols()) } |
| 73 | + } |
| 74 | + ktFile.collectDescendantsOfType<KtPropertyDelegate>().forEach { delegate -> |
| 75 | + runCatching { recordAll(delegate.mainReference?.resolveToSymbols()) } |
| 76 | + } |
| 77 | + |
| 78 | + // 2) Convention / operator call sites (no textual name reference). |
| 79 | + ktFile.collectDescendantsOfType<KtElement>().forEach { element -> |
| 80 | + val isConvention = |
| 81 | + element is KtOperationReferenceExpression || |
| 82 | + element is KtArrayAccessExpression || |
| 83 | + element is KtCallExpression || |
| 84 | + element is KtForExpression || |
| 85 | + element is KtDestructuringDeclaration || |
| 86 | + element is KtPropertyDelegate |
| 87 | + if (!isConvention) return@forEach |
| 88 | + runCatching { |
| 89 | + record(element.resolveToCall()?.successfulFunctionCallOrNull()?.symbol) |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + return ImportUsage(usedFqNames, usedPackages, unresolvedNames) |
| 94 | +} |
| 95 | + |
| 96 | +private fun KaSymbol.importableFqNameString(): String? = |
| 97 | + when (this) { |
| 98 | + // A constructor's own callableId is null, so it must map to its containing class -- the name |
| 99 | + // that's actually imported. Covers `Foo()` calls and `@Foo` annotations (both resolve to the |
| 100 | + // constructor). Must precede the KaCallableSymbol branch, which a constructor also matches. |
| 101 | + is KaConstructorSymbol -> containingClassId?.asSingleFqName()?.asString() |
| 102 | + is KaClassLikeSymbol -> classId?.asSingleFqName()?.asString() |
| 103 | + is KaCallableSymbol -> callableId?.asSingleFqName()?.asString() |
| 104 | + else -> null |
| 105 | + } |
0 commit comments