diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java index f758b39fdcb..d11aacba842 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/CapturedContext.java @@ -17,7 +17,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.function.Function; /** Stores different kind of data (arguments, locals, fields, exception) for a specific location */ public class CapturedContext implements ValueReferenceResolver { @@ -134,12 +133,12 @@ public CapturedValue getMember(Object target, String memberName) { } } } else { - Map> specialTypeAccess = + Map specialTypeAccess = WellKnownClasses.getSpecialTypeAccess(target); if (specialTypeAccess != null) { - Function specialFieldAccess = specialTypeAccess.get(memberName); - if (specialFieldAccess != null) { - CapturedValue specialField = specialFieldAccess.apply(target); + WellKnownClasses.SpecialFieldInfo specialFieldInfo = specialTypeAccess.get(memberName); + if (specialFieldInfo != null) { + CapturedValue specialField = specialFieldInfo.accessor.apply(target); if (specialField != null && specialField.getName().equals(memberName)) { return specialField; } @@ -360,6 +359,14 @@ public Status getStatus(int probeIndex) { return result; } + public void addError(ProbeImplementation probeImplementation, EvaluationError evaluationError) { + Status status = + statusByProbeId.computeIfAbsent( + probeImplementation.getProbeId().getEncodedId(), + key -> probeImplementation.createStatus()); + status.addError(evaluationError); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ConditionHelper.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ConditionHelper.java new file mode 100644 index 00000000000..ee4c46ae170 --- /dev/null +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/ConditionHelper.java @@ -0,0 +1,635 @@ +package datadog.trace.bootstrap.debugger; + +import datadog.trace.bootstrap.debugger.el.ReflectiveFieldValueResolver; +import datadog.trace.bootstrap.debugger.util.WellKnownClasses; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.DoublePredicate; +import java.util.function.IntPredicate; +import java.util.function.LongPredicate; +import java.util.function.Predicate; + +public class ConditionHelper { + + // comes from ASM + private static final int IFEQ = 153; + private static final int IFLT = 155; + private static final int IFGE = 156; + private static final int IFGT = 157; + private static final int IFLE = 158; + + public static boolean equalsForEnum(Enum enumInstance, String enumValueStr) { + Class enumClass = enumInstance.getClass(); + Enum[] enumConstants = enumClass.getEnumConstants(); + for (Enum enumConstant : enumConstants) { + // Check if string constant as value expression matches for enum constant + // the endsWith allow to match either: + // - the full enum constant name (com.datadog.debugger.MyEnum.ONE) + // - the simple name with enum class name (MyEnum.ONE) + // - the simple name (ONE) + // The second check against enumValue is to ensure the instance filtered based on the + // name is still correct because the name can partially match (CLOSE in OPENCLOSE) + // with an enum defined like (OPEN, CLOSE, OPENCLOSE) + if (enumValueStr.endsWith(enumConstant.name())) { + if (enumInstance.equals(enumConstant)) { + return true; + } + } + } + return false; + } + + public static boolean equalsWithInstanceOf(Object left, String className) { + Class clazz; + try { + clazz = Class.forName(className, false, left.getClass().getClassLoader()); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Class not found: " + className); + } + return clazz.isInstance(left); + } + + public static boolean compareTo(Object left, Object right, int cmpOpcode) { + if (!(left instanceof Number) || !(right instanceof Number)) { + throw new IllegalArgumentException( + "Incompatible types for compareTo: " + left + " and " + right); + } + Number leftNumber = (Number) left; + Number rightNumber = (Number) right; + if (isNan(leftNumber, rightNumber)) { + return false; + } + switch (cmpOpcode) { + case IFEQ: + return compare(leftNumber, rightNumber) == 0; + case IFGE: + return compare(leftNumber, rightNumber) >= 0; + case IFGT: + return compare(leftNumber, rightNumber) > 0; + case IFLE: + return compare(leftNumber, rightNumber) <= 0; + case IFLT: + return compare(leftNumber, rightNumber) < 0; + default: + throw new IllegalArgumentException("Invalid cmp opcode: " + cmpOpcode); + } + } + + public static Object resolveByFieldName(Object target, String memberName) { + try { + return ReflectiveFieldValueResolver.getFieldValue(target, memberName); + } catch (NoSuchFieldException | IllegalAccessException ex) { + throw new RuntimeException(ex); + } + } + + public static boolean isNan(Number... numbers) { + boolean result = false; + for (Number number : numbers) { + result |= number instanceof Double && Double.isNaN(number.doubleValue()); + } + return result; + } + + public static int compare(Number left, Number right) { + if (isSpecial(left) || isSpecial(right)) { + return Double.compare(left.doubleValue(), right.doubleValue()); + } else { + return toBigDecimal(left).compareTo(toBigDecimal(right)); + } + } + + public static boolean contains(Object source, Object value) { + if (source == null) { + return false; + } + if (source instanceof Collection) { + if (WellKnownClasses.isSafe((Collection) source) + && (value == null || WellKnownClasses.isEqualsSafe(value.getClass()))) { + return ((Collection) source).contains(value); + } + return false; + } + if (source instanceof Map) { + if (WellKnownClasses.isSafe((Map) source) + && (value == null || WellKnownClasses.isEqualsSafe(value.getClass()))) { + return ((Map) source).containsKey(value); + } + return false; + } + if (source.getClass().isArray()) { + Class componentType = source.getClass().getComponentType(); + int count = Array.getLength(source); + if (componentType.isPrimitive()) { + if (componentType == byte.class) { + byte byteValue = (Byte) value; + for (int i = 0; i < count; i++) { + if (Array.getByte(source, i) == byteValue) { + return true; + } + } + } else if (componentType == char.class) { + String strValue = (String) value; + if (strValue.isEmpty()) { + return false; + } + char charValue = strValue.charAt(0); + for (int i = 0; i < count; i++) { + if (Array.getChar(source, i) == charValue) { + return true; + } + } + } else if (componentType == short.class) { + int shortValue = ((Number) value).intValue(); + for (int i = 0; i < count; i++) { + if (Array.getShort(source, i) == shortValue) { + return true; + } + } + } else if (componentType == int.class) { + int intValue = ((Number) value).intValue(); + for (int i = 0; i < count; i++) { + if (Array.getInt(source, i) == intValue) { + return true; + } + } + } else if (componentType == long.class) { + long longValue = ((Number) value).longValue(); + for (int i = 0; i < count; i++) { + if (Array.getLong(source, i) == longValue) { + return true; + } + } + } else if (componentType == float.class) { + float floatValue = (Float) value; + for (int i = 0; i < count; i++) { + if (Array.getFloat(source, i) == floatValue) { + return true; + } + } + } else if (componentType == double.class) { + double doubleValue = (Double) value; + for (int i = 0; i < count; i++) { + if (Array.getDouble(source, i) == doubleValue) { + return true; + } + } + } else if (componentType == boolean.class) { + boolean booleanValue = (Boolean) value; + for (int i = 0; i < count; i++) { + if (Array.getBoolean(source, i) == booleanValue) { + return true; + } + } + } + } else { + if (WellKnownClasses.isEqualsSafe(value.getClass())) { + for (int i = 0; i < count; i++) { + if (value.equals(Array.get(source, i))) { + return true; + } + } + } + } + } + return false; + } + + public static boolean isEmpty(Object value) { + if (value == null) { + return false; + } + if (value instanceof String) { + return ((String) value).isEmpty(); + } + if (value instanceof Collection && WellKnownClasses.isSafe((Collection) value)) { + return ((Collection) value).isEmpty(); + } + if (value instanceof Map && WellKnownClasses.isSafe((Map) value)) { + return ((Map) value).isEmpty(); + } + if (value.getClass().isArray()) { + return Array.getLength(value) == 0; + } + return false; + } + + public static int len(Object value) { + if (value == null) { + return 0; + } + if (value instanceof String) { + return ((String) value).length(); + } + if (value instanceof Collection && WellKnownClasses.isSafe((Collection) value)) { + return ((Collection) value).size(); + } + if (value instanceof Map && WellKnownClasses.isSafe((Map) value)) { + return ((Map) value).size(); + } + if (value.getClass().isArray()) { + return Array.getLength(value); + } + throw new IllegalArgumentException("Unsupported class for len operation: " + value.getClass()); + } + + public static boolean stringPredicate( + Object value, String strExpr, BiPredicate strPredicateFunc) { + if (value == null) { + return false; + } + if (value instanceof String) { + return strPredicateFunc.test((String) value, strExpr); + } + // TODO throw new IllegalArgumentException(); + return false; + } + + public static String substring(Object value, int beginIndex, int endIndex) { + if (value == null) { + return null; + } + if (value instanceof String) { + return ((String) value).substring(beginIndex, endIndex); + } + return null; + } + + public static Object index(Object value, int index) { + if (value == null) { + return null; + } + if (value instanceof List && WellKnownClasses.isSafe((List) value)) { + return ((List) value).get(index); + } + if (value instanceof Map && WellKnownClasses.isSafe((Map) value)) { + return ((Map) value).get(index); + } + throw new UnsupportedOperationException( + "Unsupported type for index operation: " + value.getClass()); + } + + public static Object index(Object value, Object key) { + if (value instanceof List && WellKnownClasses.isSafe((List) value) && key instanceof Number) { + return ((List) value).get(((Number) key).intValue()); + } + if (value instanceof Map && WellKnownClasses.isSafe((Map) value)) { + return ((Map) value).get(key); + } + throw new UnsupportedOperationException( + "Unsupported type for index operation: " + value.getClass()); + } + + public interface BooleanPredicate { + boolean test(boolean value); + } + + public static boolean[] filterBooleanArray(boolean[] array, BooleanPredicate predicate) { + boolean[] result = new boolean[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + boolean value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static byte[] filterByteArray(byte[] array, IntPredicate predicate) { + byte[] result = new byte[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + byte value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static short[] filterShortArray(short[] array, IntPredicate predicate) { + short[] result = new short[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + short value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static char[] filterCharArray(char[] array, IntPredicate predicate) { + char[] result = new char[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + char value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static int[] filterIntArray(int[] array, IntPredicate predicate) { + int[] result = new int[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + int value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static long[] filterLongArray(long[] array, LongPredicate predicate) { + long[] result = new long[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + long value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static float[] filterFloatArray(float[] array, DoublePredicate predicate) { + float[] result = new float[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + float value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static double[] filterDoubleArray(double[] array, DoublePredicate predicate) { + double[] result = new double[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + double value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static Object[] filterObjectArray(Object[] array, Predicate predicate) { + Object[] result = new Object[array.length]; + int resultIndex = 0; + for (int i = 0; i < array.length; i++) { + Object value = array[i]; + if (predicate.test(value)) { + result[resultIndex++] = value; + } + } + return Arrays.copyOfRange(result, 0, resultIndex); + } + + public static Collection filterCollection( + Collection collection, Predicate predicate) { + Collection result = + new ArrayList<>(); // TODO create a similar collection type or specialized helper? + for (T value : collection) { + if (predicate.test(value)) { + result.add(value); + } + } + return result; + } + + public static Map filterMap(Map map, Predicate predicate) { + // TODO + return null; + } + + public static boolean anyLongArray(long[] array, LongPredicate predicate) { + for (int i = 0; i < array.length; i++) { + long value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyBooleanArray(boolean[] array, BooleanPredicate predicate) { + for (int i = 0; i < array.length; i++) { + boolean value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyByteArray(byte[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + byte value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyShortArray(short[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + short value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyCharArray(char[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + char value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyIntArray(int[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + int value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyFloatArray(float[] array, DoublePredicate predicate) { + for (int i = 0; i < array.length; i++) { + float value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyDoubleArray(float[] array, DoublePredicate predicate) { + for (int i = 0; i < array.length; i++) { + double value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyObjectArray(Object[] array, Predicate predicate) { + for (int i = 0; i < array.length; i++) { + Object value = array[i]; + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean anyCollection(Collection collection, Predicate predicate) { + for (T value : collection) { + if (predicate.test(value)) { + return true; + } + } + return false; + } + + public static boolean allBooleanArray(boolean[] array, BooleanPredicate predicate) { + for (int i = 0; i < array.length; i++) { + boolean value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allByteArray(byte[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + int value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allShortArray(short[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + short value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allCharArray(char[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + char value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allIntArray(int[] array, IntPredicate predicate) { + for (int i = 0; i < array.length; i++) { + int value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allLongArray(long[] array, LongPredicate predicate) { + for (int i = 0; i < array.length; i++) { + long value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allFloatArray(float[] array, DoublePredicate predicate) { + for (int i = 0; i < array.length; i++) { + float value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allDoubleArray(double[] array, DoublePredicate predicate) { + for (int i = 0; i < array.length; i++) { + double value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allObjectArray(Object[] array, Predicate predicate) { + for (int i = 0; i < array.length; i++) { + Object value = array[i]; + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + public static boolean allCollection(Collection collection, Predicate predicate) { + for (T value : collection) { + if (!predicate.test(value)) { + return false; + } + } + return true; + } + + private static boolean isSpecial(Number x) { + boolean specialDouble = x instanceof Double && Double.isInfinite((Double) x); + boolean specialFloat = x instanceof Float && Float.isInfinite((Float) x); + return specialDouble || specialFloat; + } + + private static BigDecimal toBigDecimal(Number number) throws NumberFormatException { + if (number instanceof BigDecimal) return (BigDecimal) number; + if (number instanceof BigInteger) return new BigDecimal((BigInteger) number); + if (number instanceof Byte + || number instanceof Short + || number instanceof Integer + || number instanceof Long) return BigDecimal.valueOf(number.longValue()); + if (number instanceof Float || number instanceof Double) + return BigDecimal.valueOf(number.doubleValue()); + + return new BigDecimal(number.toString()); + } +} diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java index 70f1f8c0347..39d84bdcc5c 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/DebuggerContext.java @@ -433,6 +433,7 @@ public static void commit( CapturedContext entryContext, CapturedContext exitContext, List caughtExceptions, + MethodLocation methodLocation, int... probeIndices) { try { if (entryContext == CapturedContext.EMPTY_CONTEXT @@ -445,10 +446,10 @@ public static void commit( CapturedContext.Status exitStatus = exitContext.getStatus(probeIndex); ProbeImplementation probeImplementation; if (entryStatus.probeImplementation != ProbeImplementation.UNKNOWN - && (entryStatus.probeImplementation.getEvaluateAt() == MethodLocation.ENTRY - || entryStatus.probeImplementation.getEvaluateAt() == MethodLocation.DEFAULT)) { + && (methodLocation == MethodLocation.ENTRY + || methodLocation == MethodLocation.DEFAULT)) { probeImplementation = entryStatus.probeImplementation; - } else if (exitStatus.probeImplementation.getEvaluateAt() == MethodLocation.EXIT) { + } else if (methodLocation == MethodLocation.EXIT) { probeImplementation = exitStatus.probeImplementation; } else { throw new IllegalStateException("no probe details for " + probeIndex); @@ -468,6 +469,7 @@ public static void commit( CapturedContext entryContext, CapturedContext exitContext, List caughtExceptions, + MethodLocation methodLocation, int probeIndex) { // Cannot call the multi probe version here, because it will add a new level for stacktrace // recording @@ -481,11 +483,15 @@ public static void commit( CapturedContext.Status exitStatus = exitContext.getStatus(probeIndex); ProbeImplementation probeImplementation; if (entryStatus.probeImplementation != ProbeImplementation.UNKNOWN - && (entryStatus.probeImplementation.getEvaluateAt() == MethodLocation.ENTRY - || entryStatus.probeImplementation.getEvaluateAt() == MethodLocation.DEFAULT)) { + && (methodLocation == MethodLocation.ENTRY || methodLocation == MethodLocation.DEFAULT)) { probeImplementation = entryStatus.probeImplementation; - } else if (exitStatus.probeImplementation.getEvaluateAt() == MethodLocation.EXIT) { - probeImplementation = exitStatus.probeImplementation; + } else if (methodLocation == MethodLocation.EXIT) { + if (exitStatus.probeImplementation != ProbeImplementation.UNKNOWN) { + probeImplementation = exitStatus.probeImplementation; + } else { + // otherwise nothing to do + return; + } } else { throw new IllegalStateException("no probe details for " + probeIndex); } @@ -555,4 +561,14 @@ public static boolean isClassNameExcluded(String className) { return false; } } + + public static void handleConditionException(Throwable t, int probeIndex, String dslExpr) { + ProbeImplementation probeImplementation = resolveProbe(probeIndex); + if (probeImplementation != null) { + CapturedContext capturedContext = new CapturedContext(); + capturedContext.addError(probeImplementation, new EvaluationError(dslExpr, t.toString())); + probeImplementation.commit( + CapturedContext.EMPTY_CAPTURING_CONTEXT, capturedContext, Collections.emptyList()); + } + } } diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ReflectiveFieldValueResolver.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ReflectiveFieldValueResolver.java index c5c6f5dcc29..350dc61e1cb 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ReflectiveFieldValueResolver.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ReflectiveFieldValueResolver.java @@ -7,6 +7,7 @@ import de.thetaphi.forbiddenapis.SuppressForbidden; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import org.slf4j.Logger; @@ -44,6 +45,21 @@ public class ReflectiveFieldValueResolver { INACCESSIBLE_FIELD = field; } + private static final MethodHandle CAN_ACCCESS; + + static { + MethodHandle methodHandle = null; + try { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + methodHandle = + lookup.findVirtual( + AccessibleObject.class, "canAccess", methodType(boolean.class, Object.class)); + } catch (Exception e) { + LOGGER.debug(EXCLUDE_TELEMETRY, "Looking up canAcess failed: ", e); + } + CAN_ACCCESS = methodHandle; + } + private static final Class MODULE_CLASS; private static final MethodHandle GET_MODULE; @@ -263,6 +279,18 @@ public static boolean trySetAccessible(Field field) { } } + public static boolean canAccess(Field field, Object obj) { + if (CAN_ACCCESS == null) { + return true; + } + try { + return (boolean) CAN_ACCCESS.invokeExact(field, obj); + } catch (Throwable e) { + LOGGER.debug("canAccess call failed: ", e); + return true; + } + } + public static String buildInaccessibleMsg(Field field) { if (MODULE_CLASS != null && GET_MODULE != null) { try { diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferences.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferences.java index c64844e5e83..5cd43ed021b 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferences.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/el/ValueReferences.java @@ -7,21 +7,21 @@ */ public final class ValueReferences { - public static String SYNTHETIC_PREFIX = "@"; - public static String THIS = "this"; + public static final String SYNTHETIC_PREFIX = "@"; + public static final String THIS = "this"; - public static String DURATION_EXTENSION_NAME = "duration"; - public static String RETURN_EXTENSION_NAME = "return"; - public static String ITERATOR_EXTENSION_NAME = "it"; - public static String EXCEPTION_EXTENSION_NAME = "exception"; - public static String KEY_EXTENSION_NAME = "key"; - public static String VALUE_EXTENSION_NAME = "value"; - public static String DURATION_REF = SYNTHETIC_PREFIX + DURATION_EXTENSION_NAME; - public static String RETURN_REF = SYNTHETIC_PREFIX + RETURN_EXTENSION_NAME; - public static String ITERATOR_REF = SYNTHETIC_PREFIX + ITERATOR_EXTENSION_NAME; - public static String EXCEPTION_REF = SYNTHETIC_PREFIX + EXCEPTION_EXTENSION_NAME; - public static String KEY_REF = SYNTHETIC_PREFIX + KEY_EXTENSION_NAME; - public static String VALUE_REF = SYNTHETIC_PREFIX + VALUE_EXTENSION_NAME; + public static final String DURATION_EXTENSION_NAME = "duration"; + public static final String RETURN_EXTENSION_NAME = "return"; + public static final String ITERATOR_EXTENSION_NAME = "it"; + public static final String EXCEPTION_EXTENSION_NAME = "exception"; + public static final String KEY_EXTENSION_NAME = "key"; + public static final String VALUE_EXTENSION_NAME = "value"; + public static final String DURATION_REF = SYNTHETIC_PREFIX + DURATION_EXTENSION_NAME; + public static final String RETURN_REF = SYNTHETIC_PREFIX + RETURN_EXTENSION_NAME; + public static final String ITERATOR_REF = SYNTHETIC_PREFIX + ITERATOR_EXTENSION_NAME; + public static final String EXCEPTION_REF = SYNTHETIC_PREFIX + EXCEPTION_EXTENSION_NAME; + public static final String KEY_REF = SYNTHETIC_PREFIX + KEY_EXTENSION_NAME; + public static final String VALUE_REF = SYNTHETIC_PREFIX + VALUE_EXTENSION_NAME; public static String synthetic(String name) { return SYNTHETIC_PREFIX + name; diff --git a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/WellKnownClasses.java b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/WellKnownClasses.java index e80f9c0b836..689c0f8c9c0 100644 --- a/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/WellKnownClasses.java +++ b/dd-java-agent/agent-debugger/debugger-bootstrap/src/main/java/datadog/trace/bootstrap/debugger/util/WellKnownClasses.java @@ -9,6 +9,7 @@ import java.lang.invoke.MethodHandles; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.Date; @@ -122,46 +123,83 @@ public class WellKnownClasses { private static final Set LONG_PRIMITIVES = new HashSet<>(Arrays.asList("java.util.Date")); - private static final Map, Map>> - SPECIAL_TYPE_ACCESS = new HashMap<>(); + private static final Map, Map> SPECIAL_TYPE_ACCESS = + new HashMap<>(); - private static final Map> - STACKTRACEELEMENT_SPECIAL_FIELDS = new HashMap<>(); + private static final Map STACKTRACEELEMENT_SPECIAL_FIELDS = + new HashMap<>(); private static Method getModuleNameMethod; static { - STACKTRACEELEMENT_SPECIAL_FIELDS.put("declaringClass", StackTraceElementFields::declaringClass); - STACKTRACEELEMENT_SPECIAL_FIELDS.put("methodName", StackTraceElementFields::methodName); - STACKTRACEELEMENT_SPECIAL_FIELDS.put("fileName", StackTraceElementFields::fileName); - STACKTRACEELEMENT_SPECIAL_FIELDS.put("lineNumber", StackTraceElementFields::lineNumber); - STACKTRACEELEMENT_SPECIAL_FIELDS.put("moduleName", StackTraceElementFields::moduleName); - try { - getModuleNameMethod = StackTraceElement.class.getMethod("getModuleName"); - } catch (NoSuchMethodException e) { - getModuleNameMethod = null; + STACKTRACEELEMENT_SPECIAL_FIELDS.put( + "declaringClass", + new SpecialFieldInfo( + getDeclaredMethod(StackTraceElement.class, "getClassName"), + StackTraceElementFields::declaringClass)); + STACKTRACEELEMENT_SPECIAL_FIELDS.put( + "methodName", + new SpecialFieldInfo( + getDeclaredMethod(StackTraceElement.class, "getMethodName"), + StackTraceElementFields::methodName)); + STACKTRACEELEMENT_SPECIAL_FIELDS.put( + "fileName", + new SpecialFieldInfo( + getDeclaredMethod(StackTraceElement.class, "getFileName"), + StackTraceElementFields::fileName)); + STACKTRACEELEMENT_SPECIAL_FIELDS.put( + "lineNumber", + new SpecialFieldInfo( + getDeclaredMethod(StackTraceElement.class, "getLineNumber"), + StackTraceElementFields::lineNumber)); + if (JavaVirtualMachine.isJavaVersionAtLeast(9)) { + STACKTRACEELEMENT_SPECIAL_FIELDS.put( + "moduleName", + new SpecialFieldInfo( + getDeclaredMethod(StackTraceElement.class, "getModuleName"), + StackTraceElementFields::moduleName)); + try { + getModuleNameMethod = StackTraceElement.class.getMethod("getModuleName"); + } catch (NoSuchMethodException e) { + getModuleNameMethod = null; + } } } - private static final Map> - OPTIONAL_SPECIAL_FIELDS = new HashMap<>(); - private static final Map> - OPTIONALINT_SPECIAL_FIELDS = new HashMap<>(); - private static final Map> - OPTIONALDOUBLE_SPECIAL_FIELDS = new HashMap<>(); - private static final Map> - OPTIONALLONG_SPECIAL_FIELDS = new HashMap<>(); - private static final Map> - COMPLETABLEFUTURE_SPECIAL_FIELDS = new HashMap<>(); + private static final Map OPTIONAL_SPECIAL_FIELDS = new HashMap<>(); + private static final Map OPTIONALINT_SPECIAL_FIELDS = new HashMap<>(); + private static final Map OPTIONALDOUBLE_SPECIAL_FIELDS = + new HashMap<>(); + private static final Map OPTIONALLONG_SPECIAL_FIELDS = new HashMap<>(); + private static final Map COMPLETABLEFUTURE_SPECIAL_FIELDS = + new HashMap<>(); static { - OPTIONAL_SPECIAL_FIELDS.put("value", OptionalFields::value); - OPTIONALINT_SPECIAL_FIELDS.put("value", OptionalFields::valueInt); - OPTIONALDOUBLE_SPECIAL_FIELDS.put("value", OptionalFields::valueDouble); - OPTIONALLONG_SPECIAL_FIELDS.put("value", OptionalFields::valueLong); + OPTIONAL_SPECIAL_FIELDS.put( + "value", + new SpecialFieldInfo( + getDeclaredMethod(Optional.class, "orElse", Object.class), OptionalFields::value)); + OPTIONALINT_SPECIAL_FIELDS.put( + "value", + new SpecialFieldInfo( + getDeclaredMethod(OptionalInt.class, "orElse", Integer.TYPE), + OptionalFields::valueInt)); + OPTIONALDOUBLE_SPECIAL_FIELDS.put( + "value", + new SpecialFieldInfo( + getDeclaredMethod(OptionalDouble.class, "orElse", Double.TYPE), + OptionalFields::valueDouble)); + OPTIONALLONG_SPECIAL_FIELDS.put( + "value", + new SpecialFieldInfo( + getDeclaredMethod(OptionalLong.class, "orElse", Long.TYPE), OptionalFields::valueLong)); if (JavaVirtualMachine.isJavaVersionAtLeast(19)) { // Future::resultNow method is available since JDK 19 - COMPLETABLEFUTURE_SPECIAL_FIELDS.put("result", CompletableFutureFields::result); + COMPLETABLEFUTURE_SPECIAL_FIELDS.put( + "result", + new SpecialFieldInfo( + getDeclaredMethod(CompletableFuture.class, "resultNow"), + CompletableFutureFields::result)); } } @@ -177,14 +215,50 @@ public class WellKnownClasses { } } - private static final Map> - THROWABLE_SPECIAL_FIELDS = new HashMap<>(); + private static final Map THROWABLE_SPECIAL_FIELDS = new HashMap<>(); static { - THROWABLE_SPECIAL_FIELDS.put("detailMessage", ThrowableFields::detailMessage); - THROWABLE_SPECIAL_FIELDS.put("suppressedExceptions", ThrowableFields::suppressedExceptions); - THROWABLE_SPECIAL_FIELDS.put("stackTrace", ThrowableFields::stackTrace); - THROWABLE_SPECIAL_FIELDS.put("cause", ThrowableFields::cause); + THROWABLE_SPECIAL_FIELDS.put( + "detailMessage", + new SpecialFieldInfo( + getDeclaredMethod(Throwable.class, "getMessage"), ThrowableFields::detailMessage)); + THROWABLE_SPECIAL_FIELDS.put( + "suppressedExceptions", + new SpecialFieldInfo( + getDeclaredMethod(Throwable.class, "getSuppressed"), + ThrowableFields::suppressedExceptions)); + THROWABLE_SPECIAL_FIELDS.put( + "stackTrace", + new SpecialFieldInfo( + getDeclaredMethod(Throwable.class, "getStackTrace"), ThrowableFields::stackTrace)); + THROWABLE_SPECIAL_FIELDS.put( + "cause", + new SpecialFieldInfo( + getDeclaredMethod(Throwable.class, "getCause"), ThrowableFields::cause)); + } + + public static class SpecialFieldInfo { + public final Method method; + public final Function accessor; + public final boolean checksOverride; + + public SpecialFieldInfo( + Method method, Function accessor) { + this.method = method; + this.accessor = accessor; + // checks override if declaring class & method are not final + this.checksOverride = + (method.getDeclaringClass().getModifiers() & Modifier.FINAL) == 0 + && (method.getModifiers() & Modifier.FINAL) == 0; + } + } + + private static Method getDeclaredMethod(Class clazz, String name, Class... parameterTypes) { + try { + return clazz.getDeclaredMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } } private static final List SAFE_COLLECTION_PACKAGES = @@ -268,17 +342,20 @@ public static boolean isLongPrimitive(String type) { * @return a map of fields with function to access special field of a type, or null if type is not * supported. This is used to avoid using reflection to access fields on well known types */ - public static Map> getSpecialTypeAccess( - Object value) { + public static Map getSpecialTypeAccess(Object value) { if (value == null) { return null; } - Map> specialTypeAccess = - SPECIAL_TYPE_ACCESS.get(value.getClass()); + return getSpecialTypeAccess(value.getClass()); + } + + public static Map getSpecialTypeAccess(Class clazz) { + Map specialTypeAccess = SPECIAL_TYPE_ACCESS.get(clazz); if (specialTypeAccess != null) { return specialTypeAccess; } - if (value instanceof Throwable) { + // clazz instanceof Throwable + if (Throwable.class.isAssignableFrom(clazz)) { return THROWABLE_SPECIAL_FIELDS; } return null; @@ -316,7 +393,7 @@ public static ToLongFunction getLongPrimitiveValueFunction(String typeNa return LONG_FUNCTIONS.get(typeName); } - private static class ThrowableFields { + public static class ThrowableFields { public static final String BECAUSE_OVERRIDDEN = "Special access method not safe to be called because overridden"; @@ -369,17 +446,19 @@ private static CapturedContext.CapturedValue captureIfNotOverridden( return CapturedContext.CapturedValue.of( fieldName, fieldType.getTypeName(), supplier.apply(obj)); } + } - private static boolean isOverridden( - Object value, String methodName, Class originalDeclaringClass) { - Class declaringClass = null; - try { - declaringClass = value.getClass().getMethod(methodName).getDeclaringClass(); - } catch (NoSuchMethodException e) { - LOGGER.debug("Failed to get declaring class for Throwable::getMessage", e); - } - return declaringClass != originalDeclaringClass; + public static boolean isOverridden( + Object value, String methodName, Class originalDeclaringClass) { + Class declaringClass = null; + try { + declaringClass = value.getClass().getMethod(methodName).getDeclaringClass(); + } catch (NoSuchMethodException e) { + LOGGER.debug( + "Failed to get declaring class for " + value.getClass().getTypeName() + "::" + methodName, + e); } + return declaringClass != originalDeclaringClass; } private static class StackTraceElementFields { diff --git a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ComparisonOperator.java b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ComparisonOperator.java index 8bc524c20d7..3303adc6d0c 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ComparisonOperator.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/main/java/com/datadog/debugger/el/expressions/ComparisonOperator.java @@ -5,8 +5,7 @@ import com.datadog.debugger.el.Visitor; import com.datadog.debugger.el.values.NumericValue; import com.datadog.debugger.el.values.StringValue; -import java.math.BigDecimal; -import java.math.BigInteger; +import datadog.trace.bootstrap.debugger.ConditionHelper; import java.util.Objects; public enum ComparisonOperator { @@ -16,10 +15,10 @@ public Boolean apply(Value left, Value right) { if (left instanceof NumericValue && right instanceof NumericValue) { Number leftNumber = ((NumericValue) left).getWidenValue(); Number rightNumber = ((NumericValue) right).getWidenValue(); - if (isNan(leftNumber, rightNumber)) { + if (ConditionHelper.isNan(leftNumber, rightNumber)) { return Boolean.FALSE; } - return compare(leftNumber, rightNumber) == 0; + return ConditionHelper.compare(leftNumber, rightNumber) == 0; } if (left.getValue() instanceof Enum || right.getValue() instanceof Enum) { return applyEqualityForEnum(left, right); @@ -142,53 +141,18 @@ public R accept(Visitor visitor) { return visitor.visit(this); } - protected static boolean isNan(Number... numbers) { - boolean result = false; - for (Number number : numbers) { - result |= number instanceof Double && Double.isNaN(number.doubleValue()); - } - return result; - } - protected static Integer compare(Value left, Value right) { if (left instanceof NumericValue && right instanceof NumericValue) { Number leftNumber = ((NumericValue) left).getWidenValue(); Number rightNumber = ((NumericValue) right).getWidenValue(); - if (isNan(leftNumber, rightNumber)) { + if (ConditionHelper.isNan(leftNumber, rightNumber)) { return null; } - return compare(leftNumber, rightNumber); + return ConditionHelper.compare(leftNumber, rightNumber); } if (left instanceof StringValue && right instanceof StringValue) { return ((StringValue) left).getValue().compareTo(((StringValue) right).getValue()); } return null; } - - protected static int compare(Number left, Number right) { - if (isSpecial(left) || isSpecial(right)) { - return Double.compare(left.doubleValue(), right.doubleValue()); - } else { - return toBigDecimal(left).compareTo(toBigDecimal(right)); - } - } - - private static boolean isSpecial(Number x) { - boolean specialDouble = x instanceof Double && Double.isInfinite((Double) x); - boolean specialFloat = x instanceof Float && Float.isInfinite((Float) x); - return specialDouble || specialFloat; - } - - private static BigDecimal toBigDecimal(Number number) throws NumberFormatException { - if (number instanceof BigDecimal) return (BigDecimal) number; - if (number instanceof BigInteger) return new BigDecimal((BigInteger) number); - if (number instanceof Byte - || number instanceof Short - || number instanceof Integer - || number instanceof Long) return BigDecimal.valueOf(number.longValue()); - if (number instanceof Float || number instanceof Double) - return BigDecimal.valueOf(number.doubleValue()); - - return new BigDecimal(number.toString()); - } } diff --git a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ComparisonExpressionTest.java b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ComparisonExpressionTest.java index d2b2e1cd8c4..008fa494694 100644 --- a/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ComparisonExpressionTest.java +++ b/dd-java-agent/agent-debugger/debugger-el/src/test/java/com/datadog/debugger/el/expressions/ComparisonExpressionTest.java @@ -45,7 +45,7 @@ void evaluateOperator( assertEquals(prettyPrint, print(expression)); } - private static Stream expressions() { + public static Stream expressions() { return Stream.of( Arguments.of(new NumericValue(1, INT), new NumericValue(1, INT), EQ, true, "1 == 1"), Arguments.of(new NumericValue(1L, LONG), new NumericValue(1L, LONG), EQ, true, "1 == 1"), diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java index 3f883b0bba2..ebc2cebd959 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/DebuggerTransformer.java @@ -74,6 +74,7 @@ import org.objectweb.asm.tree.analysis.BasicValue; import org.objectweb.asm.tree.analysis.BasicVerifier; import org.objectweb.asm.util.CheckClassAdapter; +import org.objectweb.asm.util.TraceClassVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -595,6 +596,9 @@ private byte[] writeClassFile( } ClassWriter writer = new SafeClassWriter(loader); ClassVisitor visitor = new JsrInliningClassVisitor(writer); + visitor = new DebugVisitor(visitor); + ClassVisitor traceVisitor = new TraceClassVisitor(null, new PrintWriter(System.out)); + classNode.accept(traceVisitor); LOGGER.debug("Generating bytecode for class: {}", Strings.getClassName(classFilePath)); try { classNode.accept(visitor); @@ -648,7 +652,9 @@ private boolean performInstrumentation( boolean transformed = false; ClassFileLines classFileLines = new ClassFileLines(classNode); Set remainingDefinitions = new HashSet<>(definitions); - for (MethodNode methodNode : classNode.methods) { + // create a new list for be able to add method during instrumentation + List methods = new ArrayList<>(classNode.methods); + for (MethodNode methodNode : methods) { List matchingDefs = new ArrayList<>(); for (ProbeDefinition definition : definitions) { Where.MethodMatching methodMatching = @@ -1036,6 +1042,37 @@ public MethodVisitor visitMethod( } } + /** A {@link org.objectweb.asm.ClassVisitor} that logs all visited methods. */ + static class DebugVisitor extends ClassVisitor { + protected DebugVisitor(ClassVisitor parent) { + super(Opcodes.ASM9, parent); + } + + // generate methods for logging all visited methods + @Override + public MethodVisitor visitMethod( + int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + LOGGER.debug("visiting method: {}", name); + return new DebugMethodVisitor(mv, name); + } + } + + static class DebugMethodVisitor extends MethodVisitor { + private final String name; + + protected DebugMethodVisitor(MethodVisitor parent, String name) { + super(Opcodes.ASM9, parent); + this.name = name; + } + + @Override + public void visitEnd() { + super.visitEnd(); + LOGGER.debug("visiting end method: {}", name); + } + } + static class SafeClassWriter extends ClassWriter { private final ClassLoader classLoader; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/StringTemplateBuilder.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/StringTemplateBuilder.java index 8bd75149e39..d817e7e95b0 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/StringTemplateBuilder.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/agent/StringTemplateBuilder.java @@ -60,7 +60,7 @@ public String evaluate(CapturedContext context, LogProbe.LogStatus status) { handleException(status, ex, segment.getExpr(), sb); } if (!status.getErrors().isEmpty()) { - status.setLogTemplateErrors(true); + status.setHasEvalutionErrors(true); } } } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java index bfe38d81fc8..ae181a8b9ab 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ASMHelper.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.signature.SignatureVisitor; import org.objectweb.asm.tree.AbstractInsnNode; @@ -32,12 +33,15 @@ import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.LineNumberNode; import org.objectweb.asm.tree.LocalVariableNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; import org.objectweb.asm.util.Printer; import org.objectweb.asm.util.Textifier; import org.objectweb.asm.util.TraceClassVisitor; @@ -79,9 +83,12 @@ static String extractMethod(ClassNode classNode, String method) { StringWriter writer = new StringWriter(); classNode.accept(new TraceClassVisitor(null, new Textifier(), new PrintWriter(writer))); List strings = Arrays.asList(writer.toString().split("\n")); + if (method.contains("$")) { + method = method.replace("$", "\\$"); + } for (int i = 0; i < strings.size(); i++) { if (strings.get(i).matches(format(".*(private|public).* %s\\(.*", method))) { - while (!strings.get(i).equals("")) + while (i < strings.size() && !strings.get(i).equals("")) joiner.add(String.format("[%3d] %s", i, strings.get(i++))); } } @@ -122,6 +129,10 @@ public static void invokeVirtual( // stack: [ret_type] } + public static boolean isStaticMethod(MethodNode methodNode) { + return (methodNode.access & Opcodes.ACC_STATIC) != 0; + } + public static boolean isStaticField(FieldNode fieldNode) { return (fieldNode.access & Opcodes.ACC_STATIC) != 0; } @@ -142,6 +153,11 @@ public static boolean isRecord(ClassNode classNode) { return (classNode.access & Opcodes.ACC_RECORD) != 0; } + public static boolean hasReturnValue(MethodNode methodNode) { + return org.objectweb.asm.Type.getReturnType(methodNode.desc) + != org.objectweb.asm.Type.VOID_TYPE; + } + public static void invokeStatic( InsnList insnList, org.objectweb.asm.Type owner, @@ -382,12 +398,31 @@ public static String toString(AbstractInsnNode node) { String opcode = node.getOpcode() >= 0 ? Printer.OPCODES[node.getOpcode()] : node.toString(); if (node instanceof LineNumberNode) { return String.format("LineNumber: %s", ((LineNumberNode) node).line); - } else if (node instanceof MethodInsnNode) { + } + if (node instanceof LabelNode) { + return String.format("Label: %s", ((LabelNode) node).getLabel()); + } + if (node instanceof MethodInsnNode) { MethodInsnNode method = (MethodInsnNode) node; return String.format("%s: [%s] %s", opcode, method.name, method.desc); - } else { - return opcode; } + if (node instanceof FieldInsnNode) { + FieldInsnNode field = (FieldInsnNode) node; + return String.format("%s: %s.%s: %s", opcode, field.owner, field.name, field.desc); + } + if (node instanceof VarInsnNode) { + VarInsnNode var = (VarInsnNode) node; + return String.format("%s: %s", opcode, var.var); + } + if (node instanceof JumpInsnNode) { + JumpInsnNode jump = (JumpInsnNode) node; + return String.format("%s: %s", opcode, jump.label.getLabel()); + } + if (node instanceof LdcInsnNode) { + LdcInsnNode ldc = (LdcInsnNode) node; + return String.format("%s: %s", opcode, ldc.cst); + } + return opcode; } private static int widenIntType(int sort) { @@ -428,6 +463,29 @@ public static List getLineNumbers(MethodNode methodNode) { return lines; } + public static void tryBox(org.objectweb.asm.Type type, InsnList insnList) { + // expected stack top is the value to be boxed + if (Types.isPrimitive(type)) { + org.objectweb.asm.Type targetType = Types.getBoxingTargetType(type); + invokeStatic(insnList, targetType, "valueOf", targetType, type); + } + } + + public static int newVar(MethodNode methodNode, org.objectweb.asm.Type type) { + int varId = methodNode.maxLocals + 1; + methodNode.maxLocals += type.getSize(); + return varId; + } + + public static int getArgOffset(MethodNode methodNode) { + int argOffset = isStaticMethod(methodNode) ? 0 : 1; + org.objectweb.asm.Type[] argTypes = org.objectweb.asm.Type.getArgumentTypes(methodNode.desc); + for (org.objectweb.asm.Type t : argTypes) { + argOffset += t.getSize(); + } + return argOffset; + } + /** Wraps ASM's {@link org.objectweb.asm.Type} with associated generic types */ public static class Type { private final org.objectweb.asm.Type mainType; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java index f51ea26dac1..d212b569d4a 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/CapturedContextInstrumenter.java @@ -32,6 +32,7 @@ import com.datadog.debugger.util.ClassFileLines; import com.datadog.debugger.util.JvmLanguage; import datadog.trace.api.Config; +import datadog.trace.bootstrap.debugger.CapturedContextProbe; import datadog.trace.bootstrap.debugger.Limits; import datadog.trace.bootstrap.debugger.MethodLocation; import datadog.trace.bootstrap.debugger.util.Redaction; @@ -69,9 +70,13 @@ public class CapturedContextInstrumenter extends Instrumenter { protected final LabelNode contextInitLabel = new LabelNode(); private int entryContextVar = -1; private int exitContextVar = -1; - private int timestampStartVar = -1; + protected int timestampStartVar = -1; private int throwableListVar = -1; private Collection hoistedLocalVars = Collections.emptyList(); + protected final boolean hasCondition; + // TODO move to specialized class + protected MethodNode conditionMethod; + protected MethodNode conditionExceptionMethod; public CapturedContextInstrumenter( ProbeDefinition definition, @@ -85,6 +90,7 @@ public CapturedContextInstrumenter( this.captureSnapshot = captureSnapshot; this.captureEntry = captureEntry; this.limits = limits; + this.hasCondition = ((CapturedContextProbe) definition).hasCondition(); } @Override @@ -268,13 +274,9 @@ private boolean isExceptionLocalDeclared(TryCatchBlockNode catchHandler, MethodN protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) { InsnList insnList = new InsnList(); // stack [ret_value] - insnList.add(new VarInsnNode(Opcodes.ALOAD, entryContextVar)); - // stack [ret_value, capturedcontext] LabelNode targetNode = new LabelNode(); LabelNode gotoNode = new LabelNode(); - invokeVirtual(insnList, CAPTURED_CONTEXT_TYPE, "isCapturing", BOOLEAN_TYPE); - // stack [ret_value, boolean] - insnList.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); + addBeforeReturnCondition(insnList, targetNode); // stack [ret_value] addEvalContextCall(insnList, Snapshot.Kind.RETURN, node, timestampStartVar, "EXIT"); // stack [ret_value] @@ -289,6 +291,14 @@ protected InsnList getBeforeReturnInsnList(AbstractInsnNode node) { return insnList; } + protected void addBeforeReturnCondition(InsnList insnList, LabelNode targetNode) { + insnList.add(new VarInsnNode(Opcodes.ALOAD, entryContextVar)); + // stack [ret_value, capturedcontext] + invokeVirtual(insnList, CAPTURED_CONTEXT_TYPE, "isCapturing", BOOLEAN_TYPE); + // stack [ret_value, boolean] + insnList.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); + } + @Override protected InsnList getReturnHandlerInsnList() { return commit(); @@ -311,6 +321,9 @@ private InsnList commit() { insnList.add(new InsnNode(Opcodes.ACONST_NULL)); } // stack [capturedcontext, capturedcontext, list] + String methodLocationStr = definition.getEvaluateAt() == MethodLocation.EXIT ? "EXIT" : "ENTRY"; + getStatic(insnList, METHOD_LOCATION_TYPE, methodLocationStr); + // stack [capturedcontext, capturedcontext, methodlocation] addCommitCall(insnList); // stack [] return insnList; @@ -328,6 +341,7 @@ protected void addCommitCall(InsnList insnList) { CAPTURED_CONTEXT_TYPE, CAPTURED_CONTEXT_TYPE, getType(List.class), + METHOD_LOCATION_TYPE, INT_ARRAY_TYPE); // stack [] } @@ -341,17 +355,9 @@ protected void addFinallyHandler(LabelNode startLabel, LabelNode endLabel) { InsnList handler = new InsnList(); handler.add(handlerLabel); // stack [exception] - LabelNode targetNode = null; - if (entryContextVar != -1) { - handler.add(new VarInsnNode(Opcodes.ALOAD, entryContextVar)); - // stack [exception, capturedcontext] - targetNode = new LabelNode(); - invokeVirtual(handler, CAPTURED_CONTEXT_TYPE, "isCapturing", BOOLEAN_TYPE); - // stack [exception, boolean] - handler.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); - } + LabelNode targetNode = addFinallyHandlerCondition(handler); if (exitContextVar == -1) { - exitContextVar = newVar(CAPTURED_CONTEXT_TYPE); + exitContextVar = ASMHelper.newVar(methodNode, CAPTURED_CONTEXT_TYPE); } // stack [exception] addEvalContextCall( @@ -370,6 +376,20 @@ protected void addFinallyHandler(LabelNode startLabel, LabelNode endLabel) { finallyBlocks.add(new FinallyBlock(startLabel, endLabel, handlerLabel)); } + protected LabelNode addFinallyHandlerCondition(InsnList handler) { + LabelNode targetNode = null; + if (entryContextVar != -1) { + // TODO check, probably not needed + handler.add(new VarInsnNode(Opcodes.ALOAD, entryContextVar)); + // stack [exception, capturedcontext] + targetNode = new LabelNode(); + invokeVirtual(handler, CAPTURED_CONTEXT_TYPE, "isCapturing", BOOLEAN_TYPE); + // stack [exception, boolean] + handler.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); + } + return targetNode; + } + protected void addEvalContextCall( InsnList insnList, Snapshot.Kind snapshotKind, @@ -643,7 +663,7 @@ private void collectArguments(InsnList insnList, Snapshot.Kind kind) { } else { insnList.add(new VarInsnNode(argType.getOpcode(Opcodes.ILOAD), slot)); // stack: [capturedcontext, capturedcontext, array, array, int, string, type_name, arg] - tryBox(argType, insnList); + ASMHelper.tryBox(argType, insnList); // stack: [capturedcontext, capturedcontext, array, array, int, string, type_name, object] addCapturedValueOf(insnList, limits); } @@ -679,14 +699,6 @@ private void captureThis(InsnList insnList) { // stack: [capturedcontext, capturedcontext, array] } - private void tryBox(Type type, InsnList insnList) { - // expected stack top is the value to be boxed - if (Types.isPrimitive(type)) { - Type targetType = Types.getBoxingTargetType(type); - invokeStatic(insnList, targetType, "valueOf", targetType, type); - } - } - private void collectLocalVariables(AbstractInsnNode location, InsnList insnList) { // expected stack top: [capturedcontext] if (location == null) { @@ -748,7 +760,7 @@ private void collectLocalVariables(AbstractInsnNode location, InsnList insnList) } else { insnList.add(new VarInsnNode(varType.getOpcode(Opcodes.ILOAD), variableNode.index)); // stack: [capturedcontext, capturedcontext, array, array, int, name, type_name, value] - tryBox(varType, insnList); + ASMHelper.tryBox(varType, insnList); // stack: [capturedcontext, capturedcontext, array, array, int, name, type_name, object] addCapturedValueOf(insnList, limits); } @@ -779,11 +791,11 @@ private void collectReturnValue(AbstractInsnNode location, InsnList insnList) { return; } // expected stack top is [ret_value, capturedcontext] - int captureVar = newVar(CAPTURED_CONTEXT_TYPE); + int captureVar = ASMHelper.newVar(methodNode, CAPTURED_CONTEXT_TYPE); insnList.add(new VarInsnNode(Opcodes.ASTORE, captureVar)); // stack: [ret_value] Type returnType = Type.getReturnType(methodNode.desc); - int retVar = newVar(returnType); + int retVar = ASMHelper.newVar(methodNode, returnType); if (returnType.getSize() == 2) { insnList.add(new InsnNode(Opcodes.DUP2)); } else { @@ -801,7 +813,7 @@ private void collectReturnValue(AbstractInsnNode location, InsnList insnList) { // stack: [ret_value, capturedcontext, capturedcontext, null, type_name] insnList.add(new VarInsnNode(returnType.getOpcode(Opcodes.ILOAD), retVar)); // stack: [ret_value, capturedcontext, capturedcontext, null, type_name, ret_value] - tryBox(returnType, insnList); + ASMHelper.tryBox(returnType, insnList); // stack: [ret_value, capturedcontext, capturedcontext, null, type_name, ret_value] // no name, no redaction addCapturedValueOf(insnList, limits); @@ -816,8 +828,8 @@ private void collectExceptionValue(AbstractInsnNode location, InsnList insnList) return; } // expected stack: [throwable, capturedcontext] - int captureVar = newVar(CAPTURED_CONTEXT_TYPE); - int throwableVar = newVar(THROWABLE_TYPE); + int captureVar = ASMHelper.newVar(methodNode, CAPTURED_CONTEXT_TYPE); + int throwableVar = ASMHelper.newVar(methodNode, THROWABLE_TYPE); insnList.add(new VarInsnNode(Opcodes.ASTORE, captureVar)); // stack: [throwable] insnList.add(new InsnNode(Opcodes.DUP)); @@ -881,7 +893,7 @@ private void collectStaticFields(InsnList insnList) { new FieldInsnNode(Opcodes.GETSTATIC, classNode.name, fieldNode.name, fieldNode.desc)); // stack: [capturedcontext, capturedcontext, array, array, int, string, type_name, // field_value] - tryBox(fieldType, insnList); + ASMHelper.tryBox(fieldType, insnList); // stack: [capturedcontext, capturedcontext, array, array, int, string, type_name, object] addCapturedValueOf(insnList, limits); } @@ -926,21 +938,21 @@ private static List extractStaticFields(ClassNode classNode, Limits l } private int declareContextVar(InsnList insnList) { - int var = newVar(CAPTURED_CONTEXT_TYPE); + int var = ASMHelper.newVar(methodNode, CAPTURED_CONTEXT_TYPE); getStatic(insnList, CAPTURED_CONTEXT_TYPE, "EMPTY_CAPTURING_CONTEXT"); insnList.add(new VarInsnNode(Opcodes.ASTORE, var)); return var; } private int declareTimestampVar(InsnList insnList) { - int var = newVar(LONG_TYPE); + int var = ASMHelper.newVar(methodNode, LONG_TYPE); invokeStatic(insnList, Type.getType(System.class), "nanoTime", LONG_TYPE); insnList.add(new VarInsnNode(Opcodes.LSTORE, var)); return var; } private int declareThrowableList(InsnList insnList) { - int var = newVar(getType(ArrayList.class)); + int var = ASMHelper.newVar(methodNode, getType(ArrayList.class)); insnList.add(new InsnNode(Opcodes.ACONST_NULL)); insnList.add(new VarInsnNode(Opcodes.ASTORE, var)); return var; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ConditionInstrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ConditionInstrumenter.java new file mode 100644 index 00000000000..8995ce29d27 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/ConditionInstrumenter.java @@ -0,0 +1,1539 @@ +package com.datadog.debugger.instrumentation; + +import static com.datadog.debugger.instrumentation.ASMHelper.getArgOffset; +import static com.datadog.debugger.instrumentation.ASMHelper.invokeConstructor; +import static com.datadog.debugger.instrumentation.ASMHelper.invokeStatic; +import static com.datadog.debugger.instrumentation.ASMHelper.invokeVirtual; +import static com.datadog.debugger.instrumentation.ASMHelper.isStaticField; +import static com.datadog.debugger.instrumentation.ASMHelper.ldc; +import static com.datadog.debugger.instrumentation.ASMHelper.newInstance; +import static com.datadog.debugger.instrumentation.ASMHelper.newVar; +import static com.datadog.debugger.instrumentation.ASMHelper.tryBox; +import static com.datadog.debugger.instrumentation.Types.CLASS_TYPE; +import static com.datadog.debugger.instrumentation.Types.COLLECTION_TYPE; +import static com.datadog.debugger.instrumentation.Types.CONDITION_HELPER_TYPE; +import static com.datadog.debugger.instrumentation.Types.DEBUGGER_CONTEXT_TYPE; +import static com.datadog.debugger.instrumentation.Types.OBJECT_TYPE; +import static com.datadog.debugger.instrumentation.Types.STRING_TYPE; +import static com.datadog.debugger.instrumentation.Types.THROWABLE_TYPE; +import static datadog.trace.bootstrap.debugger.util.WellKnownClasses.ThrowableFields.BECAUSE_OVERRIDDEN; + +import com.datadog.debugger.el.ProbeCondition; +import com.datadog.debugger.el.Visitor; +import com.datadog.debugger.el.expressions.BinaryExpression; +import com.datadog.debugger.el.expressions.BinaryOperator; +import com.datadog.debugger.el.expressions.BooleanExpression; +import com.datadog.debugger.el.expressions.ComparisonExpression; +import com.datadog.debugger.el.expressions.ComparisonOperator; +import com.datadog.debugger.el.expressions.ContainsExpression; +import com.datadog.debugger.el.expressions.EndsWithExpression; +import com.datadog.debugger.el.expressions.FilterCollectionExpression; +import com.datadog.debugger.el.expressions.GetMemberExpression; +import com.datadog.debugger.el.expressions.HasAllExpression; +import com.datadog.debugger.el.expressions.HasAnyExpression; +import com.datadog.debugger.el.expressions.IfElseExpression; +import com.datadog.debugger.el.expressions.IfExpression; +import com.datadog.debugger.el.expressions.IndexExpression; +import com.datadog.debugger.el.expressions.IsDefinedExpression; +import com.datadog.debugger.el.expressions.IsEmptyExpression; +import com.datadog.debugger.el.expressions.LenExpression; +import com.datadog.debugger.el.expressions.MatchesExpression; +import com.datadog.debugger.el.expressions.NotExpression; +import com.datadog.debugger.el.expressions.StartsWithExpression; +import com.datadog.debugger.el.expressions.StringPredicateExpression; +import com.datadog.debugger.el.expressions.SubStringExpression; +import com.datadog.debugger.el.expressions.ValueRefExpression; +import com.datadog.debugger.el.expressions.WhenExpression; +import com.datadog.debugger.el.values.BooleanValue; +import com.datadog.debugger.el.values.ListValue; +import com.datadog.debugger.el.values.MapValue; +import com.datadog.debugger.el.values.NullValue; +import com.datadog.debugger.el.values.NumericValue; +import com.datadog.debugger.el.values.ObjectValue; +import com.datadog.debugger.el.values.SetValue; +import com.datadog.debugger.el.values.StringValue; +import datadog.trace.bootstrap.debugger.el.ReflectiveFieldValueResolver; +import datadog.trace.bootstrap.debugger.el.ValueReferences; +import datadog.trace.bootstrap.debugger.util.WellKnownClasses; +import datadog.trace.util.Strings; +import java.lang.invoke.CallSite; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldInsnNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.InvokeDynamicInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TryCatchBlockNode; +import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; + +public class ConditionInstrumenter { + + public static MethodNode generateConditionMethod( + String probeId, + int probeIndex, + ProbeCondition probeCondition, + ClassLoader classLoader, + ClassNode classNode, + MethodNode methodNode, + boolean atExit) { + Type returnType = Type.getReturnType(methodNode.desc); + String paramsDesc = + generateParamsDesc( + methodNode.desc, returnType != Type.VOID_TYPE ? returnType : null, atExit); + return doGenerateConditionMethod( + probeId, + "conditionMethod_", + probeIndex, + probeCondition, + classLoader, + classNode, + methodNode, + atExit, + paramsDesc, + returnType); + } + + public static MethodNode generateConditionExceptionMethod( + String probeId, + int probeIndex, + ProbeCondition probeCondition, + ClassLoader classLoader, + ClassNode classNode, + MethodNode methodNode) { + String paramsDesc = generateParamsDesc(methodNode.desc, Types.THROWABLE_TYPE, true); + return doGenerateConditionMethod( + probeId, + "conditionExceptionMethod_", + probeIndex, + probeCondition, + classLoader, + classNode, + methodNode, + true, + paramsDesc, + Types.THROWABLE_TYPE); + } + + private static MethodNode doGenerateConditionMethod( + String probeId, + String prefix, + int probeIndex, + ProbeCondition probeCondition, + ClassLoader classLoader, + ClassNode classNode, + MethodNode methodNode, + boolean atExit, + String paramsDesc, + Type returnType) { + int staticAccess = methodNode.access & Opcodes.ACC_STATIC; + int argOffset = (staticAccess != 0 ? 0 : 1); + int returnVarIndex = returnType != Type.VOID_TYPE ? argOffset : -1; + int argSizes = Type.getArgumentsAndReturnSizes(methodNode.desc); + int argTotalSize = (argSizes >> 2) + (argSizes & 3); + int timestampVarIndex = atExit ? argTotalSize : -1; + String methodName = "$" + prefix + sanitizedProbeId(probeId) + "$"; + LabelNode startNode = new LabelNode(); + LabelNode endNode = new LabelNode(); + MethodNode conditionMethod = + generatePredicateMethod( + probeCondition.getWhen(), + classLoader, + classNode, + methodNode.localVariables, + paramsDesc, + returnType, + startNode, + endNode, + staticAccess, + methodName, + timestampVarIndex, + returnVarIndex); + LabelNode handlerLabel = + generateExceptionHandler( + probeIndex, probeCondition.getDslExpression(), conditionMethod.instructions); + conditionMethod.tryCatchBlocks.add( + new TryCatchBlockNode( + startNode, endNode, handlerLabel, Type.getInternalName(Exception.class))); + return conditionMethod; + } + + private static MethodNode generatePredicateMethod( + BooleanExpression booleanExpression, + ClassLoader classLoader, + ClassNode classNode, + List localVariables, + String paramsDesc, + Type returnType, + LabelNode startNode, + LabelNode endNode, + int staticAccess, + String methodName, + int timestampVarIndex, + int returnVarIndex) { + MethodNode conditionMethod = + new MethodNode(Opcodes.ACC_PRIVATE | staticAccess, methodName, paramsDesc, null, null); + conditionMethod.maxLocals = getArgOffset(conditionMethod); + InsnList insnList = conditionMethod.instructions; + insnList.add(startNode); + booleanExpression.accept( + new ConditionVisitor( + conditionMethod, + classLoader, + classNode, + localVariables, + timestampVarIndex, + returnVarIndex, + returnType)); + insnList.add(new InsnNode(Opcodes.IRETURN)); + insnList.add(endNode); + return conditionMethod; + } + + private static String generateParamsDesc( + String desc, Type returnOrExceptionType, boolean atExit) { + String paramsDesc = desc.substring(1, desc.indexOf(')')); + paramsDesc = paramsDesc + (atExit ? "J" : ""); + if (returnOrExceptionType != null) { + // add return value or exception as parameter in front of the args + paramsDesc = returnOrExceptionType.getDescriptor() + paramsDesc; + } + paramsDesc = "(" + paramsDesc + ")Z"; + return paramsDesc; + } + + private static LabelNode generateExceptionHandler( + int probeIndex, String dslExpression, InsnList insnList) { + InsnList handler = new InsnList(); + LabelNode handlerLabel = new LabelNode(); + handler.add(handlerLabel); + // stack [Exception] + // handler.add(new InsnNode(Opcodes.ATHROW)); + ldc(handler, probeIndex); + ldc(handler, dslExpression); + invokeStatic( + handler, + DEBUGGER_CONTEXT_TYPE, + "handleConditionException", + Type.VOID_TYPE, + THROWABLE_TYPE, + Type.INT_TYPE, + STRING_TYPE); + handler.add(new InsnNode(Opcodes.ICONST_0)); // false + handler.add(new InsnNode(Opcodes.IRETURN)); + insnList.add(handler); + return handlerLabel; + } + + private static String sanitizedProbeId(String probeId) { + return probeId.replace('-', '_'); + } + + private static class ConditionVisitor implements Visitor { + private final MethodNode conditionMethod; + private final InsnList insnList; + private final ClassLoader classLoader; + private final ClassNode classNode; + private final List localVariables; + private final int timestampVarIndex; + private final int returnVarIndex; + private final Type returnType; + private int lambdaCounter; + + public ConditionVisitor( + MethodNode conditionMethod, + ClassLoader classLoader, + ClassNode classNode, + List localVariables, + int timestampVarIndex, + int returnVarIndex, + Type returnType) { + this.conditionMethod = conditionMethod; + this.insnList = conditionMethod.instructions; + this.classLoader = classLoader; + this.classNode = classNode; + this.localVariables = localVariables; + this.timestampVarIndex = timestampVarIndex; + this.returnVarIndex = returnVarIndex; + this.returnType = returnType; + } + + @Override + public Type visit(BinaryExpression binaryExpression) { + BinaryOperator operator = binaryExpression.getOperator(); + binaryExpression.getLeft().accept(this); + int cmpOpCode = operator == BinaryOperator.OR ? Opcodes.IFNE : Opcodes.IFEQ; + // stack [boolean] + LabelNode targetNode = new LabelNode(); + LabelNode gotoNode = new LabelNode(); + insnList.add(new JumpInsnNode(cmpOpCode, targetNode)); + // stack [] + binaryExpression.getRight().accept(this); + // stack [boolean] + if (operator == BinaryOperator.OR) { + LabelNode targetRightNode = new LabelNode(); + insnList.add(new JumpInsnNode(Opcodes.IFEQ, targetRightNode)); + insnList.add(targetNode); + // stack [] + insnList.add(new InsnNode(Opcodes.ICONST_1)); + // stack [true] + insnList.add(new JumpInsnNode(Opcodes.GOTO, gotoNode)); + + insnList.add(targetRightNode); + // stack [] + insnList.add(new InsnNode(Opcodes.ICONST_0)); + // stack [false] + insnList.add(gotoNode); + } else if (operator == BinaryOperator.AND) { + insnList.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); + // stack [] + insnList.add(new InsnNode(Opcodes.ICONST_1)); + // stack [true] + insnList.add(new JumpInsnNode(Opcodes.GOTO, gotoNode)); + insnList.add(targetNode); + // stack [] + insnList.add(new InsnNode(Opcodes.ICONST_0)); + // stack [false] + insnList.add(gotoNode); + } + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(BinaryOperator operator) { + // not used + return Type.VOID_TYPE; + } + + private Type widen(Type type) { + if (isPrimitive(type)) { + if (type.getSort() == Type.INT) { + insnList.add(new InsnNode(Opcodes.I2L)); + return Type.LONG_TYPE; + } + if (type.getSort() == Type.FLOAT) { + insnList.add(new InsnNode(Opcodes.F2D)); + return Type.DOUBLE_TYPE; + } + } + return type; + } + + @Override + public Type visit(ComparisonExpression comparisonExpression) { + Type leftType = comparisonExpression.getLeft().accept(this); + if (comparisonExpression.getOperator() != ComparisonOperator.INSTANCEOF) { + // don't widen for instanceof. Want to preserve original type (ex: 1 instanceof Integer) + leftType = widen(leftType); + } + Type rightType = comparisonExpression.getRight().accept(this); + rightType = widen(rightType); + // stack [value_left, value_right] + switch (comparisonExpression.getOperator()) { + case EQ: + equalsOperator(leftType, rightType); + break; + case GT: + comparisonOperator(leftType, rightType, Opcodes.IFGT); + break; + case GE: + comparisonOperator(leftType, rightType, Opcodes.IFGE); + break; + case LT: + comparisonOperator(leftType, rightType, Opcodes.IFLT); + break; + case LE: + comparisonOperator(leftType, rightType, Opcodes.IFLE); + break; + case INSTANCEOF: + instanceOfOperator(leftType, rightType); + break; + default: + throw new IllegalArgumentException( + "Unsupported operator: " + comparisonExpression.getOperator()); + } + return Type.BOOLEAN_TYPE; + } + + private void instanceOfOperator(Type leftType, Type rightType) { + if (rightType.equals(STRING_TYPE)) { + // stack [left_value, right_value] + int varId = newVar(this.conditionMethod, rightType); + insnList.add(new VarInsnNode(rightType.getOpcode(Opcodes.ISTORE), varId)); + // stack [left_value] + tryBox(leftType, insnList); + // stack [left_value_boxed] + insnList.add(new VarInsnNode(rightType.getOpcode(Opcodes.ILOAD), varId)); + // stack [left_value_boxed, right_value] + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "equalsWithInstanceOf", + Type.BOOLEAN_TYPE, + OBJECT_TYPE, + STRING_TYPE); + } else { + throw new IllegalArgumentException("Invalid arguments for instanceof operator"); + } + } + + private void equalsOperator(Type leftType, Type rightType) { + // stack [value_left, value_right] + if (isPrimitive(leftType) || isPrimitive(rightType)) { + if (leftType == Type.LONG_TYPE && rightType == Type.LONG_TYPE) { + insnList.add(new InsnNode(Opcodes.LCMP)); + // stack [int] + addComparisonInsn(insnList, Opcodes.IFEQ); + } else if (isIntCompatible(leftType) && isIntCompatible(rightType)) { + addComparisonInsn(insnList, Opcodes.IF_ICMPEQ); + } else if (isNumeric(leftType) && isNumeric(rightType)) { + addHeterogeneousComparison(leftType, rightType, Opcodes.IFEQ); + } else { + throw new IllegalArgumentException( + "Unsupported equals comparison: " + + leftType.getClassName() + + " <=> " + + rightType.getClassName()); + } + // stack [boolean] + } else if (isEnum(leftType) && rightType.getClassName().equals(String.class.getTypeName())) { + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "equalsForEnum", + Type.BOOLEAN_TYPE, + Types.ENUM_TYPE, + STRING_TYPE); + } else if (isEnum(rightType) && leftType.getClassName().equals(String.class.getTypeName())) { + insnList.add(new InsnNode(Opcodes.SWAP)); + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "equalsForEnum", + Type.BOOLEAN_TYPE, + Types.ENUM_TYPE, + STRING_TYPE); + } else { + // if String or object, invoke Objects.equals + invokeStatic( + insnList, + Type.getType(Objects.class), + "equals", + Type.BOOLEAN_TYPE, + OBJECT_TYPE, + OBJECT_TYPE); + // stack: [boolean] + } + } + + private boolean isIntCompatible(Type type) { + return type == Type.BOOLEAN_TYPE + || type == Type.BYTE_TYPE + || type == Type.SHORT_TYPE + || type == Type.INT_TYPE; + } + + private void comparisonOperator(Type leftType, Type rightType, int cmpOpcode) { + // stack [value_left, value_right] + if (leftType == Type.LONG_TYPE && rightType == Type.LONG_TYPE) { + insnList.add(new InsnNode(Opcodes.LCMP)); + // stack [int] + addComparisonInsn(insnList, cmpOpcode); + // stack [boolean] + } else if (leftType.equals(STRING_TYPE) && rightType.equals(STRING_TYPE)) { + invokeVirtual(insnList, leftType, "compareTo", Type.INT_TYPE, STRING_TYPE); + // stack [int] + addComparisonInsn(insnList, cmpOpcode); + // stack [boolean] + } else if (leftType.equals(Type.DOUBLE_TYPE) && rightType.equals(Type.DOUBLE_TYPE)) { + if (cmpOpcode == Opcodes.IFGE || cmpOpcode == Opcodes.IFGT) { + insnList.add(new InsnNode(Opcodes.DCMPG)); + addComparisonInsn(insnList, cmpOpcode); + } + if (cmpOpcode == Opcodes.IFLE || cmpOpcode == Opcodes.IFLT) { + insnList.add(new InsnNode(Opcodes.DCMPL)); + // stack [int] + addComparisonInsn(insnList, cmpOpcode); + } + } else if (isNumeric(leftType) && isNumeric(rightType)) { + // Use RuntimeHelper to perform comparison with heterogeneous types + addHeterogeneousComparison(leftType, rightType, cmpOpcode); + } else { + throw new IllegalArgumentException( + "Unsupported comparison: " + + leftType.getClassName() + + " <=> " + + rightType.getClassName()); + } + } + + private void addHeterogeneousComparison(Type leftType, Type rightType, int cmpOpcode) { + int varId = newVar(this.conditionMethod, rightType); + insnList.add(new VarInsnNode(rightType.getOpcode(Opcodes.ISTORE), varId)); + // stack [left_value] + tryBox(leftType, insnList); + // stack [left_value_boxed] + insnList.add(new VarInsnNode(rightType.getOpcode(Opcodes.ILOAD), varId)); + // stack [left_value_boxed, right_value] + tryBox(rightType, insnList); + // stack [left_value_boxed, right_value_boxed] + ldc(insnList, cmpOpcode); + // stack [value_left_boxed, value_right_boxed, cmpOpCode] + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "compareTo", + Type.BOOLEAN_TYPE, + OBJECT_TYPE, + OBJECT_TYPE, + Type.INT_TYPE); + // stack [boolean] + } + + private boolean isNumeric(Type type) { + return type == Type.LONG_TYPE + || type == Type.DOUBLE_TYPE + || type == Type.INT_TYPE + || type == Type.FLOAT_TYPE + || type.equals(Types.BIG_DECIMAL_TYPE); + } + + @Override + public Type visit(ComparisonOperator operator) { + throw new UnsupportedOperationException("visit ComparisonOperator"); + } + + @Override + public Type visit(ContainsExpression containsExpression) { + Type sourceType = containsExpression.getTarget().accept(this); + // stack [Source] + Type valueType = containsExpression.getValue().accept(this); + // stack [Source, Value] + if (sourceType.equals(STRING_TYPE)) { + invokeVirtual( + insnList, sourceType, "contains", Type.BOOLEAN_TYPE, Type.getType(CharSequence.class)); + } else if (sourceType.getSort() == Type.OBJECT || sourceType.getSort() == Type.ARRAY) { + tryBox(valueType, insnList); + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "contains", + Type.BOOLEAN_TYPE, + OBJECT_TYPE, + OBJECT_TYPE); + } else { + throw new UnsupportedOperationException( + "Unsupported type for contains function: " + sourceType.getClassName()); + } + // stack [boolean] + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(EndsWithExpression endsWithExpression) { + return visitStringPredicate(endsWithExpression, "endsWith"); + } + + private static void addComparisonInsn(InsnList insnList, int opcode) { + LabelNode targetNode = new LabelNode(); + LabelNode gotoNode = new LabelNode(); + insnList.add(new JumpInsnNode(opcode, targetNode)); + insnList.add(new InsnNode(Opcodes.ICONST_0)); // false + insnList.add(new JumpInsnNode(Opcodes.GOTO, gotoNode)); + insnList.add(targetNode); + insnList.add(new InsnNode(Opcodes.ICONST_1)); // true + insnList.add(gotoNode); + } + + static final Class[] ITERATOR_CLASS = + new Class[] { + null, // void + Boolean.TYPE, + Character.TYPE, + Byte.TYPE, + Short.TYPE, + Integer.TYPE, + Float.TYPE, + Long.TYPE, + Double.TYPE, + null, // array + Object.class + }; + + static final String[] PREDICATE_DESCRIPTORS = + new String[] { + null, // void + "Ldatadog/trace/bootstrap/debugger/ConditionHelper$BooleanPredicate;", + "Ljava/util/function/IntPredicate;", // char + "Ljava/util/function/IntPredicate;", // byte + "Ljava/util/function/IntPredicate;", // short + "Ljava/util/function/IntPredicate;", // int + "Ljava/util/function/DoublePredicate;", // float + "Ljava/util/function/LongPredicate;", // long + "Ljava/util/function/DoublePredicate;", // double + null, // array + "Ljava/util/function/Predicate;" // Object + }; + + static final String[] COLLECTION_METHOD_SUFFIXES = { + null, // void + "BooleanArray", + "CharArray", + "ByteArray", + "ShortArray", + "IntArray", + "FloatArray", + "LongArray", + "DoubleArray", + null, // array + "ObjectArray" + }; + + static class PredicateInfo { + String iterateMethodName; + String predicateMethodRefIndyDesc; + Class iteratorClass; + Type iteratorType; + Type predicateInputType; + Type predicateReturnType; + List additionalParam; + List paramPushInsns; + String desc; + } + + private static PredicateInfo getPredicateInfo( + String prefixMethodName, + Type sourceType, + BooleanExpression booleanExpression, + List localVarNodes, + ClassNode classNode) { + PredicateInfo predicateInfo = new PredicateInfo(); + if (Types.isArray(sourceType)) { + Type elementType = sourceType.getElementType(); + int elementSort = elementType.getSort(); + if (elementSort < 0 || elementSort >= PREDICATE_DESCRIPTORS.length) { + throw new IllegalArgumentException("elementSort out of range: " + elementSort); + } + predicateInfo.iterateMethodName = + prefixMethodName + COLLECTION_METHOD_SUFFIXES[elementSort]; + predicateInfo.predicateMethodRefIndyDesc = PREDICATE_DESCRIPTORS[elementSort]; + predicateInfo.iteratorClass = ITERATOR_CLASS[elementSort]; + if (isPrimitive(elementType)) { + predicateInfo.iteratorType = elementType; + predicateInfo.predicateInputType = sourceType; + predicateInfo.predicateReturnType = sourceType; + } else { + predicateInfo.iteratorType = Types.OBJECT_TYPE; + // Object[] is covariant so accept any *[] + predicateInfo.predicateInputType = Types.OBJECT_ARRAY_TYPE; + predicateInfo.predicateReturnType = Types.OBJECT_ARRAY_TYPE; + } + } else { + predicateInfo.iterateMethodName = prefixMethodName + "Collection"; + predicateInfo.predicateMethodRefIndyDesc = "Ljava/util/function/Predicate;"; + predicateInfo.iteratorClass = Object.class; + predicateInfo.iteratorType = Types.OBJECT_TYPE; + predicateInfo.predicateInputType = COLLECTION_TYPE; + predicateInfo.predicateReturnType = COLLECTION_TYPE; + } + PredicateAnalysisVisitor analysisVisitor = new PredicateAnalysisVisitor(); + booleanExpression.accept(analysisVisitor); + // analyze captured ValueRef used in the predicate + predicateInfo.additionalParam = new ArrayList<>(); + predicateInfo.paramPushInsns = new ArrayList<>(); + if (!analysisVisitor.refVariableNames.isEmpty()) { + for (String symbolName : analysisVisitor.refVariableNames) { + if (symbolName.equals(ValueReferences.ITERATOR_REF)) { + // skip @it + continue; + } + LocalVariableNode localVariableNode = + localVarNodes.stream() + .filter(node -> node.name.equals(symbolName)) + .findFirst() + .orElse(null); + if (localVariableNode != null) { + predicateInfo.additionalParam.add( + new LocalVariableNode( + localVariableNode.name, + localVariableNode.desc, + null, + null, + null, + predicateInfo.additionalParam.size())); + InsnList insnList = new InsnList(); + Type type = Type.getType(localVariableNode.desc); + insnList.add(new VarInsnNode(type.getOpcode(Opcodes.ILOAD), localVariableNode.index)); + predicateInfo.paramPushInsns.add(insnList); + continue; + } + FieldNode fieldNode = + classNode.fields.stream() + .filter(node -> node.name.equals(symbolName)) + .findFirst() + .orElse(null); + if (fieldNode != null) { + predicateInfo.additionalParam.add( + new LocalVariableNode( + fieldNode.name, + fieldNode.desc, + null, + null, + null, + predicateInfo.additionalParam.size())); + InsnList insnList = new InsnList(); + if (isStaticField(fieldNode)) { + insnList.add( + new FieldInsnNode( + Opcodes.GETSTATIC, classNode.name, fieldNode.name, fieldNode.desc)); + } else { + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // stack [this] + insnList.add( + new FieldInsnNode( + Opcodes.GETFIELD, classNode.name, fieldNode.name, fieldNode.desc)); + // stack [field] + } + // TODO look up in inherited fields? + predicateInfo.paramPushInsns.add(insnList); + continue; + } + // search for inherited field? + throw new IllegalArgumentException( + "invalid reference for predicate method: " + symbolName); + } + } + String desc = + predicateInfo.additionalParam.stream().map(p -> p.desc).collect(Collectors.joining("")); + predicateInfo.desc = "(" + desc + predicateInfo.iteratorType.getDescriptor() + ")Z"; + predicateInfo.predicateMethodRefIndyDesc = + "(" + desc + ")" + predicateInfo.predicateMethodRefIndyDesc; + return predicateInfo; + } + + @Override + public Type visit(FilterCollectionExpression filterCollectionExpression) { + Type sourceType = filterCollectionExpression.getSource().accept(this); + // stack [sourceCollection] + PredicateInfo predicateInfo = + getPredicateInfo( + "filter", + sourceType, + filterCollectionExpression.getFilterExpression(), + localVariables, + classNode); + String methodName = + generateCollectionPredicateLambda( + filterCollectionExpression.getFilterExpression(), predicateInfo); + // push additional params (captured refs) + for (InsnList pushInsn : predicateInfo.paramPushInsns) { + insnList.add(pushInsn); + } + // stack [sourceCollection, capturedRef...] + pushPredicateMethodRef( + insnList, + classNode.name, + methodName, + predicateInfo.predicateMethodRefIndyDesc, + true, + "test", + Type.getMethodType(Type.BOOLEAN_TYPE, predicateInfo.iteratorType), + predicateInfo.desc, + Type.getMethodType(Type.BOOLEAN_TYPE, predicateInfo.iteratorType)); + // stack [sourceCollection, capturedRef..., PredicateFunc] + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + predicateInfo.iterateMethodName, + predicateInfo.predicateReturnType, + predicateInfo.predicateInputType, + Type.getReturnType(predicateInfo.predicateMethodRefIndyDesc)); + // stack [filteredCollection] + return predicateInfo.predicateReturnType; + } + + private String generateCollectionPredicateLambda( + BooleanExpression booleanExpression, PredicateInfo predicateInfo) { + LabelNode startNode = new LabelNode(); + LabelNode endNode = new LabelNode(); + String methodName = "lambda" + conditionMethod.name + lambdaCounter++; + MethodNode predicateMethod = + generatePredicateMethod( + booleanExpression, + classLoader, + classNode, + predicateInfo.additionalParam, + predicateInfo.desc, + Type.BOOLEAN_TYPE, + startNode, + endNode, + Opcodes.ACC_STATIC, + methodName, + -1, + -1); + classNode.methods.add(predicateMethod); + return methodName; + } + + @Override + public Type visit(HasAllExpression hasAllExpression) { + Type sourceType = hasAllExpression.getValueExpression().accept(this); + // stack [sourceCollection] + PredicateInfo predicateInfo = + getPredicateInfo( + "all", + sourceType, + hasAllExpression.getFilterPredicateExpression(), + localVariables, + classNode); + String methodName = + generateCollectionPredicateLambda( + hasAllExpression.getFilterPredicateExpression(), predicateInfo); + // push additional params (captured refs) + for (InsnList pushInsn : predicateInfo.paramPushInsns) { + insnList.add(pushInsn); + } + // stack [sourceCollection, capturedRef...] + pushPredicateMethodRef( + insnList, + classNode.name, + methodName, + predicateInfo.predicateMethodRefIndyDesc, + true, + "test", + Type.getMethodType(Type.BOOLEAN_TYPE, predicateInfo.iteratorType), + predicateInfo.desc, + Type.getMethodType(Type.BOOLEAN_TYPE, predicateInfo.iteratorType)); + // stack [sourceCollection, capturedRef..., PredicateFunc] + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + predicateInfo.iterateMethodName, + Type.BOOLEAN_TYPE, + predicateInfo.predicateInputType, + Type.getReturnType(predicateInfo.predicateMethodRefIndyDesc)); + // stack [boolean] + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(HasAnyExpression hasAnyExpression) { + Type sourceType = hasAnyExpression.getValueExpression().accept(this); + // stack [sourceCollection] + PredicateInfo predicateInfo = + getPredicateInfo( + "any", + sourceType, + hasAnyExpression.getFilterPredicateExpression(), + localVariables, + classNode); + String methodName = + generateCollectionPredicateLambda( + hasAnyExpression.getFilterPredicateExpression(), predicateInfo); + // push additional params (captured refs) + for (InsnList pushInsn : predicateInfo.paramPushInsns) { + insnList.add(pushInsn); + } + // stack [sourceCollection, capturedRef...] + pushPredicateMethodRef( + insnList, + classNode.name, + methodName, + predicateInfo.predicateMethodRefIndyDesc, + true, + "test", + Type.getMethodType(Type.BOOLEAN_TYPE, predicateInfo.iteratorType), + predicateInfo.desc, + Type.getMethodType(Type.BOOLEAN_TYPE, predicateInfo.iteratorType)); + // stack [sourceCollection, capturedRef..., PredicateFunc] + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + predicateInfo.iterateMethodName, + Type.BOOLEAN_TYPE, + predicateInfo.predicateInputType, + Type.getReturnType(predicateInfo.predicateMethodRefIndyDesc)); + // stack [boolean] + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(IfElseExpression ifElseExpression) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Type visit(IfExpression ifExpression) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public Type visit(IsEmptyExpression isEmptyExpression) { + Type sourceType = isEmptyExpression.getValueExpression().accept(this); + // stack [Value] + if (sourceType.getSort() == Type.ARRAY || sourceType.getSort() == Type.OBJECT) { + invokeStatic(insnList, CONDITION_HELPER_TYPE, "isEmpty", Type.BOOLEAN_TYPE, OBJECT_TYPE); + } else { + throw new UnsupportedOperationException( + "Unsupported type for isEmpty function: " + sourceType.getClassName()); + } + // stack [boolean] + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(IsDefinedExpression isDefinedExpression) { + LabelNode isDefinedStart = new LabelNode(); + insnList.add(isDefinedStart); + LabelNode isDefinedEnd = new LabelNode(); + Type exprType = isDefinedExpression.getValueExpression().accept(this); + insnList.add(new InsnNode(exprType.getSize() == 2 ? Opcodes.POP2 : Opcodes.POP)); + insnList.add(new InsnNode(Opcodes.ICONST_1)); + LabelNode afterCatch = new LabelNode(); + insnList.add(new JumpInsnNode(Opcodes.GOTO, afterCatch)); + insnList.add(isDefinedEnd); + LabelNode handlerLabel = new LabelNode(); + InsnList handler = new InsnList(); + handler.add(handlerLabel); + // stack [exception] + // swallow exception + handler.add(new InsnNode(Opcodes.POP)); + // stack [] + handler.add(new InsnNode(Opcodes.ICONST_0)); + // stack [false] + conditionMethod.instructions.add(handler); + conditionMethod.instructions.add(afterCatch); + conditionMethod.tryCatchBlocks.add( + new TryCatchBlockNode(isDefinedStart, isDefinedEnd, handlerLabel, null)); + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(LenExpression lenExpression) { + Type sourceType = lenExpression.getSource().accept(this); + // stack [object_source] + if (sourceType.equals(STRING_TYPE)) { + invokeVirtual(insnList, sourceType, "length", Type.INT_TYPE); + } else if (sourceType.getSort() == Type.OBJECT) { + invokeStatic(insnList, CONDITION_HELPER_TYPE, "len", Type.INT_TYPE, OBJECT_TYPE); + } else { + throw new UnsupportedOperationException( + "Unsupported type for len function: " + sourceType.getClassName()); + } + // stack [int] + return Type.INT_TYPE; + } + + @Override + public Type visit(MatchesExpression matchesExpression) { + return visitStringPredicate(matchesExpression, "matches"); + } + + private Type visitStringPredicate(StringPredicateExpression expression, String methodName) { + Type sourceType = expression.getSourceString().accept(this); + // stack [String] + Type exprType = expression.getStr().accept(this); + if (!exprType.equals(STRING_TYPE)) { + throw new UnsupportedOperationException("Second operand must be string"); + } + // stack [String, String] + if (sourceType.equals(STRING_TYPE)) { + invokeVirtual(insnList, sourceType, methodName, Type.BOOLEAN_TYPE, STRING_TYPE); + } else if (sourceType.getSort() == Type.OBJECT) { + pushPredicateMethodRef( + insnList, + "java/lang/String", + methodName, + "()Ljava/util/function/BiPredicate;", + false, + // method name of the SAM + "test", + // ASM Type from method in SAM + Type.getMethodType(Type.BOOLEAN_TYPE, OBJECT_TYPE, OBJECT_TYPE), + // MethodType from concrete method + MethodType.methodType(boolean.class, String.class), + // ASM type of implementation method + Type.getMethodType(Type.BOOLEAN_TYPE, STRING_TYPE, STRING_TYPE)); + // stack [Object, String, PredicateFunc] + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "stringPredicate", + Type.BOOLEAN_TYPE, + OBJECT_TYPE, + STRING_TYPE, + Type.getType(BiPredicate.class)); + } else { + throw new UnsupportedOperationException( + "Unsupported type for " + methodName + " function: " + sourceType.getClassName()); + } + // stack [boolean] + return Type.BOOLEAN_TYPE; + } + + /** + * @param owner type where the referenced method belong (ex: String class) + * @param methodName name of the referenced method (ex: matches) + * @param methodRefIndyDescriptor JVM descriptor of the method ref indy call + params (ex: + * (Ljava/lang/Object;)Ljava/util/function/BiPredicate;) + * @param isStatic + * @param samMethodName method name of the SAM (ex: test) + * @param samMethodType ASM type of the SAM (Single Abstract Method) site (ex: + * Consumer::accept) + * @param concreteMethodType MethodType of the concrete referenced method (ex String::matches => + * boolean (String)) + * @param implMethodType ASM type of the concrete referenced method (ex: boolean + * String::matches(String s) + */ + private static void pushPredicateMethodRef( + InsnList insnList, + String owner, + String methodName, + String methodRefIndyDescriptor, + boolean isStatic, + String samMethodName, + Type samMethodType, + MethodType concreteMethodType, + Type implMethodType) { + pushPredicateMethodRef( + insnList, + owner, + methodName, + methodRefIndyDescriptor, + isStatic, + samMethodName, + samMethodType, + concreteMethodType.toMethodDescriptorString(), + implMethodType); + } + + private static void pushPredicateMethodRef( + InsnList insnList, + String owner, + String methodName, + String methodRefIndyDescriptor, + boolean isStatic, + String samMethodName, + Type samMethodType, + String concreteMethodDesc, + Type implMethodType) { + MethodType bsmType = + MethodType.methodType( + CallSite.class, + MethodHandles.Lookup.class, + String.class, + MethodType.class, + MethodType.class, + MethodHandle.class, + MethodType.class); + Handle bsmHandle = + new Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/invoke/LambdaMetafactory", + "metafactory", + bsmType.toMethodDescriptorString(), + false); + Handle implHandle = + new Handle( + isStatic ? Opcodes.H_INVOKESTATIC : Opcodes.H_INVOKEVIRTUAL, + owner, + methodName, + concreteMethodDesc, + false); + insnList.add( + new InvokeDynamicInsnNode( + samMethodName, + methodRefIndyDescriptor, + bsmHandle, + samMethodType, + implHandle, + implMethodType)); + } + + @Override + public Type visit(NotExpression notExpression) { + notExpression.getPredicate().accept(this); + // stack [boolean] + addComparisonInsn(insnList, Opcodes.IFEQ); + // stack [boolean] + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(StartsWithExpression startsWithExpression) { + return visitStringPredicate(startsWithExpression, "startsWith"); + } + + @Override + public Type visit(SubStringExpression subStringExpression) { + Type sourceType = subStringExpression.getSource().accept(this); + // stack [String] + ldc(insnList, subStringExpression.getStartIndex()); + // stack [String, int] + ldc(insnList, subStringExpression.getEndIndex()); + // stack [String, int, int] + if (sourceType.equals(STRING_TYPE)) { + invokeVirtual(insnList, sourceType, "substring", STRING_TYPE, Type.INT_TYPE, Type.INT_TYPE); + } else if (sourceType.getSort() == Type.OBJECT) { + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "substring", + STRING_TYPE, + OBJECT_TYPE, + Type.INT_TYPE, + Type.INT_TYPE); + } else { + throw new UnsupportedOperationException( + "Unsupported type for substring function: " + sourceType.getClassName()); + } + // stack [String] + return STRING_TYPE; + } + + @Override + public Type visit(ValueRefExpression valueRefExpression) { + String symbolName = valueRefExpression.getSymbolName(); + if (symbolName.startsWith("@")) { + return resolveSyntheticVars(symbolName); + } + // lookup in local vars + LocalVariableNode localVariableNode = + localVariables.stream() + .filter(node -> node.name.equals(symbolName)) + .findFirst() + .orElse(null); + if (localVariableNode != null) { + int argOffset = 0; // local var indices includes this already + if (returnVarIndex >= 0) { + argOffset += returnType.getSize(); + } + Type type = Type.getType(localVariableNode.desc); + insnList.add( + new VarInsnNode(type.getOpcode(Opcodes.ILOAD), argOffset + localVariableNode.index)); + // stack [var] + return type; + } + // lookup in fields + FieldNode fieldNode = + classNode.fields.stream() + .filter(node -> node.name.equals(symbolName)) + .findFirst() + .orElse(null); + if (fieldNode != null) { + Type type = Type.getType(fieldNode.desc); // lexical type. want the dynamic type? + if (isStaticField(fieldNode)) { + insnList.add( + new FieldInsnNode(Opcodes.GETSTATIC, classNode.name, fieldNode.name, fieldNode.desc)); + } else { + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // stack [this] + insnList.add( + new FieldInsnNode(Opcodes.GETFIELD, classNode.name, fieldNode.name, fieldNode.desc)); + // stack [field] + } + return type; + } + // lookup in inherited classes ? assuming classes are already loaded so safe to to use + // Class.forName() + // going through ReflectiveFieldValueResolver.resolve(); ? too dynamic? + // optimize after first execution, to generate actual bytecode access + String superName = Strings.getClassName(classNode.superName); + boolean isInherited = false; + while (superName != null) { + Class clazz; + try { + clazz = Class.forName(superName, false, classLoader); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + Map fields = + Arrays.stream(clazz.getDeclaredFields()) + .collect(Collectors.toMap(Field::getName, Function.identity())); + Field field = fields.get(symbolName); + if (field != null) { + Type type = Type.getType(field.getType()); + // TODO check for access (public, protected, private) + boolean isPrivate = Modifier.isPrivate(field.getModifiers()); + if (isStaticField(field) && !isInherited) { + // /!\ possible duplicated class definition as accessing static inherited field can lead + // to class clinit + insnList.add( + new FieldInsnNode( + Opcodes.GETSTATIC, classNode.name, field.getName(), type.getDescriptor())); + } else { + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // stack [this] + if (isPrivate) { + // use reflection + return addReflectionFieldResolution(insnList, field); + } + insnList.add( + new FieldInsnNode( + Opcodes.GETFIELD, classNode.name, field.getName(), type.getDescriptor())); + // stack [field] + } + return type; + } + if (clazz.getSuperclass() == null) { + break; + } + isInherited = true; + superName = clazz.getSuperclass().getName(); + } + // not found symbol + throw new IllegalArgumentException("Cannot find symbol: " + symbolName); + } + + private Type resolveSyntheticVars(String symbolName) { + switch (symbolName) { + case ValueReferences.RETURN_REF: + if (returnVarIndex == -1) { + throw new IllegalArgumentException("@return not available (void?)"); + } + insnList.add(new VarInsnNode(returnType.getOpcode(Opcodes.ILOAD), returnVarIndex)); + return returnType; + case ValueReferences.DURATION_REF: + { + if (timestampVarIndex == -1) { + throw new IllegalArgumentException("@duration not available (not at exit)"); + } + invokeStatic(insnList, Type.getType(System.class), "nanoTime", Type.LONG_TYPE); + // stack [long] + insnList.add(new VarInsnNode(Opcodes.LLOAD, timestampVarIndex)); + // stack [long, long] + insnList.add(new InsnNode(Opcodes.LSUB)); + // stack [long] + return Type.LONG_TYPE; + } + case ValueReferences.EXCEPTION_REF: + // @exception is using returnVarIndex as no return type for uncaught exception + if (returnVarIndex == -1) { + throw new IllegalArgumentException("@exception not available"); + } + insnList.add(new VarInsnNode(returnType.getOpcode(Opcodes.ILOAD), returnVarIndex)); + return Types.THROWABLE_TYPE; + case ValueReferences.ITERATOR_REF: + if (!conditionMethod.name.startsWith("lambda$")) { + throw new IllegalArgumentException( + "@it not available if not used with collection functions (filter, any or all)"); + } + // assume this is a dedicated predicate static method with last argument the `it` + Type[] argumentTypes = Type.getArgumentTypes(conditionMethod.desc); + if (argumentTypes.length == 0) { + throw new IllegalArgumentException( + "@it not available for the predicate method: " + conditionMethod.name); + } + Type lastArgumentType = argumentTypes[argumentTypes.length - 1]; + int lastArgIndex = 0; + for (int i = 0; i < argumentTypes.length - 1; i++) { // except the last arg + lastArgIndex += argumentTypes[i].getSize(); + } + insnList.add(new VarInsnNode(lastArgumentType.getOpcode(Opcodes.ILOAD), lastArgIndex)); + return lastArgumentType; + default: + throw new IllegalArgumentException("Unsupported symbol: " + symbolName); + } + } + + @Override + public Type visit(GetMemberExpression getMemberExpression) { + Type targetType = getMemberExpression.getTarget().accept(this); + // stack [ref] + String memberName = getMemberExpression.getMemberName(); + Class clazz; + try { + clazz = Class.forName(targetType.getClassName(), false, classLoader); + } catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + Map specialTypeAccess = + WellKnownClasses.getSpecialTypeAccess(clazz); + if (specialTypeAccess != null) { + WellKnownClasses.SpecialFieldInfo specialFieldInfo = specialTypeAccess.get(memberName); + if (specialFieldInfo != null) { + return addSpecialFieldAccessCall(specialFieldInfo, clazz); + } + } + Map fields = + Arrays.stream(clazz.getDeclaredFields()) + .collect(Collectors.toMap(Field::getName, Function.identity())); + Field field = fields.get(memberName); + if (field != null) { + // TODO use Field::canAccess method (JDK9+) to make sure we can access it with a GETFIELD + if (field.isAccessible()) { + insnList.add( + new FieldInsnNode( + Opcodes.GETFIELD, + targetType.getClassName(), + memberName, + Type.getDescriptor(field.getType()))); + // stack [field] + return Type.getType(field.getType()); + } + // emit call to ReflectiveFieldValueResolver.getFieldValue + return addReflectionFieldResolution(insnList, field); + } + // try to resolve base on dynamic types with ConditionHelper + ldc(insnList, memberName); + invokeStatic( + insnList, + CONDITION_HELPER_TYPE, + "resolveByFieldName", + OBJECT_TYPE, + OBJECT_TYPE, + STRING_TYPE); + return OBJECT_TYPE; + } + + private Type addSpecialFieldAccessCall( + WellKnownClasses.SpecialFieldInfo specialFieldInfo, Class clazz) { + // stack [ref] + if (specialFieldInfo.checksOverride) { + insnList.add(new InsnNode(Opcodes.DUP)); + // stack [ref, ref] + ldc(insnList, specialFieldInfo.method.getName()); + // stack [ref, ref, String] + ldc(insnList, Type.getType(specialFieldInfo.method.getDeclaringClass())); + // stack [ref, ref, String, Class] + invokeStatic( + insnList, + Type.getType(WellKnownClasses.class), + "isOverridden", + Type.BOOLEAN_TYPE, + OBJECT_TYPE, + STRING_TYPE, + CLASS_TYPE); + // stack [ref, boolean] + LabelNode targetNode = new LabelNode(); + LabelNode gotoNode = new LabelNode(); + insnList.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); + // stack [ref] + newInstance(insnList, Type.getType(UnsupportedOperationException.class)); + // stack [ref, Exception] + insnList.add(new InsnNode(Opcodes.DUP)); + // stack [ref, Exception, Exception] + ldc(insnList, BECAUSE_OVERRIDDEN); + // stack [ref, Exception, Exception, String] + invokeConstructor(insnList, Type.getType(UnsupportedOperationException.class), STRING_TYPE); + // stack [ref, Exception] + insnList.add(new InsnNode(Opcodes.ATHROW)); + insnList.add(targetNode); + } + if (specialFieldInfo.method.getParameterCount() == 1) { + // special case for Optional*::orElse calls + switch (specialFieldInfo.method.getParameterTypes()[0].getName()) { + case "boolean": + case "byte": + case "short": + case "char": + case "int": + ldc(insnList, 0); + break; + case "long": + ldc(insnList, 0L); + break; + case "double": + ldc(insnList, 0.0); + break; + default: + ldc(insnList, null); + } + } + insnList.add( + new MethodInsnNode( + Opcodes.INVOKEVIRTUAL, + Type.getType(clazz).getInternalName(), + specialFieldInfo.method.getName(), + Type.getMethodDescriptor(specialFieldInfo.method))); + return Type.getReturnType(specialFieldInfo.method); + } + + private Type addReflectionFieldResolution(InsnList insnList, Field field) { + ldc(insnList, field.getName()); + if (field.getType().getTypeName().equals("int")) { + invokeStatic( + insnList, + Type.getType(ReflectiveFieldValueResolver.class), + "getFieldValueAsInt", + Type.INT_TYPE, + OBJECT_TYPE, + STRING_TYPE); + return Type.INT_TYPE; + } + // TODO other prim types + invokeStatic( + insnList, + Type.getType(ReflectiveFieldValueResolver.class), + "getFieldValue", + OBJECT_TYPE, + OBJECT_TYPE, + STRING_TYPE); + insnList.add( + new TypeInsnNode(Opcodes.CHECKCAST, Type.getType(field.getType()).getInternalName())); + return Type.getType(field.getType()); + } + + private String capitalize(String str) { + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + @Override + public Type visit(IndexExpression indexExpression) { + Type targetType = indexExpression.getTarget().accept(this); + // stack [target_object] + Type keyType = indexExpression.getKey().accept(this); + // stack [target_object, key] + if (targetType.getSort() == Type.ARRAY + && (isIntCompatible(keyType) || keyType.equals(Type.LONG_TYPE))) { + if (keyType.equals(Type.LONG_TYPE)) { + insnList.add(new InsnNode(Opcodes.L2I)); // convert key long to int + } + Type elementType = targetType.getElementType(); + insnList.add(new InsnNode(elementType.getOpcode(Opcodes.IALOAD))); + return elementType; + } else if (isPrimitive(targetType)) { + throw new UnsupportedOperationException( + "Unsupported target type for index: " + targetType.getClassName()); + } + if (isIntCompatible(keyType)) { + invokeStatic( + insnList, CONDITION_HELPER_TYPE, "index", OBJECT_TYPE, OBJECT_TYPE, Type.INT_TYPE); + } else { + tryBox(keyType, insnList); + // stack [target_object, key_boxed] + invokeStatic( + insnList, CONDITION_HELPER_TYPE, "index", OBJECT_TYPE, OBJECT_TYPE, OBJECT_TYPE); + } + return OBJECT_TYPE; + } + + @Override + public Type visit(WhenExpression whenExpression) { + return whenExpression.getExpression().accept(this); + } + + @Override + public Type visit(StringValue stringValue) { + ldc(insnList, stringValue.getValue()); + return STRING_TYPE; + } + + @Override + public Type visit(NumericValue numericValue) { + Number number = numericValue.getWidenValue(); + if (number instanceof Long) { + ldc(insnList, number.longValue()); + return Type.LONG_TYPE; + } else if (number instanceof Double) { + ldc(insnList, number); + return Type.DOUBLE_TYPE; + } else if (number instanceof BigDecimal) { + // use the toString representation to be able to create a BigDecimal instance + // toString <=> new BigDecimal(String) + newInstance(insnList, Types.BIG_DECIMAL_TYPE); + insnList.add(new InsnNode(Opcodes.DUP)); + ldc(insnList, number.toString()); + invokeConstructor(insnList, Types.BIG_DECIMAL_TYPE, STRING_TYPE); + return Types.BIG_DECIMAL_TYPE; + } + throw new IllegalArgumentException("not supported: " + number.getClass().getTypeName()); + } + + @Override + public Type visit(BooleanValue booleanValue) { + insnList.add(new InsnNode(booleanValue.getValue() ? Opcodes.ICONST_1 : Opcodes.ICONST_0)); + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(NullValue nullValue) { + ldc(insnList, null); + return OBJECT_TYPE; + } + + @Override + public Type visit(ListValue listValue) { + return null; + } + + @Override + public Type visit(MapValue mapValue) { + return null; + } + + @Override + public Type visit(SetValue setValue) { + return null; + } + + @Override + public Type visit(BooleanExpression predicate) { + insnList.add( + new InsnNode(predicate == BooleanExpression.TRUE ? Opcodes.ICONST_1 : Opcodes.ICONST_0)); + return Type.BOOLEAN_TYPE; + } + + @Override + public Type visit(ObjectValue objectValue) { + return OBJECT_TYPE; + } + + private static boolean isPrimitive(Type type) { + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.CHAR: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + case Type.FLOAT: + case Type.LONG: + case Type.DOUBLE: + return true; + } + return false; + } + + private boolean isEnum(Type type) { + Class clazz; + try { + clazz = Class.forName(type.getClassName(), false, classLoader); + return clazz.isEnum(); + } catch (ClassNotFoundException ex) { + return false; + } + } + } + + private static class PredicateAnalysisVisitor extends RefAnalysisVisitor { + final List refVariableNames = new ArrayList<>(); + + @Override + public Void visit(ValueRefExpression valueRefExpression) { + refVariableNames.add(valueRefExpression.getSymbolName()); + return null; + } + } +} diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java index 67146ec6561..9f190867775 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Instrumenter.java @@ -2,6 +2,7 @@ import static com.datadog.debugger.instrumentation.ASMHelper.adjustLocalVarsBasedOnArgs; import static com.datadog.debugger.instrumentation.ASMHelper.createLocalVarNodes; +import static com.datadog.debugger.instrumentation.ASMHelper.isStaticMethod; import static com.datadog.debugger.instrumentation.ASMHelper.ldc; import static com.datadog.debugger.instrumentation.ASMHelper.sortLocalVariables; import static com.datadog.debugger.instrumentation.Types.STRING_TYPE; @@ -62,14 +63,10 @@ public Instrumenter( this.classFileLines = methodInfo.getClassFileLines(); this.diagnostics = diagnostics; this.probeIndices = probeIndices; - isStatic = (methodNode.access & Opcodes.ACC_STATIC) != 0; + isStatic = isStaticMethod(methodNode); methodEnterLabel = insertMethodEnterLabel(); - argOffset = isStatic ? 0 : 1; - Type[] argTypes = Type.getArgumentTypes(methodNode.desc); - for (Type t : argTypes) { - argOffset += t.getSize(); - } - localVarsBySlotArray = extractLocalVariables(argTypes); + argOffset = ASMHelper.getArgOffset(methodNode); + localVarsBySlotArray = extractLocalVariables(Type.getArgumentTypes(methodNode.desc)); this.language = JvmLanguage.of(classNode.sourceFile); } @@ -234,12 +231,6 @@ protected void pushTags(InsnList insnList, ProbeDefinition.Tag[] tags) { } } - protected int newVar(Type type) { - int varId = methodNode.maxLocals + 1; - methodNode.maxLocals += type.getSize(); - return varId; - } - protected int newVar(int size) { int varId = methodNode.maxLocals + 1; methodNode.maxLocals += size; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/LocalVarHoisting.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/LocalVarHoisting.java index d4701ad14fb..ed41503392e 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/LocalVarHoisting.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/LocalVarHoisting.java @@ -1,5 +1,7 @@ package com.datadog.debugger.instrumentation; +import static com.datadog.debugger.instrumentation.ASMHelper.isStaticMethod; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -167,7 +169,7 @@ private static boolean isParameter(MethodNode method, int slot) { private static int getParameterSlotCount(MethodNode method) { Type[] argTypes = Type.getArgumentTypes(method.desc); int count = 0; - if ((method.access & Opcodes.ACC_STATIC) == 0) { + if (!isStaticMethod(method)) { count = 1; // 'this' parameter } for (Type type : argTypes) { diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/MetricInstrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/MetricInstrumenter.java index a3c02a4f365..8026b49f985 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/MetricInstrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/MetricInstrumenter.java @@ -213,7 +213,7 @@ private void createFinallyHandler(LabelNode startLabel, LabelNode endLabel, Insn InsnList handler = new InsnList(); handler.add(handlerLabel); // declare a local var to store the current Throwable of the 'finally' block - int throwableTmpVar = newVar(Type.getType(Throwable.class)); + int throwableTmpVar = ASMHelper.newVar(methodNode, Type.getType(Throwable.class)); // stack [exception] handler.add(new VarInsnNode(Opcodes.ASTORE, throwableTmpVar)); // stack [] @@ -839,7 +839,7 @@ private ASMHelper.Type tryRetrieveSynthetic(String name, InsnList insnList) { } // call System.nanoTime at the beginning of the method if (durationStartVar == -1) { - durationStartVar = instrumentor.newVar(LONG_TYPE); + durationStartVar = ASMHelper.newVar(instrumentor.methodNode, LONG_TYPE); InsnList nanoTimeList = new InsnList(); invokeStatic(nanoTimeList, Type.getType(System.class), "nanoTime", LONG_TYPE); nanoTimeList.add(new VarInsnNode(Opcodes.LSTORE, durationStartVar)); diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/RefAnalysisVisitor.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/RefAnalysisVisitor.java new file mode 100644 index 00000000000..bb10414de49 --- /dev/null +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/RefAnalysisVisitor.java @@ -0,0 +1,217 @@ +package com.datadog.debugger.instrumentation; + +import com.datadog.debugger.el.Visitor; +import com.datadog.debugger.el.expressions.BinaryExpression; +import com.datadog.debugger.el.expressions.BinaryOperator; +import com.datadog.debugger.el.expressions.BooleanExpression; +import com.datadog.debugger.el.expressions.ComparisonExpression; +import com.datadog.debugger.el.expressions.ComparisonOperator; +import com.datadog.debugger.el.expressions.ContainsExpression; +import com.datadog.debugger.el.expressions.EndsWithExpression; +import com.datadog.debugger.el.expressions.FilterCollectionExpression; +import com.datadog.debugger.el.expressions.GetMemberExpression; +import com.datadog.debugger.el.expressions.HasAllExpression; +import com.datadog.debugger.el.expressions.HasAnyExpression; +import com.datadog.debugger.el.expressions.IfElseExpression; +import com.datadog.debugger.el.expressions.IfExpression; +import com.datadog.debugger.el.expressions.IndexExpression; +import com.datadog.debugger.el.expressions.IsDefinedExpression; +import com.datadog.debugger.el.expressions.IsEmptyExpression; +import com.datadog.debugger.el.expressions.LenExpression; +import com.datadog.debugger.el.expressions.MatchesExpression; +import com.datadog.debugger.el.expressions.NotExpression; +import com.datadog.debugger.el.expressions.StartsWithExpression; +import com.datadog.debugger.el.expressions.SubStringExpression; +import com.datadog.debugger.el.expressions.ValueRefExpression; +import com.datadog.debugger.el.expressions.WhenExpression; +import com.datadog.debugger.el.values.BooleanValue; +import com.datadog.debugger.el.values.ListValue; +import com.datadog.debugger.el.values.MapValue; +import com.datadog.debugger.el.values.NullValue; +import com.datadog.debugger.el.values.NumericValue; +import com.datadog.debugger.el.values.ObjectValue; +import com.datadog.debugger.el.values.SetValue; +import com.datadog.debugger.el.values.StringValue; + +public class RefAnalysisVisitor implements Visitor { + @Override + public Void visit(BinaryExpression binaryExpression) { + binaryExpression.getLeft().accept(this); + binaryExpression.getRight().accept(this); + return null; + } + + @Override + public Void visit(BinaryOperator operator) { + operator.accept(this); + return null; + } + + @Override + public Void visit(ComparisonExpression comparisonExpression) { + comparisonExpression.getLeft().accept(this); + comparisonExpression.getRight().accept(this); + comparisonExpression.getOperator().accept(this); + return null; + } + + @Override + public Void visit(ComparisonOperator operator) { + return null; + } + + @Override + public Void visit(ContainsExpression containsExpression) { + containsExpression.getTarget().accept(this); + containsExpression.getValue().accept(this); + return null; + } + + @Override + public Void visit(EndsWithExpression endsWithExpression) { + endsWithExpression.getSourceString().accept(this); + return null; + } + + @Override + public Void visit(FilterCollectionExpression filterCollectionExpression) { + filterCollectionExpression.getSource().accept(this); + filterCollectionExpression.getFilterExpression().accept(this); + return null; + } + + @Override + public Void visit(HasAllExpression hasAllExpression) { + hasAllExpression.getFilterPredicateExpression().accept(this); + hasAllExpression.getValueExpression().accept(this); + return null; + } + + @Override + public Void visit(HasAnyExpression hasAnyExpression) { + hasAnyExpression.getFilterPredicateExpression().accept(this); + hasAnyExpression.getValueExpression().accept(this); + return null; + } + + @Override + public Void visit(IfElseExpression ifElseExpression) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Void visit(IfExpression ifExpression) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Void visit(IsEmptyExpression isEmptyExpression) { + isEmptyExpression.getValueExpression().accept(this); + return null; + } + + @Override + public Void visit(IsDefinedExpression isDefinedExpression) { + isDefinedExpression.getValueExpression().accept(this); + return null; + } + + @Override + public Void visit(LenExpression lenExpression) { + lenExpression.getSource().accept(this); + return null; + } + + @Override + public Void visit(MatchesExpression matchesExpression) { + matchesExpression.getSourceString().accept(this); + return null; + } + + @Override + public Void visit(NotExpression notExpression) { + notExpression.getPredicate().accept(this); + return null; + } + + @Override + public Void visit(StartsWithExpression startsWithExpression) { + startsWithExpression.getSourceString().accept(this); + return null; + } + + @Override + public Void visit(SubStringExpression subStringExpression) { + subStringExpression.getSource().accept(this); + return null; + } + + @Override + public Void visit(ValueRefExpression valueRefExpression) { + return null; + } + + @Override + public Void visit(GetMemberExpression getMemberExpression) { + getMemberExpression.getTarget().accept(this); + return null; + } + + @Override + public Void visit(IndexExpression indexExpression) { + indexExpression.getTarget().accept(this); + indexExpression.getKey().accept(this); + return null; + } + + @Override + public Void visit(WhenExpression whenExpression) { + whenExpression.getExpression().accept(this); + return null; + } + + @Override + public Void visit(BooleanExpression booleanExpression) { + return null; + } + + @Override + public Void visit(ObjectValue objectValue) { + return null; + } + + @Override + public Void visit(StringValue stringValue) { + return null; + } + + @Override + public Void visit(NumericValue numericValue) { + return null; + } + + @Override + public Void visit(BooleanValue booleanValue) { + return null; + } + + @Override + public Void visit(NullValue nullValue) { + return null; + } + + @Override + public Void visit(ListValue listValue) { + return null; + } + + @Override + public Void visit(MapValue mapValue) { + return null; + } + + @Override + public Void visit(SetValue setValue) { + return null; + } +} diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SingleCapturedContextInstrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SingleCapturedContextInstrumenter.java index 7194e1ffce0..36ee4007a12 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SingleCapturedContextInstrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SingleCapturedContextInstrumenter.java @@ -1,7 +1,9 @@ package com.datadog.debugger.instrumentation; import static com.datadog.debugger.instrumentation.ASMHelper.getStatic; +import static com.datadog.debugger.instrumentation.ASMHelper.hasReturnValue; import static com.datadog.debugger.instrumentation.ASMHelper.invokeStatic; +import static com.datadog.debugger.instrumentation.ASMHelper.isStaticMethod; import static com.datadog.debugger.instrumentation.ASMHelper.ldc; import static com.datadog.debugger.instrumentation.Types.CAPTURED_CONTEXT_TYPE; import static com.datadog.debugger.instrumentation.Types.CLASS_TYPE; @@ -12,16 +14,23 @@ import static org.objectweb.asm.Type.VOID_TYPE; import static org.objectweb.asm.Type.getType; +import com.datadog.debugger.el.expressions.ValueRefExpression; +import com.datadog.debugger.probe.LogProbe; import com.datadog.debugger.probe.ProbeDefinition; import com.datadog.debugger.probe.Where; import com.datadog.debugger.sink.Snapshot; import datadog.trace.bootstrap.debugger.Limits; +import datadog.trace.bootstrap.debugger.MethodLocation; import java.util.List; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.InsnList; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.JumpInsnNode; import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.MethodInsnNode; +import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.VarInsnNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,9 +39,6 @@ public class SingleCapturedContextInstrumenter extends CapturedContextInstrumenter { private static final Logger LOGGER = LoggerFactory.getLogger(SingleCapturedContextInstrumenter.class); - private final boolean captureSnapshot; - private final boolean captureEntry; - private final Limits limits; public SingleCapturedContextInstrumenter( ProbeDefinition definition, @@ -43,9 +49,51 @@ public SingleCapturedContextInstrumenter( boolean captureEntry, Limits limits) { super(definition, methodInfo, diagnostics, probeIndices, captureSnapshot, captureEntry, limits); - this.captureSnapshot = captureSnapshot; - this.captureEntry = captureEntry; - this.limits = limits; + } + + @Override + public InstrumentationResult.Status instrument() { + if (hasCondition) { + // run analysis to determine special variable usage (@exception, @duration) + // for @duration only atExit and catch uncaught + // for @exception, we assume the following: + // if you use @exception, you are not using @return on the same exception + // technically you can do not(isDefined(@exception)) and @return == 'foo', but this is + // useless. So we are considering @exception and @return exclusive + // therefore the moment you are referencing @exception you will only capture in the catch for + // uncaught exception for method probe + // arguments are passed to the generated method + // for local vars TODO: probably passed them as args to the generated method once hoisted + // There is still one case for a void method (no @return) and we want to capture only + // if no exception: we need to pattern match on not(isDefined(@exception)) alone and push the + // generation of the method at exit instead of catch uncaught + ConditionAnalysisVisitor visitor = new ConditionAnalysisVisitor(); + ((LogProbe) definition).getProbeCondition().accept(visitor); + if (visitor.useException) { + // only generate condition for exception + conditionExceptionMethod = + ConditionInstrumenter.generateConditionExceptionMethod( + definition.getId(), + probeIndices.get(0), + ((LogProbe) definition).getProbeCondition(), + classLoader, + classNode, + methodNode); + classNode.methods.add(conditionExceptionMethod); + } else { + conditionMethod = + ConditionInstrumenter.generateConditionMethod( + definition.getId(), + probeIndices.get(0), + ((LogProbe) definition).getProbeCondition(), + classLoader, + classNode, + methodNode, + definition.getEvaluateAt() == MethodLocation.EXIT); + classNode.methods.add(conditionMethod); + } + } + return super.instrument(); } @Override @@ -135,7 +183,86 @@ protected void addCommitCall(InsnList insnList) { CAPTURED_CONTEXT_TYPE, CAPTURED_CONTEXT_TYPE, getType(List.class), + METHOD_LOCATION_TYPE, INT_TYPE); // stack [] } + + @Override + protected void addBeforeReturnCondition(InsnList insnList, LabelNode targetNode) { + if (hasCondition + && definition.getEvaluateAt() == MethodLocation.EXIT + && conditionMethod != null) { + addConditionCall(insnList, conditionMethod, targetNode, false); + } + // if no condition or not exit, do nothing + // TODO check this is correct + } + + @Override + protected LabelNode addFinallyHandlerCondition(InsnList handler) { + LabelNode targetNode = null; + if (hasCondition && conditionExceptionMethod != null) { + targetNode = new LabelNode(); + addConditionCall(handler, conditionExceptionMethod, targetNode, true); + } + return targetNode; + } + + private void addConditionCall( + InsnList insnList, MethodNode conditionMethod, LabelNode targetNode, boolean useException) { + boolean isStatic = isStaticMethod(conditionMethod); + boolean hasReturnValueOrException = hasReturnValue(methodNode) || useException; + Type[] argumentTypes = Type.getArgumentTypes(methodNode.desc); + int argOffset = isStatic ? 0 : 1; + if (hasReturnValueOrException) { + insnList.add(new InsnNode(Opcodes.DUP)); + // stack [ret_value, ret_value] + } + if (!isStatic) { + // push this + insnList.add(new VarInsnNode(Opcodes.ALOAD, 0)); + // stack [ret_value, ret_value, this] + if (hasReturnValueOrException) { + insnList.add(new InsnNode(Opcodes.SWAP)); + // stack [ret_value, this, ret_value] + } + } + for (Type argType : argumentTypes) { + insnList.add(new VarInsnNode(argType.getOpcode(Opcodes.ILOAD), argOffset)); + argOffset += argType.getSize(); + } + // push timestamp start + insnList.add(new VarInsnNode(Opcodes.LLOAD, timestampStartVar)); + // stack [ret_Value, (this), (ret_value), args..., timestamp] + insnList.add( + new MethodInsnNode( + isStatic ? Opcodes.INVOKESTATIC : Opcodes.INVOKEVIRTUAL, + classNode.name, + conditionMethod.name, + conditionMethod.desc, + false)); + // stack [ret_value, boolean] + insnList.add(new JumpInsnNode(Opcodes.IFEQ, targetNode)); + } + + private static class ConditionAnalysisVisitor extends RefAnalysisVisitor { + private boolean useException; + private boolean useDuration; + + @Override + public Void visit(ValueRefExpression valueRefExpression) { + switch (valueRefExpression.getSymbolName()) { + case "@exception": + useException = true; + break; + case "@duration": + useDuration = true; + break; + default: + break; + } + return null; + } + } } diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SpanInstrumenter.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SpanInstrumenter.java index 2bb9a21f3c1..a007f8ff76c 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SpanInstrumenter.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/SpanInstrumenter.java @@ -37,7 +37,7 @@ public InstrumentationResult.Status instrument() { if (definition.isLineProbe()) { return addRangeSpan(classFileLines); } - spanVar = newVar(DEBUGGER_SPAN_TYPE); + spanVar = ASMHelper.newVar(methodNode, DEBUGGER_SPAN_TYPE); processInstructions(); LabelNode initSpanLabel = new LabelNode(); InsnList insnList = createSpan(initSpanLabel); @@ -116,7 +116,7 @@ private InstrumentationResult.Status addRangeSpan(ClassFileLines classFileLines) "No line info for " + (sourceLine.isSingleLine() ? "line " : "range ") + sourceLine); return InstrumentationResult.Status.ERROR; } - spanVar = newVar(DEBUGGER_SPAN_TYPE); + spanVar = ASMHelper.newVar(methodNode, DEBUGGER_SPAN_TYPE); LabelNode initSpanLabel = new LabelNode(); InsnList createSpaninsnList = createSpan(initSpanLabel); methodNode.instructions.insertBefore(beforeLabel.getNext(), createSpaninsnList); diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Types.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Types.java index c9f42e4d4b9..2837ece4885 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Types.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/instrumentation/Types.java @@ -18,11 +18,13 @@ import static org.objectweb.asm.Opcodes.SASTORE; import datadog.trace.bootstrap.debugger.CapturedContext; +import datadog.trace.bootstrap.debugger.ConditionHelper; import datadog.trace.bootstrap.debugger.DebuggerContext; import datadog.trace.bootstrap.debugger.DebuggerSpan; import datadog.trace.bootstrap.debugger.MethodLocation; import datadog.trace.bootstrap.debugger.ProbeId; import datadog.trace.bootstrap.debugger.el.ReflectiveFieldValueResolver; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -71,6 +73,9 @@ public final class Types { Type.getType(ReflectiveFieldValueResolver.class); public static final Type METRICKIND_TYPE = Type.getType(DebuggerContext.MetricKind.class); public static final Type PROBE_ID_TYPE = Type.getType(ProbeId.class); + public static final Type CONDITION_HELPER_TYPE = Type.getType(ConditionHelper.class); + public static final Type ENUM_TYPE = Type.getType(Enum.class); + public static final Type BIG_DECIMAL_TYPE = Type.getType(BigDecimal.class); // special initialization methods public static final String CONSTRUCTOR = ""; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/probe/LogProbe.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/probe/LogProbe.java index 5647cb371b1..6b6879051ea 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/probe/LogProbe.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/probe/LogProbe.java @@ -757,12 +757,12 @@ private void processCaptureExpressions(CapturedContext context, LogStatus logSta } } catch (EvaluationException ex) { logStatus.addError(new EvaluationError(ex.getExpr(), ex.getMessage())); - logStatus.setLogTemplateErrors(true); + logStatus.setHasEvalutionErrors(true); } catch (Exception ex) { // catch all for unexpected exceptions logStatus.addError( new EvaluationError(captureExpression.getExpr().getDsl(), ex.getMessage())); - logStatus.setLogTemplateErrors(true); + logStatus.setHasEvalutionErrors(true); } } } @@ -871,7 +871,7 @@ public static class LogStatus extends CapturedContext.Status { private boolean condition = true; private final DebugSessionStatus debugSessionStatus; - private boolean hasLogTemplateErrors; + private boolean hasEvaluationErrors; private boolean hasConditionErrors; private boolean sampled = true; private boolean forceSampling; @@ -897,6 +897,12 @@ public boolean isCapturing() { return condition; } + @Override + public void addError(EvaluationError evaluationError) { + super.addError(evaluationError); + setHasEvalutionErrors(true); + } + public boolean shouldSend() { DebugSessionStatus status = getDebugSessionStatus(); // an ACTIVE status overrides the sampling as the sampling decision was made by the trigger @@ -906,7 +912,7 @@ public boolean shouldSend() { } public boolean shouldReportError() { - return sampled && (hasConditionErrors || hasLogTemplateErrors); + return sampled && (hasConditionErrors || hasEvaluationErrors); } public boolean getCondition() { @@ -946,11 +952,11 @@ public void setConditionErrors(boolean value) { } public boolean hasLogTemplateErrors() { - return hasLogTemplateErrors; + return hasEvaluationErrors; } - public void setLogTemplateErrors(boolean value) { - this.hasLogTemplateErrors = value; + public void setHasEvalutionErrors(boolean value) { + this.hasEvaluationErrors = value; } public void setMessage(String message) { @@ -991,7 +997,7 @@ public String toString() { + ", hasConditionErrors=" + hasConditionErrors + ", hasLogTemplateErrors=" - + hasLogTemplateErrors + + hasEvaluationErrors + ", message='" + message + '\'' diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java index d705ffc0175..252b4051f87 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/symbol/SymbolExtractor.java @@ -2,6 +2,7 @@ import static com.datadog.debugger.instrumentation.ASMHelper.adjustLocalVarsBasedOnArgs; import static com.datadog.debugger.instrumentation.ASMHelper.createLocalVarNodes; +import static com.datadog.debugger.instrumentation.ASMHelper.isStaticMethod; import static com.datadog.debugger.instrumentation.ASMHelper.sortLocalVariables; import com.datadog.debugger.instrumentation.ASMHelper; @@ -264,7 +265,7 @@ private static Collection extractMethodModifiers( // if class is an interface && method has code && non-static this is a default method if ((classNode.access & Opcodes.ACC_INTERFACE) > 0 && methodNode.instructions.size() > 0 - && (methodNode.access & Opcodes.ACC_STATIC) == 0) { + && !isStaticMethod(methodNode)) { results.add("default"); } return results; @@ -337,7 +338,7 @@ private static String extractSourceFile(ClassNode classNode) { private static int extractArgs( MethodNode method, List methodSymbols, int methodStartLine) { - boolean isStatic = (method.access & Opcodes.ACC_STATIC) != 0; + boolean isStatic = isStaticMethod(method); int slot = isStatic ? 0 : 1; if (method.localVariables == null || method.localVariables.size() == 0) { return slot; diff --git a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java index 0ee03a7f52a..49387fcb248 100644 --- a/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java +++ b/dd-java-agent/agent-debugger/src/main/java/com/datadog/debugger/util/SerializerWithLimits.java @@ -261,7 +261,7 @@ private void serializePrimitive(Object value, Limits limits) throws Exception { private void serializeObjectValue(Object value, Limits limits) throws Exception { tokenWriter.objectPrologue(value); - Map> specialTypeAccess = + Map specialTypeAccess = WellKnownClasses.getSpecialTypeAccess(value); Class currentClass = value.getClass(); int processedFieldCount = 0; @@ -280,10 +280,10 @@ private void serializeObjectValue(Object value, Limits limits) throws Exception } processedFieldCount++; if (specialTypeAccess != null) { - Function specialFieldAccess = + WellKnownClasses.SpecialFieldInfo specialFieldInfo = specialTypeAccess.get(field.getName()); - if (specialFieldAccess != null) { - onSpecialField(specialFieldAccess, value, limits); + if (specialFieldInfo != null) { + onSpecialField(specialFieldInfo.accessor, value, limits); } else { onField(field, value, limits); } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotConditionTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotConditionTest.java new file mode 100644 index 00000000000..a721e1f866c --- /dev/null +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotConditionTest.java @@ -0,0 +1,1470 @@ +package com.datadog.debugger.agent; + +import static com.datadog.debugger.el.DSL.*; +import static com.datadog.debugger.el.DSL.and; +import static com.datadog.debugger.el.DSL.contains; +import static com.datadog.debugger.el.DSL.endsWith; +import static com.datadog.debugger.el.DSL.eq; +import static com.datadog.debugger.el.DSL.ge; +import static com.datadog.debugger.el.DSL.getMember; +import static com.datadog.debugger.el.DSL.gt; +import static com.datadog.debugger.el.DSL.index; +import static com.datadog.debugger.el.DSL.instanceOf; +import static com.datadog.debugger.el.DSL.le; +import static com.datadog.debugger.el.DSL.len; +import static com.datadog.debugger.el.DSL.lt; +import static com.datadog.debugger.el.DSL.matches; +import static com.datadog.debugger.el.DSL.or; +import static com.datadog.debugger.el.DSL.ref; +import static com.datadog.debugger.el.DSL.startsWith; +import static com.datadog.debugger.el.DSL.subString; +import static com.datadog.debugger.el.DSL.value; +import static com.datadog.debugger.el.DSL.when; +import static com.datadog.debugger.el.ValueType.BOOLEAN; +import static com.datadog.debugger.el.ValueType.BYTE; +import static com.datadog.debugger.el.ValueType.DOUBLE; +import static com.datadog.debugger.el.ValueType.FLOAT; +import static com.datadog.debugger.el.ValueType.INT; +import static com.datadog.debugger.el.ValueType.LONG; +import static com.datadog.debugger.el.ValueType.OBJECT; +import static com.datadog.debugger.el.ValueType.SHORT; +import static com.datadog.debugger.el.expressions.BooleanExpression.TRUE; +import static com.datadog.debugger.el.expressions.ComparisonOperator.EQ; +import static com.datadog.debugger.el.expressions.ComparisonOperator.GE; +import static com.datadog.debugger.el.expressions.ComparisonOperator.GT; +import static com.datadog.debugger.el.expressions.ComparisonOperator.INSTANCEOF; +import static com.datadog.debugger.el.expressions.ComparisonOperator.LE; +import static com.datadog.debugger.el.expressions.ComparisonOperator.LT; +import static datadog.trace.bootstrap.debugger.el.ValueReferences.ITERATOR_REF; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static utils.InstrumentationTestHelper.compileAndLoadClass; + +import com.datadog.debugger.el.DSL; +import com.datadog.debugger.el.ProbeCondition; +import com.datadog.debugger.el.expressions.BooleanExpression; +import com.datadog.debugger.el.expressions.ComparisonExpression; +import com.datadog.debugger.el.expressions.ComparisonOperator; +import com.datadog.debugger.el.expressions.ValueExpression; +import com.datadog.debugger.el.values.BooleanValue; +import com.datadog.debugger.el.values.NumericValue; +import com.datadog.debugger.el.values.StringValue; +import com.datadog.debugger.instrumentation.InstrumentationResult; +import com.datadog.debugger.probe.LogProbe; +import com.datadog.debugger.util.TestSnapshotListener; +import datadog.trace.bootstrap.debugger.MethodLocation; +import datadog.trace.bootstrap.debugger.el.ValueReferences; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URISyntaxException; +import java.util.List; +import java.util.stream.Stream; +import org.joor.Reflect; +import org.joor.ReflectException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class CapturedSnapshotConditionTest extends CapturingTestBase { + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("binaryExprs") + public void binaryComparisons( + BooleanExpression expr, String dslExpression, boolean expectedResult) + throws IOException, URISyntaxException { + doCondition08(expr, dslExpression, expectedResult); + } + + public static Stream binaryExprs() { + return Stream.of( + Arguments.of( + and( + and(eq(ref("arg"), value("5")), eq(value(5), value(5))), + eq(value(true), value(true))), + "arg == '5' && 5 == 5 && true == true", + true), + Arguments.of(not(eq(ref("arg"), nullValue())), "arg != null", true), + Arguments.of( + and(gt(ref("arg"), value("4")), gt(value(5), value(4))), "arg > '4' && 5 > 4", true), + Arguments.of( + and( + and(ge(ref("arg"), value("4")), ge(value(5), value(4))), + and(ge(ref("arg"), value("5")), ge(value(5), value(5)))), + "arg >= '4' && 5 >= 4 && arg >= '5' && 5 >= 5", + true), + Arguments.of( + and(lt(ref("arg"), value("6")), lt(value(4), value(5))), "arg < '6' && 4 < 5", true), + Arguments.of( + and( + and(le(ref("arg"), value("6")), le(value(4), value(5))), + and(le(ref("arg"), value("5")), le(value(5), value(5)))), + "arg <= '4' && 5 <= 4 && arg <= '5' && 5 <= 5", + true), + Arguments.of( + or(eq(ref("arg"), value("4")), eq(ref("arg"), value("5"))), + "arg == '4' || arg == '5'", + true), + // or(true, X): right side would NPE if evaluated; short-circuit must skip it + Arguments.of( + or(TRUE, eq(getMember(getMember(ref("nullTyped"), "fld"), "fld"), value("5"))), + "true || nullTyped.fld.fld == '5'", + true), + // and(false, X): right side would AIOOBE if evaluated; short-circuit must skip it + Arguments.of( + and(BooleanExpression.FALSE, eq(subString(ref("arg"), 100, 1), value("a"))), + "false && substring(arg, 100, 1) == 'a'", + false)); + } + + @ParameterizedTest(name = "[{index}] {4}") + @MethodSource("comparisonExprs") + public void comparisonExpressions( + ValueExpression left, + ValueExpression right, + ComparisonOperator operator, + boolean expected, + String dslExpression) + throws IOException, URISyntaxException { + ComparisonExpression expression = new ComparisonExpression(left, right, operator); + doCondition06(expression, dslExpression, expected); + } + + public static Stream comparisonExprs() { + return Stream.of( + Arguments.of( + new BooleanValue(true, BOOLEAN), + new BooleanValue(true, BOOLEAN), + EQ, + true, + "true == true"), + Arguments.of( + new BooleanValue(false, BOOLEAN), + new BooleanValue(false, BOOLEAN), + EQ, + true, + "false == false"), + Arguments.of( + new BooleanValue(true, BOOLEAN), + new BooleanValue(false, BOOLEAN), + EQ, + false, + "true == false"), + Arguments.of( + new BooleanValue(false, BOOLEAN), + new BooleanValue(true, BOOLEAN), + EQ, + false, + "false == true"), + Arguments.of( + new NumericValue((byte) 1, BYTE), new NumericValue((byte) 1, BYTE), EQ, true, "1 == 1"), + Arguments.of( + new NumericValue((short) 1, SHORT), + new NumericValue((short) 1, SHORT), + EQ, + true, + "1 == 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1, INT), EQ, true, "1 == 1"), + Arguments.of(new NumericValue(1L, LONG), new NumericValue(1L, LONG), EQ, true, "1 == 1"), + Arguments.of( + new NumericValue(1.0F, FLOAT), new NumericValue(1.0F, FLOAT), EQ, true, "1.0 == 1.0"), + Arguments.of( + new NumericValue(1.0, DOUBLE), new NumericValue(1.0, DOUBLE), EQ, true, "1.0 == 1.0"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1.0, DOUBLE), EQ, true, "1 == 1.0"), + Arguments.of(new NumericValue(1, INT), new NumericValue(2, INT), EQ, false, "1 == 2"), + Arguments.of( + new NumericValue(1, INT), new NumericValue(2.0, DOUBLE), EQ, false, "1 == 2.0"), + Arguments.of(new StringValue("foo"), new NumericValue(2, INT), EQ, false, "\"foo\" == 2"), + Arguments.of(new NumericValue(1, INT), new StringValue("foo"), EQ, false, "1 == \"foo\""), + Arguments.of(ValueExpression.NULL, new NumericValue(2, INT), EQ, false, "null == 2"), + Arguments.of( + new NumericValue(Double.NaN, DOUBLE), + new NumericValue(Double.NaN, DOUBLE), + EQ, + false, + "NaN == NaN"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(BigDecimal.valueOf(2), OBJECT), + EQ, + true, + "BigDecimal(2) == BigDecimal(2)"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(BigDecimal.valueOf(1), OBJECT), + EQ, + false, + "BigDecimal(2) == BigDecimal(1)"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(2, INT), + EQ, + true, + "BigDecimal(2) == 2"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(2L, LONG), + EQ, + true, + "BigDecimal(2) == 2L"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2.5), OBJECT), + new NumericValue(2.5, DOUBLE), + EQ, + true, + "BigDecimal(2.5) == 2.5"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(3, INT), + EQ, + false, + "BigDecimal(2) == 3"), + Arguments.of( + new NumericValue(2, INT), + new NumericValue(BigDecimal.valueOf(2), OBJECT), + EQ, + true, + "2 == BigDecimal(2)"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1, INT), GT, false, "1 > 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(2, INT), GT, false, "1 > 2"), + Arguments.of( + new NumericValue(1.0, DOUBLE), new NumericValue(1.1, DOUBLE), GT, false, "1.0 > 1.1"), + Arguments.of(new NumericValue(2, INT), new NumericValue(1, INT), GT, true, "2 > 1"), + Arguments.of( + new NumericValue(1.1, DOUBLE), new NumericValue(1.0, DOUBLE), GT, true, "1.1 > 1.0"), + Arguments.of(new NumericValue(1.1, DOUBLE), new NumericValue(1, INT), GT, true, "1.1 > 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(0.9, DOUBLE), GT, true, "1 > 0.9"), + Arguments.of(ValueExpression.NULL, new NumericValue(2, INT), GT, false, "null > 2"), + Arguments.of(new NumericValue(2, INT), ValueExpression.NULL, GT, false, "2 > null"), + Arguments.of( + new NumericValue(Double.NaN, DOUBLE), + new NumericValue(Double.NaN, DOUBLE), + GT, + false, + "NaN > NaN"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(BigDecimal.valueOf(1), OBJECT), + GT, + true, + "2 > 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(2, INT), GE, false, "1 >= 2"), + Arguments.of( + new NumericValue(1.0, DOUBLE), new NumericValue(1.1, DOUBLE), GE, false, "1.0 >= 1.1"), + Arguments.of(new NumericValue(2, INT), new NumericValue(1, INT), GE, true, "2 >= 1"), + Arguments.of( + new NumericValue(1.1, DOUBLE), new NumericValue(1.0, DOUBLE), GE, true, "1.1 >= 1.0"), + Arguments.of(new NumericValue(1.1, DOUBLE), new NumericValue(1, INT), GE, true, "1.1 >= 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(0.9, DOUBLE), GE, true, "1 >= 0.9"), + Arguments.of(ValueExpression.NULL, new NumericValue(2, INT), GE, false, "null >= 2"), + Arguments.of( + new NumericValue(Double.NaN, DOUBLE), + new NumericValue(Double.NaN, DOUBLE), + GE, + false, + "NaN >= NaN"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(2), OBJECT), + new NumericValue(BigDecimal.valueOf(1), OBJECT), + GE, + true, + "2 >= 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1, INT), LT, false, "1 < 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(2, INT), LT, true, "1 < 2"), + Arguments.of(new NumericValue(2, INT), new NumericValue(1, INT), LT, false, "2 < 1"), + Arguments.of( + new NumericValue(1.1, DOUBLE), new NumericValue(1.0, DOUBLE), LT, false, "1.1 < 1.0"), + Arguments.of( + new NumericValue(1.0, DOUBLE), new NumericValue(1.1, DOUBLE), LT, true, "1.0 < 1.1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1.1, DOUBLE), LT, true, "1 < 1.1"), + Arguments.of(new NumericValue(0.9, DOUBLE), new NumericValue(1, INT), LT, true, "0.9 < 1"), + Arguments.of(ValueExpression.NULL, new NumericValue(2, INT), LT, false, "null < 2"), + Arguments.of( + new NumericValue(Double.NaN, DOUBLE), + new NumericValue(Double.NaN, DOUBLE), + LT, + false, + "NaN < NaN"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(1), OBJECT), + new NumericValue(BigDecimal.valueOf(2), OBJECT), + LT, + true, + "1 < 2"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1, INT), LE, true, "1 <= 1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(2, INT), LE, true, "1 <= 2"), + Arguments.of(new NumericValue(2, INT), new NumericValue(1, INT), LE, false, "2 <= 1"), + Arguments.of( + new NumericValue(1.1, DOUBLE), new NumericValue(1.0, DOUBLE), LE, false, "1.1 <= 1.0"), + Arguments.of( + new NumericValue(1.0, DOUBLE), new NumericValue(1.1, DOUBLE), LE, true, "1.0 <= 1.1"), + Arguments.of(new NumericValue(1, INT), new NumericValue(1.1, DOUBLE), LE, true, "1 <= 1.1"), + Arguments.of(new NumericValue(0.9, DOUBLE), new NumericValue(1, INT), LE, true, "0.9 <= 1"), + Arguments.of(ValueExpression.NULL, new NumericValue(2, INT), LE, false, "null <= 2"), + Arguments.of( + new NumericValue(Double.NaN, DOUBLE), + new NumericValue(Double.NaN, DOUBLE), + LE, + false, + "NaN <= NaN"), + Arguments.of( + new NumericValue(BigDecimal.valueOf(1), OBJECT), + new NumericValue(BigDecimal.valueOf(2), OBJECT), + LE, + true, + "1 <= 2"), + Arguments.of(new StringValue("abc"), new StringValue("abd"), LT, true, "'abc' < 'abd'"), + Arguments.of(new StringValue("abd"), new StringValue("abc"), LT, false, "'abd' < 'abc'"), + Arguments.of(new StringValue("abc"), new StringValue("abc"), LT, false, "'abc' < 'abc'"), + Arguments.of(new StringValue("abd"), new StringValue("abc"), GT, true, "'abd' > 'abc'"), + Arguments.of(new StringValue("abc"), new StringValue("abd"), GT, false, "'abc' > 'abd'"), + Arguments.of(new StringValue("abc"), new StringValue("abc"), LE, true, "'abc' <= 'abc'"), + Arguments.of(new StringValue("abd"), new StringValue("abc"), LE, false, "'abd' <= 'abc'"), + Arguments.of(new StringValue("abc"), new StringValue("abc"), GE, true, "'abc' >= 'abc'"), + Arguments.of(new StringValue("abc"), new StringValue("abd"), GE, false, "'abc' >= 'abd'"), + Arguments.of(ref("strValue"), new StringValue("aa"), GT, true, "strValue > 'aa'"), + Arguments.of(ref("strValue"), new StringValue("zz"), LT, true, "strValue < 'zz'"), + Arguments.of( + new StringValue("foo"), + new StringValue("java.lang.String"), + INSTANCEOF, + true, + "\"foo\" instanceof \"java.lang.String\""), + Arguments.of( + new StringValue("foo"), + new StringValue("java.lang.Object"), + INSTANCEOF, + true, + "\"foo\" instanceof \"java.lang.Object\""), + Arguments.of( + ref("random"), + new StringValue("java.util.Random"), + INSTANCEOF, + true, + "java.util.Random instanceof \"java.util.Random\""), + Arguments.of( + ref("strList"), + new StringValue("java.util.List"), + INSTANCEOF, + true, + "java.util.ArrayList instanceof \"java.util.List\""), + Arguments.of( + ref("strList"), + new StringValue("java.util.Map"), + INSTANCEOF, + false, + "java.util.ArrayList instanceof \"java.util.Map\""), + Arguments.of( + ValueExpression.NULL, + new StringValue("java.lang.String"), + INSTANCEOF, + false, + "null instanceof \"java.lang.String\""), + Arguments.of( + ref("intValue"), + new StringValue("java.lang.Integer"), + INSTANCEOF, + true, + "1 instanceof \"java.lang.Integer\""), + Arguments.of( + ref("doubleValue"), + new StringValue("java.lang.Double"), + INSTANCEOF, + true, + "1.0 instanceof \"java.lang.Double\""), + Arguments.of( + ref("option"), + new StringValue("READ"), + EQ, + true, + "java.nio.file.StandardOpenOption == \"READ\""), + Arguments.of( + ref("option"), + new StringValue("StandardOpenOption.READ"), + EQ, + true, + "java.nio.file.StandardOpenOption == \"StandardOpenOption.READ\""), + Arguments.of( + ref("option"), + new StringValue("java.nio.file.StandardOpenOption.READ"), + EQ, + true, + "java.nio.file.StandardOpenOption == \"java.nio.file.StandardOpenOption.READ\""), + Arguments.of( + ref("option"), + new StringValue("CREATE"), + EQ, + false, + "java.nio.file.StandardOpenOption == \"READ\""), + Arguments.of( + new StringValue("READ"), + ref("option"), + EQ, + true, + "\"READ\" == java.nio.file.StandardOpenOption"), + // Negative swap: String literal on left, enum ref on right — exercises the SWAP + // path with a non-matching enum value + Arguments.of( + new StringValue("CREATE"), + ref("option"), + EQ, + false, + "\"CREATE\" == java.nio.file.StandardOpenOption")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("invalidComparisonTypesExprs") + public void invalidComparisonTypes( + BooleanExpression expr, String dslExpression, String expectedMsg) + throws IOException, URISyntaxException { + doCondition06Failure(expr, dslExpression, expectedMsg); + } + + private static Stream invalidComparisonTypesExprs() { + return Stream.of( + Arguments.of( + eq(ref("intValue"), value("foo")), + "intValue == 'foo'", + "Unsupported equals comparison: long <=> java.lang.String"), + Arguments.of( + ge(ref("intValue"), value("foo")), + "intValue >= 'foo'", + "Unsupported comparison: long <=> java.lang.String"), + Arguments.of( + instanceOf(ref("intValue"), value(5)), + "intValue instanceof 5", + "Invalid arguments for instanceof operator"), + Arguments.of( + eq(ref("doesNotExist"), value(0)), + "doesNotExist == 0", + "Cannot find symbol: doesNotExist"), + Arguments.of( + eq(value(true), value(1)), + "true == 1", + "Unsupported equals comparison: boolean <=> long"), + Arguments.of( + eq(ref("strValue"), value(true)), + "strValue == true", + "Unsupported equals comparison: java.lang.String <=> boolean"), + Arguments.of( + ge(value(true), value(false)), + "true >= false", + "Unsupported comparison: boolean <=> boolean"), + Arguments.of( + gt(ref("strValue"), value(5)), + "strValue > 5", + "Unsupported comparison: java.lang.String <=> long")); + } + + // Numeric type matrix: covers primitive-typed refs from CapturedSnapshot25 (int/long/float/ + // double/boolean/byte/short/char) against literals. NumericValue literals widen Byte/Short/ + // Integer → Long and Float → Double, and the visitor widens INT-typed refs → LONG and + // FLOAT-typed refs → DOUBLE before comparison. BYTE/SHORT/CHAR refs are NOT widened and + // are not in isNumeric, so they error out at instrumentation time. CHAR is also not in + // isIntCompatible — so even same-type CHAR == CHAR fails. + @ParameterizedTest(name = "[{index}] {3}") + @MethodSource("numericTypeMatrixPositiveExprs") + public void numericTypeMatrixPositive( + String methodName, String methodArg, BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition25(methodName, methodArg, expr, dslExpression); + } + + private static Stream numericTypeMatrixPositiveExprs() { + // Only probes methods that return a size-1 value (int/byte/short/char/boolean). + // Probing long/float/double-returning methods hits an unrelated SingleCapturedContext- + // Instrumenter bug (uses DUP rather than DUP2 to duplicate the return value). + return Stream.of( + // INT ref widens to LONG; literal 42 is LONG → LONG-LONG works + Arguments.of("intFunction", "int", eq(ref("arg"), value(42)), "arg == 42"), + // Heterogeneous: INT ref (→ LONG) vs DOUBLE literal — addHeterogeneousComparison + Arguments.of("intFunction", "int", lt(ref("arg"), value(42.5)), "arg < 42.5"), + // Heterogeneous: INT ref (→ LONG) vs DOUBLE field — same path, ref on right + Arguments.of("intFunction", "int", gt(ref("arg"), ref("doubleField")), "arg > doubleField"), + // BOOLEAN ref vs BOOLEAN literal — IF_ICMPEQ path (boolean is isIntCompatible) + Arguments.of("booleanFunction", "boolean", eq(ref("arg"), value(true)), "arg == true"), + // BYTE ref vs BYTE ref — same-type IF_ICMPEQ works (arg=0x42, byteField=0 at EXIT) + Arguments.of( + "byteFunction", "byte", not(eq(ref("arg"), ref("byteField"))), "arg != byteField"), + // SHORT ref vs SHORT ref — same-type works + Arguments.of( + "shortFunction", "short", not(eq(ref("arg"), ref("shortField"))), "arg != shortField")); + } + + @ParameterizedTest(name = "[{index}] {3}") + @MethodSource("numericTypeMatrixFailureExprs") + public void numericTypeMatrixFailures( + String methodName, + String methodArg, + BooleanExpression expr, + String dslExpression, + String expectedMsg) + throws IOException, URISyntaxException { + doCondition25Failure(methodName, methodArg, expr, dslExpression, expectedMsg); + } + + private static Stream numericTypeMatrixFailureExprs() { + return Stream.of( + // BYTE ref vs LONG literal — BYTE not widened, not in isNumeric + Arguments.of( + "byteFunction", + "byte", + eq(ref("arg"), value(0x42)), + "arg == 66", + "Unsupported equals comparison: byte <=> long"), + // SHORT ref vs LONG literal + Arguments.of( + "shortFunction", + "short", + eq(ref("arg"), value(1001)), + "arg == 1001", + "Unsupported equals comparison: short <=> long"), + // CHAR ref vs LONG literal — CHAR not handled at all + Arguments.of( + "charFunction", + "char", + eq(ref("arg"), value(97)), + "arg == 97", + "Unsupported equals comparison: char <=> long"), + // CHAR ref vs CHAR ref — fails even for same type (CHAR not in isIntCompatible/isNumeric) + Arguments.of( + "charFunction", + "char", + eq(ref("arg"), ref("charField")), + "arg == charField", + "Unsupported equals comparison: char <=> char"), + // BOOLEAN ref vs LONG literal under inequality + Arguments.of( + "booleanFunction", + "boolean", + gt(ref("arg"), value(0)), + "arg > 0", + "Unsupported comparison: boolean <=> long"), + // BOOLEAN ref vs BOOLEAN literal under inequality — also unsupported + Arguments.of( + "booleanFunction", + "boolean", + gt(ref("arg"), value(true)), + "arg > true", + "Unsupported comparison: boolean <=> boolean")); + } + + @Test + public void durationAtEntryInvalid() throws IOException, URISyntaxException { + doCondition06Failure( + ge(ref("@duration"), value(0L)), + "@duration >= 0", + "@duration not available (not at exit)", + MethodLocation.ENTRY); + } + + @Test + public void unsupportedSyntheticSymbol() throws IOException, URISyntaxException { + doCondition06Failure(eq(ref("@foo"), nullValue()), "@foo == null", "Unsupported symbol: @foo"); + } + + // Operations expecting a String / collection / array applied to a primitive scalar source + // must fail at instrumentation time rather than emit bytecode that the JVM verifier rejects. + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("invalidScalarOperationExprs") + public void invalidScalarOperation( + BooleanExpression expr, String dslExpression, String expectedMsg) + throws IOException, URISyntaxException { + doCondition06Failure(expr, dslExpression, expectedMsg); + } + + private static Stream invalidScalarOperationExprs() { + return Stream.of( + Arguments.of( + eq(len(ref("intValue")), value(0)), + "len(intValue) == 0", + "Unsupported type for len function: int"), + Arguments.of( + eq(index(ref("intValue"), value(0)), value(0)), + "intValue[0] == 0", + "Unsupported target type for index: int"), + Arguments.of( + isEmpty(ref("intValue")), + "isEmpty(intValue)", + "Unsupported type for isEmpty function: int"), + Arguments.of( + contains(ref("intValue"), value(1)), + "contains(intValue, 1)", + "Unsupported type for contains function: int"), + Arguments.of( + eq(subString(ref("intValue"), 0, 1), value("x")), + "substring(intValue, 0, 1) == 'x'", + "Unsupported type for substring function: int"), + Arguments.of( + startsWith(ref("intValue"), new StringValue("x")), + "startsWith(intValue, 'x')", + "Unsupported type for startsWith function: int"), + Arguments.of( + endsWith(ref("intValue"), new StringValue("x")), + "endsWith(intValue, 'x')", + "Unsupported type for endsWith function: int"), + Arguments.of( + matches(ref("intValue"), new StringValue("x")), + "matches(intValue, 'x')", + "Unsupported type for matches function: int")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("invalidSyntheticExprs") + public void invalidSynthetic(BooleanExpression expr, String dslExpression, String expectedMsg) + throws IOException, URISyntaxException { + doCondition06FailureOnVoidMethod(expr, dslExpression, expectedMsg, MethodLocation.EXIT); + } + + private static Stream invalidSyntheticExprs() { + return Stream.of( + // iteratorOutsideCollectionInvalid + Arguments.of( + eq(ref(ITERATOR_REF), value(0)), + "@it == 0", + "@it not available if not used with collection functions (filter, any or all)"), + // returnOnVoidMethodInvalid + Arguments.of( + eq(ref("@return"), value(0)), "@return == 0", "@return not available (void?)")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("booleanExprs") + public void booleanLiteral(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream booleanExprs() { + return Stream.of( + Arguments.of(TRUE, "true"), + Arguments.of(not(BooleanExpression.FALSE), "not(false)"), + Arguments.of(not(not(TRUE)), "not(not(true))"), + Arguments.of(not(not(not(BooleanExpression.FALSE))), "not(not(not(false)))")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("isEmptyExprs") + public void isEmptyOperation(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream isEmptyExprs() { + return Stream.of( + Arguments.of(not(isEmpty(ref("strList"))), "!isEmpty(strList)"), + Arguments.of(not(isEmpty(ref("strArray"))), "!isEmpty(strArray)"), + Arguments.of(not(isEmpty(ref("strMap"))), "!isEmpty(strMap)"), + Arguments.of(not(isEmpty(ref("strSet"))), "!isEmpty(strSet)"), + Arguments.of(not(isEmpty(ref("longArray"))), "!isEmpty(longArray)")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("isDefinedExprs") + public void isDefinedOperation(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition08(expr, dslExpression); + } + + private static Stream isDefinedExprs() { + return Stream.of( + Arguments.of( + not(isDefined(getMember(getMember(ref("nullTyped"), "fld"), "fld"))), + "!isDefined(nullTyped.fld.fld)"), + Arguments.of(isDefined(ref("@duration")), "isDefined(@duration)")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("lenExprs") + public void lenExpressions(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream lenExprs() { + return Stream.of( + Arguments.of(eq(len(ref("strValue")), value(4)), "len(strValue) == 4"), + Arguments.of(eq(len(ref("strList")), value(3)), "len(strList) == 3"), + Arguments.of(eq(len(ref("strArray")), value(2)), "len(strArray) == 2"), + Arguments.of(eq(len(ref("strMap")), value(1)), "len(strMap) == 1"), + Arguments.of(eq(len(ref("strSet")), value(2)), "len(strSet) == 2"), + Arguments.of(eq(len(ref("longArray")), value(10)), "len(longArray) == 10")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("indexExprs") + public void indexArrayOperation(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream indexExprs() { + return Stream.of( + Arguments.of(eq(index(ref("strArray"), value(1)), value("bar")), "strArray[1] == 'bar'"), + Arguments.of(eq(index(ref("longArray"), value(7)), value(7)), "longArray[7] == 7"), + Arguments.of( + eq(index(ref("longArray"), len(ref("strList"))), value(3)), + "longArray[len(strList)] == 3"), + Arguments.of( + eq(index(ref("longArray"), len(index(ref("strList"), value(0)))), value(3)), + "longArray[len(strList[0])] == 3"), + Arguments.of(eq(index(ref("strList"), value(1)), value("bar")), "strList[1] == 'bar'"), + Arguments.of(eq(index(ref("strList"), value(1)), value("bar")), "strList[1] == 'bar'"), + Arguments.of( + eq(index(ref("strMap"), value("foo")), value("bar")), "strMap['foo'] == 'bar'"), + Arguments.of( + eq(index(ref("strMap"), index(ref("strList"), value(0))), value("bar")), + "strMap[strList[0]] == 'bar'")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("stringPredicateExprs") + public void stringPredicateOperation(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream stringPredicateExprs() { + return Stream.of( + Arguments.of( + matches(ref("strValue"), new StringValue("^do[en]+$")), + "matches(strValue, '^do[en]+$')"), + Arguments.of( + matches(ref("objectStrValue"), new StringValue("^foob[ar]+$")), + "matches(objectStrValue, '^foob[ar]+$')"), + Arguments.of( + startsWith(ref("strValue"), new StringValue("don")), "startsWith(strValue, 'don')"), + Arguments.of( + startsWith(ref("objectStrValue"), new StringValue("foob")), + "startsWith(strValue, 'foob')"), + Arguments.of( + endsWith(ref("strValue"), new StringValue("one")), "startsWith(strValue, 'one')"), + Arguments.of( + endsWith(ref("objectStrValue"), new StringValue("bar")), + "startsWith(strValue, 'bar')")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("containsExprs") + public void containsOperation(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream containsExprs() { + return Stream.of( + Arguments.of(contains(ref("strValue"), new StringValue("on")), "contains(strValue, 'on')"), + Arguments.of(contains(ref("strList"), new StringValue("bar")), "contains(strList, 'bar')"), + Arguments.of( + contains(ref("longArray"), new NumericValue(9L, LONG)), "contains(longArray, 9)"), + Arguments.of( + contains(ref("strArray"), new StringValue("bar")), "contains(strArray, 'bar')"), + Arguments.of(contains(ref("strSet"), new StringValue("bar")), "contains(strSet, 'bar')"), + Arguments.of(contains(ref("strMap"), new StringValue("foo")), "contains(strMap, 'foo')")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("substringExprs") + public void substringOperation(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream substringExprs() { + return Stream.of( + Arguments.of( + eq(subString(ref("strValue"), 1, 3), value("on")), "substring(strValue, 1, 3) == 'on'"), + Arguments.of( + eq(subString(ref("objectStrValue"), 1, 3), value("oo")), + "substring(objectStrValue, 1, 3) == 'oo'")); + } + + @Test + public void fieldAccessGetmember() throws IOException, URISyntaxException { + doCondition08( + eq(getMember(getMember(getMember(ref("typed"), "fld"), "fld"), "msg"), value("hello")), + "typed.fld.fld.msg == 'hello'"); + } + + @Test + public void fieldAccessGetmemberPrivate() throws IOException, URISyntaxException { + doCondition08( + eq(getMember(getMember(getMember(ref("typed"), "fld"), "fld"), "value"), value(42)), + "typed.fld.fld.value == 42"); + } + + @Test + public void fieldAccessGetmemberException() throws IOException, URISyntaxException { + doCondition05( + not(eq(getMember(ref("@exception"), "detailMessage"), nullValue())), + "@exception.detailMessage != null"); + } + + @Test + public void fieldAccessGetmemberOptional() throws IOException, URISyntaxException { + doCondition08( + eq(getMember(ref("maybeStr"), "value"), new StringValue("maybe foo")), + "maybeStr.value == 'maybe foo'"); + } + + @Test + public void fieldAccessGetmemberOptionalEmpty() throws IOException, URISyntaxException { + // Optional.empty().value resolves to Optional.orElse(null) → null; + doCondition08( + and( + not(eq(getMember(ref("maybeEmptyStr"), "value"), nullValue())), + eq(getMember(ref("maybeEmptyStr"), "value"), new StringValue("anything"))), + "maybeEmptyStr.value != null && maybeEmptyStr.value == 'anything'", + false); + } + + // OptionalInt/OptionalLong/OptionalDouble .value resolves via .orElse(0) / .orElse(0L) / + // .orElse(0.0). Exercises the int / long / double switch arms in + // ConditionInstrumenter#addSpecialFieldAccessCall. + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("optionalPrimitiveExprs") + public void fieldAccessOptionalPrimitive(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition08(expr, dslExpression); + } + + private static Stream optionalPrimitiveExprs() { + return Stream.of( + // present values + Arguments.of(eq(getMember(ref("maybeInt"), "value"), value(42)), "maybeInt.value == 42"), + Arguments.of( + eq(getMember(ref("maybeLong"), "value"), value(1001L)), "maybeLong.value == 1001"), + Arguments.of( + eq(getMember(ref("maybeDouble"), "value"), value(3.14)), "maybeDouble.value == 3.14"), + // empty values fall back to orElse(0 / 0L / 0.0) + Arguments.of( + eq(getMember(ref("maybeEmptyInt"), "value"), value(0)), "maybeEmptyInt.value == 0"), + Arguments.of( + eq(getMember(ref("maybeEmptyLong"), "value"), value(0L)), "maybeEmptyLong.value == 0"), + Arguments.of( + eq(getMember(ref("maybeEmptyDouble"), "value"), value(0.0)), + "maybeEmptyDouble.value == 0.0")); + } + + @Test + public void fieldAccessProtectedInherited() throws IOException, URISyntaxException { + doCondition06Inherited(ge(ref("doubleValue"), value(3.0D)), "this.doubleValue >= 3"); + } + + @Test + public void fieldAccessPrivateInherited() throws IOException, URISyntaxException { + doCondition06Inherited(eq(ref("intValue"), value(48)), "this.intValue == 48"); + } + + @Test + public void fieldAccessStaticFinalString() throws IOException, URISyntaxException { + doCondition06(eq(ref("STR_CONSTANT"), value("strConst")), "STR_CONSTANT == 'strConst'"); + } + + @Test + public void fieldAccessStaticFinalInt() throws IOException, URISyntaxException { + doCondition06(eq(ref("INT_CONSTANT"), value(1001)), "INT_CONSTANT == 1001"); + } + + @Test + public void fieldAccessStaticString() throws IOException, URISyntaxException { + doCondition06(eq(ref("STATIC_STR"), value("strStatic")), "STATIC_STR == 'strStatic'"); + } + + // Two exit-only synthetics combined in a single condition. Both load from the condition + // method's parameter slots: @duration from timestampVarIndex, @return from returnVarIndex. + @Test + public void syntheticDurationAndReturnCombined() throws IOException, URISyntaxException { + doCondition06( + and(ge(ref("@duration"), value(0L)), eq(ref("@return"), value(42))), + "@duration >= 0 && @return == 42"); + } + + @Test + public void syntheticDuration() throws IOException, URISyntaxException { + doCondition06(ge(ref("@duration"), value(0L)), "@duration >= 0"); + } + + @Test + public void syntheticReturn() throws IOException, URISyntaxException { + doCondition06(eq(ref("@return"), value(42)), "@return == 42"); + } + + // When the probed method returns a String, @return resolves to STRING_TYPE and can be used + // as the target of String operations — equality, len, startsWith/endsWith. + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("returnStringExprs") + public void returnStringOperations(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06ReturnString(expr, dslExpression); + } + + private static Stream returnStringExprs() { + // CapturedSnapshot06.returnString() returns "foo" (length 3). + return Stream.of( + Arguments.of(eq(ref("@return"), value("foo")), "@return == 'foo'"), + Arguments.of(eq(len(ref("@return")), value(3)), "len(@return) == 3"), + Arguments.of( + startsWith(ref("@return"), new StringValue("fo")), "startsWith(@return, 'fo')"), + Arguments.of(endsWith(ref("@return"), new StringValue("oo")), "endsWith(@return, 'oo')")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("entryExprs") + public void conditionAtEntry(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06AtEntry(expr, dslExpression); + } + + private static Stream entryExprs() { + // CapturedSnapshot06.f() mutates intValue (24 -> 48) and strValue ("foobar" -> "done") + // so these conditions only hold when evaluated at ENTRY, not EXIT. + return Stream.of( + Arguments.of(eq(ref("intValue"), value(24)), "intValue == 24"), + Arguments.of(eq(ref("strValue"), value("foobar")), "strValue == 'foobar'")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("exceptionExprs") + public void syntheticException(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition05(expr, dslExpression); + } + + private static Stream exceptionExprs() { + return Stream.of( + Arguments.of(isDefined(ref("@exception")), "isDefined(@exception)"), + Arguments.of( + and(isDefined(ref("@exception")), not(eq(ref("arg"), DSL.nullValue()))), + "isDefined(@exception) or arg != null"), + // TODO special case need to be separate and will capture at exit, not at catch uncaught + // Arguments.of(not(isDefined(ref("@exception"))), "not(isDefined(@exception)"), + Arguments.of( + and( + instanceOf(ref("@exception"), value("CapturedSnapshot05$CustomException")), + eq(getMember(ref("@exception"), "detailMessage"), value("oops")), + eq(getMember(ref("@exception"), "additionalMsg"), value("I did it again"))), + "@exception instanceof \"CapturedSnapshot05$CustomException\" and @exception.detailMessage == 'oops' and @exception.additionalMsg == 'I did it again'"), + Arguments.of( + eq( + getMember( + index(getMember(ref("@exception"), "stackTrace"), value(0)), "declaringClass"), + value("CapturedSnapshot05")), + "@exception.stackTrace[0].declaringClass == 'CapturedSnapshot05'"), + Arguments.of( + any( + getMember(ref("@exception"), "stackTrace"), + eq(getMember(ref(ITERATOR_REF), "declaringClass"), value("CapturedSnapshot05"))), + "any(@exception.stackTrace, {@it.declaringClass == 'CapturedSnapshot05'})"), + Arguments.of( + all( + getMember(ref("@exception"), "stackTrace"), + instanceOf(ref(ITERATOR_REF), value("java.lang.StackTraceElement"))), + "all(@exception.stackTrace, {@it instanceof 'java.lang.StackTraceElement'})")); + } + + @Test + public void evalError() throws IOException, URISyntaxException { + doCondition08( + eq(getMember(getMember(ref("nullTyped"), "fld"), "fld"), value("5")), + "nullTyped.fld.fld == '5'", + true, + "java.lang.NullPointerException"); + } + + // The right operand of instanceof is required to be STRING_TYPE at instrumentation time, + // but a String *ref* satisfies that check while only revealing the bogus class name at + // runtime via Class.forName. + @Test + public void instanceOfWithRuntimeClassNameLookup() throws IOException, URISyntaxException { + doCondition06EvalError( + instanceOf(ref("intValue"), ref("strValue")), + "intValue instanceof strValue", + "java.lang.IllegalArgumentException: Class not found: done"); + } + + // Missing map keys resolve to null via ConditionHelper.index, so the condition simply + // evaluates to false rather than producing an evaluation error — distinct from list / + // array index OOB above. + @Test + public void mapMissingKeyEvaluatesFalse() throws IOException, URISyntaxException { + doCondition06( + eq(index(ref("strMap"), value("nonexistent")), value("bar")), + "strMap['nonexistent'] == 'bar'", + false); + } + + // Mirror of the existing or(TRUE, X) / and(FALSE, X) short-circuit cases in binaryExprs: + // here the would-error expression is on the LEFT and the short-circuit value on the + // RIGHT. Left-to-right evaluation order must propagate the error — the right operand + // cannot rescue a faulted left operand. + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("shortCircuitLeftErrorExprs") + public void shortCircuitLeftErrorOrdering( + BooleanExpression expr, String dslExpression, String expectedEvalErrorMsg) + throws IOException, URISyntaxException { + doCondition08(expr, dslExpression, true, expectedEvalErrorMsg); + } + + private static Stream shortCircuitLeftErrorExprs() { + return Stream.of( + Arguments.of( + or(eq(getMember(getMember(ref("nullTyped"), "fld"), "fld"), value("5")), TRUE), + "nullTyped.fld.fld == '5' || true", + "java.lang.NullPointerException"), + Arguments.of( + and( + eq(getMember(getMember(ref("nullTyped"), "fld"), "fld"), value("5")), + BooleanExpression.FALSE), + "nullTyped.fld.fld == '5' && false", + "java.lang.NullPointerException")); + } + + // Pin down semantics for empty / null collections, nested collection ops, and indexing + // a filter result. Empty collections are derived from `filter(X, FALSE)` to avoid + // touching CapturedSnapshot06's field set (its fields count is asserted elsewhere). + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("collectionEdgeExprs") + public void collectionEdgeCases(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream collectionEdgeExprs() { + return Stream.of( + // any over empty collection: false → !any holds + Arguments.of( + not( + any( + filter(ref("strList"), BooleanExpression.FALSE), + eq(ref(ITERATOR_REF), value("foo")))), + "!any(filter(strList, false), {@it == 'foo'})"), + // all over empty collection: true (vacuously) + Arguments.of( + all( + filter(ref("strList"), BooleanExpression.FALSE), + eq(ref(ITERATOR_REF), value("foo"))), + "all(filter(strList, false), {@it == 'foo'})"), + // len(filter(empty)) == 0 + Arguments.of( + eq( + len( + filter( + filter(ref("strList"), BooleanExpression.FALSE), + eq(ref(ITERATOR_REF), value("foo")))), + value(0)), + "len(filter(filter(strList, false), {@it == 'foo'})) == 0"), + // any over empty primitive array: false + Arguments.of( + not( + any( + filter(ref("longArray"), BooleanExpression.FALSE), + eq(ref(ITERATOR_REF), value(0L)))), + "!any(filter(longArray, false), {@it == 0})"), + // all over empty primitive array: true + Arguments.of( + all( + filter(ref("longArray"), BooleanExpression.FALSE), + eq(ref(ITERATOR_REF), value(0L))), + "all(filter(longArray, false), {@it == 0})"), + // nested filter inside any: longArray={0..9}, filter > 5 → {6,7,8,9}, any < 8 → true + Arguments.of( + any( + filter(ref("longArray"), gt(ref(ITERATOR_REF), value(5L))), + lt(ref(ITERATOR_REF), value(8L))), + "any(filter(longArray, {@it > 5}), {@it < 8})"), + // filter result indexed: strList at EXIT = ["foo","bar","done"], filter == "bar" → ["bar"], + // [0] == "bar" + Arguments.of( + eq( + index(filter(ref("strList"), eq(ref(ITERATOR_REF), value("bar"))), value(0)), + value("bar")), + "filter(strList, {@it == 'bar'})[0] == 'bar'")); + } + + // any/all on a null collection: NPE at iteration time → eval error. + @ParameterizedTest + @MethodSource("nullCollectionExprs") + public void nullCollectionEvalError( + BooleanExpression expr, String dslExpression, String expectedEvalErrorMsg) + throws IOException, URISyntaxException { + doCondition06EvalError(expr, dslExpression, expectedEvalErrorMsg); + } + + private static Stream nullCollectionExprs() { + return Stream.of( + Arguments.of( + any(nullValue(), eq(ref(ITERATOR_REF), value("foo"))), + "any(null, {@it == 'foo'})", + "java.lang.NullPointerException: Cannot invoke \"java.util.Collection.iterator()\" because \"collection\" is null"), + Arguments.of( + all(nullValue(), eq(ref(ITERATOR_REF), value("foo"))), + "all(null, {@it == 'foo'})", + "java.lang.NullPointerException: Cannot invoke \"java.util.Collection.iterator()\" because \"collection\" is null")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("filterExprs") + public void filterExpressions(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream filterExprs() { + return Stream.of( + Arguments.of( + eq(len(filter(ref("longArray"), lt(ref(ITERATOR_REF), value(2)))), value(2)), + "len(filter(longArray, {@it < 2})) == 2"), + Arguments.of( + eq( + len( + filter( + ref("longArray"), + lt(ref(ValueReferences.ITERATOR_REF), len(ref("strSet"))))), + value(2)), + "len(filter(longArray, {@it < len(strSet)})) == 2"), + Arguments.of( + eq(len(filter(ref("strArray"), eq(ref(ITERATOR_REF), value("foo")))), value(1)), + "len(filter(strArray, {@it == 'foo'})) == 2"), + Arguments.of( + eq(len(filter(ref("boolArray"), eq(ref(ITERATOR_REF), value(true)))), value(4)), + "len(filter(boolArray, {@it == true})) == 4"), + Arguments.of( + eq(len(filter(ref("strList"), eq(ref(ITERATOR_REF), value("foo")))), value(1)), + "len(filter(strList, {@it == 'foo'})) == 1"), + Arguments.of( + eq(len(filter(ref("strSet"), eq(ref(ITERATOR_REF), value("foo")))), value(1)), + "len(filter(strSet, {@it == 'foo'})) == 1"), + Arguments.of( + eq( + len( + filter( + filter( + filter(ref("longArray"), gt(ref(ITERATOR_REF), value(-2))), + gt(ref(ITERATOR_REF), value(-1))), + gt(ref(ITERATOR_REF), value(0)))), + value(9)), + "len(filter(filter(filter(longArray, {@it > -2}), {@it > -1}), {@it > 0})) == 9")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("anyExprs") + public void anyExpressions(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream anyExprs() { + return Stream.of( + Arguments.of( + any(ref("longArray"), eq(ref(ITERATOR_REF), value(2))), "any(longArray, {@it == 2})"), + Arguments.of( + any(ref("longArray"), lt(ref(ValueReferences.ITERATOR_REF), len(ref("strSet")))), + "any(longArray, {@it < len(strSet)})"), + Arguments.of( + any(ref("strArray"), eq(ref(ITERATOR_REF), value("foo"))), + "any(strArray, {@it == 'foo'})"), + Arguments.of( + any(ref("boolArray"), eq(ref(ITERATOR_REF), value(true))), + "any(boolArray, {@it == true})"), + Arguments.of( + any(ref("strList"), eq(ref(ITERATOR_REF), value("foo"))), + "any(strList, {@it == 'foo'})"), + Arguments.of( + any(ref("strSet"), eq(ref(ITERATOR_REF), value("foo"))), "any(strSet, {@it == 'foo'})"), + Arguments.of( + any(ref("strList"), instanceOf(ref(ITERATOR_REF), value("java.lang.String"))), + "any(strList, {@it instanceof 'java.lang.String'})"), + Arguments.of( + not(any(ref("strList"), instanceOf(ref(ITERATOR_REF), value("java.lang.Integer")))), + "!any(strList, {@it instanceof 'java.lang.Integer'})")); + } + + @ParameterizedTest(name = "[{index}] {1}") + @MethodSource("allExprs") + public void allExpressions(BooleanExpression expr, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(expr, dslExpression); + } + + private static Stream allExprs() { + return Stream.of( + Arguments.of( + all(ref("longArray"), ge(ref(ITERATOR_REF), value(0))), "all(longArray, {@it >= 0})"), + Arguments.of( + any(ref("longArray"), lt(ref(ValueReferences.ITERATOR_REF), len(ref("strSet")))), + "any(longArray, {@it < len(strSet)})"), + Arguments.of( + all(ref("strArray"), eq(len(ref(ITERATOR_REF)), value(3))), + "all(strArray, {len(@it) == 3})"), + Arguments.of( + all( + ref("boolArray"), + or(eq(ref(ITERATOR_REF), value(true)), eq(ref(ITERATOR_REF), value(false)))), + "all(boolArray, {@it == true || @it == false})"), + Arguments.of( + all(ref("strList"), ge(len(ref(ITERATOR_REF)), value(2))), + "all(strList, {len(@it) >= 2})"), + Arguments.of( + all(ref("strSet"), eq(len(ref(ITERATOR_REF)), value(3))), + "all(strSet, {len(@it) == 3})"), + Arguments.of( + all(ref("strList"), instanceOf(ref(ITERATOR_REF), value("java.lang.String"))), + "all(strList, {@it instanceof 'java.lang.String'})"), + Arguments.of( + all(ref("strList"), instanceOf(ref(ITERATOR_REF), value("java.lang.CharSequence"))), + "all(strList, {@it instanceof 'java.lang.CharSequence'})")); + } + + void doCondition05(BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot05"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "triggerUncaughtException", "(String)") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + try { + Reflect.onClass(testClass).call("main", "triggerUncaughtException").get(); + Assertions.fail("should not reach this code"); + } catch (ReflectException ex) { + assertEquals("oops", ex.getCause().getCause().getMessage()); + } + assertEquals(1, listener.snapshots.size()); + assertNotNull(listener.snapshots.get(0).getCaptures().getReturn()); + } + + void doCondition06(BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + doCondition06(condition, dslExpression, true); + } + + void doCondition06(BooleanExpression condition, String dslExpression, boolean expected) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "f", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "f").get(); + assertEquals(42, result); + if (expected) { + assertEquals(1, listener.snapshots.size()); + assertNotNull(listener.snapshots.get(0).getCaptures().getReturn()); + } else { + assertEquals(0, listener.snapshots.size()); + } + } + + void doCondition06EvalError( + BooleanExpression condition, String dslExpression, String expectedEvalErrorMsg) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "f", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "f").get(); + assertEquals(42, result); + assertEquals(1, listener.snapshots.size()); + assertEquals( + expectedEvalErrorMsg, listener.snapshots.get(0).getEvaluationErrors().get(0).getMessage()); + assertEquals(dslExpression, listener.snapshots.get(0).getEvaluationErrors().get(0).getExpr()); + } + + void doCondition06ReturnString(BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "returnString", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "returnString").get(); + assertEquals(42, result); + assertEquals(1, listener.snapshots.size()); + assertNotNull(listener.snapshots.get(0).getCaptures().getReturn()); + } + + void doCondition06AtEntry(BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "f", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.ENTRY) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "f").get(); + assertEquals(42, result); + assertEquals(1, listener.snapshots.size()); + assertNotNull(listener.snapshots.get(0).getCaptures().getEntry()); + } + + void doCondition06Failure(BooleanExpression condition, String dslExpression, String expectedMsg) + throws IOException, URISyntaxException { + doCondition06Failure(condition, dslExpression, expectedMsg, MethodLocation.EXIT); + } + + void doCondition06Failure( + BooleanExpression condition, + String dslExpression, + String expectedMsg, + MethodLocation methodLocation) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "f", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(methodLocation) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "f").get(); + assertEquals(42, result); + assertEquals(1, instrumentationListener.results.size()); + InstrumentationResult instrumentationResult0 = + instrumentationListener.results.get(PROBE_ID.getId()); + assertTrue(instrumentationResult0.isError()); + assertEquals( + expectedMsg, + instrumentationResult0.getDiagnostics().get(PROBE_ID).get(0).getThrowable().getMessage()); + } + + void doCondition06FailureOnVoidMethod( + BooleanExpression condition, + String dslExpression, + String expectedMsg, + MethodLocation methodLocation) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "g", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(methodLocation) + .build(); + installProbes(logProbe); + compileAndLoadClass(CLASS_NAME); + assertEquals(1, instrumentationListener.results.size()); + InstrumentationResult instrumentationResult0 = + instrumentationListener.results.get(PROBE_ID.getId()); + assertTrue(instrumentationResult0.isError()); + assertEquals( + expectedMsg, + instrumentationResult0.getDiagnostics().get(PROBE_ID).get(0).getThrowable().getMessage()); + } + + void doCondition06Inherited(BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot06"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME + "$Inherited", "f", "()") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "inherited").get(); + assertEquals(42, result); + assertEquals(1, listener.snapshots.size()); + assertNotNull(listener.snapshots.get(0).getCaptures().getReturn()); + } + + void doCondition08(BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + doCondition08(condition, dslExpression, true, null); + } + + void doCondition08(BooleanExpression condition, String dslExpression, boolean expectedResult) + throws IOException, URISyntaxException { + doCondition08(condition, dslExpression, expectedResult, null); + } + + void doCondition08( + BooleanExpression condition, + String dslExpression, + boolean expectedResult, + String expectedEvalErrorMsg) + throws IOException, URISyntaxException { + final String CLASS_NAME = "CapturedSnapshot08"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, "doit", "int (java.lang.String)") + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", "5").get(); + assertEquals(3, result); + if (expectedEvalErrorMsg != null) { + assertEquals( + expectedEvalErrorMsg, + listener.snapshots.get(0).getEvaluationErrors().get(0).getMessage()); + assertEquals(dslExpression, listener.snapshots.get(0).getEvaluationErrors().get(0).getExpr()); + } else { + if (expectedResult) { + assertEquals(1, listener.snapshots.size()); + List errors = listener.snapshots.get(0).getEvaluationErrors(); + assertTrue(errors == null || errors.isEmpty()); + assertNotNull(listener.snapshots.get(0).getCaptures().getReturn()); + } else { + assertEquals(0, listener.snapshots.size()); + } + } + } + + void doCondition25( + String methodName, String methodArg, BooleanExpression condition, String dslExpression) + throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot25"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, methodName, null) + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + TestSnapshotListener listener = installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + int result = Reflect.on(testClass).call("main", methodArg).get(); + assertEquals(42, result); + assertEquals(1, listener.snapshots.size()); + assertNotNull(listener.snapshots.get(0).getCaptures().getReturn()); + } + + void doCondition25Failure( + String methodName, + String methodArg, + BooleanExpression condition, + String dslExpression, + String expectedMsg) + throws IOException, URISyntaxException { + final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot25"; + LogProbe logProbe = + createProbeBuilder(PROBE_ID, CLASS_NAME, methodName, null) + .when(new ProbeCondition(when(condition), dslExpression)) + .evaluateAt(MethodLocation.EXIT) + .build(); + installProbes(logProbe); + Class testClass = compileAndLoadClass(CLASS_NAME); + Reflect.on(testClass).call("main", methodArg).get(); + assertEquals(1, instrumentationListener.results.size()); + InstrumentationResult instrumentationResult0 = + instrumentationListener.results.get(PROBE_ID.getId()); + assertTrue(instrumentationResult0.isError()); + assertEquals( + expectedMsg, + instrumentationResult0.getDiagnostics().get(PROBE_ID).get(0).getThrowable().getMessage()); + } +} diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java index d40fe6659d1..22100f8fa19 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/CapturedSnapshotTest.java @@ -992,7 +992,8 @@ public void fieldExtractorNotAccessible() throws IOException, URISyntaxException public void uncaughtException() throws IOException, URISyntaxException { final String CLASS_NAME = "CapturedSnapshot05"; TestSnapshotListener listener = - installProbes(createMethodProbe(PROBE_ID, CLASS_NAME, "triggerUncaughtException", "()")); + installProbes( + createMethodProbe(PROBE_ID, CLASS_NAME, "triggerUncaughtException", "(String)")); Class testClass = compileAndLoadClass(CLASS_NAME); try { Reflect.onClass(testClass).call("main", "triggerUncaughtException").get(); @@ -1022,7 +1023,7 @@ public void uncaughtExceptionCondition() throws IOException, URISyntaxException final String CLASS_NAME = "CapturedSnapshot05"; final String LOG_TEMPLATE = "exception msg={@exception.detailMessage}"; LogProbe probe = - createProbeBuilder(PROBE_ID, CLASS_NAME, "triggerUncaughtException", "()") + createProbeBuilder(PROBE_ID, CLASS_NAME, "triggerUncaughtException", "(String)") .evaluateAt(MethodLocation.EXIT) .when( new ProbeCondition( @@ -1697,7 +1698,7 @@ public void fields() throws IOException, URISyntaxException { int result = Reflect.onClass(testClass).call("main", "f").get(); assertEquals(42, result); Snapshot snapshot = assertOneSnapshot(listener); - assertCaptureFieldCount(snapshot.getCaptures().getEntry(), 5); + assertCaptureFieldCount(snapshot.getCaptures().getEntry(), 12); assertCaptureFields(snapshot.getCaptures().getEntry(), "intValue", "int", "24"); assertCaptureFields(snapshot.getCaptures().getEntry(), "doubleValue", "double", "3.14"); assertCaptureFields( @@ -1709,7 +1710,11 @@ public void fields() throws IOException, URISyntaxException { Arrays.asList("foo", "bar")); assertCaptureFields( snapshot.getCaptures().getEntry(), "strMap", "java.util.HashMap", Collections.emptyMap()); - assertCaptureFieldCount(snapshot.getCaptures().getReturn(), 5); + assertCaptureFields( + snapshot.getCaptures().getEntry(), "strArray", "java.lang.String[]", "[foo, bar]"); + assertCaptureFields( + snapshot.getCaptures().getEntry(), "longArray", "long[]", "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"); + assertCaptureFieldCount(snapshot.getCaptures().getReturn(), 12); assertCaptureFields(snapshot.getCaptures().getReturn(), "intValue", "int", "48"); assertCaptureFields(snapshot.getCaptures().getReturn(), "doubleValue", "double", "3.14"); assertCaptureFields(snapshot.getCaptures().getReturn(), "strValue", "java.lang.String", "done"); @@ -1765,14 +1770,15 @@ public void staticFields() throws IOException, URISyntaxException { } @Test - public void staticInheritedFields() throws IOException, URISyntaxException { + public void staticFieldsCondition() throws IOException, URISyntaxException { final String CLASS_NAME = "com.datadog.debugger.CapturedSnapshot19"; final String INHERITED_CLASS_NAME = CLASS_NAME + "$Inherited"; LogProbe logProbe = createProbeBuilder(PROBE_ID, INHERITED_CLASS_NAME, "f", "()") .when( new ProbeCondition( - DSL.when(DSL.eq(DSL.ref("intValue"), DSL.value(48))), "intValue == 48")) + DSL.when(DSL.eq(DSL.ref("strValue"), DSL.value("barfoo"))), + "strValue == 'barfoo'")) .evaluateAt(MethodLocation.EXIT) .build(); TestSnapshotListener listener = installProbes(logProbe); @@ -2040,8 +2046,6 @@ public void evaluateAtExitFalse() throws IOException, URISyntaxException { int result = Reflect.onClass(testClass).call("main", "1").get(); assertEquals(3, result); assertEquals(0, listener.snapshots.size()); - assertTrue(listener.skipped); - assertEquals(DebuggerContext.SkipCause.CONDITION, listener.cause); } @Test diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java index 6858a40f40f..83518031a1b 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/agent/LogProbesInstrumentationTest.java @@ -393,7 +393,7 @@ public void lineTemplateThisLog() throws IOException, URISyntaxException { Assertions.assertEquals(42, result); Snapshot snapshot = assertOneSnapshot(LINE_PROBE_ID1, listener); assertEquals( - "this is log line for this={intValue=48, doubleValue=3.14, strValue=done, strList=..., strMap=...}", + "this is log line for this={intValue=48, doubleValue=3.14, strValue=done, strList=..., strMap=...}, ...", snapshot.getMessage()); } diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/probe/LogProbeTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/probe/LogProbeTest.java index 07b7ae05185..29b72e64168 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/probe/LogProbeTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/probe/LogProbeTest.java @@ -281,8 +281,8 @@ private void fillStatus( entryStatus.setSampled(sampled); entryStatus.setCondition(condition); entryStatus.setConditionErrors(conditionErrors); - entryStatus.setLogTemplateErrors(logTemplateErrors); - entryStatus.setLogTemplateErrors(logTemplateErrors); + entryStatus.setHasEvalutionErrors(logTemplateErrors); + entryStatus.setHasEvalutionErrors(logTemplateErrors); } private LogStatus prepareContext( @@ -324,12 +324,12 @@ public void fillSnapshot_shouldSend_evalErrors() { CapturedContext entryContext = new CapturedContext(); LogStatus logStatus = prepareContext(entryContext, logProbe, MethodLocation.ENTRY); logStatus.addError(new EvaluationError("expr", "msg1")); - logStatus.setLogTemplateErrors(true); + logStatus.setHasEvalutionErrors(true); entryContext.addThrowable(new RuntimeException("errorEntry")); CapturedContext exitContext = new CapturedContext(); logStatus = prepareContext(exitContext, logProbe, MethodLocation.EXIT); logStatus.addError(new EvaluationError("expr", "msg2")); - logStatus.setLogTemplateErrors(true); + logStatus.setHasEvalutionErrors(true); exitContext.addThrowable(new RuntimeException("errorExit")); Snapshot snapshot = new Snapshot(currentThread(), logProbe, 10); assertTrue(logProbe.fillSnapshot(entryContext, exitContext, null, snapshot)); diff --git a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/trigger/TriggerProbeTest.java b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/trigger/TriggerProbeTest.java index 6b895a9dc13..55a61e521a0 100644 --- a/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/trigger/TriggerProbeTest.java +++ b/dd-java-agent/agent-debugger/src/test/java/com/datadog/debugger/trigger/TriggerProbeTest.java @@ -32,6 +32,7 @@ import java.util.List; import org.joor.Reflect; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; public class TriggerProbeTest extends CapturingTestBase { @@ -104,6 +105,7 @@ private static TriggerProbe createTriggerProbe( .setSessionId(sessionId); } + @Disabled @Test public void cooldown() throws IOException, URISyntaxException { try { diff --git a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot05.java b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot05.java index 470d22b1ba4..a1591a3b7c6 100644 --- a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot05.java +++ b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot05.java @@ -4,7 +4,7 @@ public class CapturedSnapshot05 { - void triggerUncaughtException() { + void triggerUncaughtException(String arg) { throw new CustomException("oops", "I did it again"); } @@ -38,7 +38,7 @@ public static int main(String arg) { CapturedSnapshot05 cs5 = new CapturedSnapshot05(); long before = System.currentTimeMillis(); if ("triggerUncaughtException".equals(arg)) { - cs5.triggerUncaughtException(); + cs5.triggerUncaughtException(arg); } else if ("triggerCaughtException".equals(arg)) { return cs5.triggerCaughtException(); } else if ("triggerSwallowedException".equals(arg)) { diff --git a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot06.java b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot06.java index 61b6ac9fe60..6b2c4a58a1d 100644 --- a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot06.java +++ b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot06.java @@ -1,8 +1,12 @@ +import java.nio.file.StandardOpenOption; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.ArrayList; +import java.util.Random; public class CapturedSnapshot06 { @@ -15,6 +19,13 @@ public class CapturedSnapshot06 { private String strValue = "foobar"; private final List strList = new ArrayList<>(Arrays.asList("foo", "bar")); private final Map strMap = new HashMap<>(); + private final String[] strArray = new String[] {"foo", "bar"}; + private final long[] longArray = new long[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + private final boolean[] boolArray = new boolean[] {true, false, true, false, true, false, true}; + private final Set strSet = new HashSet<>(Arrays.asList("foo", "bar")); + private final Random random = new Random(); + private StandardOpenOption option = StandardOpenOption.READ; + private Object objectStrValue = strValue; int f() { intValue *= 2; @@ -24,10 +35,20 @@ int f() { return 42; // beae1817-f3b0-4ea8-a74f-000000000001 } + static void g() {} + + String returnString() { + return "foo"; + } + public static int main(String arg) { if ("f".equals(arg)) { CapturedSnapshot06 cs6 = new CapturedSnapshot06(); return cs6.f(); + } else if ("returnString".equals(arg)) { + CapturedSnapshot06 cs6 = new CapturedSnapshot06(); + cs6.returnString(); + return 42; } else { Base base = new Inherited(); return base.f(); diff --git a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot08.java b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot08.java index 11c124e51dc..4dddd201d61 100644 --- a/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot08.java +++ b/dd-java-agent/agent-debugger/src/test/resources/CapturedSnapshot08.java @@ -1,4 +1,7 @@ import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; public class CapturedSnapshot08 { public static int main(String arg) { @@ -16,6 +19,7 @@ private int doit(String arg) { // beae1817-f3b0-4ea8-a74f-000000000002 } static class Type3 { + private final int value = 42; final String msg; Type3(String msg) { this.msg = msg; @@ -30,7 +34,7 @@ static final class Type2 { } static final class Type1 { - final Type2 fld; + public final Type2 fld; Type1(Type2 fld) { this.fld = fld; } @@ -40,6 +44,13 @@ static final class Type1 { private Type1 typed = new Type1(new Type2(new Type3("hello"))); private Type1 nullTyped = new Type1(null); private Optional maybeStr = Optional.of("maybe foo"); + private Optional maybeEmptyStr = Optional.empty(); + private OptionalInt maybeInt = OptionalInt.of(42); + private OptionalInt maybeEmptyInt = OptionalInt.empty(); + private OptionalLong maybeLong = OptionalLong.of(1001L); + private OptionalLong maybeEmptyLong = OptionalLong.empty(); + private OptionalDouble maybeDouble = OptionalDouble.of(3.14); + private OptionalDouble maybeEmptyDouble = OptionalDouble.empty(); private static final CapturedSnapshot08 INSTANCE = new CapturedSnapshot08(); } diff --git a/dd-java-agent/agent-debugger/src/testFixtures/java/com/datadog/debugger/util/MoshiSnapshotTestHelper.java b/dd-java-agent/agent-debugger/src/testFixtures/java/com/datadog/debugger/util/MoshiSnapshotTestHelper.java index 80c0931d236..425c42d354f 100644 --- a/dd-java-agent/agent-debugger/src/testFixtures/java/com/datadog/debugger/util/MoshiSnapshotTestHelper.java +++ b/dd-java-agent/agent-debugger/src/testFixtures/java/com/datadog/debugger/util/MoshiSnapshotTestHelper.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -362,6 +363,13 @@ public CapturedContext.CapturedValue fromJson(JsonReader jsonReader) throws IOEx } } else if (type.equals("java.util.Collections$EmptyList")) { value = Collections.emptyList(); + } else if (type.equals(Set.class.getTypeName()) + || type.equals(HashSet.class.getTypeName())) { + Set set = new HashSet<>(); + for (CapturedContext.CapturedValue cValue : values) { + set.add(cValue.getValue()); + } + value = set; } else { throw new RuntimeException("Cannot deserialize type: " + type); }