contract inference index
authorpeter <peter@jetbrains.com>
Fri, 28 Oct 2016 11:09:25 +0000 (13:09 +0200)
committerpeter <peter@jetbrains.com>
Fri, 28 Oct 2016 13:34:10 +0000 (15:34 +0200)
15 files changed:
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInference.java
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt [new file with mode: 0644]
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceInterpreter.java
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/InferenceFromSourceUtil.java
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodContract.java
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt [new file with mode: 0644]
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/NullityInference.java
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/PurityInference.java
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/inferenceResults.kt
java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/preContracts.kt
java/java-psi-impl/src/com/intellij/psi/impl/source/JavaLightStubBuilder.java
java/java-tests/testSrc/com/intellij/codeInspection/ContractInferenceFromSourceTest.groovy
java/java-tests/testSrc/com/intellij/codeInspection/NullityInferenceFromSourceTestCase.groovy
java/java-tests/testSrc/com/intellij/codeInspection/PurityInferenceFromSourceTest.groovy
resources/src/idea/RichPlatformPlugin.xml

index 4509e0aff2d253469d9d9fb5645fa975c73f6685..dcc1f0da0ee8cb7d9f9c4dbffb955f653a8e5db4 100644 (file)
 package com.intellij.codeInspection.dataFlow;
 
 import com.intellij.codeInsight.NullableNotNullManager;
-import com.intellij.lang.ASTNode;
-import com.intellij.lang.TreeBackedLighterAST;
 import com.intellij.openapi.diagnostic.Logger;
 import com.intellij.openapi.util.RecursionManager;
-import com.intellij.psi.PsiCodeBlock;
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.PsiPrimitiveType;
 import com.intellij.psi.PsiType;
@@ -50,22 +47,17 @@ public class ContractInference {
     }
 
     return CachedValuesManager.getCachedValue(method, () -> {
-      TreeBackedLighterAST tree = new TreeBackedLighterAST(method.getContainingFile().getNode());
-      PsiCodeBlock body = method.getBody();
-      assert body != null;
-      ASTNode methodNode = method.getNode();
-      ASTNode bodyNode = body.getNode();
-      List<PreContract> preContracts = methodNode == null || bodyNode == null ? Collections.emptyList() :
-                                       new ContractInferenceInterpreter(tree, TreeBackedLighterAST.wrap(methodNode), TreeBackedLighterAST.wrap(bodyNode)).inferContracts();
-      List<MethodContract> result = RecursionManager.doPreventingRecursion(method, true, () -> postProcessContracts(method, body, preContracts));
+      MethodData data = ContractInferenceIndexKt.getIndexedData(method);
+      List<PreContract> preContracts = data == null ? Collections.emptyList() : data.getContracts();
+      List<MethodContract> result = RecursionManager.doPreventingRecursion(method, true, () -> postProcessContracts(method, data, preContracts));
       if (result == null) result = Collections.emptyList();
       return CachedValueProvider.Result.create(result, method, PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT);
     });
   }
 
   @NotNull
