diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 4a6319aa..20887f61 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1927,6 +1927,125 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? return ParseAsLambda(id); } + // Handle explicit generic type arguments like MethodName(...) + if (_textParser.CurrentToken.Id == TokenId.LessThan) + { + // Parse type-argument list + var typeArgTypes = new List(); + _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(); + 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().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; @@ -2002,7 +2121,7 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? 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) diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 3d423495..4c5ab951 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -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"; } \ No newline at end of file