Incomplete analysis results are available for light class generation

If we have the following classes

 class A // Kotlin
 class B extends A {} // Java
 class C : B() // Kotlin

 The analysis runs into infinite recursion, because C needs all members of B (to compute overrides),
 and B needs all members of A, and A is not available from KotlinCacheManager.getDeclarationsFromProject() -- it is being computed right now,
 so the analysis runs again...

 Our workaround is:
  * when analysis starts put its trace into user data on the project
  * when a light class is about to generate a stub, it requests a trace, and if there is such an incomplete trace, it is returned
  * the logic of TopDownAnalyzer that works in descending order (down the class hierarchy) guarantees that all the superclasses are already processed
This commit is contained in:
Andrey Breslav
2013-03-27 19:46:12 +04:00
parent 83e2986a7b
commit 1ad0cad020
7 changed files with 88 additions and 15 deletions
@@ -54,8 +54,9 @@ public class IDELightClassGenerationSupport extends LightClassGenerationSupport
@NotNull
@Override
public LightClassConstructionContext analyzeRelevantCode(@NotNull Collection<JetFile> files) {
KotlinDeclarationsCache cache = KotlinCacheManager.getInstance(project).getDeclarationsFromProject();
return new LightClassConstructionContext(cache.getBindingContext(), null);
KotlinCacheManager cacheManager = KotlinCacheManager.getInstance(project);
KotlinDeclarationsCache declarationsCache = cacheManager.getPossiblyIncompleteDeclarationsForLightClassGeneration();
return new LightClassConstructionContext(declarationsCache.getBindingContext(), null);
}
@NotNull
@@ -31,6 +31,9 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jet.analyzer.AnalyzeExhaust;
import org.jetbrains.jet.lang.resolve.AnalyzerScriptParameter;
import org.jetbrains.jet.lang.resolve.BindingContext;
import org.jetbrains.jet.lang.resolve.BindingTrace;
import org.jetbrains.jet.lang.resolve.BindingTraceContext;
import org.jetbrains.jet.lang.resolve.java.AnalyzerFacadeForJVM;
import org.jetbrains.jet.lang.resolve.java.JetFilesProvider;
@@ -42,21 +45,34 @@ public class KotlinCacheManager {
return ServiceManager.getService(project, KotlinCacheManager.class);
}
private final Key<CachedValue<KotlinDeclarationsCache>> KOTLIN_DECLARATIONS_CACHE = Key.create("KOTLIN_DECLARATIONS_CACHE");
private static final Key<CachedValue<KotlinDeclarationsCache>> KOTLIN_DECLARATIONS_CACHE = Key.create("KOTLIN_DECLARATIONS_CACHE");
private final Project project;
private final Object declarationAnalysisLock = new Object();
private BindingTrace incompleteTrace;
public KotlinCacheManager(@NotNull Project project) {
this.project = project;
}
@NotNull
public KotlinDeclarationsCache getDeclarationsFromProject() {
private KotlinDeclarationsCache getDeclarations(boolean allowIncomplete) {
// To prevent dead locks, the lock below must be obtained only inside a read action
ApplicationManager.getApplication().assertReadAccessAllowed();
synchronized (declarationAnalysisLock) {
if (allowIncomplete) {
if (incompleteTrace != null) {
return new KotlinDeclarationsCache() {
@NotNull
@Override
public BindingContext getBindingContext() {
return incompleteTrace.getBindingContext();
}
};
}
}
return CachedValuesManager.getManager(project).getCachedValue(
project,
KOTLIN_DECLARATIONS_CACHE,
@@ -66,21 +82,60 @@ public class KotlinCacheManager {
}
}
@NotNull
public KotlinDeclarationsCache getDeclarationsFromProject() {
return getDeclarations(false);
}
@NotNull
public KotlinDeclarationsCache getPossiblyIncompleteDeclarationsForLightClassGeneration() {
/*
* If we have the following classes
*
* class A // Kotlin
* class B extends A {} // Java
* class C : B() // Kotlin
*
* The analysis runs into infinite recursion, because
* C needs all members of B (to compute overrides),
* and B needs all members of A,
* and A is not available from KotlinCacheManager.getDeclarationsFromProject() -- it is being computed right now,
* so the analysis runs again...
*
* Our workaround is to return partially complete results when we generate light classes
*/
return getDeclarations(true);
}
private class KotlinDeclarationsCacheProvider implements CachedValueProvider<KotlinDeclarationsCache> {
@Nullable
@Override
public Result<KotlinDeclarationsCache> compute() {
AnalyzeExhaust analyzeExhaust = AnalyzerFacadeForJVM.INSTANCE.analyzeFiles(
project,
JetFilesProvider.getInstance(project).allInScope(GlobalSearchScope.allScope(project)),
Collections.<AnalyzerScriptParameter>emptyList(),
Predicates.<PsiFile>alwaysFalse()
);
return Result.<KotlinDeclarationsCache>create(
new KotlinDeclarationsCacheImpl(analyzeExhaust),
PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT
);
}
// This lock is already acquired by the calling method,
// but we put it here to guard for the case of further modifications
synchronized (declarationAnalysisLock) {
BindingTraceContext trace = new BindingTraceContext();
incompleteTrace = trace;
AnalyzeExhaust analyzeExhaust;
try {
analyzeExhaust = AnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
project,
JetFilesProvider.getInstance(project).allInScope(GlobalSearchScope.allScope(project)),
trace,
Collections.<AnalyzerScriptParameter>emptyList(),
Predicates.<PsiFile>alwaysFalse(),
true);
}
finally {
incompleteTrace = null;
}
return Result.<KotlinDeclarationsCache>create(
new KotlinDeclarationsCacheImpl(analyzeExhaust),
PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT
);
}
}
}
}
@@ -0,0 +1 @@
public open class A<caret>
@@ -0,0 +1,3 @@
public class MyClass<caret> extends A {
}
@@ -0,0 +1 @@
public class B: MyClass()
@@ -0,0 +1,7 @@
<node text="Object (java.lang)">
<node text="A ()" base="true">
<node text="MyClass ()">
<node text="B ()"/>
</node>
</node>
</node>
@@ -133,6 +133,11 @@ public class HierarchyTestGenerated extends AbstractHierarchyTest {
doTypeClassHierarchyTest("idea/testData/hierarchy/class/type/JavaFromKotlinForKotlinClass");
}
@TestMetadata("KJKHierarchy")
public void testKJKHierarchy() throws Exception {
doTypeClassHierarchyTest("idea/testData/hierarchy/class/type/KJKHierarchy");
}
@TestMetadata("KotlinFromJava")
public void testKotlinFromJava() throws Exception {
doTypeClassHierarchyTest("idea/testData/hierarchy/class/type/KotlinFromJava");