KTIJ-25530 [Analysis API] Correctly collect references to implicitly dispatched callables in Import Optimizer

N.B. There is a case which is not covered ATM due to the bug in the
compiler, see KT-58980

^KTIJ-25530 Fixed
diff --git a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirImportOptimizer.kt b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirImportOptimizer.kt
index 20fe4e5..972ce8a 100644
--- a/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirImportOptimizer.kt
+++ b/analysis/analysis-api-fir/src/org/jetbrains/kotlin/analysis/api/fir/components/KtFirImportOptimizer.kt
@@ -5,6 +5,7 @@
 
 package org.jetbrains.kotlin.analysis.api.fir.components
 
+import org.jetbrains.kotlin.KtFakeSourceElementKind
 import org.jetbrains.kotlin.KtRealSourceElementKind
 import org.jetbrains.kotlin.analysis.api.components.KtImportOptimizer
 import org.jetbrains.kotlin.analysis.api.components.KtImportOptimizerResult
@@ -33,10 +34,7 @@
 import org.jetbrains.kotlin.fir.types.FirErrorTypeRef
 import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
 import org.jetbrains.kotlin.fir.types.classId
-import org.jetbrains.kotlin.name.ClassId
-import org.jetbrains.kotlin.name.FqName
-import org.jetbrains.kotlin.name.Name
-import org.jetbrains.kotlin.name.parentOrNull
+import org.jetbrains.kotlin.name.*
 import org.jetbrains.kotlin.psi.*
 import org.jetbrains.kotlin.psi.psiUtil.getCallNameExpression
 import org.jetbrains.kotlin.psi.psiUtil.getPossiblyQualifiedCallExpression
@@ -178,17 +176,13 @@
                 processErrorNameReference(functionCall)
 
                 val referencesByName = functionCall.functionReferenceName ?: return
-                val functionSymbol = functionCall.referencedCallableSymbol ?: return
-
-                saveCallable(functionSymbol, referencesByName)
+                saveCallable(functionCall, referencesByName)
             }
 
             private fun processImplicitFunctionCall(implicitInvokeCall: FirImplicitInvokeCall) {
                 processErrorNameReference(implicitInvokeCall)
 
-                val functionSymbol = implicitInvokeCall.referencedCallableSymbol ?: return
-
-                saveCallable(functionSymbol, OperatorNameConventions.INVOKE)
+                saveCallable(implicitInvokeCall, OperatorNameConventions.INVOKE)
             }
 
             private fun processPropertyAccessExpression(propertyAccessExpression: FirPropertyAccessExpression) {
@@ -196,9 +190,8 @@
                 processErrorNameReference(propertyAccessExpression)
 
                 val referencedByName = propertyAccessExpression.propertyReferenceName ?: return
-                val propertySymbol = propertyAccessExpression.referencedCallableSymbol ?: return
 
-                saveCallable(propertySymbol, referencedByName)
+                saveCallable(propertyAccessExpression, referencedByName)
             }
 
             private fun processTypeRef(resolvedTypeRef: FirResolvedTypeRef) {
@@ -212,9 +205,7 @@
                 processErrorNameReference(callableReferenceAccess)
 
                 val referencedByName = callableReferenceAccess.callableReferenceName ?: return
-                val resolvedSymbol = callableReferenceAccess.referencedCallableSymbol ?: return
-
-                saveCallable(resolvedSymbol, referencedByName)
+                saveCallable(callableReferenceAccess, referencedByName)
             }
 
             private fun processResolvedQualifier(resolvedQualifier: FirResolvedQualifier) {
@@ -237,12 +228,58 @@
                 saveReferencedItem(importableName, referencedByName)
             }
 
