diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java index 9817c68f677b..ae97a8db4283 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisConverter.java @@ -53,10 +53,11 @@ public class BytecodeAnalysisConverter { ProgressManager.checkCanceled(); Result rhs = equation.rhs; - HResult result; + HResult hResult; if (rhs instanceof Final) { - result = new HFinal(((Final)rhs).value); - } else { + hResult = new HFinal(((Final)rhs).value); + } + else { Pending pending = (Pending)rhs; Set> sumOrigin = pending.sum; HComponent[] components = new HComponent[sumOrigin.size()]; @@ -64,17 +65,17 @@ public class BytecodeAnalysisConverter { for (Product prod : sumOrigin) { HKey[] intProd = new HKey[prod.ids.size()]; int idI = 0; - for (Key id : prod.ids) { - intProd[idI] = asmKey(id, md); + for (Key key : prod.ids) { + intProd[idI] = asmKey(key, md); idI++; } HComponent intIdComponent = new HComponent(prod.value, intProd); components[componentI] = intIdComponent; componentI++; } - result = new HPending(components); + hResult = new HPending(components); } - return new DirectionResultPair(mkDirectionKey(equation.id.direction), result); + return new DirectionResultPair(mkDirectionKey(equation.id.direction), hResult); } /** @@ -274,6 +275,23 @@ public class BytecodeAnalysisConverter { } + /** + * Converts Direction object to int. + * + * 0 - Out + * 1 - NullableOut + * 2 - Pure + * + * 3 - 0-th NOT_NULL + * 4 - 0-th NULLABLE + * ... + * + * 11 - 1-st NOT_NULL + * 12 - 1-st NULLABLE + * + * @param dir direction of analysis + * @return unique int for direction + */ static int mkDirectionKey(Direction dir) { if (dir == Out) { return 0; @@ -281,17 +299,28 @@ public class BytecodeAnalysisConverter { else if (dir == NullableOut) { return 1; } + else if (dir == Pure) { + return 2; + } else if (dir instanceof In) { In in = (In)dir; // nullity mask is 0/1 - return 2 + 8 * in.paramId() + in.nullityMask; + return 3 + 8 * in.paramId() + in.nullityMask; } else { + // valueId is [1-5] InOut inOut = (InOut)dir; - return 4 + 8 * inOut.paramId() + inOut.valueId(); + return 3 + 8 * inOut.paramId() + 2 + inOut.valueId(); } } + /** + * Converts int to Direction object. + * + * @param directionKey int representation of direction + * @return Direction object + * @see #mkDirectionKey(Direction) + */ @NotNull private static Direction extractDirection(int directionKey) { if (directionKey == 0) { @@ -300,15 +329,21 @@ public class BytecodeAnalysisConverter { else if (directionKey == 1) { return NullableOut; } + else if (directionKey == 2) { + return Pure; + } else { - directionKey--; - int paramId = directionKey / 8; - int subDirection = directionKey % 8; - if (subDirection <= 2) { - return new In(paramId, subDirection - 1); + int paramKey = directionKey - 3; + int paramId = paramKey / 8; + // shifting first 3 values - now we have key [0 - 7] + int subDirectionId = paramKey % 8; + // 0 - 1 - @NotNull, @Nullable, parameter + if (subDirectionId <= 1) { + return new In(paramId, subDirectionId); } else { - return new InOut(paramId, Value.values()[subDirection - 3]); + int valueId = subDirectionId - 2; + return new InOut(paramId, Value.values()[valueId]); } } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisIndex.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisIndex.java index e89acbb8e247..3f2bb5646eed 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisIndex.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/BytecodeAnalysisIndex.java @@ -88,6 +88,9 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension, DifferentSerializableBytesImplyNonEqualityPolicy { @Override @@ -115,6 +118,9 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension, DifferentSerializableBytesImplyNonEqualityPolicy { @Override public void save(@NotNull DataOutput out, HEquations eqs) throws IOException { diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java index ad856455de79..900c137c7c1f 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ClassDataIndexer.java @@ -42,7 +42,6 @@ public class ClassDataIndexer implements DataIndexer 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); - private static final List> EMPTY_EQUATIONS = Collections.EMPTY_LIST; @NotNull @Override @@ -50,17 +49,19 @@ public class ClassDataIndexer implements DataIndexer map = new HashMap(); try { MessageDigest md = BytecodeAnalysisConverter.getMessageDigest(); - Map>> rawEquations = processClass(new ClassReader(inputData.getContent()), inputData.getFile().getPresentableUrl()); - for (Map.Entry>> entry: rawEquations.entrySet()) { - Key primaryKey = entry.getKey(); - Key serKey = new Key(primaryKey.method, primaryKey.direction, true); + Map>> allEquations = processClass(new ClassReader(inputData.getContent()), inputData.getFile().getPresentableUrl()); + for (Map.Entry>> entry: allEquations.entrySet()) { - List> equations = entry.getValue(); - List result = new ArrayList(equations.size()); - for (Equation equation : equations) { - result.add(BytecodeAnalysisConverter.convert(equation, md)); + Key methodKey = entry.getKey(); + // method equations with raw (not-compressed keys) + List> rawMethodEquations = entry.getValue(); + // + List compressedMethodEquations = + new ArrayList(rawMethodEquations.size()); + for (Equation equation : rawMethodEquations) { + compressedMethodEquations.add(BytecodeAnalysisConverter.convert(equation, md)); } - map.put(new Bytes(BytecodeAnalysisConverter.asmKey(serKey, md).key), new HEquations(result, primaryKey.stable)); + map.put(new Bytes(BytecodeAnalysisConverter.asmKey(methodKey, md).key), new HEquations(compressedMethodEquations, methodKey.stable)); } } catch (ProcessCanceledException e) { @@ -71,7 +72,6 @@ public class ClassDataIndexer implements DataIndexer>> processMethod(final MethodNode methodNode, boolean jsr) { ProgressManager.checkCanceled(); final Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc); @@ -125,8 +132,15 @@ public class ClassDataIndexer implements DataIndexer".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> equations = new ArrayList>(argumentTypes.length * 4 + 3); + equations.add(PurityAnalysis.analyze(method, methodNode, stable)); + if (argumentTypes.length == 0 && !isInterestingResult) { - return Pair.create(primaryKey, EMPTY_EQUATIONS); + // no need to continue analysis + return Pair.create(primaryKey, equations); } try { @@ -142,19 +156,18 @@ public class ClassDataIndexer implements DataIndexer> processBranchingMethod(final Method method, - final MethodNode methodNode, - final RichControlFlow richControlFlow, - Type[] argumentTypes, - boolean isReferenceResult, - boolean isInterestingResult, - final boolean stable, - boolean jsr) throws AnalyzerException { - - // 4 = @NotNull parameter, @Nullable parameter, null -> ..., !null -> ... - List> result = new ArrayList>(argumentTypes.length * 4 + 2); + private void processBranchingMethod(final Method method, + final MethodNode methodNode, + final RichControlFlow richControlFlow, + Type[] argumentTypes, + boolean isReferenceResult, + boolean isInterestingResult, + final boolean stable, + boolean jsr, + List> result) throws AnalyzerException { boolean maybeLeakingParameter = isInterestingResult; for (Type argType : argumentTypes) { if (ASMUtils.isReferenceType(argType)) { @@ -218,7 +229,7 @@ public class ClassDataIndexer implements DataIndexer notNullParamEquation = notNullInAnalysis.analyze(); possibleNPE = notNullInAnalysis.possibleNPE; notNullParam = notNullParamEquation.rhs.equals(FINAL_NOT_NULL); @@ -226,19 +237,19 @@ public class ClassDataIndexer implements DataIndexer(new Key(method, new In(i, In.NOT_NULL), stable), FINAL_TOP)); + result.add(new Equation(new Key(method, new In(i, In.NOT_NULL_MASK), stable), FINAL_TOP)); } if (leakingNullableParameters[i]) { if (notNullParam || possibleNPE) { - result.add(new Equation(new Key(method, new In(i, In.NULLABLE), stable), FINAL_TOP)); + result.add(new Equation(new Key(method, new In(i, In.NULLABLE_MASK), stable), FINAL_TOP)); } else { - result.add(new NullableInAnalysis(richControlFlow, new In(i, In.NULLABLE), stable).analyze()); + result.add(new NullableInAnalysis(richControlFlow, new In(i, In.NULLABLE_MASK), stable).analyze()); } } else { - result.add(new Equation(new Key(method, new In(i, In.NULLABLE), stable), FINAL_NULL)); + result.add(new Equation(new Key(method, new In(i, In.NULLABLE_MASK), stable), FINAL_NULL)); } if (isInterestingResult) { @@ -261,17 +272,15 @@ public class ClassDataIndexer implements DataIndexer> processNonBranchingMethod(Method method, - Type[] argumentTypes, - ControlFlowGraph graph, - boolean isReferenceResult, - boolean isBooleanResult, - boolean stable) throws AnalyzerException { - // 4 = @NotNull parameter, @Nullable parameter, null -> ..., !null -> ... - List> result = new ArrayList>(argumentTypes.length * 4 + 2); + private void processNonBranchingMethod(Method method, + Type[] argumentTypes, + ControlFlowGraph graph, + boolean isReferenceResult, + boolean isBooleanResult, + boolean stable, + List> result) throws AnalyzerException { CombinedAnalysis analyzer = new CombinedAnalysis(method, graph); analyzer.analyze(); if (isReferenceResult) { @@ -289,7 +298,6 @@ public class ClassDataIndexer implements DataIndexer> topEquations(Method method, @@ -305,8 +313,8 @@ public class ClassDataIndexer implements DataIndexer(new Key(method, new In(i, In.NOT_NULL), stable), FINAL_TOP)); - result.add(new Equation(new Key(method, new In(i, In.NULLABLE), stable), FINAL_TOP)); + result.add(new Equation(new Key(method, new In(i, In.NOT_NULL_MASK), stable), FINAL_TOP)); + result.add(new Equation(new Key(method, new In(i, In.NULLABLE_MASK), stable), FINAL_TOP)); if (isInterestingResult) { result.add(new Equation(new Key(method, new InOut(i, Value.Null), stable), FINAL_TOP)); result.add(new Equation(new Key(method, new InOut(i, Value.NotNull), stable), FINAL_TOP)); diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java index dd10492785e7..3f2ec6e3f429 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Combined.java @@ -194,7 +194,7 @@ final class CombinedAnalysis { } final Equation notNullParamEquation(int i, boolean stable) { - final Key key = new Key(method, new In(i, In.NOT_NULL), stable); + final Key key = new Key(method, new In(i, In.NOT_NULL_MASK), stable); final Result result; if (interpreter.dereferencedParams[i]) { result = new Final(Value.NotNull); @@ -207,7 +207,7 @@ final class CombinedAnalysis { else { Set keys = new HashSet(); for (ParamKey pk: calls) { - keys.add(new Key(pk.method, new In(pk.i, In.NOT_NULL), pk.stable)); + keys.add(new Key(pk.method, new In(pk.i, In.NOT_NULL_MASK), pk.stable)); } result = new Pending(new SingletonSet>(new Product(Value.Top, keys))); } @@ -216,7 +216,7 @@ final class CombinedAnalysis { } final Equation nullableParamEquation(int i, boolean stable) { - final Key key = new Key(method, new In(i, In.NULLABLE), stable); + final Key key = new Key(method, new In(i, In.NULLABLE_MASK), stable); final Result result; if (interpreter.dereferencedParams[i] || interpreter.notNullableParams[i] || returnValue instanceof NthParamValue && ((NthParamValue)returnValue).n == i) { result = new Final(Value.Top); @@ -229,7 +229,7 @@ final class CombinedAnalysis { else { Set> sum = new HashSet>(); for (ParamKey pk: calls) { - sum.add(new Product(Value.Top, Collections.singleton(new Key(pk.method, new In(pk.i, In.NULLABLE), pk.stable)))); + sum.add(new Product(Value.Top, Collections.singleton(new Key(pk.method, new In(pk.i, In.NULLABLE_MASK), pk.stable)))); } result = new Pending(sum); } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Data.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Data.java index b76e31197482..1a2c2a0a7866 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Data.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Data.java @@ -15,6 +15,8 @@ */ package com.intellij.codeInspection.bytecodeAnalysis; +import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode; + final class Method { final String internalClassName; final String methodName; @@ -36,12 +38,30 @@ final class Method { return result; } + /** + * Primary constructor + * + * @param internalClassName class name in asm format + * @param methodName method name + * @param methodDesc method descriptor in asm format + */ Method(String internalClassName, String methodName, String methodDesc) { this.internalClassName = internalClassName; this.methodName = methodName; this.methodDesc = methodDesc; } + /** + * Convenient constructor to convert asm instruction into method key + * + * @param mNode asm node from which method key is extracted + */ + Method(MethodInsnNode mNode) { + this.internalClassName = mNode.owner; + this.methodName = mNode.name; + this.methodDesc = mNode.desc; + } + @Override public String toString() { return internalClassName + ' ' + methodName + ' ' + methodDesc; @@ -49,14 +69,19 @@ final class Method { } enum Value { - Bot, NotNull, Null, True, False, Top + Bot, NotNull, Null, True, False, Pure, Top } interface Direction { final class In implements Direction { - static final int NOT_NULL = 0; - static final int NULLABLE = 1; final int paramIndex; + + static final int NOT_NULL_MASK = 0; + static final int NULLABLE_MASK = 1; + /** + * @see #NOT_NULL_MASK + * @see #NULLABLE_MASK + */ final int nullityMask; In(int paramIndex, int nullityMask) { @@ -155,6 +180,18 @@ interface Direction { return -2; } }; + + Direction Pure = new Direction() { + @Override + public int hashCode() { + return -3; + } + + @Override + public String toString() { + return "Pure"; + } + }; } final class Key { diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/HData.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/HData.java index 7c938347ccb9..3415a74ad850 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/HData.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/HData.java @@ -166,7 +166,11 @@ final class HEquation { return result1; } } -class Bytes { + +/** + * Bytes of primary HKey of a method. + */ +final class Bytes { @NotNull final byte[] bytes; Bytes(@NotNull byte[] bytes) { @@ -177,12 +181,7 @@ class Bytes { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - - Bytes bytes1 = (Bytes)o; - - if (!Arrays.equals(bytes, bytes1.bytes)) return false; - - return true; + return Arrays.equals(bytes, ((Bytes)o).bytes); } @Override diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Parameters.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Parameters.java index 6102c44792bf..8bf3f5961a89 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Parameters.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/Parameters.java @@ -750,7 +750,7 @@ abstract class NullityInterpreter extends BasicInterpreter { class NotNullInterpreter extends NullityInterpreter { NotNullInterpreter() { - super(false, In.NOT_NULL); + super(false, In.NOT_NULL_MASK); } @Override @@ -762,7 +762,7 @@ class NotNullInterpreter extends NullityInterpreter { class NullableInterpreter extends NullityInterpreter { NullableInterpreter() { - super(true, In.NULLABLE); + super(true, In.NULLABLE_MASK); } @Override diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java index 66ed59a1579d..42ebdfbe725e 100644 --- a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/ProjectBytecodeAnalysis.java @@ -201,7 +201,7 @@ public class ProjectBytecodeAnalysis { PsiElement gParent = parent.getParent(); if (gParent instanceof PsiMethod) { final int index = ((PsiParameterList)parent).getParameterIndex((PsiParameter)owner); - return BytecodeAnalysisConverter.psiKey((PsiMethod)gParent, new In(index, In.NOT_NULL), md); + return BytecodeAnalysisConverter.psiKey((PsiMethod)gParent, new In(index, In.NOT_NULL_MASK), md); } } } diff --git a/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/PurityAnalysis.java b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/PurityAnalysis.java new file mode 100644 index 000000000000..0f2bff537908 --- /dev/null +++ b/java/java-analysis-impl/src/com/intellij/codeInspection/bytecodeAnalysis/PurityAnalysis.java @@ -0,0 +1,107 @@ +/* + * Copyright 2000-2014 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.NotNull; +import org.jetbrains.org.objectweb.asm.Opcodes; +import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode; +import org.jetbrains.org.objectweb.asm.tree.InsnList; +import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode; +import org.jetbrains.org.objectweb.asm.tree.MethodNode; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Produces equations for inference of @Contract(pure=true) annotations. + * + * This simple analysis infers @Contract(pure=true) only if the method doesn't have following instructions. + *
    + *
  • PUTFIELD
  • + *
  • PUTSTATIC
  • + *
  • IASTORE
  • + *
  • LASTORE
  • + *
  • FASTORE
  • + *
  • DASTORE
  • + *
  • AASTORE
  • + *
  • BASTORE
  • + *
  • CASTORE
  • + *
  • SASTORE
  • + *
  • INVOKEDYNAMIC
  • + *
  • INVOKEINTERFACE
  • + *
+ * + * - Nested calls (via INVOKESPECIAL, INVOKESTATIC, INVOKEVIRTUAL) are processed by transitivity. + * @author lambdamix + */ +public class PurityAnalysis { + private static final Result FINAL_TOP = new Final(Value.Top); + private static final Result FINAL_PURE = new Final(Value.Pure); + + static final int UN_ANALYZABLE_FLAG = Opcodes.ACC_ABSTRACT | Opcodes.ACC_NATIVE | Opcodes.ACC_INTERFACE; + + @NotNull + public static Equation analyze(Method method, MethodNode methodNode, boolean stable) { + Key key = new Key(method, Direction.Pure, stable); + + if ((methodNode.access & UN_ANALYZABLE_FLAG) != 0) { + return new Equation(key, FINAL_TOP); + } + + InsnList insns = methodNode.instructions; + // primary keys of invoked methods + Set invokedKeys = new HashSet(); + for (int i = 0; i < insns.size(); i++) { + AbstractInsnNode insn = insns.get(i); + switch (insn.getOpcode()) { + case Opcodes.PUTFIELD: + case Opcodes.PUTSTATIC: + case Opcodes.IASTORE: + case Opcodes.LASTORE: + case Opcodes.FASTORE: + case Opcodes.DASTORE: + case Opcodes.AASTORE: + case Opcodes.BASTORE: + case Opcodes.CASTORE: + case Opcodes.SASTORE: + case Opcodes.INVOKEDYNAMIC: + case Opcodes.INVOKEINTERFACE: + return new Equation(key, FINAL_TOP); + case Opcodes.INVOKESPECIAL: + case Opcodes.INVOKESTATIC: + invokedKeys.add(new Key(new Method((MethodInsnNode)insn), Direction.Pure, true)); + break; + case Opcodes.INVOKEVIRTUAL: + invokedKeys.add(new Key(new Method((MethodInsnNode)insn), Direction.Pure, false)); + break; + default: + break; + } + } + + if (invokedKeys.isEmpty()) { + return new Equation(key, FINAL_PURE); + } + else { + HashSet> sumOfProducts = new HashSet>(); + for (Key call : invokedKeys) { + sumOfProducts.add(new Product(Value.Top, Collections.singleton(call))); + } + return new Equation(key, new Pending(sumOfProducts)); + } + } +}