Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,12 @@ private Expression Predicate(JsonTextReader reader, List<ParameterExpression> pa
ReturnDefaultValueExpression();
}

if (!IsSafeCollection(source.Type))
if (!IsSafeCollection(source.Type) && !IsSafeNonGenericDictionary(source.Type))
{
throw new InvalidOperationException("Source must be an array or implement ICollection or IReadOnlyCollection");
throw new InvalidOperationException("Source must be an array or implement ICollection, IReadOnlyCollection, or IDictionary");
}

Type itParameterType = null;
if (source.Type.IsArray)
{
itParameterType = source.Type.GetElementType();
}
else
{
if (source.Type.GetGenericArguments().Length > 0)
{
itParameterType = source.Type.GetGenericArguments()[0];
}
}
var itParameterType = GetIteratorParameterType(source.Type);

if (predicateMethod == null)
{
Expand All @@ -74,7 +63,7 @@ private Expression Predicate(JsonTextReader reader, List<ParameterExpression> pa
var predicate = ParseTree(reader, new List<ParameterExpression> { Expression.Parameter(source.Type) }, itParameter);
var lambda = Expression.Lambda(predicate, itParameter);
var genericPredicateMethod = predicateMethod.MakeGenericMethod(itParameterType);
callExpression = Expression.Call(null, genericPredicateMethod, source, lambda);
callExpression = Expression.Call(null, genericPredicateMethod, PredicateSource(source, itParameterType), lambda);
if (IsIEnumerable(callExpression.Type))
{
var toListMethod = ProbeExpressionParserHelper.GetMethodByReflection(typeof(Enumerable), nameof(Enumerable.ToList), null);
Expand Down Expand Up @@ -215,6 +204,69 @@ private MethodCallExpression CollectionAndStringLengthExpression(Expression sour
return Expression.Call(source, countOrLength);
}

private Type GetIteratorParameterType(Type sourceType)
{
if (sourceType.IsArray)
{
return sourceType.GetElementType();
}

var genericDictionaryType = sourceType.IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(IDictionary<,>)
? sourceType
: sourceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>));

if (genericDictionaryType != null)
{
return typeof(KeyValuePair<,>).MakeGenericType(genericDictionaryType.GetGenericArguments());
}

if (typeof(IDictionary).IsAssignableFrom(sourceType))
{
return typeof(DictionaryEntry);
}
Comment on lines +223 to +226

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle non-generic dictionaries in the predicate path

For non-generic dictionary inputs such as Hashtable/IDictionary, this new branch is never reached: Predicate first rejects the source with IsSafeCollection, and IsCollection only recognizes generic collection interfaces. That means any(ref HashtableLocal, { @key == ... }) is compiled as a default-true condition with an error instead of evaluating the entries, despite the added DictionaryEntry path; the source also needs to be allowed/cast into an IEnumerable<DictionaryEntry> path before invoking LINQ.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


var enumerableType = sourceType.IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
? sourceType
: sourceType.GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));

if (enumerableType != null)
{
return enumerableType.GetGenericArguments()[0];
}

throw new InvalidOperationException("Fail to determined the iterator parameter type");
}

private Expression PredicateSource(Expression source, Type itParameterType)
{
if (itParameterType != typeof(DictionaryEntry) || !IsSafeNonGenericDictionary(source.Type))
{
return source;
}

var castMethod = ProbeExpressionParserHelper.GetMethodByReflection(typeof(Enumerable), nameof(Enumerable.Cast), [typeof(IEnumerable)], [typeof(DictionaryEntry)]);
return Expression.Call(null, castMethod, source);
}

private bool TryGetCollectionIteratorProperty(ParameterExpression itParameter, string propertyName, out MemberExpression propertyExpression)
{
propertyExpression = null;

if (itParameter.Type == typeof(DictionaryEntry))
{
propertyExpression = Expression.Property(itParameter, propertyName);
return true;
}

if (itParameter.Type.IsGenericType && itParameter.Type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
propertyExpression = Expression.Property(itParameter, propertyName);
return true;
}

return false;
}

private bool IsSafeCollection(Type type)
{
if (type == null)
Expand All @@ -225,6 +277,11 @@ private bool IsSafeCollection(Type type)
return type.IsArray || (IsMicrosoftType(type) && IsCollection(type));
}

private bool IsSafeNonGenericDictionary(Type type)
{
return type != null && IsMicrosoftType(type) && typeof(IDictionary).IsAssignableFrom(type);
}

private bool IsIEnumerable(Type type)
{
return IsSafeCollection(type) || type.GetInterface(nameof(IEnumerable)) != null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ internal partial class ProbeExpressionParser<T>
private const string @Exceptions = "@exception";
private const string @Duration = "@duration";
private const string @It = "@it";
private const string @Key = "@key";
private const string @Value = "@value";
private const string @This = "this";

private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(ProbeExpressionParser<T>));
Expand Down Expand Up @@ -55,7 +57,8 @@ static ProbeExpressionParser()

