make bytecode analysis lazy (IDEA-133768)
authorpeter <peter@jetbrains.com>
Wed, 9 Nov 2016 07:34:12 +0000 (08:34 +0100)
committerpeter <peter@jetbrains.com>
Wed, 9 Nov 2016 07:43:19 +0000 (08:43 +0100)
java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisIndex.java
java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java
java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/KeyedMethodVisitor.java [new file with mode: 0644]
java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java

index 82d1402bb6d31c18979be9fbf8eed16368b533b4..8991ed837e4c18f5d5ee2108520c65a3a31d3bc1 100644 (file)
 package com.intellij.codeInspection.bytecodeAnalysis;
 
 import com.intellij.ide.highlighter.JavaClassFileType;
-import com.intellij.openapi.vfs.VirtualFile;
-import com.intellij.util.SystemProperties;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.util.Pair;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.gist.GistManager;
+import com.intellij.util.gist.VirtualFileGist;
 import com.intellij.util.indexing.*;
 import com.intellij.util.io.DataExternalizer;
 import com.intellij.util.io.DataInputOutputUtil;
 import com.intellij.util.io.DifferentSerializableBytesImplyNonEqualityPolicy;
 import com.intellij.util.io.KeyDescriptor;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.org.objectweb.asm.ClassReader;
+import org.jetbrains.org.objectweb.asm.MethodVisitor;
+import org.jetbrains.org.objectweb.asm.tree.MethodNode;
 
 import java.io.DataInput;
 import java.io.DataOutput;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis.LOG;
 
 /**
  * @author lambdamix
  */
