IDEA-200232 Support numeric casts in dataflow

This commit is contained in:
Tagir Valeev
2018-10-10 11:22:53 +07:00
parent 92c7436e21
commit aff8f9f280
7 changed files with 265 additions and 45 deletions
@@ -31,7 +31,6 @@ import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FList;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.numeric.UnnecessaryExplicitNumericCastInspection;
import com.siyeh.ig.psiutils.*;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
@@ -1977,23 +1976,15 @@ public class ControlFlowAnalyzer extends JavaElementVisitor {
if (operand != null) {
operand.accept(this);
generateBoxingUnboxingInstructionFor(operand, castExpression.getType());
generateBoxingUnboxingInstructionFor(castExpression, operand.getType(), castExpression.getType());
}
else {
addInstruction(new PushInstruction(myFactory.createTypeValue(castExpression.getType(), Nullability.UNKNOWN), null));
}
final PsiTypeElement typeElement = castExpression.getCastType();
if (typeElement != null && operand != null && operand.getType() != null) {
if (typeElement.getType() instanceof PsiPrimitiveType &&
!UnnecessaryExplicitNumericCastInspection.isUnnecessaryPrimitiveNumericCast(castExpression)) {
if (!typeElement.getType().equals(PsiPrimitiveType.getUnboxedType(operand.getType()))) {
addInstruction(new PopInstruction());
pushUnknown();
}
} else {
addInstruction(new TypeCastInstruction(castExpression, operand, typeElement.getType()));
}
if (typeElement != null && operand != null && operand.getType() != null && !(typeElement.getType() instanceof PsiPrimitiveType)) {
addInstruction(new TypeCastInstruction(castExpression, operand, typeElement.getType()));
}
finishElement(castExpression);
}
@@ -541,11 +541,20 @@ public class StandardInstructionVisitor extends InstructionVisitor {
if (methodType == MethodCallInstruction.MethodType.CAST) {
assert qualifierValue != null;
if (qualifierValue instanceof DfaConstValue && type != null) {
Object casted = TypeConversionUtil.computeCastTo(((DfaConstValue)qualifierValue).getValue(), type);
if (qualifierValue instanceof DfaVariableValue && TypeConversionUtil.isSafeConversion(type, qualifierValue.getType())) {
return qualifierValue;
}
DfaConstValue constValue = state.getConstantValue(qualifierValue);
if (constValue != null && type != null) {
Object casted = TypeConversionUtil.computeCastTo(constValue.getValue(), type);
return factory.getConstFactory().createFromValue(casted, type);
}
return qualifierValue;
if (type instanceof PsiPrimitiveType && TypeConversionUtil.isIntegralNumberType(type)) {
LongRangeSet range = state.getValueFact(qualifierValue, DfaFactType.RANGE);
if (range == null) range = LongRangeSet.all();
return factory.getFactValue(DfaFactType.RANGE, range.castTo((PsiPrimitiveType)type));
}
return DfaUnknownValue.getInstance();
}
if (type != null && !(type instanceof PsiPrimitiveType)) {
@@ -6,6 +6,7 @@ import com.intellij.codeInspection.dataFlow.DfaFactType;
import com.intellij.codeInspection.dataFlow.value.*;
import com.intellij.psi.*;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.ThreeState;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Contract;
@@ -192,6 +193,8 @@ public abstract class LongRangeSet {
return null;
}
public abstract LongRangeSet castTo(PsiPrimitiveType type);
/**
* Returns a range which represents all the possible values after applying {@link Math#abs(int)} or {@link Math#abs(long)}
* to the values from this set
@@ -404,6 +407,12 @@ public abstract class LongRangeSet {
if (leftFrom == leftTo && rightFrom == rightTo) {
return point(leftFrom & rightFrom);
}
if (leftFrom == leftTo && Long.bitCount(leftFrom+1) == 1) {
return bitwiseMask(rightFrom, rightTo, leftFrom);
}
if (rightFrom == rightTo && Long.bitCount(rightFrom+1) == 1) {
return bitwiseMask(leftFrom, leftTo, rightFrom);
}
ThreeState[] leftBits = bits(leftFrom, leftTo);
ThreeState[] rightBits = bits(rightFrom, rightTo);
ThreeState[] resultBits = new ThreeState[Long.SIZE];
@@ -421,6 +430,22 @@ public abstract class LongRangeSet {
return fromBits(resultBits);
}
/**
* Returns the range after applying the mask to the input range which looks like 0..01..1 in binary
* @param from input range start
* @param to input range end
* @param mask mask
* @return range set after applying the mask
*/
private static LongRangeSet bitwiseMask(long from, long to, long mask) {
if (to - from > mask) return range(0, mask);
long min = from & mask;
long max = to & mask;
assert min != max;
if (min < max) return range(min, max);
return new RangeSet(new long[] {0, max, min, mask});
}
/**
* Creates a set which contains all the numbers satisfying the supplied bit vector.
* Vector format is the same as returned by {@link #bits(long, long)}. The resulting set may
@@ -712,6 +737,14 @@ public abstract class LongRangeSet {
return other.isEmpty();
}
@Override
public LongRangeSet castTo(PsiPrimitiveType type) {
if (TypeConversionUtil.isIntegralNumberType(type)) {
return this;
}
throw new IllegalArgumentException(type.toString());
}
@NotNull
@Override
public LongRangeSet abs(boolean isLong) {
@@ -804,6 +837,28 @@ public abstract class LongRangeSet {
return other.isEmpty() || equals(other);
}
@Override
public LongRangeSet castTo(PsiPrimitiveType type) {
if (PsiType.LONG.equals(type)) return this;
long newValue;
if (PsiType.CHAR.equals(type)) {
newValue = (char)myValue;
}
else if (PsiType.INT.equals(type)) {
newValue = (int)myValue;
}
else if (PsiType.SHORT.equals(type)) {
newValue = (short)myValue;
}
else if (PsiType.BYTE.equals(type)) {
newValue = (byte)myValue;
}
else {
throw new IllegalArgumentException(type.toString());
}
return newValue == myValue ? this : point(newValue);
}
@NotNull
@Override
public LongRangeSet abs(boolean isLong) {
@@ -924,10 +979,8 @@ public abstract class LongRangeSet {
if (from <= myFrom) {
return range(to + 1, myTo);
}
if (to >= myTo) {
return range(myFrom, from - 1);
}
throw new InternalError("Impossible: " + this + ":" + other);
assert to >= myTo;
return range(myFrom, from - 1);
}
long[] ranges = ((RangeSet)other).myRanges;
LongRangeSet result = this;
@@ -998,6 +1051,35 @@ public abstract class LongRangeSet {
return other.isEmpty() || other.min() >= myFrom && other.max() <= myTo;
}
@Override
public LongRangeSet castTo(PsiPrimitiveType type) {
if (PsiType.LONG.equals(type)) return this;
if (PsiType.BYTE.equals(type)) {
return mask(Byte.SIZE, type);
}
if (PsiType.SHORT.equals(type)) {
return mask(Short.SIZE, type);
}
if (PsiType.INT.equals(type)) {
return mask(Integer.SIZE, type);
}
if (PsiType.CHAR.equals(type)) {
if (myFrom <= Character.MIN_VALUE && myTo >= Character.MAX_VALUE) return CHAR_RANGE;
if (myFrom >= Character.MIN_VALUE && myTo <= Character.MAX_VALUE) return this;
return bitwiseAnd(point(0xFFFF));
}
throw new IllegalArgumentException(type.toString());
}
@NotNull
private LongRangeSet mask(int size, PsiPrimitiveType type) {
long addend = 1L << (size - 1);
if (myFrom <= -addend && myTo >= addend - 1) return Objects.requireNonNull(fromType(type));
if (myFrom >= -addend && myTo <= addend - 1) return this;
long mask = (1L << size) - 1;
return plus(myFrom, myTo, addend, addend, true).bitwiseAnd(point(mask)).plus(point(-addend), true);
}
@NotNull
@Override
public LongRangeSet abs(boolean isLong) {
@@ -1233,6 +1315,15 @@ public abstract class LongRangeSet {
return false;
}
@Override
public LongRangeSet castTo(PsiPrimitiveType type) {
LongRangeSet result = all();
for (int i = 0; i < myRanges.length; i += 2) {
result = result.subtract(range(myRanges[i], myRanges[i + 1]).castTo(type));
}
return all().subtract(result);
}
@NotNull
@Override
public LongRangeSet abs(boolean isLong) {
@@ -1477,6 +1477,40 @@ public class TypeConversionUtil {
parameter.putUserData(LOWER_BOUND, lowerBound);
}
/**
* Returns true if numeric conversion (widening or narrowing) does not lose the information.
* This differs slightly from {@link #isAssignable(PsiType, PsiType)} result as some assignable types
* still may lose the information. E.g. {@code double doubleVar = longVar} may lose round the long value.
*
* @param target target type
* @param source source type
* @return true if numeric conversion (widening or narrowing) does not lose the information.
*/
public static boolean isSafeConversion(PsiType target, PsiType source) {
/* From \ To byte short char int long float double
* byte + + - + + + +
* short - + - + + + +
* char - - + + + + +
* int - - - + + - +
* long - - - - + - -
* float - - - - - + +
* double - - - - - - +
*/
if (target == null || source == null) return false;
if (target.equals(source)) return true;
int sourceRank = TYPE_TO_RANK_MAP.get(source);
int targetRank = TYPE_TO_RANK_MAP.get(target);
if (sourceRank == 0 || sourceRank > MAX_NUMERIC_RANK ||
targetRank == 0 || targetRank > MAX_NUMERIC_RANK ||
!IS_ASSIGNABLE_BIT_SET[sourceRank-1][targetRank-1]) {
return false;
}
if (PsiType.INT.equals(source) && PsiType.FLOAT.equals(target)) return false;
if (PsiType.LONG.equals(source) && isFloatOrDoubleType(target)) return false;
return true;
}
@FunctionalInterface
private interface Caster {
@NotNull
@@ -0,0 +1,22 @@
class NumericCast {
void test(int i) {
if(i > 10 && i < 200) {
byte b = (byte) i;
if (<warning descr="Condition 'b == 0' is always 'false'">b == 0</warning>) {
System.out.println("impossible");
}
}
byte b = 100;
b+=100;
if (<warning descr="Condition 'b == -56' is always 'true'">b == -56</warning>) {
System.out.println("always");
}
}
void testMask(long x) {
int bits = (int) ((x >> 16) & 0xFFFF);
if (<warning descr="Condition 'bits >= 0' is always 'true'">bits >= 0</warning>) {
System.out.println("Always");
}
}
}
@@ -660,4 +660,5 @@ public class DataFlowInspectionTest extends DataFlowInspectionTestCase {
public void testAlwaysTrueSwitchLabel() { doTest(); }
public void testWideningToDouble() { doTest(); }
public void testCompoundAssignment() { doTest(); }
public void testNumericCast() { doTest(); }
}
@@ -17,7 +17,10 @@ package com.intellij.java.codeInspection.dataFlow.rangeSet;
import com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet;
import com.intellij.codeInspection.dataFlow.value.DfaRelationValue.RelationType;
import com.intellij.psi.PsiPrimitiveType;
import com.intellij.psi.PsiType;
import com.intellij.psi.util.TypeConversionUtil;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
import java.util.HashMap;
@@ -25,12 +28,27 @@ import java.util.Random;
import java.util.function.Function;
import java.util.function.LongBinaryOperator;
import java.util.function.LongPredicate;
import java.util.function.LongUnaryOperator;
import java.util.stream.Collectors;
import static com.intellij.codeInspection.dataFlow.rangeSet.LongRangeSet.*;
import static org.junit.Assert.*;
public class LongRangeSetTest {
@NotNull
private static LongRangeSet fromTypeStrict(PsiType type) {
LongRangeSet range = fromType(type);
assertNotNull(range);
return range;
}
@NotNull
private static LongRangeSet fromConstantStrict(Object constant) {
LongRangeSet range = fromConstant(constant);
assertNotNull(range);
return range;
}
@Test
public void testToString() {
assertEquals("{}", empty().toString());
@@ -44,12 +62,12 @@ public class LongRangeSetTest {
public void testFromType() {
assertNull(fromType(PsiType.FLOAT));
assertNull(fromType(PsiType.NULL));
assertEquals("{-128..127}", fromType(PsiType.BYTE).toString());
assertEquals("{0..65535}", fromType(PsiType.CHAR).toString());
assertEquals("{-32768..32767}", fromType(PsiType.SHORT).toString());
assertEquals("{Integer.MIN_VALUE..Integer.MAX_VALUE}", fromType(PsiType.INT).toString());
assertEquals("{-128..127}", fromTypeStrict(PsiType.BYTE).toString());
assertEquals("{0..65535}", fromTypeStrict(PsiType.CHAR).toString());
assertEquals("{-32768..32767}", fromTypeStrict(PsiType.SHORT).toString());
assertEquals("{Integer.MIN_VALUE..Integer.MAX_VALUE}", fromTypeStrict(PsiType.INT).toString());
assertEquals("{0..Integer.MAX_VALUE}", indexRange().toString());
assertEquals("{Long.MIN_VALUE..Long.MAX_VALUE}", fromType(PsiType.LONG).toString());
assertEquals("{Long.MIN_VALUE..Long.MAX_VALUE}", fromTypeStrict(PsiType.LONG).toString());
}
@Test
@@ -84,7 +102,7 @@ public class LongRangeSetTest {
assertEquals("{Long.MIN_VALUE}", all().subtract(range(Long.MIN_VALUE + 1, Long.MAX_VALUE)).toString());
assertEquals("{Long.MAX_VALUE}", all().subtract(range(Long.MIN_VALUE, Long.MAX_VALUE - 1)).toString());
assertTrue(all().subtract(range(Long.MIN_VALUE, Long.MAX_VALUE)).isEmpty());
assertEquals(indexRange(), fromType(PsiType.INT).subtract(range(Long.MIN_VALUE, (long)-1)));
assertEquals(indexRange(), fromTypeStrict(PsiType.INT).subtract(range(Long.MIN_VALUE, (long)-1)));
assertTrue(all().subtract(all()).isEmpty());
}
@@ -102,9 +120,9 @@ public class LongRangeSetTest {
assertEquals("{0..2, 5..15, 19, 20}",
range(0, 20).subtract(range(3, 18).subtract(range(5, 15))).toString());
LongRangeSet first = fromType(PsiType.CHAR).without(45);
LongRangeSet first = fromTypeStrict(PsiType.CHAR).without(45);
LongRangeSet second =
fromType(PsiType.CHAR).without(32).without(40).without(44).without(45).without(46).without(58).without(59).without(61);
fromTypeStrict(PsiType.CHAR).without(32).without(40).without(44).without(45).without(46).without(58).without(59).without(61);
assertEquals("{0..44, 46..65535}", first.toString());
assertEquals("{0..31, 33..39, 41..43, 47..57, 60, 62..65535}", second.toString());
assertEquals("{32, 40, 44, 46, 58, 59, 61}", first.subtract(second).toString());
@@ -118,7 +136,7 @@ public class LongRangeSetTest {
map.put(range(10, 10), "10-10");
map.put(range(10, 11), "10-11");
map.put(range(10, 12), "10-12");
LongRangeSet longNotChar = fromType(PsiType.LONG).subtract(fromType(PsiType.CHAR));
LongRangeSet longNotChar = fromTypeStrict(PsiType.LONG).subtract(fromTypeStrict(PsiType.CHAR));
map.put(longNotChar, "longNotChar");
assertEquals("empty", map.get(empty()));
@@ -126,13 +144,13 @@ public class LongRangeSetTest {
assertEquals("10-11", map.get(range(10, 11)));
assertEquals("10-12", map.get(range(10, 12)));
assertNull(map.get(range(11, 11)));
assertEquals("longNotChar", map.get(fromType(PsiType.LONG).subtract(fromType(PsiType.CHAR))));
assertEquals("longNotChar", map.get(fromTypeStrict(PsiType.LONG).subtract(fromTypeStrict(PsiType.CHAR))));
}
@Test
public void testIntersects() {
assertFalse(empty().intersects(fromType(PsiType.LONG)));
assertTrue(point(Long.MIN_VALUE).intersects(fromType(PsiType.LONG)));
assertFalse(empty().intersects(fromTypeStrict(PsiType.LONG)));
assertTrue(point(Long.MIN_VALUE).intersects(fromTypeStrict(PsiType.LONG)));
assertFalse(point(10).intersects(point(11)));
assertTrue(point(10).intersects(point(10)));
@@ -202,8 +220,8 @@ public class LongRangeSetTest {
assertTrue(message, intersection.min() >= Math.max(left.min(), right.min()));
assertTrue(message, intersection.max() <= Math.min(left.max(), right.max()));
}
assertEquals(message, intersection, right.subtract(fromType(PsiType.LONG).subtract(left)));
assertEquals(message, intersection, left.subtract(fromType(PsiType.LONG).subtract(right)));
assertEquals(message, intersection, right.subtract(fromTypeStrict(PsiType.LONG).subtract(left)));
assertEquals(message, intersection, left.subtract(fromTypeStrict(PsiType.LONG).subtract(right)));
intersection.stream().limit(1000).forEach(e -> {
assertTrue(left.contains(e));
assertTrue(right.contains(e));
@@ -232,10 +250,10 @@ public class LongRangeSetTest {
@Test
public void testFromConstant() {
assertEquals("{0}", fromConstant(0).toString());
assertEquals("{0}", fromConstant(0L).toString());
assertEquals("{1}", fromConstant((byte)1).toString());
assertEquals("{97}", fromConstant('a').toString());
assertEquals("{0}", fromConstantStrict(0).toString());
assertEquals("{0}", fromConstantStrict(0L).toString());
assertEquals("{1}", fromConstantStrict((byte)1).toString());
assertEquals("{97}", fromConstantStrict('a').toString());
assertNull(fromConstant(null));
assertNull(fromConstant(1.0));
}
@@ -248,7 +266,7 @@ public class LongRangeSetTest {
assertEquals(range(Long.MIN_VALUE, 200), range(100, 200).fromRelation(RelationType.LE));
assertEquals(range(100, 200), range(100, 200).fromRelation(RelationType.EQ));
assertNull(range(100, 200).fromRelation(RelationType.IS));
assertEquals(fromType(PsiType.LONG), range(100, 200).fromRelation(RelationType.NE));
assertEquals(fromTypeStrict(PsiType.LONG), range(100, 200).fromRelation(RelationType.NE));
assertEquals("{Long.MIN_VALUE..99, 101..Long.MAX_VALUE}", point(100).fromRelation(RelationType.NE).toString());
}
@@ -291,6 +309,37 @@ public class LongRangeSetTest {
assertEquals("{-1000..-701, -499..-101, 301..599, 801..900}", set.negate(false).toString());
}
@Test
public void testCastTo() {
PsiPrimitiveType[] types = {PsiType.BYTE, PsiType.SHORT, PsiType.CHAR, PsiType.INT, PsiType.LONG};
for (PsiPrimitiveType type : types) {
assertTrue(empty().castTo(type).isEmpty());
assertEquals(point(0), point(0).castTo(type));
}
assertEquals(point(0x1234_5678_9ABC_DEF0L), point(0x1234_5678_9ABC_DEF0L).castTo(PsiType.LONG));
assertEquals(point(0x9ABC_DEF0), point(0x1234_5678_9ABC_DEF0L).castTo(PsiType.INT));
assertEquals(point(0xDEF0), point(0x1234_5678_9ABC_DEF0L).castTo(PsiType.CHAR));
assertEquals(point(-8464), point(0x1234_5678_9ABC_DEF0L).castTo(PsiType.SHORT));
assertEquals(point(-16), point(0x1234_5678_9ABC_DEF0L).castTo(PsiType.BYTE));
LongRangeSet longSet = fromTypeStrict(PsiType.LONG);
assertNotNull(longSet);
LongRangeSet byteSet = fromTypeStrict(PsiType.BYTE);
assertNotNull(byteSet);
for (PsiPrimitiveType type : types) {
LongRangeSet set = fromTypeStrict(type);
assertNotNull(set);
assertEquals(set, set.castTo(type));
assertEquals(set, longSet.castTo(type));
assertEquals(type.equals(PsiType.CHAR) ? range(0, 127).unite(range(0xFF80, 0xFFFF)) : byteSet, byteSet.castTo(type));
}
checkCast(range(-10, 1000), "{-128..127}", PsiType.BYTE);
checkCast(range(-10, 200), "{-128..-56, -10..127}", PsiType.BYTE);
checkCast(range(-1, 255), "{0..255, 65535}", PsiType.CHAR);
checkCast(range(0, 100000), "{-32768..32767}", PsiType.SHORT);
checkCast(range(0, 50000), "{-32768..-15536, 0..32767}", PsiType.SHORT);
assertEquals(fromTypeStrict(PsiType.INT), range(Long.MIN_VALUE, Integer.MAX_VALUE-1).castTo(PsiType.INT));
}
@Test
public void testBitwiseAnd() {
assertTrue(empty().bitwiseAnd(all()).isEmpty());
@@ -371,10 +420,10 @@ public class LongRangeSetTest {
assertEquals(empty(), empty().shiftRight(all(), true));
assertEquals(empty(), all().shiftRight(empty(), true));
assertEquals(all(), all().shiftRight(all(), true));
assertEquals(fromType(PsiType.INT), all().shiftRight(point(32), true));
assertEquals(fromType(PsiType.SHORT), fromType(PsiType.INT).shiftRight(point(16), false));
assertEquals(fromType(PsiType.BYTE), fromType(PsiType.INT).shiftRight(point(24), false));
assertEquals(range(-1, 0), fromType(PsiType.INT).shiftRight(point(31), false));
assertEquals(fromTypeStrict(PsiType.INT), all().shiftRight(point(32), true));
assertEquals(fromTypeStrict(PsiType.SHORT), fromTypeStrict(PsiType.INT).shiftRight(point(16), false));
assertEquals(fromTypeStrict(PsiType.BYTE), fromTypeStrict(PsiType.INT).shiftRight(point(24), false));
assertEquals(range(-1, 0), fromTypeStrict(PsiType.INT).shiftRight(point(31), false));
checkShr(range(-20, 20), point(31), false, "{-1, 0}");
checkShr(range(-20, 20), point(31), true, "{-1, 0}");
@@ -389,9 +438,9 @@ public class LongRangeSetTest {
assertEquals(empty(), all().unsignedShiftRight(empty(), true));
assertEquals(all(), all().unsignedShiftRight(all(), true));
assertEquals(range(0, 4294967295L), all().unsignedShiftRight(point(32), true));
assertEquals(fromType(PsiType.CHAR), fromType(PsiType.INT).unsignedShiftRight(point(16), false));
assertEquals(range(0, 255), fromType(PsiType.INT).unsignedShiftRight(point(24), false));
assertEquals(range(0, 1), fromType(PsiType.INT).unsignedShiftRight(point(31), false));
assertEquals(fromTypeStrict(PsiType.CHAR), fromTypeStrict(PsiType.INT).unsignedShiftRight(point(16), false));
assertEquals(range(0, 255), fromTypeStrict(PsiType.INT).unsignedShiftRight(point(24), false));
assertEquals(range(0, 1), fromTypeStrict(PsiType.INT).unsignedShiftRight(point(31), false));
checkUShr(range(-20, 20), point(31), false, "{0, 1}");
checkUShr(range(-20, 20), point(31), true, "{0, 8589934591}");
@@ -495,5 +544,28 @@ public class LongRangeSetTest {
}
}
void checkCast(LongRangeSet operand, String expected, PsiPrimitiveType castType) {
LongRangeSet result = operand.castTo(castType);
assertEquals(expected, result.toString());
checkUnOp(operand, result,
castType.equals(PsiType.CHAR) ? x -> (char)x : x -> ((Number)TypeConversionUtil.computeCastTo(x, castType)).longValue(),
expected, castType.getCanonicalText());
}
void checkUnOp(LongRangeSet operand,
LongRangeSet result,
LongUnaryOperator operator,
String expected,
String sign) {
assertEquals(expected, result.toString());
String errors = operand.stream()
.filter(arg -> !result.contains(operator.applyAsLong(arg)))
.mapToObj(arg -> sign + " (" + arg + ") = " + operator.applyAsLong(arg))
.collect(Collectors.joining("\n"));
if (!errors.isEmpty()) {
fail("Expected range " + expected + " is not satisfied:\n" + errors);
}
}
}