IDEA-144129: support for inferred negated contracts

This commit is contained in:
Ilya Klyuchnikov
2015-09-25 15:01:31 +03:00
parent 6bebad8b8a
commit ac3ee3b425
10 changed files with 372 additions and 28 deletions
@@ -107,7 +107,7 @@ public class BytecodeAnalysisConverter {
byte[] digest = new byte[HASH_SIZE];
System.arraycopy(classDigest, 0, digest, 0, CLASS_HASH_SIZE);
System.arraycopy(sigDigest, 0, digest, CLASS_HASH_SIZE, SIGNATURE_HASH_SIZE);
return new HKey(digest, mkDirectionKey(key.direction), key.stable);
return new HKey(digest, mkDirectionKey(key.direction), key.stable, key.negated);
}
/**
@@ -131,7 +131,7 @@ public class BytecodeAnalysisConverter {
byte[] digest = new byte[HASH_SIZE];
System.arraycopy(classDigest, 0, digest, 0, CLASS_HASH_SIZE);
System.arraycopy(sigDigest, 0, digest, CLASS_HASH_SIZE, SIGNATURE_HASH_SIZE);
return new HKey(digest, mkDirectionKey(direction), true);
return new HKey(digest, mkDirectionKey(direction), true, false);
}
@Nullable
@@ -40,7 +40,7 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
private static final ClassDataIndexer INDEXER = new ClassDataIndexer();
private static final HKeyDescriptor KEY_DESCRIPTOR = new HKeyDescriptor();
private static final int ourInternalVersion = 4;
private static final int ourInternalVersion = 5;
private static final boolean ourEnabled = SystemProperties.getBooleanProperty("idea.enable.bytecode.contract.inference", true);
@NotNull
@@ -145,7 +145,8 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
DataInputOutputUtil.writeINT(out, ids.length);
for (HKey hKey : ids) {
out.write(hKey.key);
DataInputOutputUtil.writeINT(out, hKey.dirKey);
int rawDirKey = hKey.negated ? -hKey.dirKey : hKey.dirKey;
DataInputOutputUtil.writeINT(out, rawDirKey);
out.writeBoolean(hKey.stable);
}
}
@@ -180,7 +181,8 @@ public class BytecodeAnalysisIndex extends FileBasedIndexExtension<Bytes, HEquat
for (int bi = 0; bi < bytes.length; bi++) {
bytes[bi] = in.readByte();
}
ids[j] = new HKey(bytes, DataInputOutputUtil.readINT(in), in.readBoolean());
int rawDirKey = DataInputOutputUtil.readINT(in);
ids[j] = new HKey(bytes, Math.abs(rawDirKey), in.readBoolean(), rawDirKey < 0);
}
components[i] = new HComponent(value, ids);
}
@@ -166,7 +166,8 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
if (branching) {
RichControlFlow richControlFlow = new RichControlFlow(graph, dfs);
if (richControlFlow.reducible()) {
processBranchingMethod(method, methodNode, richControlFlow, argumentTypes, isReferenceResult, isInterestingResult, stable, jsr, equations);
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);
}
LOG.debug(method + ": CFG is not reducible");
@@ -190,15 +191,107 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
}
private NegationAnalysis tryNegation(final Method method,
final Type[] argumentTypes,
final ControlFlowGraph graph,
final boolean isBooleanResult,
final DFSTree dfs,
final boolean jsr) throws AnalyzerException {
class Util {
boolean isMethodCall(int opCode) {
return opCode == Opcodes.INVOKESTATIC ||
opCode == Opcodes.INVOKESPECIAL ||
opCode == Opcodes.INVOKEVIRTUAL ||
opCode == Opcodes.INVOKEINTERFACE;
}
boolean singleIfBranch() {
int branch = 0;
for (int i = 0; i < graph.transitions.length; i++) {
int[] transition = graph.transitions[i];
if (transition.length == 2) {
branch++;
int opCode = graph.methodNode.instructions.get(i).getOpcode();
boolean isIfInsn = opCode == Opcodes.IFEQ || opCode == Opcodes.IFNE;
if (!isIfInsn) {
return false;
}
}
if (branch > 1)
return false;
}
return branch == 1;
}
boolean singleMethodCall() {
int callCount = 0;
for (int i = 0; i < graph.transitions.length; i++) {
if (isMethodCall(graph.methodNode.instructions.get(i).getOpcode())) {
callCount++;
if (callCount > 1) {
return false;
}
}
}
return callCount == 1;
}
public boolean booleanConstResult() {
try {
final boolean[] origins =
OriginsAnalysis.resultOrigins(
leakingParametersAndFrames(method, graph.methodNode, argumentTypes, jsr).frames,
graph.methodNode.instructions,
graph);
for (int i = 0; i < origins.length; i++) {
if (origins[i]) {
int opCode = graph.methodNode.instructions.get(i).getOpcode();
boolean isBooleanConst = opCode == Opcodes.ICONST_0 || opCode == Opcodes.ICONST_1;
if (!isBooleanConst) {
return false;
}
}
}
return true;
}
catch (AnalyzerException ignore) {
}
return false;
}
}
if (graph.methodNode.instructions.size() < 20 && isBooleanResult && dfs.back.isEmpty() && !jsr) {
Util util = new Util();
if (util.singleIfBranch() && util.singleMethodCall() && util.booleanConstResult()) {
NegationAnalysis analyzer = new NegationAnalysis(method, graph);
try {
analyzer.analyze();
return analyzer;
}
catch (NegationAnalysisFailure ignore) {
return null;
}
}
}
return null;
}
private void processBranchingMethod(final Method method,
final MethodNode methodNode,
final RichControlFlow richControlFlow,
Type[] argumentTypes,
boolean isReferenceResult,
boolean isInterestingResult,
boolean isBooleanResult,
final boolean stable,
boolean jsr,
List<Equation<Key, Value>> result) throws AnalyzerException {
List<Equation<Key, Value>> result,
NegationAnalysis negatedAnalysis) throws AnalyzerException {
boolean isInterestingResult = isBooleanResult || isReferenceResult;
boolean maybeLeakingParameter = isInterestingResult;
for (Type argType : argumentTypes) {
if (ASMUtils.isReferenceType(argType)) {
@@ -268,9 +361,19 @@ public class ClassDataIndexer implements DataIndexer<Bytes, HEquations, FileCont
}
else {
// may be null on some branch, running "null->..." analysis
result.add(new InOutAnalysis(richControlFlow, new InOut(i, Value.Null), origins, stable, sharedPendingStates).analyze());
if (isBooleanResult && negatedAnalysis != null) {
result.add(negatedAnalysis.contractEquation(i, Value.Null, stable));
}
else {
result.add(new InOutAnalysis(richControlFlow, new InOut(i, Value.Null), origins, stable, sharedPendingStates).analyze());
}
}
if (isBooleanResult && negatedAnalysis != null) {
result.add(negatedAnalysis.contractEquation(i, Value.NotNull, stable));
}
else {
result.add(new InOutAnalysis(richControlFlow, new InOut(i, Value.NotNull), origins, stable, sharedPendingStates).analyze());
}
result.add(new InOutAnalysis(richControlFlow, new InOut(i, Value.NotNull), origins, stable, sharedPendingStates).analyze());
}
else {
// parameter is not leaking, so a contract is the same as for the whole method
@@ -345,7 +345,7 @@ final class CombinedAnalysis {
Type[] args = Type.getArgumentTypes(methodNode.desc);
int local = 0;
if ((methodNode.access & Opcodes.ACC_STATIC) == 0) {
if ((methodNode.access & ACC_STATIC) == 0) {
frame.setLocal(local++, ThisValue);
}
for (int i = 0; i < args.length; i++) {
@@ -582,3 +582,196 @@ final class CombinedInterpreter extends BasicInterpreter {
return track(origin, super.naryOperation(insn, values));
}
}
class NegationAnalysisFailure extends Exception {
}
final class NegationAnalysis {
private final ControlFlowGraph controlFlow;
private final Method method;
private final NegationInterpreter interpreter;
private final MethodNode methodNode;
private TrackableCallValue conditionValue;
private BasicValue trueBranchValue;
private BasicValue falseBranchValue;
NegationAnalysis(Method method, ControlFlowGraph controlFlow) {
this.method = method;
this.controlFlow = controlFlow;
methodNode = controlFlow.methodNode;
interpreter = new NegationInterpreter(methodNode.instructions);
}
private static void checkAssertion(boolean assertion) throws NegationAnalysisFailure {
if (!assertion) {
throw new NegationAnalysisFailure();
}
}
final void analyze() throws AnalyzerException, NegationAnalysisFailure {
Frame<BasicValue> frame = createStartFrame();
int insnIndex = 0;
while (true) {
AbstractInsnNode insnNode = methodNode.instructions.get(insnIndex);
switch (insnNode.getType()) {
case AbstractInsnNode.LABEL:
case AbstractInsnNode.LINE:
case AbstractInsnNode.FRAME:
insnIndex = controlFlow.transitions[insnIndex][0];
break;
default:
switch (insnNode.getOpcode()) {
case IFEQ:
case IFNE:
BasicValue conValue = popValue(frame);
checkAssertion(conValue instanceof TrackableCallValue);
frame.execute(insnNode, interpreter);
conditionValue = (TrackableCallValue)conValue;
int jumpIndex = methodNode.instructions.indexOf(((JumpInsnNode)insnNode).label);
int nextIndex = insnIndex + 1;
proceedBranch(frame, jumpIndex, IFNE == insnNode.getOpcode());
proceedBranch(frame, nextIndex, IFEQ == insnNode.getOpcode());
checkAssertion(FalseValue == trueBranchValue);
checkAssertion(TrueValue == falseBranchValue);
return;
default:
frame.execute(insnNode, interpreter);
insnIndex = controlFlow.transitions[insnIndex][0];
}
}
}
}
private void proceedBranch(Frame<BasicValue> startFrame, int startIndex, boolean branchValue)
throws NegationAnalysisFailure, AnalyzerException {
Frame<BasicValue> frame = new Frame<BasicValue>(startFrame);
int insnIndex = startIndex;
while (true) {
AbstractInsnNode insnNode = methodNode.instructions.get(insnIndex);
switch (insnNode.getType()) {
case AbstractInsnNode.LABEL:
case AbstractInsnNode.LINE:
case AbstractInsnNode.FRAME:
insnIndex = controlFlow.transitions[insnIndex][0];
break;
default:
switch (insnNode.getOpcode()) {
case IRETURN:
BasicValue returnValue = frame.pop();
if (branchValue) {
trueBranchValue = returnValue;
}
else {
falseBranchValue = returnValue;
}
return;
default:
checkAssertion(controlFlow.transitions[insnIndex].length == 1);
frame.execute(insnNode, interpreter);
insnIndex = controlFlow.transitions[insnIndex][0];
}
}
}
}
final Equation<Key, Value> contractEquation(int i, Value inValue, boolean stable) {
final Key key = new Key(method, new InOut(i, inValue), stable);
final Result<Key, Value> result;
HashSet<Key> keys = new HashSet<Key>();
for (int argI = 0; argI < conditionValue.args.size(); argI++) {
BasicValue arg = conditionValue.args.get(argI);
if (arg instanceof NthParamValue) {
NthParamValue npv = (NthParamValue)arg;
if (npv.n == i) {
keys.add(new Key(conditionValue.method, new InOut(argI, inValue), conditionValue.stableCall, true));
}
}
}
if (keys.isEmpty()) {
result = new Final<Key, Value>(Value.Top);
} else {
result = new Pending<Key, Value>(new SingletonSet<Product<Key, Value>>(new Product<Key, Value>(Value.Top, keys)));
}
return new Equation<Key, Value>(key, result);
}
final Frame<BasicValue> createStartFrame() {
Frame<BasicValue> frame = new Frame<BasicValue>(methodNode.maxLocals, methodNode.maxStack);
Type returnType = Type.getReturnType(methodNode.desc);
BasicValue returnValue = Type.VOID_TYPE.equals(returnType) ? null : new BasicValue(returnType);
frame.setReturn(returnValue);
Type[] args = Type.getArgumentTypes(methodNode.desc);
int local = 0;
if ((methodNode.access & ACC_STATIC) == 0) {
frame.setLocal(local++, ThisValue);
}
for (int i = 0; i < args.length; i++) {
BasicValue value = new NthParamValue(args[i], i);
frame.setLocal(local++, value);
if (args[i].getSize() == 2) {
frame.setLocal(local++, BasicValue.UNINITIALIZED_VALUE);
}
}
while (local < methodNode.maxLocals) {
frame.setLocal(local++, BasicValue.UNINITIALIZED_VALUE);
}
return frame;
}
private static BasicValue popValue(Frame<BasicValue> frame) {
return frame.getStack(frame.getStackSize() - 1);
}
}
final class NegationInterpreter extends BasicInterpreter {
private final InsnList insns;
NegationInterpreter(InsnList insns) {
this.insns = insns;
}
@Override
public BasicValue newOperation(AbstractInsnNode insn) throws AnalyzerException {
switch (insn.getOpcode()) {
case ICONST_0:
return FalseValue;
case ICONST_1:
return TrueValue;
default:
return super.newOperation(insn);
}
}
@Override
public BasicValue naryOperation(AbstractInsnNode insn, List<? extends BasicValue> values) throws AnalyzerException {
int opCode = insn.getOpcode();
int shift = opCode == INVOKESTATIC ? 0 : 1;
int origin = insns.indexOf(insn);
switch (opCode) {
case INVOKESTATIC:
case INVOKESPECIAL:
case INVOKEVIRTUAL:
case INVOKEINTERFACE:
boolean stable = opCode == INVOKESTATIC || opCode == INVOKESPECIAL;
MethodInsnNode mNode = (MethodInsnNode)insn;
Method method = new Method(mNode.owner, mNode.name, mNode.desc);
Type retType = Type.getReturnType(mNode.desc);
BasicValue receiver = null;
if (shift == 1) {
receiver = values.remove(0);
}
boolean thisCall = (opCode == INVOKEINTERFACE || opCode == INVOKEVIRTUAL) && receiver == ThisValue;
return new TrackableCallValue(origin, retType, method, values, stable, thisCall);
default:
return super.naryOperation(insn, values);
}
}
}
@@ -198,11 +198,20 @@ final class Key {
final Method method;
final Direction direction;
final boolean stable;
final boolean negated;
Key(Method method, Direction direction, boolean stable) {
this.method = method;
this.direction = direction;
this.stable = stable;
this.negated = false;
}
Key(Method method, Direction direction, boolean stable, boolean negated) {
this.method = method;
this.direction = direction;
this.stable = stable;
this.negated = negated;
}
@Override
@@ -29,11 +29,13 @@ final class HKey {
final byte[] key;
final int dirKey;
final boolean stable;
final boolean negated;
HKey(@NotNull byte[] key, int dirKey, boolean stable) {
HKey(@NotNull byte[] key, int dirKey, boolean stable, boolean negated) {
this.key = key;
this.dirKey = dirKey;
this.stable = stable;
this.negated = negated;
}
@Override
@@ -43,6 +45,7 @@ final class HKey {
HKey hKey = (HKey)o;
if (dirKey != hKey.dirKey) return false;
if (stable != hKey.stable) return false;
if (negated != hKey.negated) return false;
if (!Arrays.equals(key, hKey.key)) return false;
return true;
}
@@ -52,27 +55,32 @@ final class HKey {
int result = Arrays.hashCode(key);
result = 31 * result + dirKey;
result = 31 * result + (stable ? 1 : 0);
result = 31 * result + (negated ? 1 : 0);
return result;
}
HKey negate() {
return new HKey(key, dirKey, !stable);
HKey invertStability() {
return new HKey(key, dirKey, !stable, negated);
}
HKey mkStable() {
return stable ? this : new HKey(key, dirKey, true);
return stable ? this : new HKey(key, dirKey, true, negated);
}
HKey mkUnstable() {
return stable ? new HKey(key, dirKey, false) : this;
return stable ? new HKey(key, dirKey, false, negated) : this;
}
public HKey mkBase() {
return dirKey == 0 ? this : new HKey(key, 0, stable);
return dirKey == 0 ? this : new HKey(key, 0, stable, false);
}
HKey updateDirection(int newDirKey) {
return new HKey(key, newDirKey, stable);
return new HKey(key, newDirKey, stable, false);
}
HKey negate() {
return new HKey(key, dirKey, stable, true);
}
}
@@ -283,7 +283,7 @@ public class ProjectBytecodeAnalysis {
(Value.NotNull == notNullSolutions.get(notNullKey)) || (Value.NotNull == notNullSolutions.get(notNullKey.mkUnstable()));
final Solver nullableSolver = new Solver(new ELattice<Value>(Value.Null, Value.Top), Value.Top);
final HKey nullableKey = new HKey(notNullKey.key, notNullKey.dirKey + 1, true);
final HKey nullableKey = new HKey(notNullKey.key, notNullKey.dirKey + 1, true, false);
collectEquations(Collections.singletonList(nullableKey), nullableSolver, equationsCache);
HashMap<HKey, Value> nullableSolutions = nullableSolver.solve();
// subtle point
@@ -313,7 +313,7 @@ public class ProjectBytecodeAnalysis {
collectSingleEquation(nullableKey, nullableMethodSolver, equationsCache);
}
HashMap<HKey, Value> nullableSolutions = nullableMethodSolver.solve();
if (nullableSolutions.get(nullableKey) == Value.Null || nullableSolutions.get(nullableKey.negate()) == Value.Null) {
if (nullableSolutions.get(nullableKey) == Value.Null || nullableSolutions.get(nullableKey.invertStability()) == Value.Null) {
result.nullables.add(key);
}
}
@@ -354,7 +354,7 @@ public class ProjectBytecodeAnalysis {
if (dirKey == hKey.dirKey) {
HResult result = pair.hResult;
solver.addEquation(new HEquation(new HKey(bytes.bytes, dirKey, stable), result));
solver.addEquation(new HEquation(new HKey(bytes.bytes, dirKey, stable, false), result));
if (result instanceof HPending) {
HPending pending = (HPending)result;
for (HComponent component : pending.delta) {
@@ -392,7 +392,7 @@ public class ProjectBytecodeAnalysis {
int dirKey = pair.directionKey;
if (dirKey == hKey.dirKey) {
HResult result = pair.hResult;
solver.addEquation(new HEquation(new HKey(bytes.bytes, dirKey, stable), result));
solver.addEquation(new HEquation(new HKey(bytes.bytes, dirKey, stable, false), result));
}
}
}
@@ -281,7 +281,7 @@ final class Solver {
if (previousEquation == null) {
equations.put(coreKey, equation);
} else {
HKey joinKey = new HKey(coreKey.key, coreKey.dirKey, equation.key.stable && previousEquation.key.stable);
HKey joinKey = new HKey(coreKey.key, coreKey.dirKey, equation.key.stable && previousEquation.key.stable, true);
HResult joinResult = resultUtil.join(equation.result, previousEquation.result);
HEquation joinEquation = new HEquation(joinKey, joinResult);
equations.put(coreKey, joinEquation);
@@ -317,6 +317,17 @@ final class Solver {
}
}
Value negate(Value value) {
switch (value) {
case True:
return Value.False;
case False:
return Value.True;
default:
return value;
}
}
HashMap<HKey, Value> solve() {
for (HEquation hEquation : equations.values()) {
queueEquation(hEquation);
@@ -325,8 +336,11 @@ final class Solver {
HKey id = moving.pop();
Value value = solved.get(id);
HKey[] pIds = id.stable ? new HKey[]{id, id.negate()} : new HKey[]{id.negate(), id};
Value[] pVals = id.stable ? new Value[]{value, value} : new Value[]{value, unstableValue};
HKey[] initialPIds = id.stable ? new HKey[]{id, id.invertStability()} : new HKey[]{id.invertStability(), id};
Value[] initialPVals = id.stable ? new Value[]{value, value} : new Value[]{value, unstableValue};
HKey[] pIds = new HKey[]{initialPIds[0], initialPIds[1], initialPIds[0].negate(), initialPIds[1].negate()};
Value[] pVals = new Value[]{initialPVals[0], initialPVals[1], negate(initialPVals[0]), negate(initialPVals[1])};
for (int i = 0; i < pIds.length; i++) {
HKey pId = pIds[i];
@@ -1153,7 +1153,7 @@
</item>
<item name="org.apache.commons.lang.BooleanUtils boolean isNotFalse(java.lang.Boolean)">
<annotation name="org.jetbrains.annotations.Contract">
<val val="pure=true"/>
<val val="value=&quot;null-&gt;true&quot;,pure=true"/>
</annotation>
</item>
<item name="org.apache.commons.lang.BooleanUtils boolean isNotTrue(java.lang.Boolean) 0">
@@ -1161,7 +1161,7 @@
</item>
<item name="org.apache.commons.lang.BooleanUtils boolean isNotTrue(java.lang.Boolean)">
<annotation name="org.jetbrains.annotations.Contract">
<val val="pure=true"/>
<val val="value=&quot;null-&gt;true&quot;,pure=true"/>
</annotation>
</item>
<item name="org.apache.commons.lang.BooleanUtils boolean isTrue(java.lang.Boolean) 0">
@@ -2612,12 +2612,17 @@
<item name="org.apache.commons.lang.StringUtils boolean isNotBlank(java.lang.String) 0">
<annotation name="org.jetbrains.annotations.Nullable"/>
</item>
<item name="org.apache.commons.lang.StringUtils boolean isNotBlank(java.lang.String)">
<annotation name="org.jetbrains.annotations.Contract">
<val val="&quot;null-&gt;false&quot;"/>
</annotation>
</item>
<item name="org.apache.commons.lang.StringUtils boolean isNotEmpty(java.lang.String) 0">
<annotation name="org.jetbrains.annotations.Nullable"/>
</item>
<item name="org.apache.commons.lang.StringUtils boolean isNotEmpty(java.lang.String)">
<annotation name="org.jetbrains.annotations.Contract">
<val val="pure=true"/>
<val val="value=&quot;null-&gt;false&quot;,pure=true"/>
</annotation>
</item>
<item name="org.apache.commons.lang.StringUtils boolean isNumeric(java.lang.String) 0">
@@ -79,4 +79,14 @@ public class Test01 {
return new Test01();
}
@ExpectContract(value = "!null->false;null->true", pure = true)
static boolean isNull(Object o) {
return o == null;
}
@ExpectContract(value = "!null->true;null->false", pure = true)
static boolean isNotNull(Object o) {
return !isNull(o);
}
}