-            private fun saveCallable(resolvedSymbol: FirCallableSymbol<*>, referencedByName: Name) {
-                val importableName = resolvedSymbol.computeImportableName(firSession) ?: return
+            private fun saveCallable(qualifiedCall: FirQualifiedAccessExpression, referencedByName: Name) {
+                val importableName = importableNameForReferencedSymbol(qualifiedCall) ?: return
 
                 saveReferencedItem(importableName, referencedByName)
             }
 
+            private fun importableNameForReferencedSymbol(qualifiedCall: FirQualifiedAccessExpression): FqName? {
+                return qualifiedCall.importableNameForImplicitlyDispatchedCallable()
+                    ?: qualifiedCall.referencedCallableSymbol?.computeImportableName(firSession)
+            }
+
+            /**
+             * Returns correct importable name for implicitly dispatched callable - that is, a callable
+             * which has a dispatch receiver, but whose dispatch receiver is present implicitly. The most
+             * important case for that is the following:
+             *
+             * ```kt
+             * import MyObject.bar
+             *
+             * open class Base { fun bar() {} }
+             *
+             * object MyObject : Base()
+             *
+             * fun test() {
+             *   bar()
+             * }
+             * ```
+             *
+             * For the `bar()` call, `MyObject` instance is an implicit dispatch receiver.
+             *
+             * In such case, [FirQualifiedAccessExpression] representing the call references
+             * the original `Base.bar` callable symbol instead of `MyObject.bar`, because there are
+             * no separate symbol for that case.
+             *
+             * Java statics present a similar case - they can be imported not only from the declaring class,
+             * but also from any subclass.
+             */
+            private fun FirQualifiedAccessExpression.importableNameForImplicitlyDispatchedCallable(): FqName? {
+                val dispatchReceiver = dispatchReceiver
+                if (
+                    dispatchReceiver !is FirResolvedQualifier ||
+                    dispatchReceiver.source?.kind != KtFakeSourceElementKind.ImplicitReceiver
+                ) {
+                    return null
+                }
+
+                val dispatcherClass = dispatchReceiver.classId ?: return null
+                val referencedSymbolName = referencedCallableSymbol?.name ?: return null
+
+                return CallableId(dispatcherClass, referencedSymbolName).asSingleFqName()
+            }
+
             private fun saveReferencedItem(importableName: FqName, referencedByName: Name) {
                 usedImports.getOrPut(importableName) { hashSetOf() } += referencedByName
             }
@@ -271,7 +308,7 @@
 
 
 /**
- * An actual name by which this callable reference were used.
+ * An actual name by which this callable reference was used.
  */
 private val FirCallableReferenceAccess.callableReferenceName: Name?
     get() {
diff --git a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/importOptimizer/FirIdeNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/importOptimizer/FirIdeNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java
index 62e157f..094ee81 100644
--- a/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/importOptimizer/FirIdeNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java
+++ b/analysis/analysis-api-fir/tests-gen/org/jetbrains/kotlin/analysis/api/fir/test/cases/generated/cases/components/importOptimizer/FirIdeNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java
@@ -65,6 +65,12 @@
     }
 
     @Test
+    @TestMetadata("unusedFunctionImportedFromObjectSuperClass.kt")
+    public void testUnusedFunctionImportedFromObjectSuperClass() throws Exception {
+        runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.kt");
+    }
+
+    @Test
     @TestMetadata("unusedFunctionImports.kt")
     public void testUnusedFunctionImports() throws Exception {
         runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImports.kt");
@@ -119,6 +125,12 @@
     }
 
     @Test
+    @TestMetadata("usedFunctionImportedFromObjectSuperClass.kt")
+    public void testUsedFunctionImportedFromObjectSuperClass() throws Exception {
+        runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.kt");
+    }
+
+    @Test
     @TestMetadata("usedGenericTypeQualifier.kt")
     public void testUsedGenericTypeQualifier() throws Exception {
         runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedGenericTypeQualifier.kt");
diff --git a/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/importOptimizer/FirStandaloneNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java b/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/importOptimizer/FirStandaloneNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java
index e0b6dd5..6271772 100644
--- a/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/importOptimizer/FirStandaloneNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java
+++ b/analysis/analysis-api-standalone/tests-gen/org/jetbrains/kotlin/analysis/api/standalone/fir/test/cases/generated/cases/components/importOptimizer/FirStandaloneNormalAnalysisSourceModuleAnalysisApiImportOptimizerTestGenerated.java
@@ -65,6 +65,12 @@
     }
 
     @Test
+    @TestMetadata("unusedFunctionImportedFromObjectSuperClass.kt")
+    public void testUnusedFunctionImportedFromObjectSuperClass() throws Exception {
+        runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.kt");
+    }
+
+    @Test
     @TestMetadata("unusedFunctionImports.kt")
     public void testUnusedFunctionImports() throws Exception {
         runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImports.kt");
@@ -119,6 +125,12 @@
     }
 
     @Test
+    @TestMetadata("usedFunctionImportedFromObjectSuperClass.kt")
+    public void testUsedFunctionImportedFromObjectSuperClass() throws Exception {
+        runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.kt");
+    }
+
+    @Test
     @TestMetadata("usedGenericTypeQualifier.kt")
     public void testUsedGenericTypeQualifier() throws Exception {
         runTest("analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedGenericTypeQualifier.kt");
diff --git a/analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.imports b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.imports
new file mode 100644
index 0000000..2719695
--- /dev/null
+++ b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.imports
@@ -0,0 +1,6 @@
+test.Foo.callableReference
+test.Foo.extFuncFromBase
+test.Foo.extPropFromBase
+test.Foo.funcFromBase
+test.Foo.invoke
+test.Foo.propFromBase
diff --git a/analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.kt b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.kt
new file mode 100644
index 0000000..6522399
--- /dev/null
+++ b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/unusedFunctionImportedFromObjectSuperClass.kt
@@ -0,0 +1,36 @@
+package test
+
+import test.Foo.funcFromBase
+import test.Foo.extFuncFromBase
+import test.Foo.propFromBase
+import test.Foo.extPropFromBase
+import test.Foo.invoke
+import test.Foo.callableReference
+
+open class Base {
+    fun funcFromBase() {}
+    fun Int.extFuncFromBase() {}
+
+    val propFromBase: Int = 0
+    val Int.extPropFromBase: Int get() = 0
+
+    operator fun String.invoke() {}
+
+    fun callableReference() {}
+}
+
+object Foo : Base()
+
+fun usage() {
+    with(Foo) {
+        funcFromBase()
+        10.extFuncFromBase()
+
+        propFromBase
+        10.extPropFromBase
+
+        "hello"()
+
+        ::callableReference
+    }
+}
\ No newline at end of file
diff --git a/analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.imports b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.imports
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.imports
diff --git a/analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.kt b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.kt
new file mode 100644
index 0000000..5dcd012
--- /dev/null
+++ b/analysis/analysis-api/testData/components/importOptimizer/analyseImports/usedFunctionImportedFromObjectSuperClass.kt
@@ -0,0 +1,34 @@
+package test
+
+import test.Foo.funcFromBase
+import test.Foo.extFuncFromBase
+import test.Foo.propFromBase
+import test.Foo.extPropFromBase
+import test.Foo.invoke
+import test.Foo.callableReference
+
+open class Base {
+    fun funcFromBase() {}
+    fun Int.extFuncFromBase() {}
+
+    val propFromBase: Int = 0
+    val Int.extPropFromBase: Int get() = 0
+
+    operator fun String.invoke() {}
+    
+    fun callableReference() {}
+}
+
+object Foo : Base()
+
+fun usage() {
+    funcFromBase()
+    10.extFuncFromBase()
+
+    propFromBase
+    10.extPropFromBase
+
+    "hello"()
+    
+    ::callableReference
+}
\ No newline at end of file