IDELightClassTest: provide tools to check laziness of light class construction

StubComputationTracker knows which context was used to construct light class
This commit is contained in:
Pavel V. Talanov
2017-03-03 18:18:20 +03:00
parent daef8a0eed
commit 3e7357a5d7
7 changed files with 172 additions and 70 deletions
@@ -80,7 +80,7 @@ fun buildLightClass(
LOG.error("Unbalanced stack operations: " + pop) LOG.error("Unbalanced stack operations: " + pop)
} }
ServiceManager.getService(project, StubComputationTracker::class.java)?.onStubComputed(javaFileStub) ServiceManager.getService(project, StubComputationTracker::class.java)?.onStubComputed(javaFileStub, context)
return LightClassBuilderResult(javaFileStub, bindingContext, state.collectedExtraJvmDiagnostics) return LightClassBuilderResult(javaFileStub, bindingContext, state.collectedExtraJvmDiagnostics)
} }
catch (e: ProcessCanceledException) { catch (e: ProcessCanceledException) {
@@ -1,41 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.asJava.builder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.kotlin.descriptors.ModuleDescriptor;
import org.jetbrains.kotlin.resolve.BindingContext;
public class LightClassConstructionContext {
private final BindingContext bindingContext;
private final ModuleDescriptor module;
public LightClassConstructionContext(@NotNull BindingContext bindingContext, @NotNull ModuleDescriptor module) {
this.bindingContext = bindingContext;
this.module = module;
}
@NotNull
public BindingContext getBindingContext() {
return bindingContext;
}
@NotNull
public ModuleDescriptor getModule() {
return module;
}
}
@@ -0,0 +1,25 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.asJava.builder
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.resolve.BindingContext
open class LightClassConstructionContext(
val bindingContext: BindingContext,
val module: ModuleDescriptor
)
@@ -212,5 +212,5 @@ sealed class LightClassDataProviderForFileFacade constructor(
interface StubComputationTracker { interface StubComputationTracker {
fun onStubComputed(javaFileStub: PsiJavaFileStub) fun onStubComputed(javaFileStub: PsiJavaFileStub, context: LightClassConstructionContext)
} }
@@ -37,6 +37,8 @@ import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl
import org.jetbrains.kotlin.frontend.di.configureModule import org.jetbrains.kotlin.frontend.di.configureModule
import org.jetbrains.kotlin.idea.caches.resolve.getModuleInfo import org.jetbrains.kotlin.idea.caches.resolve.getModuleInfo
import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade import org.jetbrains.kotlin.idea.caches.resolve.getResolutionFacade
import org.jetbrains.kotlin.idea.caches.resolve.lightClasses.IDELightClassConstructionContext.Mode.EXACT
import org.jetbrains.kotlin.idea.caches.resolve.lightClasses.IDELightClassConstructionContext.Mode.LIGHT
import org.jetbrains.kotlin.idea.project.IdeaEnvironment import org.jetbrains.kotlin.idea.project.IdeaEnvironment
import org.jetbrains.kotlin.idea.project.ResolveElementCache import org.jetbrains.kotlin.idea.project.ResolveElementCache
import org.jetbrains.kotlin.incremental.components.LookupTracker import org.jetbrains.kotlin.incremental.components.LookupTracker
@@ -68,6 +70,15 @@ import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.WrappedTypeFactory import org.jetbrains.kotlin.types.WrappedTypeFactory
import org.jetbrains.kotlin.utils.sure import org.jetbrains.kotlin.utils.sure
class IDELightClassConstructionContext(bindingContext: BindingContext, module: ModuleDescriptor, val mode: Mode)
: LightClassConstructionContext(bindingContext, module) {
enum class Mode {
LIGHT,
EXACT
}
}
object IDELightClassContexts { object IDELightClassContexts {
private val LOG = Logger.getInstance(this::class.java) private val LOG = Logger.getInstance(this::class.java)
@@ -87,7 +98,7 @@ object IDELightClassContexts {
"Class descriptor was not found for ${classOrObject.getElementTextWithContext()}" "Class descriptor was not found for ${classOrObject.getElementTextWithContext()}"
} }
ForceResolveUtil.forceResolveAllContents(classDescriptor) ForceResolveUtil.forceResolveAllContents(classDescriptor)
return LightClassConstructionContext(bindingContext, resolutionFacade.moduleDescriptor) return IDELightClassConstructionContext(bindingContext, resolutionFacade.moduleDescriptor, EXACT)
} }
fun contextForLocalClassOrObject(classOrObject: KtClassOrObject): LightClassConstructionContext { fun contextForLocalClassOrObject(classOrObject: KtClassOrObject): LightClassConstructionContext {
@@ -98,12 +109,12 @@ object IDELightClassContexts {
if (descriptor == null) { if (descriptor == null) {
LOG.warn("No class descriptor in context for class: " + classOrObject.getElementTextWithContext()) LOG.warn("No class descriptor in context for class: " + classOrObject.getElementTextWithContext())
return LightClassConstructionContext(bindingContext, resolutionFacade.moduleDescriptor) return IDELightClassConstructionContext(bindingContext, resolutionFacade.moduleDescriptor, EXACT)
} }
ForceResolveUtil.forceResolveAllContents(descriptor) ForceResolveUtil.forceResolveAllContents(descriptor)
return LightClassConstructionContext(bindingContext, resolutionFacade.moduleDescriptor) return IDELightClassConstructionContext(bindingContext, resolutionFacade.moduleDescriptor, EXACT)
} }
@@ -111,8 +122,7 @@ object IDELightClassContexts {
val resolveSession = files.first().getResolutionFacade().getFrontendService(ResolveSession::class.java) val resolveSession = files.first().getResolutionFacade().getFrontendService(ResolveSession::class.java)
forceResolvePackageDeclarations(files, resolveSession) forceResolvePackageDeclarations(files, resolveSession)
return IDELightClassConstructionContext(resolveSession.bindingContext, resolveSession.moduleDescriptor, EXACT)
return LightClassConstructionContext(resolveSession.bindingContext, resolveSession.moduleDescriptor)
} }
fun lightContextForClassOrObject(classOrObject: KtClassOrObject): LightClassConstructionContext { fun lightContextForClassOrObject(classOrObject: KtClassOrObject): LightClassConstructionContext {
@@ -120,7 +130,7 @@ object IDELightClassContexts {
ForceResolveUtil.forceResolveAllContents(resolveSession.resolveToDescriptor(classOrObject)) ForceResolveUtil.forceResolveAllContents(resolveSession.resolveToDescriptor(classOrObject))
return LightClassConstructionContext(resolveSession.bindingContext, resolveSession.moduleDescriptor) return IDELightClassConstructionContext(resolveSession.bindingContext, resolveSession.moduleDescriptor, LIGHT)
} }
fun lightContextForFacade(files: List<KtFile>): LightClassConstructionContext { fun lightContextForFacade(files: List<KtFile>): LightClassConstructionContext {
@@ -129,7 +139,7 @@ object IDELightClassContexts {
forceResolvePackageDeclarations(files, resolveSession) forceResolvePackageDeclarations(files, resolveSession)
return LightClassConstructionContext(resolveSession.bindingContext, resolveSession.moduleDescriptor) return IDELightClassConstructionContext(resolveSession.bindingContext, resolveSession.moduleDescriptor, LIGHT)
} }
fun forceResolvePackageDeclarations(files: Collection<KtFile>, session: ResolveSession) { fun forceResolvePackageDeclarations(files: Collection<KtFile>, session: ResolveSession) {
@@ -20,6 +20,7 @@ import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.psi.impl.java.stubs.PsiClassStub import com.intellij.psi.impl.java.stubs.PsiClassStub
import com.intellij.psi.impl.java.stubs.PsiJavaFileStub import com.intellij.psi.impl.java.stubs.PsiJavaFileStub
import org.jetbrains.kotlin.asJava.builder.LightClassConstructionContext
import org.jetbrains.kotlin.asJava.builder.StubComputationTracker import org.jetbrains.kotlin.asJava.builder.StubComputationTracker
import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.jetbrains.kotlin.test.InTextDirectivesUtils
import org.junit.Assert import org.junit.Assert
@@ -34,13 +35,13 @@ object LightClassComputationControl {
val actualFqNames = ArrayList<String>() val actualFqNames = ArrayList<String>()
val stubComputationTracker = object : StubComputationTracker { val stubComputationTracker = object : StubComputationTracker {
override fun onStubComputed(javaFileStub: PsiJavaFileStub) { override fun onStubComputed(javaFileStub: PsiJavaFileStub, context: LightClassConstructionContext) {
val qualifiedName = (javaFileStub.childrenStubs.single() as PsiClassStub<*>).qualifiedName!! val qualifiedName = (javaFileStub.childrenStubs.single() as PsiClassStub<*>).qualifiedName!!
actualFqNames.add(qualifiedName) actualFqNames.add(qualifiedName)
} }
} }
project.withServiceRegistered<StubComputationTracker>(stubComputationTracker) { project.withServiceRegistered<StubComputationTracker, Unit>(stubComputationTracker) {
testBody() testBody()
} }
@@ -58,13 +59,13 @@ object LightClassComputationControl {
private fun List<String>.prettyToString() = if (isEmpty()) "<empty>" else joinToString() private fun List<String>.prettyToString() = if (isEmpty()) "<empty>" else joinToString()
} }
private inline fun <reified T : Any> ComponentManager.withServiceRegistered(instance: T, body: () -> Unit) { inline fun <reified T : Any, R> ComponentManager.withServiceRegistered(instance: T, body: () -> R): R {
val picoContainer = picoContainer as MutablePicoContainer val picoContainer = picoContainer as MutablePicoContainer
val key = T::class.java.name val key = T::class.java.name
try { try {
picoContainer.unregisterComponent(key) picoContainer.unregisterComponent(key)
picoContainer.registerComponentInstance(key, instance) picoContainer.registerComponentInstance(key, instance)
body() return body()
} }
finally { finally {
picoContainer.unregisterComponent(key) picoContainer.unregisterComponent(key)
@@ -21,11 +21,22 @@ import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.util.io.FileUtilRt import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.psi.JavaPsiFacade import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClass import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import com.intellij.psi.impl.java.stubs.PsiJavaFileStub
import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.PsiTreeUtil import com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.asJava.LightClassTestCommon import org.jetbrains.kotlin.asJava.LightClassTestCommon
import org.jetbrains.kotlin.asJava.builder.LightClassConstructionContext
import org.jetbrains.kotlin.asJava.builder.StubComputationTracker
import org.jetbrains.kotlin.asJava.classes.KtLightClass
import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration import org.jetbrains.kotlin.asJava.classes.KtLightClassForSourceDeclaration
import org.jetbrains.kotlin.asJava.elements.KtLightField
import org.jetbrains.kotlin.asJava.elements.KtLightMethod
import org.jetbrains.kotlin.idea.KotlinDaemonAnalyzerTestCase import org.jetbrains.kotlin.idea.KotlinDaemonAnalyzerTestCase
import org.jetbrains.kotlin.idea.caches.resolve.LightClassLazinessChecker.Tracker.Level.*
import org.jetbrains.kotlin.idea.caches.resolve.lightClasses.IDELightClassConstructionContext
import org.jetbrains.kotlin.idea.completion.test.withServiceRegistered
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
import org.jetbrains.kotlin.load.java.JvmAnnotationNames import org.jetbrains.kotlin.load.java.JvmAnnotationNames
@@ -33,13 +44,25 @@ import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.test.KotlinTestUtils import org.jetbrains.kotlin.test.KotlinTestUtils
import org.jetbrains.kotlin.test.MockLibraryUtil import org.jetbrains.kotlin.test.MockLibraryUtil
import org.jetbrains.kotlin.utils.keysToMap
import org.junit.Assert import org.junit.Assert
import java.io.File import java.io.File
import kotlin.test.assertEquals
abstract class AbstractIdeLightClassTest : KotlinLightCodeInsightFixtureTestCase() { abstract class AbstractIdeLightClassTest : KotlinLightCodeInsightFixtureTestCase() {
fun doTest(testDataPath: String) { fun doTest(testDataPath: String) {
myFixture.configureByFile(testDataPath) myFixture.configureByFile(testDataPath)
testLightClass(project, myFixture.file as KtFile, testDataPath, { LightClassTestCommon.removeEmptyDefaultImpls(it) }) val ktFile = myFixture.file as KtFile
testLightClass(testDataPath, { LightClassTestCommon.removeEmptyDefaultImpls(it) }) { fqName ->
val tracker = LightClassLazinessChecker.Tracker(fqName)
project.withServiceRegistered<StubComputationTracker, PsiClass?>(tracker) {
findClass(fqName, ktFile, project)?.apply {
LightClassLazinessChecker.check(this as KtLightClass, tracker)
tracker.allowLevel(EXACT)
PsiElementChecker.checkPsiElementStructure(this)
}
}
}
} }
override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE override fun getProjectDescriptor() = KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
@@ -64,26 +87,18 @@ abstract class AbstractIdeCompiledLightClassTest : KotlinDaemonAnalyzerTestCase(
private fun libName() = "libFor" + getTestName(false) private fun libName() = "libFor" + getTestName(false)
fun doTest(testDataPath: String) { fun doTest(testDataPath: String) {
testLightClass(project, null, testDataPath, { it }) testLightClass(testDataPath, { it }) {
findClass(it, null, project)?.apply {
PsiElementChecker.checkPsiElementStructure(this)
}
}
} }
} }
private fun testLightClass(project: Project, ktFile: KtFile?, testDataPath: String, normalize: (String) -> String) { private fun testLightClass(testDataPath: String, normalize: (String) -> String, findLightClass: (String) -> PsiClass?) {
LightClassTestCommon.testLightClass( LightClassTestCommon.testLightClass(
File(testDataPath), File(testDataPath),
findLightClass = { fqName -> findLightClass,
var clazz: PsiClass? = JavaPsiFacade.getInstance(project).findClass(fqName, GlobalSearchScope.allScope(project))
if (clazz == null) {
clazz = PsiTreeUtil.findChildrenOfType(ktFile, KtClassOrObject::class.java)
.find { fqName.endsWith(it.nameAsName!!.asString()) }
?.let { KtLightClassForSourceDeclaration.create(it) }
}
if (clazz != null) {
PsiElementChecker.checkPsiElementStructure(clazz)
}
clazz
},
normalizeText = { text -> normalizeText = { text ->
//NOTE: ide and compiler differ in names generated for parameters with unspecified names //NOTE: ide and compiler differ in names generated for parameters with unspecified names
text text
@@ -97,6 +112,98 @@ private fun testLightClass(project: Project, ktFile: KtFile?, testDataPath: Stri
) )
} }
private fun findClass(fqName: String, ktFile: KtFile?, project: Project): PsiClass? {
return JavaPsiFacade.getInstance(project).findClass(fqName, GlobalSearchScope.allScope(project)) ?:
PsiTreeUtil.findChildrenOfType(ktFile, KtClassOrObject::class.java)
.find { fqName.endsWith(it.nameAsName!!.asString()) }
?.let { KtLightClassForSourceDeclaration.create(it) }
}
object LightClassLazinessChecker {
class Tracker(private val fqName: String) : StubComputationTracker {
private var level = NONE
set(newLevel) {
if (newLevel.ordinal <= field.ordinal) {
error("Level should not decrease at any point: $level -> $newLevel, allowed: $allowedLevel")
}
if (newLevel.ordinal > allowedLevel.ordinal) {
error("Level increased before it was expected: $level -> $newLevel, allowed: $allowedLevel")
}
field = newLevel
}
private var allowedLevel = NONE
enum class Level {
NONE,
LIGHT,
EXACT
}
override fun onStubComputed(javaFileStub: PsiJavaFileStub, context: LightClassConstructionContext) {
if (fqName != javaFileStub.classes.single().qualifiedName!!) return
if (context !is IDELightClassConstructionContext) error("Unknown context ${context::class}")
level = when (context.mode) {
IDELightClassConstructionContext.Mode.LIGHT -> LIGHT
IDELightClassConstructionContext.Mode.EXACT -> EXACT
}
}
fun checkLevel(expectedLevel: Level) {
assert(level == expectedLevel)
}
fun allowLevel(newAllowed: Level) {
allowedLevel = newAllowed
}
}
fun check(lightClass: KtLightClass, tracker: Tracker) {
// lighter classes not implemented for locals
if (lightClass.kotlinOrigin?.isLocal ?: false) return
tracker.allowLevel(LIGHT)
// collect method class results on light members that should not trigger exact context evaluation
val fieldsToInfo = lightClass.fields.asList().keysToMap { fieldInfo(it) }
val methodsToInfo = lightClass.methods.asList().keysToMap { methodInfo(it) }
tracker.allowLevel(EXACT)
lightClass.clsDelegate // trigger exact context
tracker.checkLevel(EXACT)
// check collected data against delegates which should contain correct data
for ((field, lightFieldInfo) in fieldsToInfo) {
val delegate = (field as KtLightField).clsDelegate
assertEquals(fieldInfo(delegate), lightFieldInfo)
}
for ((method, lightMethodInfo) in methodsToInfo) {
val delegate = (method as KtLightMethod).clsDelegate
assertEquals(methodInfo(delegate), lightMethodInfo)
}
}
private data class FieldInfo(
val name: String
)
private fun fieldInfo(field: PsiField) = with(field) {
FieldInfo(name!!)
}
private data class MethodInfo(
val name: String
)
private fun methodInfo(method: PsiMethod) = with(method) {
MethodInfo(name)
}
}
private fun String.removeLinesStartingWith(prefix: String): String { private fun String.removeLinesStartingWith(prefix: String): String {
return lines().filterNot { it.trimStart().startsWith(prefix) }.joinToString(separator = "\n") return lines().filterNot { it.trimStart().startsWith(prefix) }.joinToString(separator = "\n")
} }