-public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquations> {
-  public static final ID<Bytes, HEquations> NAME = ID.create("bytecodeAnalysis");
-  private final HEquationsExternalizer myExternalizer = new HEquationsExternalizer();
-  private static final ClassDataIndexer INDEXER = new ClassDataIndexer();
+public class BytecodeAnalysisIndex extends ScalarIndexExtension<Bytes> {
+  private static final ID<Bytes, Void> NAME = ID.create("bytecodeAnalysis");
   private static final HKeyDescriptor KEY_DESCRIPTOR = new HKeyDescriptor();
-
-  private static final int ourInternalVersion = 9;
-  private static final boolean ourEnabled = SystemProperties.getBooleanProperty("idea.enable.bytecode.contract.inference", true);
+  private static final VirtualFileGist<Map<Bytes, HEquations>> ourGist = GistManager.getInstance().newVirtualFileGist(
+    "BytecodeAnalysisIndex", 0, new HEquationsExternalizer(), new ClassDataIndexer());
 
   @NotNull
   @Override
-  public ID<Bytes, HEquations> getName() {
+  public ID<Bytes, Void> getName() {
     return NAME;
   }
 
   @NotNull
   @Override
-  public DataIndexer<Bytes, HEquations, FileContent> getIndexer() {
-    return INDEXER;
+  public DataIndexer<Bytes, Void, FileContent> getIndexer() {
+    return inputData -> {
+      try {
+        return collectKeys(inputData.getContent());
+      }
+      catch (ProcessCanceledException e) {
+        throw e;
+      }
+      catch (Throwable e) {
+        // incorrect bytecode may result in Runtime exceptions during analysis
+        // so here we suppose that exception is due to incorrect bytecode
+        LOG.debug("Unexpected Error during indexing of bytecode", e);
+        return Collections.emptyMap();
+      }
+    };
   }
 
   @NotNull
-  @Override
-  public KeyDescriptor<Bytes> getKeyDescriptor() {
-    return KEY_DESCRIPTOR;
+  private static Map<Bytes, Void> collectKeys(byte[] content) throws NoSuchAlgorithmException {
+    HashMap<Bytes, Void> map = new HashMap<>();
+    MessageDigest md = BytecodeAnalysisConverter.getMessageDigest();
+    new ClassReader(content).accept(new KeyedMethodVisitor() {
+      @Nullable
+      @Override
+      MethodVisitor visitMethod(MethodNode node, Key key) {
+        map.put(ClassDataIndexer.compressKey(md, key), null);
+        return null;
+      }
+    }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+    return map;
   }
 
   @NotNull
   @Override
-  public DataExternalizer<HEquations> getValueExternalizer() {
-    return myExternalizer;
+  public KeyDescriptor<Bytes> getKeyDescriptor() {
+    return KEY_DESCRIPTOR;
   }
 
   @Override
@@ -77,12 +108,7 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
   @NotNull
   @Override
   public FileBasedIndex.InputFilter getInputFilter() {
-    return new DefaultFileTypeSpecificInputFilter(JavaClassFileType.INSTANCE) {
-      @Override
-      public boolean acceptInput(@NotNull VirtualFile file) {
-        return ourEnabled && super.acceptInput(file);
-      }
-    };
+    return new DefaultFileTypeSpecificInputFilter(JavaClassFileType.INSTANCE);
   }
 
   @Override
@@ -92,7 +118,14 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
 
   @Override
   public int getVersion() {
-    return ourInternalVersion + (ourEnabled ? 0xFF : 0);
+    return 10;
+  }
+
+  @NotNull
+  static List<HEquations> getEquations(GlobalSearchScope scope, Bytes key) {
+    Project project = ProjectManager.getInstance().getDefaultProject(); // the data is project-independent
+    return ContainerUtil.mapNotNull(FileBasedIndex.getInstance().getContainingFiles(NAME, key, scope),
+                                    file -> ourGist.getFileData(project, file).get(key));
   }
 
   /**
@@ -126,9 +159,22 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
   /**
    * Externalizer for compressed equations.
    */
-  public static class HEquationsExternalizer implements DataExternalizer<HEquations> {
+  public static class HEquationsExternalizer implements DataExternalizer<Map<Bytes, HEquations>> {
     @Override
-    public void save(@NotNull DataOutput out, HEquations eqs) throws IOException {
+    public void save(@NotNull DataOutput out, Map<Bytes, HEquations> value) throws IOException {
+      DataInputOutputUtil.writeSeq(out, value.entrySet(), entry -> {
+        KEY_DESCRIPTOR.save(out, entry.getKey());
+        saveEquations(out, entry.getValue());
+      });
+    }
+
+    @Override
+    public Map<Bytes, HEquations> read(@NotNull DataInput in) throws IOException {
+      return DataInputOutputUtil.readSeq(in, () -> Pair.create(KEY_DESCRIPTOR.read(in), readEquations(in))).
+        stream().collect(Collectors.toMap(p -> p.getFirst(), p -> p.getSecond()));
+    }
+
+    private static void saveEquations(@NotNull DataOutput out, HEquations eqs) throws IOException {
       out.writeBoolean(eqs.stable);
       DataInputOutputUtil.writeINT(out, eqs.results.size());
       for (DirectionResultPair pair : eqs.results) {
@@ -203,8 +249,7 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
       }
     }
 
-    @Override
-    public HEquations read(@NotNull DataInput in) throws IOException {
+    private static HEquations readEquations(@NotNull DataInput in) throws IOException {
       boolean stable = in.readBoolean();
       int size = DataInputOutputUtil.readINT(in);
       ArrayList<DirectionResultPair> results = new ArrayList<>(size);
index c8bc75671412e19794999d01e71b73c77801354c..dc6f90404f269862b57f0efb7ca718d10fd5f034 100644 (file)
@@ -18,16 +18,22 @@ package com.intellij.codeInspection.bytecodeAnalysis;
 import com.intellij.codeInspection.bytecodeAnalysis.asm.*;
 import com.intellij.openapi.progress.ProcessCanceledException;
 import com.intellij.openapi.progress.ProgressManager;
-import com.intellij.openapi.util.Pair;
-import com.intellij.util.indexing.DataIndexer;
-import com.intellij.util.indexing.FileContent;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.ContainerUtil;
+import com.intellij.util.gist.VirtualFileGist;
+import gnu.trove.THashMap;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.jetbrains.org.objectweb.asm.*;
 import org.jetbrains.org.objectweb.asm.tree.MethodNode;
 import org.jetbrains.org.objectweb.asm.tree.analysis.AnalyzerException;
 
 import java.security.MessageDigest;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import static com.intellij.codeInspection.bytecodeAnalysis.Direction.*;
 import static com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalysis.LOG;
@@ -40,33 +46,23 @@ import static com.intellij.codeInspection.bytecodeAnalysis.ProjectBytecodeAnalys
  *
  * @author lambdamix
  */
-public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileContent> {
+public class ClassDataIndexer implements VirtualFileGist.GistCalculator<Map<Bytes, HEquations>> {
 
-  private static final int STABLE_FLAGS = Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
   public static final Final FINAL_TOP = new Final(Value.Top);
   public static final Final FINAL_BOT = new Final(Value.Bot);
   public static final Final FINAL_NOT_NULL = new Final(Value.NotNull);
   public static final Final FINAL_NULL = new Final(Value.Null);
 
-  @NotNull
+  @Nullable
   @Override
-  public Map<Bytes, HEquations> map(@NotNull FileContent inputData) {
+  public Map<Bytes, HEquations> calcData(@NotNull Project project, @NotNull VirtualFile file) {
     HashMap<Bytes, HEquations> map = new HashMap<>();
     try {
       MessageDigest md = BytecodeAnalysisConverter.getMessageDigest();
-      Map<Key, List<Equation>> allEquations = processClass(new ClassReader(inputData.getContent()), inputData.getFile().getPresentableUrl());
+      Map<Key, List<Equation>> allEquations = processClass(new ClassReader(file.contentsToByteArray()), file.getPresentableUrl());
       for (Map.Entry<Key, List<Equation>> entry: allEquations.entrySet()) {
-
         Key methodKey = entry.getKey();
-        // method equations with raw (not-compressed keys)
-        List<Equation> rawMethodEquations = entry.getValue();
-        //
-        List<DirectionResultPair> compressedMethodEquations =
-          new ArrayList<>(rawMethodEquations.size());
-        for (Equation equation : rawMethodEquations) {
-          compressedMethodEquations.add(BytecodeAnalysisConverter.convert(equation, md));
-        }
-        map.put(new Bytes(BytecodeAnalysisConverter.asmKey(methodKey, md).key), new HEquations(compressedMethodEquations, methodKey.stable));
+        map.put(compressKey(md, methodKey), convertEquations(md, methodKey, entry.getValue()));
       }
     }
     catch (ProcessCanceledException e) {
@@ -80,6 +76,18 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
     return map;
   }
 
+  @NotNull
+  static Bytes compressKey(MessageDigest md, Key methodKey) {
+    return new Bytes(BytecodeAnalysisConverter.asmKey(methodKey, md).key);
+  }
+
+  @NotNull
+  private static HEquations convertEquations(MessageDigest md, Key methodKey, List<Equation> rawMethodEquations) {
+    List<DirectionResultPair> compressedMethodEquations =
+      ContainerUtil.map(rawMethodEquations, equation -> BytecodeAnalysisConverter.convert(equation, md));
+    return new HEquations(compressedMethodEquations, methodKey.stable);
+  }
+
   public static Map<Key, List<Equation>> processClass(final ClassReader classReader, final String presentableUrl) {
 
     // It is OK to share pending states, actions and results for analyses.
@@ -91,20 +99,9 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
     final PResults.PResult[] sharedResults = new PResults.PResult[Analysis.STEPS_LIMIT];
     final Map<Key, List<Equation>> equations = new HashMap<>();
 
-    classReader.accept(new ClassVisitor(Opcodes.API_VERSION) {
-      private String className;
-      private boolean stableClass;
-
-      @Override
-      public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
-        className = name;
-        stableClass = (access & Opcodes.ACC_FINAL) != 0;
-        super.visit(version, access, name, signature, superName, interfaces);
-      }
+    classReader.accept(new KeyedMethodVisitor() {
 
-      @Override
-      public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
-        final MethodNode node = new MethodNode(Opcodes.API_VERSION, access, name, desc, signature, exceptions);
+      protected MethodVisitor visitMethod(final MethodNode node, final Key key) {
         return new MethodVisitor(Opcodes.API_VERSION, node) {
           private boolean jsr;
 
@@ -119,8 +116,7 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
           @Override
           public void visitEnd() {
             super.visitEnd();
-            Pair<Key, List<Equation>> methodEquations = processMethod(node, jsr);
-            equations.put(methodEquations.first, methodEquations.second);
+            equations.put(key, processMethod(node, jsr, key.method, key.stable));
           }
         };
       }
@@ -130,9 +126,8 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
        *
        * @param methodNode asm node for method
        * @param jsr whether a method has jsr instruction
-       * @return pair of (primaryKey, equations)
        */
-      private Pair<Key, List<Equation>> processMethod(final MethodNode methodNode, boolean jsr) {
+      private List<Equation> processMethod(final MethodNode methodNode, boolean jsr, Method method, boolean stable) {
         ProgressManager.checkCanceled();
         final Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc);
         final Type resultType = Type.getReturnType(methodNode.desc);
@@ -140,11 +135,6 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
         final boolean isBooleanResult = ASMUtils.isBooleanType(resultType);
         final boolean isInterestingResult = isReferenceResult || isBooleanResult;
 
-        final Method method = new Method(className, methodNode.name, methodNode.desc);
-        final boolean stable = stableClass || (methodNode.access & STABLE_FLAGS) != 0 || "<init>".equals(methodNode.name);
-
-        Key primaryKey = new Key(method, Out, stable);
-
         // 4*n: for each reference parameter: @NotNull IN, @Nullable, null -> ... contract, !null -> contract
         // 3: @NotNull OUT, @Nullable OUT, purity analysis
         List<Equation> equations = new ArrayList<>(argumentTypes.length * 4 + 3);
@@ -152,7 +142,7 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
 
         if (argumentTypes.length == 0 && !isInterestingResult) {
           // no need to continue analysis
-          return Pair.create(primaryKey, equations);
+          return equations;
         }
 
         try {
@@ -173,17 +163,17 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
               if (richControlFlow.reducible()) {
                 NegationAnalysis negated = tryNegation(method, argumentTypes, graph, isBooleanResult, dfs, jsr);
                 processBranchingMethod(method, methodNode, richControlFlow, argumentTypes, isReferenceResult, isBooleanResult, stable, jsr, equations, negated);
-                return Pair.create(primaryKey, equations);
+                return equations;
               }
               LOG.debug(method + ": CFG is not reducible");
             }
             // simple
             else {
               processNonBranchingMethod(method, argumentTypes, graph, isReferenceResult, isBooleanResult, stable, equations);
-              return Pair.create(primaryKey, equations);
+              return equations;
             }
           }
-          return Pair.create(primaryKey, topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable));
+          return topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable);
         }
         catch (ProcessCanceledException e) {
           throw e;
@@ -192,7 +182,7 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
           // incorrect bytecode may result in Runtime exceptions during analysis
           // so here we suppose that exception is due to incorrect bytecode
           LOG.debug("Unexpected Error during processing of " + method + " in " + presentableUrl, e);
-          return Pair.create(primaryKey, topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable));
+          return topEquations(method, argumentTypes, isReferenceResult, isInterestingResult, stable);
         }
       }
 
diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/KeyedMethodVisitor.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/KeyedMethodVisitor.java
new file mode 100644 (file)
index 0000000..d90d0eb
--- /dev/null
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2000-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 com.intellij.codeInspection.bytecodeAnalysis;
+
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.org.objectweb.asm.ClassVisitor;
+import org.jetbrains.org.objectweb.asm.MethodVisitor;
+import org.jetbrains.org.objectweb.asm.Opcodes;
+import org.jetbrains.org.objectweb.asm.tree.MethodNode;
+
+import static com.intellij.codeInspection.bytecodeAnalysis.Direction.Out;
+
+/**
+ * @author peter
+ */
+abstract class KeyedMethodVisitor extends ClassVisitor {
+  private static final int STABLE_FLAGS = Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC;
+
+  KeyedMethodVisitor() {
+    super(Opcodes.API_VERSION);
+  }
+
+  String className;
+  private boolean stableClass;
+
+  @Override
+  public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
+    className = name;
+    stableClass = (access & Opcodes.ACC_FINAL) != 0;
+    super.visit(version, access, name, signature, superName, interfaces);
+  }
+
+  @Override
+  public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
+    MethodNode node = new MethodNode(Opcodes.API_VERSION, access, name, desc, signature, exceptions);
+    Method method = new Method(className, node.name, node.desc);
+    boolean stable = stableClass || (node.access & STABLE_FLAGS) != 0 || "<init>".equals(node.name);
+
+    return visitMethod(node, new Key(method, Out, stable));
+  }
+
+  @Nullable
+  abstract MethodVisitor visitMethod(final MethodNode node, final Key key);
+}
index c0f23760f896f64f1c54d476930fa09678f46525..d7353d9e2896b2640378a93151ce80d7053deb4f 100644 (file)
@@ -26,16 +26,16 @@ import com.intellij.openapi.util.ModificationTracker;
 import com.intellij.openapi.util.registry.Registry;
 import com.intellij.openapi.vfs.VirtualFile;
 import com.intellij.psi.*;
-import com.intellij.psi.search.GlobalSearchScope;
 import com.intellij.psi.search.ProjectScope;
 import com.intellij.psi.util.CachedValueProvider;
 import com.intellij.psi.util.CachedValuesManager;
 import com.intellij.psi.util.PsiFormatUtil;
+import com.intellij.psi.util.PsiModificationTracker;
 import com.intellij.testFramework.LightVirtualFile;
 import com.intellij.util.IncorrectOperationException;
 import com.intellij.util.containers.ConcurrentFactoryMap;
+import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.containers.Stack;
-import com.intellij.util.indexing.FileBasedIndex;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -57,6 +57,7 @@ public class ProjectBytecodeAnalysis {
   private final Project myProject;
   private final boolean nullableMethod;
   private final boolean nullableMethodTransitivity;
+  private final Map<Bytes, List<HEquations>> myEquationCache = ContainerUtil.createConcurrentSoftValueMap();
 
   public static ProjectBytecodeAnalysis getInstance(@NotNull Project project) {
     return ServiceManager.getService(project, ProjectBytecodeAnalysis.class);
@@ -66,6 +67,7 @@ public class ProjectBytecodeAnalysis {
     myProject = project;
     nullableMethod = Registry.is(NULLABLE_METHOD);
     nullableMethodTransitivity = Registry.is(NULLABLE_METHOD_TRANSITIVITY);
+    myProject.getMessageBus().connect().subscribe(PsiModificationTracker.TOPIC, () -> myEquationCache.clear());
   }
 
   @Nullable
@@ -270,10 +272,8 @@ public class ProjectBytecodeAnalysis {
   private ParameterAnnotations loadParameterAnnotations(@NotNull HKey notNullKey)
     throws EquationsLimitException {
 
-    Map<Bytes, List<HEquations>> equationsCache = new HashMap<>();
-
     final Solver notNullSolver = new Solver(new ELattice<>(Value.NotNull, Value.Top), Value.Top);
-    collectEquations(Collections.singletonList(notNullKey), notNullSolver, equationsCache);
+    collectEquations(Collections.singletonList(notNullKey), notNullSolver);
 
     Map<HKey, Value> notNullSolutions = notNullSolver.solve();
     // subtle point
@@ -282,7 +282,7 @@ public class ProjectBytecodeAnalysis {
 
     final Solver nullableSolver = new Solver(new ELattice<>(Value.Null, Value.Top), Value.Top);
     final HKey nullableKey = new HKey(notNullKey.key, notNullKey.dirKey + 1, true, false);
-    collectEquations(Collections.singletonList(nullableKey), nullableSolver, equationsCache);
+    collectEquations(Collections.singletonList(nullableKey), nullableSolver);
     Map<HKey, Value> nullableSolutions = nullableSolver.solve();
     // subtle point
     boolean nullable =
@@ -293,12 +293,11 @@ public class ProjectBytecodeAnalysis {
   private MethodAnnotations loadMethodAnnotations(@NotNull PsiMethod owner, @NotNull HKey key, ArrayList<HKey> allKeys)
     throws EquationsLimitException {
     MethodAnnotations result = new MethodAnnotations();
-    Map<Bytes, List<HEquations>> equationsCache = new HashMap<>();
 
     final Solver outSolver = new Solver(new ELattice<>(Value.Bot, Value.Top), Value.Top);
     final PuritySolver puritySolver = new PuritySolver();
-    collectEquations(allKeys, outSolver, equationsCache);
-    collectPurityEquations(key.updateDirection(BytecodeAnalysisConverter.mkDirectionKey(Pure)), puritySolver, equationsCache);
+    collectEquations(allKeys, outSolver);
+    collectPurityEquations(key.updateDirection(BytecodeAnalysisConverter.mkDirectionKey(Pure)), puritySolver);
 
     Map<HKey, Value> solutions = outSolver.solve();
     Map<HKey, Set<HEffectQuantum>> puritySolutions = puritySolver.solve();
@@ -312,10 +311,10 @@ public class ProjectBytecodeAnalysis {
       final Solver nullableMethodSolver = new Solver(new ELattice<>(Value.Bot, Value.Null), Value.Bot);
       HKey nullableKey = key.updateDirection(BytecodeAnalysisConverter.mkDirectionKey(NullableOut));
       if (nullableMethodTransitivity) {
-        collectEquations(Collections.singletonList(nullableKey), nullableMethodSolver, equationsCache);
+        collectEquations(Collections.singletonList(nullableKey), nullableMethodSolver);
       }
       else {
-        collectSingleEquation(nullableKey, nullableMethodSolver, equationsCache);
+        collectSingleEquation(nullableKey, nullableMethodSolver);
       }
       Map<HKey, Value> nullableSolutions = nullableMethodSolver.solve();
       if (nullableSolutions.get(nullableKey) == Value.Null || nullableSolutions.get(nullableKey.invertStability()) == Value.Null) {
@@ -325,17 +324,22 @@ public class ProjectBytecodeAnalysis {
     return result;
   }
 
-  private void collectPurityEquations(HKey key, PuritySolver puritySolver, Map<Bytes, List<HEquations>> cache)
+  private List<HEquations> getEquations(Bytes key) {
+    List<HEquations> result = myEquationCache.get(key);
+    if (result == null) {
+      myEquationCache.put(key, result = BytecodeAnalysisIndex.getEquations(ProjectScope.getLibrariesScope(myProject), key));
+    }
+    return result;
+  }
+
+  private void collectPurityEquations(HKey key, PuritySolver puritySolver)
     throws EquationsLimitException {
-    GlobalSearchScope librariesScope = ProjectScope.getLibrariesScope(myProject);
     HashSet<HKey> queued = new HashSet<>();
     Stack<HKey> queue = new Stack<>();
 
     queue.push(key);
     queued.add(key);
 
-    FileBasedIndex index = FileBasedIndex.getInstance();
-
     while (!queue.empty()) {
       if (queued.size() > EQUATIONS_LIMIT) {
         throw new EquationsLimitException();
@@ -344,13 +348,7 @@ public class ProjectBytecodeAnalysis {
       HKey hKey = queue.pop();
       Bytes bytes = new Bytes(hKey.key);
 
-      List<HEquations> hEquationss = cache.get(bytes);
-      if (hEquationss == null) {
-        hEquationss = index.getValues(BytecodeAnalysisIndex.NAME, bytes, librariesScope);
-        cache.put(bytes, hEquationss);
-      }
-
-      for (HEquations hEquations : hEquationss) {
+      for (HEquations hEquations : getEquations(bytes)) {
         boolean stable = hEquations.stable;
         for (DirectionResultPair pair : hEquations.results) {
           int dirKey = pair.directionKey;
@@ -372,9 +370,7 @@ public class ProjectBytecodeAnalysis {
     }
   }
 
-  private void collectEquations(List<HKey> keys, Solver solver, @NotNull Map<Bytes, List<HEquations>> cache) throws EquationsLimitException {
-
-    GlobalSearchScope librariesScope = ProjectScope.getLibrariesScope(myProject);
+  private void collectEquations(List<HKey> keys, Solver solver) throws EquationsLimitException {
     HashSet<HKey> queued = new HashSet<>();
     Stack<HKey> queue = new Stack<>();
 
@@ -383,8 +379,6 @@ public class ProjectBytecodeAnalysis {
       queued.add(key);
     }
 
-    FileBasedIndex index = FileBasedIndex.getInstance();
-
     while (!queue.empty()) {
       if (queued.size() > EQUATIONS_LIMIT) {
         throw new EquationsLimitException();
@@ -393,13 +387,7 @@ public class ProjectBytecodeAnalysis {
       HKey hKey = queue.pop();
       Bytes bytes = new Bytes(hKey.key);
 
-      List<HEquations> hEquationss = cache.get(bytes);
-      if (hEquationss == null) {
-        hEquationss = index.getValues(BytecodeAnalysisIndex.NAME, bytes, librariesScope);
-        cache.put(bytes, hEquationss);
-      }
-
-      for (HEquations hEquations : hEquationss) {
+      for (HEquations hEquations : getEquations(bytes)) {
         boolean stable = hEquations.stable;
         for (DirectionResultPair pair : hEquations.results) {
           int dirKey = pair.directionKey;
@@ -424,21 +412,11 @@ public class ProjectBytecodeAnalysis {
     }
   }
 
-  private void collectSingleEquation(HKey hKey, Solver solver, @NotNull Map<Bytes, List<HEquations>> cache) throws EquationsLimitException {
-    GlobalSearchScope librariesScope = ProjectScope.getLibrariesScope(myProject);
-
-    FileBasedIndex index = FileBasedIndex.getInstance();
-
+  private void collectSingleEquation(HKey hKey, Solver solver) throws EquationsLimitException {
     ProgressManager.checkCanceled();
     Bytes bytes = new Bytes(hKey.key);
 
-    List<HEquations> hEquationss = cache.get(bytes);
-    if (hEquationss == null) {
-      hEquationss = index.getValues(BytecodeAnalysisIndex.NAME, bytes, librariesScope);
-      cache.put(bytes, hEquationss);
-    }
-
-    for (HEquations hEquations : hEquationss) {
+    for (HEquations hEquations : getEquations(bytes)) {
       boolean stable = hEquations.stable;
       for (DirectionResultPair pair : hEquations.results) {
         int dirKey = pair.directionKey;