[LL FIR] optimize search for fir declaration by kt declaration
Introduce new API to traverse FirFile by known declaration path instead of unoptimized findSourceByTraversingWholeTree ^KT-59454
This commit is contained in:
committed by
Space Team
parent
7b77ec9930
commit
ca521a81f8
@@ -76,6 +76,7 @@ allprojects {
|
|||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.fir.symbols.SymbolInternals"
|
freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.fir.symbols.SymbolInternals"
|
||||||
freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals"
|
freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.analysis.low.level.api.fir.LLFirInternals"
|
||||||
|
freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.analysis.api.KtAnalysisApiInternals"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-43
@@ -10,80 +10,104 @@ import org.jetbrains.kotlin.fir.FirElement
|
|||||||
import org.jetbrains.kotlin.fir.declarations.*
|
import org.jetbrains.kotlin.fir.declarations.*
|
||||||
import org.jetbrains.kotlin.fir.declarations.utils.classId
|
import org.jetbrains.kotlin.fir.declarations.utils.classId
|
||||||
import org.jetbrains.kotlin.fir.packageFqName
|
import org.jetbrains.kotlin.fir.packageFqName
|
||||||
|
import org.jetbrains.kotlin.fir.psi
|
||||||
import org.jetbrains.kotlin.fir.render
|
import org.jetbrains.kotlin.fir.render
|
||||||
import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid
|
import org.jetbrains.kotlin.fir.visitors.FirVisitorVoid
|
||||||
import org.jetbrains.kotlin.name.ClassId
|
import org.jetbrains.kotlin.name.ClassId
|
||||||
|
import org.jetbrains.kotlin.psi.KtDeclaration
|
||||||
|
import org.jetbrains.kotlin.psi.psiUtil.containingClassOrObject
|
||||||
|
|
||||||
object FirElementFinder {
|
object FirElementFinder {
|
||||||
fun findClassifierWithClassId(
|
fun findClassifierWithClassId(
|
||||||
firFile: FirFile,
|
firFile: FirFile,
|
||||||
classId: ClassId,
|
classId: ClassId,
|
||||||
): FirClassLikeDeclaration? = findPathToClassifierWithClassId(
|
): FirClassLikeDeclaration? = collectDesignationPath(
|
||||||
firFile = firFile,
|
firFile = firFile,
|
||||||
classId = classId,
|
containerClassId = classId.outerClassId,
|
||||||
expectedMemberDeclaration = null
|
expectedDeclarationAcceptor = { it is FirClassLikeDeclaration && it.symbol.name == classId.shortClassName },
|
||||||
)?.second
|
)?.target?.let { it as FirClassLikeDeclaration }
|
||||||
|
|
||||||
fun findClassPathToDeclaration(
|
fun findClassPathToDeclaration(
|
||||||
firFile: FirFile,
|
firFile: FirFile,
|
||||||
declarationContainerClassId: ClassId,
|
declarationContainerClassId: ClassId,
|
||||||
targetMemberDeclaration: FirDeclaration,
|
targetMemberDeclaration: FirDeclaration,
|
||||||
): List<FirRegularClass>? = findPathToClassifierWithClassId(
|
): List<FirRegularClass>? = collectDesignationPath(
|
||||||
firFile = firFile,
|
firFile = firFile,
|
||||||
classId = declarationContainerClassId,
|
containerClassId = declarationContainerClassId,
|
||||||
expectedMemberDeclaration = targetMemberDeclaration
|
expectedDeclarationAcceptor = { it == targetMemberDeclaration },
|
||||||
)?.first
|
)?.path
|
||||||
|
|
||||||
private fun findPathToClassifierWithClassId(
|
fun findDeclaration(firFile: FirFile, nonLocalDeclaration: KtDeclaration): FirDeclaration? = collectDesignationPath(
|
||||||
|
firFile = firFile,
|
||||||
|
nonLocalDeclaration = nonLocalDeclaration,
|
||||||
|
)?.target
|
||||||
|
|
||||||
|
fun findPathToDeclarationWithTarget(
|
||||||
firFile: FirFile,
|
firFile: FirFile,
|
||||||
classId: ClassId,
|
nonLocalDeclaration: KtDeclaration,
|
||||||
expectedMemberDeclaration: FirDeclaration?,
|
): List<FirDeclaration>? = collectDesignationPath(
|
||||||
): Pair<List<FirRegularClass>, FirClassLikeDeclaration>? {
|
firFile = firFile,
|
||||||
requireWithAttachmentBuilder(!classId.isLocal, { "ClassId should not be local" }) {
|
nonLocalDeclaration = nonLocalDeclaration,
|
||||||
withEntry("classId", classId) { it.asString() }
|
)?.pathWithTarget
|
||||||
}
|
|
||||||
requireWithAttachmentBuilder(
|
private class FirDeclarationDesignation(
|
||||||
firFile.packageFqName == classId.packageFqName,
|
val path: List<FirRegularClass>,
|
||||||
{ "ClassId should not be local" }
|
val target: FirDeclaration,
|
||||||
) {
|
) {
|
||||||
withEntry("FirFile.packageName", firFile.packageFqName) { it.asString() }
|
val pathWithTarget: List<FirDeclaration> get() = path + target
|
||||||
withEntry("ClassId.packageName", classId.packageFqName) { it.asString() }
|
}
|
||||||
|
|
||||||
|
private fun collectDesignationPath(
|
||||||
|
firFile: FirFile,
|
||||||
|
nonLocalDeclaration: KtDeclaration,
|
||||||
|
): FirDeclarationDesignation? = collectDesignationPath(
|
||||||
|
firFile = firFile,
|
||||||
|
containerClassId = nonLocalDeclaration.containingClassOrObject?.getClassId(),
|
||||||
|
expectedDeclarationAcceptor = { it.psi == nonLocalDeclaration },
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun collectDesignationPath(
|
||||||
|
firFile: FirFile,
|
||||||
|
containerClassId: ClassId?,
|
||||||
|
expectedDeclarationAcceptor: (FirDeclaration) -> Boolean,
|
||||||
|
): FirDeclarationDesignation? {
|
||||||
|
if (containerClassId != null) {
|
||||||
|
requireWithAttachmentBuilder(!containerClassId.isLocal, { "ClassId should not be local" }) {
|
||||||
|
withEntry("classId", containerClassId) { it.asString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
requireWithAttachmentBuilder(
|
||||||
|
firFile.packageFqName == containerClassId.packageFqName,
|
||||||
|
{ "ClassId should not be local" }
|
||||||
|
) {
|
||||||
|
withEntry("FirFile.packageName", firFile.packageFqName) { it.asString() }
|
||||||
|
withEntry("ClassId.packageName", containerClassId.packageFqName) { it.asString() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val classIdPathSegment = classId.relativeClassName.pathSegments()
|
val classIdPathSegment = containerClassId?.relativeClassName?.pathSegments().orEmpty()
|
||||||
val path = ArrayList<FirRegularClass>(classIdPathSegment.size)
|
val path = ArrayList<FirRegularClass>(classIdPathSegment.size)
|
||||||
var result: FirClassLikeDeclaration? = null
|
var result: FirDeclaration? = null
|
||||||
|
|
||||||
fun find(declarations: Iterable<FirDeclaration>, classIdPathIndex: Int): Boolean {
|
fun find(declarations: Iterable<FirDeclaration>, classIdPathIndex: Int): Boolean {
|
||||||
val currentClassSegment = classIdPathSegment[classIdPathIndex]
|
val currentClassSegment = classIdPathSegment.getOrNull(classIdPathIndex)
|
||||||
|
|
||||||
for (subDeclaration in declarations) {
|
for (subDeclaration in declarations) {
|
||||||
if (subDeclaration is FirScript) {
|
when {
|
||||||
val scriptDeclarations = subDeclaration.statements.asSequence().filterIsInstance<FirDeclaration>()
|
currentClassSegment == null && expectedDeclarationAcceptor(subDeclaration) -> {
|
||||||
if (find(scriptDeclarations.asIterable(), classIdPathIndex)) {
|
result = subDeclaration
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
subDeclaration is FirScript -> {
|
||||||
}
|
val scriptDeclarations = subDeclaration.statements.asSequence().filterIsInstance<FirDeclaration>()
|
||||||
|
if (find(scriptDeclarations.asIterable(), classIdPathIndex)) {
|
||||||
if (subDeclaration is FirClassLikeDeclaration && currentClassSegment == subDeclaration.symbol.name) {
|
|
||||||
if (classIdPathIndex == classIdPathSegment.lastIndex) {
|
|
||||||
if (expectedMemberDeclaration == null ||
|
|
||||||
subDeclaration is FirRegularClass && expectedMemberDeclaration in subDeclaration.declarations
|
|
||||||
) {
|
|
||||||
if (subDeclaration is FirRegularClass) {
|
|
||||||
path += subDeclaration
|
|
||||||
}
|
|
||||||
|
|
||||||
result = subDeclaration
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subDeclaration is FirRegularClass) {
|
subDeclaration is FirRegularClass && currentClassSegment == subDeclaration.symbol.name -> {
|
||||||
path += subDeclaration
|
path += subDeclaration
|
||||||
if (find(subDeclaration.declarations, classIdPathIndex + 1)) {
|
if (find(subDeclaration.declarations, classIdPathIndex + 1)) {
|
||||||
return true
|
return true
|
||||||
@@ -99,7 +123,10 @@ object FirElementFinder {
|
|||||||
|
|
||||||
find(firFile.declarations, classIdPathIndex = 0)
|
find(firFile.declarations, classIdPathIndex = 0)
|
||||||
return result?.let {
|
return result?.let {
|
||||||
(path as List<FirRegularClass>) to it
|
FirDeclarationDesignation(
|
||||||
|
path = if (path.isEmpty()) emptyList() else path,
|
||||||
|
target = it,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+79
-86
@@ -5,7 +5,10 @@
|
|||||||
|
|
||||||
package org.jetbrains.kotlin.analysis.low.level.api.fir.util
|
package org.jetbrains.kotlin.analysis.low.level.api.fir.util
|
||||||
|
|
||||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.throwUnexpectedFirElementError
|
import com.intellij.psi.PsiElement
|
||||||
|
import org.jetbrains.kotlin.analysis.api.KtAnalysisApiInternals
|
||||||
|
import org.jetbrains.kotlin.analysis.low.level.api.fir.api.LLFirResolveSession
|
||||||
|
import org.jetbrains.kotlin.analysis.low.level.api.fir.element.builder.canBePartOfParentDeclaration
|
||||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.element.builder.containingDeclaration
|
import org.jetbrains.kotlin.analysis.low.level.api.fir.element.builder.containingDeclaration
|
||||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.element.builder.getNonLocalContainingOrThisDeclaration
|
import org.jetbrains.kotlin.analysis.low.level.api.fir.element.builder.getNonLocalContainingOrThisDeclaration
|
||||||
import org.jetbrains.kotlin.analysis.low.level.api.fir.file.builder.LLFirFileBuilder
|
import org.jetbrains.kotlin.analysis.low.level.api.fir.file.builder.LLFirFileBuilder
|
||||||
@@ -29,39 +32,59 @@ internal fun KtDeclaration.findSourceNonLocalFirDeclaration(
|
|||||||
provider: FirProvider,
|
provider: FirProvider,
|
||||||
containerFirFile: FirFile? = null,
|
containerFirFile: FirFile? = null,
|
||||||
): FirDeclaration {
|
): FirDeclaration {
|
||||||
//TODO test what way faster
|
val firFile = containerFirFile ?: firFileBuilder.buildRawFirFileWithCaching(containingKtFile)
|
||||||
findSourceNonLocalFirDeclarationByProvider(firFileBuilder, provider, containerFirFile)?.let { return it }
|
|
||||||
findSourceByTraversingWholeTree(firFileBuilder, containerFirFile)?.let { return it }
|
// TODO test what way faster
|
||||||
|
if (isPhysical) {
|
||||||
|
// do not request providers with non-physical psi in order not to leak them there and
|
||||||
|
// to avoid inconsistency between physical psi and its copy during completion
|
||||||
|
findSourceNonLocalFirDeclarationByProvider(
|
||||||
|
firDeclarationProvider = { declaration ->
|
||||||
|
if (declaration is KtClassLikeDeclaration) {
|
||||||
|
declaration.findFir(provider)
|
||||||
|
} else {
|
||||||
|
val containingClassOrObject = declaration.containingClassOrObject
|
||||||
|
val declarations = if (containingClassOrObject != null) {
|
||||||
|
val containerClassFir = containingClassOrObject.findFir(provider) as? FirRegularClass
|
||||||
|
containerClassFir?.declarations
|
||||||
|
} else {
|
||||||
|
if (declaration.containingKtFile.isScript()) {
|
||||||
|
// .kts will have a single [FirScript] as a declaration. We need to unwrap statements in it.
|
||||||
|
(firFile.declarations.singleOrNull() as? FirScript)?.statements?.filterIsInstance<FirDeclaration>()
|
||||||
|
} else {
|
||||||
|
firFile.declarations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is possible that we will not be able to find the needed declaration here when the code is invalid
|
||||||
|
// e.g., we have two conflicting declarations with the same name,
|
||||||
|
// and we are searching for the wrong one
|
||||||
|
declarations?.find { it.psi == declaration }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)?.let { return it }
|
||||||
|
}
|
||||||
|
|
||||||
|
findSourceNonLocalFirDeclarationByProvider(
|
||||||
|
firDeclarationProvider = { declaration ->
|
||||||
|
FirElementFinder.findDeclaration(firFile, declaration)
|
||||||
|
},
|
||||||
|
)?.let { return it }
|
||||||
|
|
||||||
errorWithFirSpecificEntries(
|
errorWithFirSpecificEntries(
|
||||||
"No fir element was found for",
|
"No fir element was found for",
|
||||||
psi = this,
|
psi = this,
|
||||||
fir = containerFirFile ?: firFileBuilder.buildRawFirFileWithCaching(containingKtFile),
|
fir = firFile,
|
||||||
additionalInfos = { withEntry("isPhysical", isPhysical.toString()) }
|
additionalInfos = { withEntry("isPhysical", isPhysical.toString()) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun KtDeclaration.findFirDeclarationForAnyFirSourceDeclaration(
|
@KtAnalysisApiInternals
|
||||||
firFileBuilder: LLFirFileBuilder,
|
fun PsiElement.collectContainingDeclarationsIfNonLocal(session: LLFirResolveSession): List<FirDeclaration>? {
|
||||||
provider: FirProvider,
|
val ktFile = containingFile as? KtFile ?: return null
|
||||||
): FirDeclaration {
|
val ktDeclaration = getNonLocalContainingOrThisDeclaration { !it.canBePartOfParentDeclaration } ?: return null
|
||||||
val nonLocalDeclaration = getNonLocalContainingOrThisDeclaration()
|
val firFile = session.getOrBuildFirFile(ktFile)
|
||||||
?.findSourceNonLocalFirDeclaration(firFileBuilder, provider)
|
return FirElementFinder.findPathToDeclarationWithTarget(firFile, ktDeclaration)
|
||||||
?: firFileBuilder.buildRawFirFileWithCaching(containingKtFile)
|
|
||||||
val originalDeclaration = originalDeclaration
|
|
||||||
val fir = FirElementFinder.findElementIn<FirDeclaration>(nonLocalDeclaration) { firDeclaration ->
|
|
||||||
firDeclaration.psi == this || firDeclaration.psi == originalDeclaration
|
|
||||||
}
|
|
||||||
return fir
|
|
||||||
?: errorWithFirSpecificEntries("FirDeclaration was not found", psi = this)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal inline fun <reified F : FirDeclaration> KtDeclaration.findFirDeclarationForAnyFirSourceDeclarationOfType(
|
|
||||||
firFileBuilder: LLFirFileBuilder,
|
|
||||||
provider: FirProvider,
|
|
||||||
): FirDeclaration {
|
|
||||||
val fir = findFirDeclarationForAnyFirSourceDeclaration(firFileBuilder, provider)
|
|
||||||
if (fir !is F) throwUnexpectedFirElementError(fir, this, F::class)
|
|
||||||
return fir
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun KtElement.findSourceByTraversingWholeTree(
|
internal fun KtElement.findSourceByTraversingWholeTree(
|
||||||
@@ -81,56 +104,22 @@ internal fun KtElement.findSourceByTraversingWholeTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun KtDeclaration.findSourceNonLocalFirDeclarationByProvider(
|
private fun KtDeclaration.findSourceNonLocalFirDeclarationByProvider(
|
||||||
firFileBuilder: LLFirFileBuilder,
|
firDeclarationProvider: (KtDeclaration) -> FirDeclaration?,
|
||||||
provider: FirProvider,
|
|
||||||
containerFirFile: FirFile?,
|
|
||||||
): FirDeclaration? {
|
): FirDeclaration? {
|
||||||
if (!isPhysical) {
|
val candidate = when (this) {
|
||||||
//do not request providers with non-physical psi in order not to leak them there and
|
is KtClassOrObject,
|
||||||
//to avoid inconsistency between physical psi and its copy during completion
|
is KtProperty,
|
||||||
return null
|
is KtNamedFunction,
|
||||||
}
|
is KtConstructor<*>,
|
||||||
val candidate = when {
|
is KtClassInitializer,
|
||||||
this is KtClassOrObject -> findFir(provider)
|
is KtTypeAlias,
|
||||||
this is KtNamedDeclaration && (this is KtProperty || this is KtNamedFunction) -> {
|
is KtDestructuringDeclaration,
|
||||||
val containerClass = containingClassOrObject
|
is KtScript,
|
||||||
val declarations = if (containerClass != null) {
|
-> firDeclarationProvider(this)
|
||||||
val containerClassFir = containerClass.findFir(provider) as? FirRegularClass
|
|
||||||
containerClassFir?.declarations
|
|
||||||
} else {
|
|
||||||
val ktFile = containingKtFile
|
|
||||||
val firFile = containerFirFile ?: firFileBuilder.buildRawFirFileWithCaching(ktFile)
|
|
||||||
if (ktFile.isScript()) {
|
|
||||||
// .kts will have a single [FirScript] as a declaration. We need to unwrap statements in it.
|
|
||||||
(firFile.declarations.singleOrNull() as? FirScript)?.statements?.filterIsInstance<FirDeclaration>()
|
|
||||||
} else {
|
|
||||||
firFile.declarations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
is KtPropertyAccessor -> {
|
||||||
It is possible that we will not be able to find needed declaration here when the code is invalid,
|
|
||||||
e.g, we have two conflicting declarations with the same name and we are searching in the wrong one
|
|
||||||
*/
|
|
||||||
declarations?.firstOrNull { it.psi == this }
|
|
||||||
}
|
|
||||||
this is KtConstructor<*> || this is KtClassInitializer -> {
|
|
||||||
val containingClass = containingClassOrObject
|
|
||||||
?: errorWithFirSpecificEntries("Container class should be not null for KtConstructor", psi = this)
|
|
||||||
val containerClassFir = containingClass.findFir(provider) as? FirRegularClass ?: return null
|
|
||||||
containerClassFir.declarations.firstOrNull { it.psi == this }
|
|
||||||
}
|
|
||||||
this is KtTypeAlias -> findFir(provider)
|
|
||||||
this is KtDestructuringDeclaration -> {
|
|
||||||
val firFile = containerFirFile ?: firFileBuilder.buildRawFirFileWithCaching(containingKtFile)
|
|
||||||
firFile.declarations.firstOrNull { it.psi == this }
|
|
||||||
}
|
|
||||||
this is KtScript -> containerFirFile?.declarations?.singleOrNull { it is FirScript }
|
|
||||||
this is KtPropertyAccessor -> {
|
|
||||||
val firPropertyDeclaration = property.findSourceNonLocalFirDeclarationByProvider(
|
val firPropertyDeclaration = property.findSourceNonLocalFirDeclarationByProvider(
|
||||||
firFileBuilder,
|
firDeclarationProvider,
|
||||||
provider,
|
|
||||||
containerFirFile,
|
|
||||||
) as? FirVariable ?: return null
|
) as? FirVariable ?: return null
|
||||||
|
|
||||||
if (isGetter) {
|
if (isGetter) {
|
||||||
@@ -139,32 +128,36 @@ private fun KtDeclaration.findSourceNonLocalFirDeclarationByProvider(
|
|||||||
firPropertyDeclaration.setter
|
firPropertyDeclaration.setter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this is KtParameter -> {
|
|
||||||
val ownerFunction = ownerFunction
|
is KtParameter -> {
|
||||||
?: errorWithFirSpecificEntries("Containing function should be not null for KtParameter", psi = this)
|
val ownerFunction = ownerFunction ?: errorWithFirSpecificEntries(
|
||||||
|
"Containing function should be not null for KtParameter",
|
||||||
|
psi = this,
|
||||||
|
)
|
||||||
|
|
||||||
val firFunctionDeclaration = ownerFunction.findSourceNonLocalFirDeclarationByProvider(
|
val firFunctionDeclaration = ownerFunction.findSourceNonLocalFirDeclarationByProvider(
|
||||||
firFileBuilder,
|
firDeclarationProvider,
|
||||||
provider,
|
|
||||||
containerFirFile,
|
|
||||||
) as? FirFunction ?: return null
|
) as? FirFunction ?: return null
|
||||||
|
|
||||||
firFunctionDeclaration.valueParameters[parameterIndex()]
|
firFunctionDeclaration.valueParameters[parameterIndex()]
|
||||||
}
|
}
|
||||||
this is KtTypeParameter -> {
|
|
||||||
val declaration = containingDeclaration
|
is KtTypeParameter -> {
|
||||||
?: errorWithFirSpecificEntries("Containing declaration should be not null for KtTypeParameter", psi = this)
|
val declaration = containingDeclaration ?: errorWithFirSpecificEntries(
|
||||||
|
"Containing declaration should be not null for KtTypeParameter",
|
||||||
|
psi = this,
|
||||||
|
)
|
||||||
|
|
||||||
val firTypeParameterOwner = declaration.findSourceNonLocalFirDeclarationByProvider(
|
val firTypeParameterOwner = declaration.findSourceNonLocalFirDeclarationByProvider(
|
||||||
firFileBuilder,
|
firDeclarationProvider,
|
||||||
provider,
|
|
||||||
containerFirFile,
|
|
||||||
) as? FirTypeParameterRefsOwner ?: return null
|
) as? FirTypeParameterRefsOwner ?: return null
|
||||||
|
|
||||||
firTypeParameterOwner.typeParameters.firstOrNull { it.psi == this } as FirDeclaration
|
firTypeParameterOwner.typeParameters.firstOrNull { it.psi == this } as FirDeclaration
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> errorWithFirSpecificEntries("Invalid container", psi = this)
|
else -> errorWithFirSpecificEntries("Invalid container", psi = this)
|
||||||
}
|
}
|
||||||
|
|
||||||
return candidate?.takeIf { it.realPsi == this }
|
return candidate?.takeIf { it.realPsi == this }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user