diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptDependenciesClassFinder.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptDependenciesClassFinder.kt index b29fe58e74f..65d776cd224 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptDependenciesClassFinder.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/KotlinScriptDependenciesClassFinder.kt @@ -22,9 +22,11 @@ import com.intellij.openapi.roots.impl.PackageDirectoryCache import com.intellij.openapi.vfs.VirtualFile import com.intellij.psi.NonClasspathClassFinder import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassOwner import com.intellij.psi.search.EverythingGlobalScope import com.intellij.psi.search.GlobalSearchScope import com.intellij.util.containers.ConcurrentFactoryMap +import com.intellij.util.containers.ContainerUtil import org.jetbrains.kotlin.idea.caches.resolve.ScriptModuleSearchScope import org.jetbrains.kotlin.load.java.AbstractJavaClassFinder import org.jetbrains.kotlin.resolve.jvm.KotlinSafeClassFinder @@ -56,21 +58,69 @@ class KotlinScriptDependenciesClassFinder(project: Project, myCaches.clear() } - override fun findClass(qualifiedName: String, scope: GlobalSearchScope): PsiClass? = - super.findClass(qualifiedName, scope)?.let { aClass -> - when { - scope is ScriptModuleSearchScope || - (scope as? AbstractJavaClassFinder.FilterOutKotlinSourceFilesScope)?.base is ScriptModuleSearchScope || - scope is EverythingGlobalScope || - aClass.containingFile?.virtualFile.let { file -> - file != null && - with (ProjectFileIndex.SERVICE.getInstance(myProject)) { - !isInContent(file) && - !isInLibraryClasses(file) && - !isInLibrarySource(file) - } - } -> aClass - else -> null - } + override fun findClass(qualifiedName: String, scope: GlobalSearchScope): PsiClass? { + val psiClass = findClassInCache(qualifiedName, scope) ?: return null + return when { + scope is ScriptModuleSearchScope || + (scope as? AbstractJavaClassFinder.FilterOutKotlinSourceFilesScope)?.base is ScriptModuleSearchScope || + scope is EverythingGlobalScope || + psiClass.containingFile?.virtualFile.let { file -> + file != null && + with(ProjectFileIndex.SERVICE.getInstance(myProject)) { + !isInContent(file) && + !isInLibraryClasses(file) && + !isInLibrarySource(file) + } + } -> psiClass + else -> null } + } + + private fun findClassInCache(qualifiedName: String, scope: GlobalSearchScope): PsiClass? { + if (qualifiedName.isEmpty()) return null + + return splitDotQualifiedName(qualifiedName).map { (packageName, classNames) -> + findClassInPackage(packageName, classNames, scope) + }.find { it != null } + } + + private fun findClassInPackage(packageName: String, classNames: List, scope: GlobalSearchScope): PsiClass? { + var result: PsiClass? = null + ContainerUtil.process(getCache(scope).getDirectoriesByPackageName(packageName)) { dir -> + if (dir !in scope) return@process true + + findClassInDir(dir, classNames)?.let { + result = it + return@process false + } + return@process true + } + return result + } + + private fun findClassInDir(dir: VirtualFile, classNames: List): PsiClass? { + val firstClassName = classNames.first() + val virtualFile = dir.findChild("$firstClassName.class") ?: return null + val psiFile = psiManager.findFile(virtualFile) as? PsiClassOwner ?: return null + val topLevelClass = psiFile.classes.singleOrNull() ?: return null + return classNames.subList(1, classNames.size).fold(topLevelClass) { currentPsiClass, className -> + currentPsiClass?.findInnerClassByName(className, false) + } + } + + private fun splitDotQualifiedName(qualifiedName: String): Sequence>> { + val (packageName, className) = qualifiedName.splitByLastDot() + + return generateSequence(Pair(packageName, listOf(className))) { + (prevPackageName, prevClassNames) -> + if (prevPackageName == "") return@generateSequence null + + val (newPackageName, newTopLevelClassName) = prevPackageName.splitByLastDot() + Pair(newPackageName, listOf(newTopLevelClassName) + prevClassNames) + } + } + + private fun String.splitByLastDot(): Pair { + return Pair(substringBeforeLast('.', missingDelimiterValue = ""), substringAfterLast('.')) + } } \ No newline at end of file diff --git a/idea/testData/script/definition/highlighting/javaNestedClass/lib/lib/JavaInterface.java b/idea/testData/script/definition/highlighting/javaNestedClass/lib/lib/JavaInterface.java new file mode 100644 index 00000000000..54a61054cac --- /dev/null +++ b/idea/testData/script/definition/highlighting/javaNestedClass/lib/lib/JavaInterface.java @@ -0,0 +1,13 @@ +package lib; + +public interface JavaInterface { + public interface F { + public interface I { + + } + } + + public class G { + + } +} \ No newline at end of file diff --git a/idea/testData/script/definition/highlighting/javaNestedClass/script.kts b/idea/testData/script/definition/highlighting/javaNestedClass/script.kts new file mode 100644 index 00000000000..e78b27e8e4a --- /dev/null +++ b/idea/testData/script/definition/highlighting/javaNestedClass/script.kts @@ -0,0 +1,9 @@ +import lib.* + +fun f( + g: JavaInterface.G, + f: JavaInterface.F, + i: JavaInterface.F.I +) { + +} \ No newline at end of file diff --git a/idea/testData/script/definition/highlighting/javaNestedClass/template/template.kt b/idea/testData/script/definition/highlighting/javaNestedClass/template/template.kt new file mode 100644 index 00000000000..95829012dfe --- /dev/null +++ b/idea/testData/script/definition/highlighting/javaNestedClass/template/template.kt @@ -0,0 +1,28 @@ +package custom.scriptDefinition + +import kotlin.script.dependencies.* +import kotlin.script.templates.* +import java.io.File +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Future + +class TestDependenciesResolver : ScriptDependenciesResolver { + override fun resolve( + script: ScriptContents, + environment: Map?, + report: (ScriptDependenciesResolver.ReportSeverity, String, ScriptContents.Position?) -> Unit, previousDependencies: KotlinScriptExternalDependencies? + ): Future { + return CompletableFuture.completedFuture( + object : KotlinScriptExternalDependencies { + override val classpath: Iterable = listOf( + environment?.get("lib-classes") as File + ) + + override val imports: Iterable = listOf("custom.library.*") + }) + + } +} + +@ScriptTemplateDefinition(TestDependenciesResolver::class, scriptFilePattern = "script.kts") +class Template \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java index 4a44fb98cf0..1b1585008d0 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java @@ -84,6 +84,12 @@ public class ScriptConfigurationHighlightingTestGenerated extends AbstractScript doTest(fileName); } + @TestMetadata("javaNestedClass") + public void testJavaNestedClass() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/script/definition/highlighting/javaNestedClass/"); + doTest(fileName); + } + @TestMetadata("noResolver") public void testNoResolver() throws Exception { String fileName = KotlinTestUtils.navigationMetadata("idea/testData/script/definition/highlighting/noResolver/");