Skip to content
Closed
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
121 changes: 120 additions & 1 deletion src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@
{
if (!propertyNames.Add(propName!))
{
throw ParseError(exprPos, Res.DuplicateIdentifier, propName);

Check warning on line 1517 in src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

View workflow job for this annotation

GitHub Actions / Windows: Build and Tests

Possible null reference argument for parameter 'args' in 'Exception ExpressionParser.ParseError(int pos, string format, params object[] args)'.
}

properties.Add(new DynamicProperty(propName!, expr.Type));
Expand Down Expand Up @@ -1927,6 +1927,125 @@
return ParseAsLambda(id);
}

// Handle explicit generic type arguments like MethodName<Arg1, Arg2>(...)
if (_textParser.CurrentToken.Id == TokenId.LessThan)
{
// Parse type-argument list
var typeArgTypes = new List<Type>();
_textParser.NextToken(); // move past '<'

while (true)
{
// Build a qualified type name (allow dots and '+' for nested)
if (_textParser.CurrentToken.Id != TokenId.Identifier)
{
throw ParseError(_textParser.CurrentToken.Pos, Res.TypeExpected);
}

var typeName = _textParser.CurrentToken.Text;
_textParser.NextToken();

while (_textParser.CurrentToken.Id == TokenId.Dot || _textParser.CurrentToken.Id == TokenId.Plus)
{
var sep = _textParser.CurrentToken.Text;
_textParser.NextToken();
if (_textParser.CurrentToken.Id != TokenId.Identifier)
{
throw ParseError(Res.IdentifierExpected);
}
typeName += sep + _textParser.CurrentToken.Text;
_textParser.NextToken();
}

// Try resolve type name in the current context (_it, _parent, _root)
if (!_typeFinder.TryFindTypeByName(typeName, new[] { _it, _parent, _root }, true, out var resolvedType))
{
throw ParseError(_textParser.CurrentToken.Pos, Res.TypeNotFound, typeName);
}

typeArgTypes.Add(resolvedType!);

if (_textParser.CurrentToken.Id == TokenId.Comma)
{
_textParser.NextToken();
continue;
}

if (_textParser.CurrentToken.Id == TokenId.GreaterThan)
{
_textParser.NextToken();
break;
}

throw ParseError(_textParser.CurrentToken.Pos, Res.CommaOrGreaterThanExpected);
}

// After parsing generic arguments, expect either a call '(' or member access etc.
if (_textParser.CurrentToken.Id == TokenId.OpenParen)
{
// Parse argument list for the method call
Expression[]? args = ParseArgumentList();

var isStaticAccess = expression == null;
// Find candidate generic method definitions with matching arity
var bindingFlags = isStaticAccess ? BindingFlags.Public | BindingFlags.Static : BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
var candidateDefs = type!.GetMethods(bindingFlags)
.Where(m => m.Name == id && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == typeArgTypes.Count)
.ToArray();

if (candidateDefs.Length == 0)
{
throw ParseError(errorPos, Res.NoApplicableMethod, id, TypeHelper.GetTypeName(type));
}

// Make generic methods with the provided type arguments
var candidateMethods = new List<MethodInfo>();
foreach (var def in candidateDefs)
{
try
{
candidateMethods.Add(def.MakeGenericMethod(typeArgTypes.ToArray()));
}
catch
{
// ignore methods that can't be constructed with the provided type arguments
}
}

if (candidateMethods.Count == 0)
{
throw ParseError(errorPos, Res.NoApplicableMethod, id, TypeHelper.GetTypeName(type));
}

// Filter out methods with pointer parameters
var methodsWithOutPointerArguments = candidateMethods
.Where(m => !m.GetParameters().Any(p => p.ParameterType.GetTypeInfo().IsPointer))
.ToArray();

// Try to select the best candidate based on the provided args
var argsForMatch = args;
switch (_methodFinder.FindBestMethodBasedOnArguments(methodsWithOutPointerArguments.Cast<MethodBase>().ToArray(), ref argsForMatch, out var selectedMethod))
{
case 0:
throw ParseError(errorPos, Res.NoApplicableMethod, id, TypeHelper.GetTypeName(type));

case 1:
var method = (MethodInfo)selectedMethod!;
if (!PredefinedTypesHelper.IsPredefinedType(_parsingConfig, method.DeclaringType!) && !PredefinedMethodsHelper.IsPredefinedMethod(_parsingConfig, method))
{
throw ParseError(errorPos, Res.MethodIsInaccessible, id, TypeHelper.GetTypeName(method.DeclaringType!));
}

return CallMethod(expression, method, argsForMatch);

default:
throw ParseError(errorPos, Res.AmbiguousMethodInvocation, id, TypeHelper.GetTypeName(type));
}
}

// If it's not a call, continue normal handling (e.g. .Member after generic-typed identifier)
}

if (_textParser.CurrentToken.Id == TokenId.OpenParen)
{
Expression[]? args = null;
Expand Down Expand Up @@ -2002,7 +2121,7 @@
var indexerName = isDynamicClass ? DynamicClass.IndexerName : "Item";

// Try to get the indexer property "Item" or "DynamicClass_Indexer" which takes a string as parameter
var indexerMethod = expression?.Type.GetMethod($"get_{indexerName}", [typeof(string)]);
var indexerMethod = expression?.Type.GetMethod($"get_{indexerName}", new[] { typeof(string) });
if (indexerMethod != null)
{
if (!isDynamicClass)
Expand Down
2 changes: 2 additions & 0 deletions src/System.Linq.Dynamic.Core/Res.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,6 @@ internal static class Res
public const string UnknownIdentifier = "Unknown identifier '{0}'";
public const string UnknownPropertyOrField = "No property or field '{0}' exists in type '{1}'";
public const string UnterminatedStringLiteral = "Unterminated string literal";
public const string TypeExpected = "Type expected in generic definition";
public const string CommaOrGreaterThanExpected = "',' or '>' expected in generic definition";
}
Loading