private Expression ParseRoot(
JsonTextReader reader,
List<ParameterExpression> parameters)
List<ParameterExpression> parameters,
ParameterExpression itParameter = null)
{
var readerValue = reader.Value?.ToString();
switch (reader.TokenType)
Expand All @@ -65,27 +68,27 @@ private Expression ParseRoot(
{
case "and":
{
return ConditionalOperator(reader, Expression.AndAlso, parameters);
return ConditionalOperator(reader, Expression.AndAlso, parameters, itParameter);
}

case "or":
{
return ConditionalOperator(reader, Expression.OrElse, parameters);
return ConditionalOperator(reader, Expression.OrElse, parameters, itParameter);
}

default:
return ParseTree(reader, parameters, null, false);
return ParseTree(reader, parameters, itParameter, false);
}
}

return null;
}

private Expression ConditionalOperator(JsonTextReader reader, Combiner combiner, List<ParameterExpression> parameters)
private Expression ConditionalOperator(JsonTextReader reader, Combiner combiner, List<ParameterExpression> parameters, ParameterExpression itParameter)
{
_arrayStack++;
reader.Read();
var right = ParseTree(reader, parameters, null);
var right = ParseTree(reader, parameters, itParameter);
var left = Combine(null, right, combiner);

while (reader.Read())
Expand All @@ -111,7 +114,7 @@ private Expression ConditionalOperator(JsonTextReader reader, Combiner combiner,
break;
}

right = ParseTree(reader, parameters, null, false);
right = ParseTree(reader, parameters, itParameter, false);
left = Combine(left, right, combiner);
}

Expand Down Expand Up @@ -153,14 +156,14 @@ private Expression ParseTree(
case "and":
case "&&":
{
var right = ParseRoot(reader, parameters);
var right = ParseRoot(reader, parameters, itParameter);
return right;
}

case "or":
case "||":
{
var right = ParseRoot(reader, parameters);
var right = ParseRoot(reader, parameters, itParameter);
return right;
}

Expand Down Expand Up @@ -358,6 +361,40 @@ private Expression ParseTree(
return itParameter;
}

if (readerValue == Key)
{
if (itParameter == null)
{
AddError(readerValue, "current item in iterator is null");
return UndefinedValue();
}

if (TryGetCollectionIteratorProperty(itParameter, nameof(KeyValuePair<int, int>.Key), out var keyExpression))
{
return keyExpression;
}

AddError(readerValue, $"{readerValue} is only supported when iterating over dictionary entries");
return UndefinedValue();
}

if (readerValue == Value)
{
if (itParameter == null)
{
AddError(readerValue, "current item in iterator is null");
return UndefinedValue();
}

if (TryGetCollectionIteratorProperty(itParameter, nameof(KeyValuePair<int, int>.Value), out var valueExpression))
{
return valueExpression;
}

AddError(readerValue, $"{readerValue} is only supported when iterating over dictionary entries");
return UndefinedValue();
}

return Expression.Constant(readerValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// </copyright>

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -44,7 +45,11 @@ public DebuggerExpressionLanguageTests()
HashInt = [1, 2, 3],
Array = ["first", "second"],
CustomArray = [new TestStruct.NestedObject() { NestedString = "Nested" }, new TestStruct.ChildNestedObject() { NestedString = "Nested Child" }],
Dictionary = new Dictionary<string, string> { { "hello", "world" } },
Dictionary = new Dictionary<string, string>
{
{ "hello", "world" },
{ "goodbye", "moon" },
},
IntNumber = 42,
DoubleNumber = 3.14159,
String = "Hello world!",
Expand Down Expand Up @@ -212,6 +217,95 @@ public void ProbeExpressionParser_ValueTypeNull_UsesDefaultValue()
Assert.True(compiled.Errors == null || compiled.Errors.Length == 0);
}

[Theory]
[InlineData("""
{
"any": [
{
"ref": "HashtableLocal"
},
{
"and": [
{
"eq": [
"@key",
"hello"
]
},
{
"eq": [
"@value",
"world"
]
}
]
}
]
}
""")]
[InlineData("""
{
"all": [
{
"ref": "HashtableLocal"
},
{
"ne": [
"@value",
"sun"
]
}
]
}
""")]
[InlineData("""
{
"any": [
{
"filter": [
{
"ref": "HashtableLocal"
},
{
"eq": [
"@key",
"hello"
]
}
]
},
{
"eq": [
"@value",
"world"
]
}
]
}
""")]
public void ProbeExpressionParser_NonGenericDictionaryPredicates_CanUseKeyAndValue(string json)
{
var hashtable = new Hashtable
{
{ "hello", "world" },
{ "goodbye", "moon" },
};

var scopeMembers = CreateScopeMembers();
scopeMembers.AddMember(new ScopeMember("HashtableLocal", typeof(Hashtable), hashtable, ScopeMemberKind.Local));

var compiled = ProbeExpressionParser<bool>.ParseExpression(json, scopeMembers);
var result = compiled.Delegate(
scopeMembers.InvocationTarget,
scopeMembers.Return,
scopeMembers.Duration,
scopeMembers.Exception,
scopeMembers.Members);

Assert.True(result);
Assert.True(compiled.Errors == null || compiled.Errors.Length == 0);
}

private async Task Test(string expressionTestFilePath)
{
// Arrange
Expand Down
Loading
Loading