-  private static List<MethodContract> postProcessContracts(@NotNull PsiMethod method, @NotNull PsiCodeBlock body, List<PreContract> rawContracts) {
-    List<MethodContract> contracts = ContainerUtil.concat(rawContracts, c -> c.toContracts(method, body));
+  private static List<MethodContract> postProcessContracts(@NotNull PsiMethod method, MethodData data, List<PreContract> rawContracts) {
+    List<MethodContract> contracts = ContainerUtil.concat(rawContracts, c -> c.toContracts(method, data.methodBody(method)));
     if (contracts.isEmpty()) return Collections.emptyList();
     
     final PsiType returnType = method.getReturnType();
diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/ContractInferenceIndex.kt
new file mode 100644 (file)
index 0000000..ee11a21
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * 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.dataFlow
+
+import com.intellij.ide.highlighter.JavaFileType
+import com.intellij.lang.LighterAST
+import com.intellij.lang.LighterASTNode
+import com.intellij.lang.TreeBackedLighterAST
+import com.intellij.openapi.util.Ref
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.impl.source.JavaFileElementType
+import com.intellij.psi.impl.source.JavaLightStubBuilder
+import com.intellij.psi.impl.source.PsiFileImpl
+import com.intellij.psi.impl.source.PsiMethodImpl
+import com.intellij.psi.impl.source.tree.JavaElementType
+import com.intellij.psi.impl.source.tree.LightTreeUtil
+import com.intellij.psi.impl.source.tree.RecursiveLighterASTNodeWalkingVisitor
+import com.intellij.psi.search.GlobalSearchScope
+import com.intellij.psi.util.CachedValueProvider
+import com.intellij.psi.util.CachedValuesManager
+import com.intellij.util.indexing.*
+import com.intellij.util.io.DataExternalizer
+import com.intellij.util.io.IntInlineKeyDescriptor
+import java.util.*
+
+/**
+ * @author peter
+ */
+
+private val INDEX_ID = ID.create<Int, MethodData>("java.inferred.contracts")
+
+class ContractInferenceIndex : FileBasedIndexExtension<Int, MethodData>(), PsiDependentIndex {
+  override fun getName() = INDEX_ID
+  override fun getVersion() = 0
+  override fun dependsOnFileContent() = true
+  override fun getKeyDescriptor() = IntInlineKeyDescriptor()
+  override fun getValueExternalizer(): DataExternalizer<MethodData> = MethodDataExternalizer
+
+  override fun getInputFilter() = FileBasedIndex.InputFilter {
+    it.fileType == JavaFileType.INSTANCE && JavaFileElementType.isInSourceContent(it)
+  }
+
+  override fun getIndexer() = DataIndexer<Int, MethodData, FileContent> { fc ->
+    val result = HashMap<Int, MethodData>()
+
+    val tree = (fc as FileContentImpl).lighterASTForPsiDependentIndex
+    object : RecursiveLighterASTNodeWalkingVisitor(tree) {
+      var methodIndex = 0
+
+      override fun visitNode(element: LighterASTNode) {
+        if (element.tokenType === JavaElementType.METHOD) {
+          calcData(tree, element)?.let { data -> result[methodIndex] = data }
+          methodIndex++
+        }
+
+        if (JavaLightStubBuilder.isCodeBlockWithoutStubs(element)) return
+
+        super.visitNode(element)
+      }
+    }.visitNode(tree.root)
+
+    result
+  }
+
+}
+
+private fun calcData(tree: LighterAST, method: LighterASTNode): MethodData? {
+  val body = LightTreeUtil.firstChildOfType(tree, method, JavaElementType.CODE_BLOCK) ?: return null
+
+  val nullity = NullityInference.doInferNullity(tree, body)
+  val purity = PurityInference.doInferPurity(body, tree)
+  val contracts = ContractInferenceInterpreter(tree, method, body).inferContracts()
+  if (nullity == null && purity == null && !contracts.isNotEmpty()) return null
+
+  return MethodData(nullity, purity, contracts, body.startOffset, body.endOffset)
+}
+
+fun getIndexedData(method: PsiMethod): MethodData? {
+  if (method !is PsiMethodImpl || !InferenceFromSourceUtil.shouldInferFromSource(method)) return null
+  val vFile = method.containingFile.virtualFile ?: return calcNonPhysicalMethodData(method)
+
+  val ref = Ref<MethodData>()
+  val scope = GlobalSearchScope.fileScope(method.project, vFile)
+  FileBasedIndex.getInstance().processValues(INDEX_ID, methodIndex(method), vFile, { file, data -> ref.set(data); true }, scope)
+  return ref.get()
+}
+
+private fun methodIndex(method: PsiMethodImpl): Int {
+  val file = method.containingFile as PsiFileImpl
+  val stubTree = file.stubTree ?: file.calcStubTree()
+  return stubTree.plainList.filter { it.stubType == JavaElementType.METHOD }.map { it.psi }.indexOf(method)
+}
+
+private fun calcNonPhysicalMethodData(method: PsiMethodImpl): MethodData? {
+  return CachedValuesManager.getCachedValue(method) {
+    CachedValueProvider.Result(calcData(method.containingFile.node.lighterAST, TreeBackedLighterAST.wrap(method.node)), method)
+  }
+}
\ No newline at end of file
index a56d2c0626f53995f4cf4207196ec2c0a86b8954..08cb3000cae358965c9aa6bdb6e88901bb6eaf26 100644 (file)
@@ -27,6 +27,7 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -54,6 +55,7 @@ class ContractInferenceInterpreter {
     return paramList != null ? getChildrenOfType(myTree, paramList, PARAMETER) : Collections.emptyList();
   }
 
+  @NotNull
   List<PreContract> inferContracts() {
     LighterASTNode[] statements = getStatements(myBody);
     if (statements.length == 0) return Collections.emptyList();
@@ -153,7 +155,8 @@ class ContractInferenceInterpreter {
       return asPreContracts(toContracts(states, NOT_NULL_VALUE));
     }
     if (type == METHOD_CALL_EXPRESSION) {
-      return Collections.singletonList(new MethodCallContract(ExpressionRange.create(expr, myBody.getStartOffset()), states));
+      return Collections.singletonList(new MethodCallContract(ExpressionRange.create(expr, myBody.getStartOffset()),
+                                                              ContainerUtil.map(states, Arrays::asList)));
     }
 
     final ValueConstraint constraint = getLiteralConstraint(expr);
index dacd067718b5ef8e340747df06b6b44937c6ddae..aa1cf0896eb67f3fb2d713600257c4b550e79658 100644 (file)
@@ -39,8 +39,7 @@ public class InferenceFromSourceUtil {
   private static boolean calcShouldInferFromSource(@NotNull PsiMethod method) {
     if (isLibraryCode(method) ||
         method.hasModifierProperty(PsiModifier.ABSTRACT) ||
-        PsiUtil.canBeOverriden(method) ||
-        method.getBody() == null) {
+        PsiUtil.canBeOverriden(method)) {
       return false;
     }
 
index 3d92a10278aeb05f65d82ad9ae02a6d9a46ec9a1..c962fdd6a156fa7de44487dfd3025bc0f438fac6 100644 (file)
@@ -22,6 +22,7 @@ import com.intellij.util.containers.ContainerUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
+import java.util.Arrays;
 import java.util.List;
 
 /**
@@ -45,6 +46,29 @@ public class MethodContract {
     return args;
   }
 
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (!(o instanceof MethodContract)) return false;
+
+    MethodContract contract = (MethodContract)o;
+
+    if (!Arrays.equals(arguments, contract.arguments)) return false;
+    if (returnValue != contract.returnValue) return false;
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    int result = 0;
+    for (ValueConstraint argument : arguments) {
+      result = 31 * result + argument.ordinal();
+    }
+    result = 31 * result + returnValue.ordinal();
+    return result;
+  }
+
   @Override
   public String toString() {
     return StringUtil.join(arguments, constraint -> constraint.toString(), ", ") + " -> " + returnValue;
diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt b/java/java-analysis-impl/src/com/intellij/codeInspection/dataFlow/MethodDataExternalizer.kt
new file mode 100644 (file)
index 0000000..c094d31
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * 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.dataFlow
+
+import com.intellij.util.io.DataExternalizer
+import com.intellij.util.io.DataInputOutputUtil
+import com.intellij.util.io.IOUtil
+import java.io.DataInput
+import java.io.DataOutput
+
+/**
+ * @author peter
+ */
+internal object MethodDataExternalizer : DataExternalizer<MethodData> {
+
+  override fun save(out: DataOutput, data: MethodData) {
+    writeNullable(out, data.nullity) { writeNullity(out, it) }
+    writeNullable(out, data.purity) { writePurity(out, it) }
+    writeList(out, data.contracts) { writeContract(out, it) }
+    DataInputOutputUtil.writeINT(out, data.bodyStart)
+    DataInputOutputUtil.writeINT(out, data.bodyEnd)
+  }
+
+  override fun read(input: DataInput): MethodData {
+    val nullity = readNullable(input) { readNullity(input) }
+    val purity = readNullable(input) { readPurity(input) }
+    val contracts = readList(input) { readContract(input) }
+    return MethodData(nullity, purity, contracts, DataInputOutputUtil.readINT(input), DataInputOutputUtil.readINT(input))
+  }
+
+  private fun writeNullity(out: DataOutput, nullity: NullityInferenceResult) = when (nullity) {
+    is NullityInferenceResult.Predefined -> { out.writeByte(0); out.writeByte(nullity.value.ordinal) }
+    is NullityInferenceResult.FromDelegate -> { out.writeByte(1); writeRanges(out, nullity.delegateCalls) }
+    else -> throw IllegalArgumentException(nullity.toString())
+  }
+  private fun readNullity(input: DataInput): NullityInferenceResult = when (input.readByte().toInt()) {
+    0 -> NullityInferenceResult.Predefined(Nullness.values()[input.readByte().toInt()])
+    else -> NullityInferenceResult.FromDelegate(readRanges(input))
+  }
+
+  private fun writeRanges(out: DataOutput, ranges: List<ExpressionRange>) = writeList(out, ranges) { writeRange(out, it) }
+  private fun readRanges(input: DataInput) = readList(input) { readRange(input) }
+
+  private fun writeRange(out: DataOutput, range: ExpressionRange) {
+    DataInputOutputUtil.writeINT(out, range.startOffset)
+    DataInputOutputUtil.writeINT(out, range.endOffset)
+  }
+  private fun readRange(input: DataInput) = ExpressionRange(DataInputOutputUtil.readINT(input), DataInputOutputUtil.readINT(input))
+
+  private fun writePurity(out: DataOutput, purity: PurityInferenceResult) {
+    writeRanges(out, purity.mutatedRefs)
+    writeNullable(out, purity.singleCall) { writeRange(out, it) }
+  }
+  private fun readPurity(input: DataInput) = PurityInferenceResult(readRanges(input), readNullable(input) { readRange(input) })
+
+  private fun writeContract(out: DataOutput, contract: PreContract): Unit = when (contract) {
+    is DelegationContract -> { out.writeByte(0); writeRange(out, contract.expression); out.writeBoolean(contract.negated) }
+    is KnownContract -> { out.writeByte(1);
+      writeContractArguments(out, contract.contract.arguments.toList())
+      out.writeByte(contract.contract.returnValue.ordinal)
+    }
+    is MethodCallContract -> { out.writeByte(2);
+      writeRange(out, contract.call);
+      writeList(out, contract.states) { writeContractArguments(out, it) }
+    }
+    is NegatingContract -> { out.writeByte(3); writeContract(out, contract.negated) }
+    is SideEffectFilter -> { out.writeByte(4);
+      writeRanges(out, contract.expressionsToCheck)
+      writeList(out, contract.contracts) { writeContract(out, it) }
+    }
+    else -> throw IllegalArgumentException(contract.toString())
+  }
+  private fun readContract(input: DataInput): PreContract = when (input.readByte().toInt()) {
+    0 -> DelegationContract(readRange(input), input.readBoolean())
+    1 -> KnownContract(MethodContract(readContractArguments(input).toTypedArray(), readValueConstraint(input)))
+    2 -> MethodCallContract(readRange(input), readList(input) { readContractArguments(input) })
+    3 -> NegatingContract(readContract(input))
+    else -> SideEffectFilter(readRanges(input), readList(input) { readContract(input) })
+  }
+
+  private fun writeContractArguments(out: DataOutput, arguments: List<MethodContract.ValueConstraint>) =
+      writeList(out, arguments) { out.writeByte(it.ordinal) }
+  private fun readContractArguments(input: DataInput) = readList(input, { readValueConstraint(input) })
+
+  private fun readValueConstraint(input: DataInput) = MethodContract.ValueConstraint.values()[input.readByte().toInt()]
+
+}
+
+// utils
+
+private fun <T> writeNullable(out: DataOutput, value: T?, writeItem: (T) -> Unit) = when (value) {
+  null -> out.writeBoolean(false)
+  else -> { out.writeBoolean(true); writeItem(value) }
+}
+private fun <T> readNullable(input: DataInput, readEach: () -> T): T? = if (input.readBoolean()) readEach() else null
+
+private fun <T> writeList(out: DataOutput, list: List<T>, writeEach: (T) -> Unit) {
+  DataInputOutputUtil.writeINT(out, list.size)
+  list.forEach(writeEach)
+}
+private fun <T> readList(input: DataInput, readEach: () -> T) = (0 until DataInputOutputUtil.readINT(input)).map { readEach() }
index 08493ec1a8f8e0ef9a588672621981027cc066a5..b6ac3456926783219f08bdd9eb72a464039e95d3 100644 (file)
  */
 package com.intellij.codeInspection.dataFlow;
 
-import com.intellij.lang.ASTNode;
 import com.intellij.lang.LighterAST;
 import com.intellij.lang.LighterASTNode;
-import com.intellij.lang.TreeBackedLighterAST;
 import com.intellij.openapi.util.RecursionManager;
 import com.intellij.psi.*;
 import com.intellij.psi.impl.source.JavaLightTreeUtil;
@@ -27,7 +25,7 @@ import com.intellij.psi.tree.IElementType;
 import com.intellij.psi.util.CachedValueProvider;
 import com.intellij.psi.util.CachedValuesManager;
 import com.intellij.psi.util.PsiModificationTracker;
-import com.intellij.util.ObjectUtils;
+import com.intellij.util.containers.ContainerUtil;
 import com.intellij.util.containers.MultiMap;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -53,18 +51,16 @@ public class NullityInference {
     }
 
     return CachedValuesManager.getCachedValue(method, () -> {
-      TreeBackedLighterAST tree = new TreeBackedLighterAST(method.getContainingFile().getNode());
-      PsiCodeBlock body = ObjectUtils.assertNotNull(method.getBody());
-      ASTNode node = body.getNode();
-      NullityInferenceResult result = node == null ? null : doInferNullity(tree, TreeBackedLighterAST.wrap(node));
-      Nullness nullness = result == null ? null : RecursionManager.doPreventingRecursion(method, true, () -> result.getNullness(method, body));
+      MethodData data = ContractInferenceIndexKt.getIndexedData(method);
+      NullityInferenceResult result = data == null ? null : data.getNullity();
+      Nullness nullness = result == null ? null : RecursionManager.doPreventingRecursion(method, true, () -> result.getNullness(method, data.methodBody(method)));
       if (nullness == null) nullness = Nullness.UNKNOWN;
       return CachedValueProvider.Result.create(nullness, method, PsiModificationTracker.JAVA_STRUCTURE_MODIFICATION_COUNT);
     });
   }
 
   @Nullable
-  private static NullityInferenceResult doInferNullity(LighterAST tree, LighterASTNode body) {
+  static NullityInferenceResult doInferNullity(LighterAST tree, LighterASTNode body) {
     AtomicBoolean hasErrors = new AtomicBoolean();
     AtomicBoolean hasNotNulls = new AtomicBoolean();
     AtomicBoolean hasNulls = new AtomicBoolean();
@@ -134,7 +130,7 @@ public class NullityInference {
       return null;
     }
     if (delegates.size() == 1) {
-      return new NullityInferenceResult.FromDelegate(delegates.get(delegates.keySet().iterator().next()));
+      return new NullityInferenceResult.FromDelegate(ContainerUtil.newArrayList(delegates.get(delegates.keySet().iterator().next())));
     }
 
     if (hasNotNulls.get()) {
index 0b8602c287e0263378ee01f22cf12543a6282d32..0baaf1a29372801c41641ba2491e2da1c20b1701 100644 (file)
  */
 package com.intellij.codeInspection.dataFlow;
 
-import com.intellij.lang.ASTNode;
 import com.intellij.lang.LighterAST;
 import com.intellij.lang.LighterASTNode;
-import com.intellij.lang.TreeBackedLighterAST;
 import com.intellij.openapi.util.RecursionManager;
 import com.intellij.openapi.util.Ref;
 import com.intellij.psi.JavaTokenType;
-import com.intellij.psi.PsiCodeBlock;
 import com.intellij.psi.PsiMethod;
 import com.intellij.psi.PsiType;
 import com.intellij.psi.impl.source.JavaLightTreeUtil;
@@ -31,7 +28,6 @@ import com.intellij.psi.impl.source.tree.RecursiveLighterASTNodeWalkingVisitor;
 import com.intellij.psi.tree.IElementType;
 import com.intellij.psi.util.CachedValueProvider;
 import com.intellij.psi.util.CachedValuesManager;
-import com.intellij.psi.util.PropertyUtil;
 import com.intellij.util.containers.ContainerUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -49,24 +45,20 @@ public class PurityInference {
   public static boolean inferPurity(@NotNull final PsiMethod method) {
     if (!InferenceFromSourceUtil.shouldInferFromSource(method) ||
         PsiType.VOID.equals(method.getReturnType()) ||
-        method.getBody() == null ||
-        method.isConstructor() || 
-        PropertyUtil.isSimpleGetter(method)) {
+        method.isConstructor()) {
       return false;
     }
 
     return CachedValuesManager.getCachedValue(method, () -> {
-      TreeBackedLighterAST tree = new TreeBackedLighterAST(method.getContainingFile().getNode());
-      PsiCodeBlock body = method.getBody();
-      ASTNode node = body.getNode();
-      PurityInferenceResult result = node == null ? null : doInferPurity(TreeBackedLighterAST.wrap(node), tree);
-      Boolean pure = RecursionManager.doPreventingRecursion(method, true, () -> result != null && result.isPure(method, body));
+      MethodData data = ContractInferenceIndexKt.getIndexedData(method);
+      PurityInferenceResult result = data == null ? null : data.getPurity();
+      Boolean pure = RecursionManager.doPreventingRecursion(method, true, () -> result != null && result.isPure(method, data.methodBody(method)));
       return CachedValueProvider.Result.create(pure == Boolean.TRUE, method);
     });
   }
 
   @Nullable
-  private static PurityInferenceResult doInferPurity(LighterASTNode body, LighterAST tree) {
+  static PurityInferenceResult doInferPurity(LighterASTNode body, LighterAST tree) {
     List<LighterASTNode> mutatedRefs = new ArrayList<>();
     Ref<Boolean> hasReturns = Ref.create(false);
     List<LighterASTNode> calls = new ArrayList<>();
@@ -74,6 +66,8 @@ public class PurityInference {
       @Override
       public void visitNode(@NotNull LighterASTNode element) {
         IElementType type = element.getTokenType();
+        if (type == CLASS || type == ANONYMOUS_CLASS || type == LAMBDA_EXPRESSION) return;
+
         if (type == ASSIGNMENT_EXPRESSION) {
           mutatedRefs.add(tree.getChildren(element).get(0));
         }
index fc8c78144ba81017468fa3bdd5ffc39b1de5c1ef..72691488393bd22911cd6c9c2d1c90b97c9169e8 100644 (file)
@@ -20,13 +20,15 @@ import com.intellij.lang.LighterASTNode
 import com.intellij.psi.*
 import com.intellij.psi.search.LocalSearchScope
 import com.intellij.psi.search.searches.ReferencesSearch
+import com.intellij.psi.util.CachedValueProvider
+import com.intellij.psi.util.CachedValuesManager
 import com.intellij.psi.util.PsiTreeUtil
 import com.intellij.psi.util.PsiUtil
 
 /**
  * @author peter
  */
-data class ExpressionRange private constructor (private val startOffset: Int, private val endOffset: Int) {
+data class ExpressionRange internal constructor (internal val startOffset: Int, internal val endOffset: Int) {
 
   companion object {
     @JvmStatic
@@ -41,18 +43,18 @@ data class ExpressionRange private constructor (private val startOffset: Int, pr
 
 }
 
-data class PurityInferenceResult(private val mutatedRefs: List<ExpressionRange>, private val singleCall: ExpressionRange?) {
+data class PurityInferenceResult(internal val mutatedRefs: List<ExpressionRange>, internal val singleCall: ExpressionRange?) {
 
-  fun isPure(method: PsiMethod, body: PsiCodeBlock) = !mutatesNonLocals(method, body) && callsOnlyPureMethods(body)
+  fun isPure(method: PsiMethod, body: () -> PsiCodeBlock) = !mutatesNonLocals(method, body) && callsOnlyPureMethods(body)
 
-  private fun mutatesNonLocals(method: PsiMethod, body: PsiCodeBlock): Boolean {
-    return mutatedRefs.any { range -> !isLocalVarReference(range.restoreExpression(body), method) }
+  private fun mutatesNonLocals(method: PsiMethod, body: () -> PsiCodeBlock): Boolean {
+    return mutatedRefs.any { range -> !isLocalVarReference(range.restoreExpression(body()), method) }
   }
 
-  private fun callsOnlyPureMethods(body: PsiCodeBlock): Boolean {
+  private fun callsOnlyPureMethods(body: () -> PsiCodeBlock): Boolean {
     if (singleCall == null) return true
 
-    val called = (singleCall.restoreExpression(body) as PsiCall).resolveMethod()
+    val called = (singleCall.restoreExpression(body()) as PsiCall).resolveMethod()
     return called != null && ControlFlowAnalyzer.isPure(called)
   }
 
@@ -86,20 +88,20 @@ data class PurityInferenceResult(private val mutatedRefs: List<ExpressionRange>,
 
 
 interface NullityInferenceResult {
-  fun getNullness(method: PsiMethod, body: PsiCodeBlock): Nullness
+  fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock): Nullness
 
-  data class Predefined(private val value: Nullness) : NullityInferenceResult {
-
-    override fun getNullness(method: PsiMethod, body: PsiCodeBlock) = when {
+  @Suppress("EqualsOrHashCode")
+  data class Predefined(internal val value: Nullness) : NullityInferenceResult {
+    override fun hashCode() = value.ordinal
+    override fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock) = when {
       value == Nullness.NULLABLE && InferenceFromSourceUtil.suppressNullable(method) -> Nullness.UNKNOWN
       else -> value
     }
   }
 
-  data class FromDelegate (private val delegateCalls: Collection<ExpressionRange>) : NullityInferenceResult {
-
-    override fun getNullness(method: PsiMethod, body: PsiCodeBlock) = when {
-      delegateCalls.all { range -> isNotNullCall(range, body) } -> Nullness.NOT_NULL
+  data class FromDelegate(internal val delegateCalls: List<ExpressionRange>) : NullityInferenceResult {
+    override fun getNullness(method: PsiMethod, body: () -> PsiCodeBlock) = when {
+      delegateCalls.all { range -> isNotNullCall(range, body()) } -> Nullness.NOT_NULL
       else -> Nullness.UNKNOWN
     }
 
@@ -111,4 +113,25 @@ interface NullityInferenceResult {
       return target != null && NullableNotNullManager.isNotNull(target)
     }
   }
+}
+
+data class MethodData(
+    val nullity: NullityInferenceResult?,
+    val purity: PurityInferenceResult?,
+    val contracts: List<PreContract>,
+    internal val bodyStart: Int,
+    internal val bodyEnd: Int
+) {
+  fun methodBody(method: PsiMethod): () -> PsiCodeBlock = {
+    if ((method as StubBasedPsiElement<*>?)?.stub != null)
+      CachedValuesManager.getCachedValue(method) { CachedValueProvider.Result(getDetachedBody(method), method) }
+    else
+      method.body!!
+  }
+
+  private fun getDetachedBody(method: PsiMethod): PsiCodeBlock {
+    val document = method.containingFile.viewProvider.document ?: return method.body!!
+    val bodyText = PsiDocumentManager.getInstance(method.project).getLastCommittedText(document).substring(bodyStart, bodyEnd)
+    return JavaPsiFacade.getElementFactory(method.project).createCodeBlockFromText(bodyText, method)
+  }
 }
\ No newline at end of file
index ed6b52164e6102f1f24ec58737380cdf1795b4a3..2c3c7901f0c502fa2dd9560ffa538cc2771bed52 100644 (file)
@@ -27,19 +27,19 @@ import com.siyeh.ig.psiutils.SideEffectChecker
  * @author peter
  */
 interface PreContract {
-  fun toContracts(method: PsiMethod, body: PsiCodeBlock): List<MethodContract>
+  fun toContracts(method: PsiMethod, body: () -> PsiCodeBlock): List<MethodContract>
   fun negate(): PreContract? = NegatingContract(this)
 }
 
 internal data class KnownContract(val contract: MethodContract) : PreContract {
-  override fun toContracts(method: PsiMethod, body: PsiCodeBlock) = listOf(contract)
+  override fun toContracts(method: PsiMethod, body: () -> PsiCodeBlock) = listOf(contract)
   override fun negate() = negateContract(contract)?.let(::KnownContract)
 }
 
-internal data class DelegationContract(private val expression: ExpressionRange, private val negated: Boolean) : PreContract {
+internal data class DelegationContract(internal val expression: ExpressionRange, internal val negated: Boolean) : PreContract {
 
-  override fun toContracts(method: PsiMethod, body: PsiCodeBlock): List<MethodContract> {
-    val call = expression.restoreExpression(body) as PsiMethodCallExpression? ?: return emptyList()
+  override fun toContracts(method: PsiMethod, body: () -> PsiCodeBlock): List<MethodContract> {
+    val call = expression.restoreExpression(body()) as PsiMethodCallExpression? ?: return emptyList()
 
     val result = call.resolveMethodGenerics()
     val targetMethod = result.element as PsiMethod? ?: return emptyList()
@@ -103,10 +103,10 @@ internal data class DelegationContract(private val expression: ExpressionRange,
   }
 }
 
-internal data class SideEffectFilter(private val expressionsToCheck: List<ExpressionRange>, private val contracts: List<PreContract>) : PreContract {
+internal data class SideEffectFilter(internal val expressionsToCheck: List<ExpressionRange>, internal val contracts: List<PreContract>) : PreContract {
 
-  override fun toContracts(method: PsiMethod, body: PsiCodeBlock): List<MethodContract> {
-    if (expressionsToCheck.any { d -> mayHaveSideEffects(body, d) }) {
+  override fun toContracts(method: PsiMethod, body: () -> PsiCodeBlock): List<MethodContract> {
+    if (expressionsToCheck.any { d -> mayHaveSideEffects(body(), d) }) {
       return emptyList()
     }
     return contracts.flatMap { c -> c.toContracts(method, body) }
@@ -116,8 +116,8 @@ internal data class SideEffectFilter(private val expressionsToCheck: List<Expres
       range.restoreExpression(body)?.let { SideEffectChecker.mayHaveSideEffects(it) } ?: false
 }
 
-internal data class NegatingContract(private val negated: PreContract) : PreContract {
-  override fun toContracts(method: PsiMethod, body: PsiCodeBlock) = negated.toContracts(method, body).mapNotNull(::negateContract)
+internal data class NegatingContract(internal val negated: PreContract) : PreContract {
+  override fun toContracts(method: PsiMethod, body: () -> PsiCodeBlock) = negated.toContracts(method, body).mapNotNull(::negateContract)
 }
 
 private fun negateContract(c: MethodContract): MethodContract? {
@@ -125,12 +125,14 @@ private fun negateContract(c: MethodContract): MethodContract? {
   return if (ret == TRUE_VALUE || ret == FALSE_VALUE) MethodContract(c.arguments, negateConstraint(ret)) else null
 }
 
-internal data class MethodCallContract(private val call: ExpressionRange, private val states: List<Array<MethodContract.ValueConstraint>>) : PreContract {
+@Suppress("EqualsOrHashCode")
+internal data class MethodCallContract(internal val call: ExpressionRange, internal val states: List<List<MethodContract.ValueConstraint>>) : PreContract {
+  override fun hashCode() = call.hashCode() * 31 + states.flatten().map { it.ordinal }.hashCode()
 
-  override fun toContracts(method: PsiMethod, body: PsiCodeBlock): List<MethodContract> {
-    val target = (call.restoreExpression(body) as PsiMethodCallExpression?)?.resolveMethod()
+  override fun toContracts(method: PsiMethod, body: () -> PsiCodeBlock): List<MethodContract> {
+    val target = (call.restoreExpression(body()) as PsiMethodCallExpression?)?.resolveMethod()
     if (target != null && NullableNotNullManager.isNotNull(target)) {
-      return ContractInferenceInterpreter.toContracts(states, NOT_NULL_VALUE)
+      return ContractInferenceInterpreter.toContracts(states.map { it.toTypedArray() }, NOT_NULL_VALUE)
     }
     return emptyList()
   }
index b13629f0ac2c1b81fc2c55ef53a75041a05a9c64..3470ad3a1ca837744a3d8c16faf900b774670aa9 100644 (file)
@@ -67,12 +67,11 @@ public class JavaLightStubBuilder extends LightStubBuilder {
 
   @Override
   protected boolean skipChildProcessingWhenBuildingStubs(@NotNull LighterAST tree, @NotNull LighterASTNode parent, @NotNull LighterASTNode node) {
-    IElementType parentType = parent.getTokenType();
-    IElementType nodeType = node.getTokenType();
-
-    if (checkByTypes(parentType, nodeType)) return true;
+    return checkByTypes(parent.getTokenType(), node.getTokenType()) || isCodeBlockWithoutStubs(node);
+  }
 
-    if (nodeType == JavaElementType.CODE_BLOCK) {
+  public static boolean isCodeBlockWithoutStubs(@NotNull LighterASTNode node) {
+    if (node.getTokenType() == JavaElementType.CODE_BLOCK && node instanceof LighterLazyParseableNode) {
       CodeBlockVisitor visitor = new CodeBlockVisitor();
       ((LighterLazyParseableNode)node).accept(visitor);
       return visitor.result;
index 9f1ec912fa931a8d841e8d16f49a47df20da6286..f03845a25b19ecf8b2af615d2eb6af0b2eca0089 100644 (file)
@@ -17,9 +17,9 @@ package com.intellij.codeInspection
 
 import com.intellij.codeInspection.dataFlow.ContractInference
 import com.intellij.psi.PsiAnonymousClass
+import com.intellij.psi.impl.source.PsiFileImpl
 import com.intellij.psi.util.PsiTreeUtil
 import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
-
 /**
  * @author peter
  */
@@ -569,6 +569,9 @@ class Foo {{
 
   private List<String> inferContracts(String method) {
     def clazz = myFixture.addClass("final class Foo { $method }")
-    return ContractInference.inferContracts(clazz.methods[0]).collect { it as String }
+    assert !((PsiFileImpl) clazz.containingFile).contentsLoaded
+    def contracts = ContractInference.inferContracts(clazz.methods[0])
+    assert !((PsiFileImpl) clazz.containingFile).contentsLoaded
+    return contracts.collect { it as String }
   }
 }
index dfa86716bf34783d2e5c548f49c360389c5fa27d..fdab395534ee4d0f8cc7c604b33861ddbd13becf 100644 (file)
@@ -19,6 +19,7 @@ import com.intellij.codeInsight.NullableNotNullManager
 import com.intellij.codeInspection.dataFlow.DfaUtil
 import com.intellij.codeInspection.dataFlow.Nullness
 import com.intellij.psi.PsiMethod
+import com.intellij.psi.impl.source.PsiFileImpl
 import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
 import org.jetbrains.annotations.Contract
 
@@ -124,7 +125,10 @@ Object foo(Object o) { if (o == null) return o.hashCode(); return 2; }
 
   static class LightInferenceTest extends NullityInferenceFromSourceTestCase {
     Nullness inferNullity(PsiMethod method) {
-      return NullableNotNullManager.isNotNull(method) ? NOT_NULL : NullableNotNullManager.isNullable(method) ? NULLABLE : UNKNOWN
+      assert !((PsiFileImpl) method.containingFile).contentsLoaded
+      def result = NullableNotNullManager.isNotNull(method) ? NOT_NULL : NullableNotNullManager.isNullable(method) ? NULLABLE : UNKNOWN
+      assert !((PsiFileImpl) method.containingFile).contentsLoaded
+      return result
     }
 
     void "test skip when errors"() {
index 1cacdb4e0f4a32743527b4a9f5432f3e8f7c0cbe..7d5846021f31918bae95e4d72921a6c8d690bc96 100644 (file)
@@ -16,6 +16,7 @@
 package com.intellij.codeInspection
 
 import com.intellij.codeInspection.dataFlow.PurityInference
+import com.intellij.psi.impl.source.PsiFileImpl
 import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
 /**
  * @author peter
@@ -164,7 +165,10 @@ public Foo() {
 
   private void assertPure(boolean expected, String classBody) {
     def clazz = myFixture.addClass("final class Foo { $classBody }")
-    assert expected == PurityInference.inferPurity(clazz.methods[0])
+    assert !((PsiFileImpl) clazz.containingFile).contentsLoaded
+    def purity = PurityInference.inferPurity(clazz.methods[0])
+    assert !((PsiFileImpl) clazz.containingFile).contentsLoaded
+    assert expected == purity
   }
 
 }
index 79f8d9d9e66a48d4eafbcaec5ccb19d0f0085fa2..d2a9d01ca2c13f6c5204f93ebd4e083d45379515 100644 (file)
     <projectService serviceInterface="com.intellij.codeInsight.InferredAnnotationsManager"
                     serviceImplementation="com.intellij.codeInsight.InferredAnnotationsManagerImpl"/>
 
+    <fileBasedIndex implementation="com.intellij.codeInspection.dataFlow.ContractInferenceIndex"/>
+
     <projectService serviceInterface="com.intellij.task.ProjectTaskManager"
                     serviceImplementation="com.intellij.task.impl.ProjectTaskManagerImpl"/>