mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
* Chip and ChipList components added Code improved demos improved Update Chip styles Exctract mixins for button, badge and chip styles Update Chip demos Update Chip styles in themes Fix chip styles in dropdowns Fix icon position in different chip sizes Fix chip border-radius Optimize styles Update Chip icon Update premium themes Chip/ChipList keyboard navigation fixed Invalid code fixed Update Chip focus styles * Gantt added code improved demo update with more data more demo improvements more improvements rendering updated Gantt Splitter ChangeStateOnResize set to false Gantt connecting lines added Tooltips added FIltering demo added Gantt editing demos added demo updated scrollbar improved Gantt ExpandRows() added thead height made equal material fixed filtering improved Subtitle2 removed Gantt drag & resize added Critical Path, Dependency Types and Baselines added Today Line, Non-Working Days, TaskRender Callback, Vertical Date Markers and Custom Task Bar Template added Zoom to fit and multiline headers added Rendering optimized Grouped header should be static Invalidate row count on expand/collapse and filter progress, critical path, red vertical line and baseline styling improved frozen columns in demos should have Width set alignment improved Auto scroll to events when switching between views Dependency Data added * Move scheduler month view "more" button to slot header The "+X more" button was absolutely positioned inside the events area, occupying space that could hold an additional appointment. Move it inline into the slot title header next to the day number so it no longer consumes an event slot. - Fix operator precedence bug in MaxAppointmentsInSlot auto-calculation - Style the button end-aligned in the header using flex + margin-inline-start: auto - Add demo appointments to trigger the "more" button in the default demo * Use ResizeObserver to measure month view content height Replace the hardcoded (Height - 60) / 6 calculation with actual measurements from a ResizeObserver on the view content element. This fixes appointment overflow at non-default scheduler heights (e.g. 500px) where the 60px header estimate was inaccurate (~91px actual). The MonthView now uses the existing createResizable JS pattern to observe its own content area and dynamically computes the correct MaxAppointmentsInSlot from the measured height and actual week count. * Replace createScheduler with createResizable for ResizeObserver support The scheduler-specific JS functions only used window resize events. Switch to the shared createResizable/destroyResizable pattern which uses ResizeObserver with a window resize fallback, consistent with charts and gauges. * Replace DocFX with Blazor class library for API reference generation Add Radzen.Blazor.Api and Radzen.Blazor.Api.Generator projects that generate .razor pages from Radzen.Blazor assembly metadata at build time. Pages are generated conditionally (Release builds or -p:GenerateApiPages=true). The API assembly is lazy-loaded to avoid initial payload penalty. Old .html URLs are preserved via 301 redirects in CanonicalRedirectMiddleware. * Improve API reference navigation and member anchors - Add anchor links to member headings via RadzenText Anchor parameter - Fix inherited member links to navigate to the specific member, not just the declaring type - Add nav panel that expands/selects the current type on navigation - Fix RadzenBody to scroll to fragment target instead of resetting to top * Stop RadzenToc from updating URL hash on scroll and fix scroll container - Remove navigateTo call from scroll handler so URL hash is not overwritten - Set TOC Selector to .rz-body so it monitors the correct scrollable container * Align API layout header with MainLayout * Refactor RadzenToc scroll listener disposal and item link updates. Return a disposable JS listener handle instead of storing expando handlers on elements, and move TOC item href refresh to NavigationManager location-change handling so links stay in sync with path/query changes. * Add v10 RadzenToc changelog notes. Document the RadzenToc breaking changes around link rendering, aria-selected semantics, and address bar behavior during scrolling, plus the high-level scroll tracking update. * Fix RadzenToc NullReferenceException during concurrent disposal and API reference redirect loop. Capture scrollListenerRef in a local variable before disposing to prevent a race condition when multiple RadzenTocItem components dispose concurrently. Skip scroll listener re-registration when the parent RadzenToc is already disposed. Remove UseDefaultFiles middleware and fix API Reference link to prevent redirect loops. * Fix RadzenToc scrollspy to match Bootstrap ScrollSpy behavior. Remove fallback that always highlighted the first item on load. Require actual scroll (scrollY > 0) before activating the at-bottom branch so short pages don't highlight the last item. Add click handler to RadzenTocItem so clicking an item activates it immediately (matching the DocFX affix click behavior). * Fix misaligned code examples in generated API documentation XML doc comment code blocks retained source-level indentation after parsing, causing excessive leading whitespace in rendered examples. Replace simple .Trim() with indentation normalization that strips the common leading whitespace prefix from all lines. * Update ApiLayout and MainTopNav * Fixed .Server project Cannot provide a value for property 'AssemblyLoader' on type 'RadzenBlazorDemos.Routes'. * Scatter and Bubble series added to Chart * LinearGauge added * Popup cut off on rightmost columns and flickers Fix #2414, #2181 * CheckBoxList filter options update without clicking Apply Fix #2155 * tests updated * DataGrid Reload() lags one render behind Fix #2214 * Gaps in Data for Charts Fix #765 * DataGrid Fixed Column resizing will show visual white space between fixed columns Fix #2228 * Setting Parameter on RadzenDataGridColumn<T> does not always trigger render of content Fix #2215 * DataGrid column resizing does not work on mobile and tablet devices Fix #2168 * RadzenAccordion invalid ARIA aatributes Fix #1931 * DropDownDataGrid search text clear button added Fix #1555 * Caruosel AnimationDuration added Fix #2438 * Tooltip ARIA attributes improved Fix #2314 * Long tooltip does not wrap Fix #2320 * Clicking into Padding of PanelMenuItem does navigate but not expand Item Fix #2244 * RadzenDatePicker FocusedDate synced with current date Fix #1991 * Dialog OnSideClose gets called when opening and closing Fix #1976 * RadzenNumeric RemoveNonNumericCharacters improved Fix #1937 * Optimize CSS rules and SCSS mixins * Fix percentage values in SCSS * Update premium themes * Line Chart markers cannot be dynamically turned off using @if Fix #1229 * DropDownDataGrid exception when changing data object Fix #1941 * RadzenChat new messages button added Fix #2375 * RadzenHtmlEditor Immediate property added Fix #2279 * Radzen Popup shows wrongly in a RTL Fix #2271 * RadzenListBox/RadzenPickList EmptyTemplate and EmptyText added Fix #2018 * DataGrid footer and header is being incorrectly rendered for frozen columns Fix #1402 * PropertyAccess.GetValue() fixed to work with sub interfaces Fix #1249 * RadzenPickList AllowVirtualization added Fix #2456 * DataGrid add new row issue with LoadData method Fix #365 * Various components keyboard navigation not working as described in the demos Fix #2446 * RadzenDataGrid Keyboard Selection jumps when AllowVirtualization is enabled Fix #2439 * RadzenPanelMenu MenuItemDisplayStyle.IconAndTextStacked added Fix #2368 * RadzenPickList SelectedSourceChanged/SelectedTargetChanged added Fix #2324 * RadzenTabs AllowReorder property added Fix #2318 * RadzenAccordion ExpandAll()/CollapseAll() added Fix #2206 * RadzenDropZoneContainer Start/End Drag events added Fix #2158 * RadzenDataGrid doesn't appear to do colspan/rowspan properly Fix #1064 * Update Gantt styles * Update Gantt styles and premium themes * Update Gantt styles * Update Gantt demos * Update Linear Gauge demos * Update component icons * tests fixed --------- Co-authored-by: Atanas Korchev <akorchev@gmail.com> Co-authored-by: yordanov <vasil@yordanov.info>
1152 lines
38 KiB
C#
1152 lines
38 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Linq;
|
|
|
|
namespace Radzen;
|
|
|
|
#nullable enable
|
|
|
|
/// <summary>
|
|
/// Parse lambda expressions from strings.
|
|
/// </summary>
|
|
public class ExpressionParser
|
|
{
|
|
/// <summary>
|
|
/// Parses a lambda expression that returns a boolean value.
|
|
/// </summary>
|
|
public static Expression<Func<T, bool>> ParsePredicate<T>(string expression, Func<string, Type?>? typeResolver = null)
|
|
{
|
|
return ParseLambda<T, bool>(expression, typeResolver);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a lambda expression that returns a typed result.
|
|
/// </summary>
|
|
public static Expression<Func<T, TResult>> ParseLambda<T, TResult>(string expression, Func<string, Type?>? typeResolver = null)
|
|
{
|
|
var lambda = ParseLambda<T>(expression, typeResolver);
|
|
|
|
return Expression.Lambda<Func<T, TResult>>(lambda.Body, lambda.Parameters[0]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a lambda expression that returns untyped result.
|
|
/// </summary>
|
|
public static LambdaExpression ParseLambda<T>(string expression, Func<string, Type?>? typeLocator = null)
|
|
{
|
|
return ParseLambda(expression, typeof(T), typeLocator);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a lambda expression that returns untyped result.
|
|
/// </summary>
|
|
public static LambdaExpression ParseLambda(string expression, Type type, Func<string, Type?>? typeResolver = null)
|
|
{
|
|
var parser = new ExpressionParser(expression, typeResolver);
|
|
|
|
return parser.ParseLambda(type);
|
|
}
|
|
|
|
private readonly List<Token> tokens;
|
|
private int position;
|
|
private readonly Func<string, Type?>? typeResolver;
|
|
private readonly Stack<ParameterExpression> parameterStack = new();
|
|
|
|
private ExpressionParser(string expression, Func<string, Type?>? typeResolver = null)
|
|
{
|
|
this.typeResolver = typeResolver;
|
|
tokens = ExpressionLexer.Scan(expression);
|
|
}
|
|
|
|
Token Expect(TokenType type)
|
|
{
|
|
if (position >= tokens.Count)
|
|
{
|
|
throw new InvalidOperationException($"Unexpected end of expression. Expected token: {type}");
|
|
}
|
|
|
|
var token = tokens[position];
|
|
|
|
if (token.Type != type)
|
|
{
|
|
throw new InvalidOperationException($"Unexpected token: {token.Type}. Expected: {type}");
|
|
}
|
|
|
|
position++;
|
|
|
|
return token;
|
|
}
|
|
|
|
void Advance(int count)
|
|
{
|
|
position += count;
|
|
}
|
|
|
|
Token Peek(int offset = 0)
|
|
{
|
|
if (position + offset >= tokens.Count)
|
|
{
|
|
return new Token(TokenType.None, string.Empty);
|
|
}
|
|
|
|
return tokens[position + offset];
|
|
}
|
|
|
|
private LambdaExpression ParseLambda(Type paramType)
|
|
{
|
|
var parameterIdentifier = Expect(TokenType.Identifier);
|
|
|
|
var parameter = Expression.Parameter(paramType, parameterIdentifier.Value);
|
|
|
|
parameterStack.Push(parameter);
|
|
|
|
Expect(TokenType.EqualsGreaterThan);
|
|
|
|
var body = ParseExpression(parameter);
|
|
|
|
parameterStack.Pop();
|
|
|
|
return Expression.Lambda(body, parameter);
|
|
}
|
|
|
|
private Expression ParseExpression(ParameterExpression parameter)
|
|
{
|
|
var left = ParseBinary(parameter);
|
|
var token = Peek();
|
|
|
|
if (token.Type is TokenType.AmpersandAmpersand)
|
|
{
|
|
Advance(1);
|
|
|
|
var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
|
|
left = Expression.AndAlso(left, right);
|
|
}
|
|
else if (token.Type is TokenType.BarBar)
|
|
{
|
|
Advance(1);
|
|
|
|
var right = ParseExpression(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
|
|
left = Expression.OrElse(left, right);
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseBinary(ParameterExpression parameter)
|
|
{
|
|
var left = ParseNullCoalescing(parameter);
|
|
var token = Peek();
|
|
|
|
if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual)
|
|
{
|
|
Advance(1);
|
|
var right = ParseBinary(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
AlignBinaryOperandTypes(ref left, ref right);
|
|
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseNullCoalescing(ParameterExpression parameter)
|
|
{
|
|
var left = ParseTernary(parameter);
|
|
var token = Peek();
|
|
|
|
while (token.Type == TokenType.QuestionMarkQuestionMark)
|
|
{
|
|
Advance(1);
|
|
|
|
var right = ParseTernary(parameter) ?? throw new InvalidOperationException($"Expected expression after ?? at position {position}");
|
|
|
|
if (right.Type == typeof(object))
|
|
{
|
|
right = ConvertIfNeeded(right, left.Type);
|
|
}
|
|
|
|
left = Expression.Coalesce(left, right);
|
|
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseTernary(ParameterExpression parameter)
|
|
{
|
|
var condition = ParseOr(parameter);
|
|
|
|
if (Peek().Type == TokenType.QuestionMark)
|
|
{
|
|
Advance(1);
|
|
|
|
var trueExpression = ParseOr(parameter);
|
|
|
|
Expect(TokenType.Colon);
|
|
|
|
var falseExpression = ParseOr(parameter);
|
|
|
|
if (trueExpression is ConstantExpression trueConst && trueConst.Value == null && falseExpression is not ConstantExpression)
|
|
{
|
|
trueExpression = Expression.Constant(null, falseExpression.Type);
|
|
}
|
|
else if (falseExpression is ConstantExpression falseConst && falseConst.Value == null && trueExpression is not ConstantExpression)
|
|
{
|
|
falseExpression = Expression.Constant(null, trueExpression.Type);
|
|
}
|
|
|
|
var ternary = Expression.Condition(condition, trueExpression, falseExpression);
|
|
|
|
return ParseMemberAccess(ternary, parameter);
|
|
}
|
|
|
|
return ParseMemberAccess(condition, parameter);
|
|
}
|
|
|
|
private Expression ParseMemberAccess(Expression expression, ParameterExpression parameter)
|
|
{
|
|
var token = Peek();
|
|
while (token.Type is TokenType.Dot or TokenType.QuestionDot or TokenType.OpenBracket)
|
|
{
|
|
if (token.Type == TokenType.Dot)
|
|
{
|
|
Advance(1);
|
|
token = Expect(TokenType.Identifier);
|
|
if (Peek().Type == TokenType.OpenParen)
|
|
{
|
|
expression = ParseInvocation(expression, token.Value, parameter);
|
|
}
|
|
else
|
|
{
|
|
expression = Expression.PropertyOrField(expression, token.Value);
|
|
}
|
|
}
|
|
else if (token.Type == TokenType.QuestionDot)
|
|
{
|
|
Advance(1);
|
|
token = Expect(TokenType.Identifier);
|
|
|
|
var check = Expression.Equal(expression, Expression.Constant(null));
|
|
|
|
if (Peek().Type == TokenType.OpenParen)
|
|
{
|
|
var call = ParseInvocation(expression, token.Value, parameter);
|
|
expression = Expression.Condition(check, Expression.Constant(null, call.Type), call);
|
|
}
|
|
else
|
|
{
|
|
var access = Expression.PropertyOrField(expression, token.Value);
|
|
|
|
expression = Expression.Condition(check, Expression.Default(access.Type), access);
|
|
|
|
var nextToken = Peek();
|
|
|
|
if (nextToken.Type == TokenType.Dot || nextToken.Type == TokenType.QuestionDot)
|
|
{
|
|
var nextAccess = ParseMemberAccess(access, parameter);
|
|
|
|
expression = Expression.Condition(check, Expression.Default(nextAccess.Type), nextAccess);
|
|
}
|
|
}
|
|
}
|
|
else if (token.Type == TokenType.OpenBracket)
|
|
{
|
|
Advance(1);
|
|
var index = ParseExpression(parameter);
|
|
Expect(TokenType.CloseBracket);
|
|
|
|
if (expression.Type.IsArray)
|
|
{
|
|
expression = Expression.ArrayIndex(expression, index);
|
|
}
|
|
else
|
|
{
|
|
var indexer = expression.Type.GetProperty("Item") ?? throw new InvalidOperationException($"Type {expression.Type} does not have an indexer property");
|
|
|
|
expression = Expression.Property(expression, indexer, index);
|
|
}
|
|
}
|
|
|
|
token = Peek();
|
|
}
|
|
|
|
return expression;
|
|
}
|
|
|
|
private MethodCallExpression ParseInvocation(Expression expression, string methodName, ParameterExpression parameter)
|
|
{
|
|
Advance(1);
|
|
|
|
var arguments = new List<Expression>();
|
|
|
|
if (Peek().Type != TokenType.CloseParen)
|
|
{
|
|
while (Peek().Type != TokenType.CloseParen)
|
|
{
|
|
var token = Peek();
|
|
|
|
if (token.Type == TokenType.Identifier && Peek(1).Type == TokenType.EqualsGreaterThan)
|
|
{
|
|
var lambdaParameterName = token.Value;
|
|
|
|
Advance(2);
|
|
|
|
Type? lambdaParameterType = null;
|
|
|
|
var extensionMethod = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static)
|
|
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == 2);
|
|
|
|
if (extensionMethod != null)
|
|
{
|
|
lambdaParameterType = GetItemType(expression.Type);
|
|
}
|
|
|
|
if (lambdaParameterType == null)
|
|
{
|
|
throw new InvalidOperationException($"Could not infer type for lambda parameter {lambdaParameterName}");
|
|
}
|
|
|
|
var lambdaParameter = Expression.Parameter(lambdaParameterType, lambdaParameterName);
|
|
parameterStack.Push(lambdaParameter);
|
|
var lambdaBody = ParseExpression(lambdaParameter);
|
|
parameterStack.Pop();
|
|
arguments.Add(Expression.Lambda(lambdaBody, lambdaParameter));
|
|
}
|
|
else
|
|
{
|
|
arguments.Add(ParseExpression(parameter));
|
|
}
|
|
|
|
if (Peek().Type == TokenType.Comma)
|
|
{
|
|
Advance(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
Expect(TokenType.CloseParen);
|
|
|
|
var argumentTypes = arguments.Select(a => a.Type).ToArray();
|
|
|
|
var method = expression.Type.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance, null, argumentTypes, null);
|
|
|
|
if (method != null)
|
|
{
|
|
return Expression.Call(expression, method, arguments);
|
|
}
|
|
|
|
method = typeof(Enumerable).GetMethods(BindingFlags.Public | BindingFlags.Static)
|
|
.FirstOrDefault(m => m.Name == methodName && m.GetParameters().Length == arguments.Count + 1);
|
|
|
|
if (method != null)
|
|
{
|
|
var argumentType = GetItemType(expression.Type);
|
|
|
|
if (argumentType == null)
|
|
{
|
|
throw new InvalidOperationException($"Cannot determine item type for {expression.Type}");
|
|
}
|
|
|
|
if (method.IsGenericMethodDefinition)
|
|
{
|
|
method = method.MakeGenericMethod(argumentType);
|
|
}
|
|
|
|
var parameters = method.GetParameters();
|
|
|
|
var argumentsWithInstance = new[] { expression }.Concat(arguments).ToArray();
|
|
|
|
return Expression.Call(method, argumentsWithInstance.Select((a, index) => ConvertIfNeeded(a, parameters[index].ParameterType)));
|
|
}
|
|
|
|
throw new InvalidOperationException($"No suitable method '{methodName}' found for type '{expression.Type}'");
|
|
}
|
|
|
|
private static Type? GetItemType(Type enumerableOrArray)
|
|
{
|
|
return enumerableOrArray.IsArray ? enumerableOrArray.GetElementType() : enumerableOrArray.GetGenericArguments()[0];
|
|
}
|
|
|
|
private Expression? ParseTerm(ParameterExpression parameter)
|
|
{
|
|
var token = Peek();
|
|
|
|
if (token.Type == TokenType.None)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (token.Type == TokenType.OpenParen)
|
|
{
|
|
Advance(1);
|
|
|
|
if (TryParseCastExpression(parameter, out var expression))
|
|
{
|
|
return expression;
|
|
}
|
|
|
|
expression = ParseExpression(parameter);
|
|
|
|
Expect(TokenType.CloseParen);
|
|
|
|
return expression;
|
|
}
|
|
|
|
if (token.Type == TokenType.Identifier)
|
|
{
|
|
if (token.Value == "default" && Peek(1).Type == TokenType.OpenParen)
|
|
{
|
|
Advance(2);
|
|
|
|
if (!TryParseTypeReference(out var defaultType))
|
|
{
|
|
throw new InvalidOperationException($"Could not resolve type in default expression at position {position}");
|
|
}
|
|
|
|
Expect(TokenType.CloseParen);
|
|
|
|
return Expression.Default(defaultType);
|
|
}
|
|
|
|
var matchingParameter = parameterStack.FirstOrDefault(p => p.Name == token.Value);
|
|
if (matchingParameter != null)
|
|
{
|
|
Advance(1);
|
|
return ParseMemberAccess(matchingParameter, parameter);
|
|
}
|
|
|
|
var type = GetWellKnownType(token.Value);
|
|
|
|
if (type != null)
|
|
{
|
|
Advance(1);
|
|
return ParseStaticMemberAccess(type, parameter);
|
|
}
|
|
|
|
if (TryParseQualifiedType(out var qualifiedType))
|
|
{
|
|
return ParseStaticMemberAccess(qualifiedType, parameter);
|
|
}
|
|
|
|
if (Peek(1).Type == TokenType.OpenParen)
|
|
{
|
|
Advance(1);
|
|
return ParseInvocation(parameter, token.Value, parameter);
|
|
}
|
|
|
|
throw new InvalidOperationException($"Unexpected identifier: {token.Value}");
|
|
}
|
|
|
|
if (token.Type == TokenType.ExclamationMark)
|
|
{
|
|
Advance(1);
|
|
|
|
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after ! at position {position}");
|
|
|
|
operand = ConvertIfNeeded(operand, typeof(bool));
|
|
|
|
return Expression.Not(operand);
|
|
}
|
|
|
|
if (token.Type == TokenType.Minus)
|
|
{
|
|
Advance(1);
|
|
|
|
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after - at position {position}");
|
|
|
|
return Expression.Negate(operand);
|
|
}
|
|
|
|
if (token.Type == TokenType.Plus)
|
|
{
|
|
Advance(1);
|
|
|
|
var operand = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after + at position {position}");
|
|
|
|
return operand;
|
|
}
|
|
|
|
switch (token.Type)
|
|
{
|
|
case TokenType.CharacterLiteral:
|
|
case TokenType.StringLiteral:
|
|
case TokenType.NullLiteral:
|
|
case TokenType.NumericLiteral:
|
|
case TokenType.TrueLiteral:
|
|
case TokenType.FalseLiteral:
|
|
Advance(1);
|
|
return token.ToConstantExpression();
|
|
case TokenType.New:
|
|
Advance(1);
|
|
|
|
token = Peek();
|
|
|
|
if (token.Type == TokenType.OpenBrace)
|
|
{
|
|
Advance(1);
|
|
|
|
var properties = new List<(string Name, Expression Expression)>();
|
|
|
|
if (Peek().Type != TokenType.CloseBrace)
|
|
{
|
|
do
|
|
{
|
|
token = Peek();
|
|
string propertyName;
|
|
Expression propertyExpression;
|
|
|
|
if (token.Type == TokenType.Identifier)
|
|
{
|
|
propertyName = token.Value;
|
|
Advance(1);
|
|
if (Peek().Type == TokenType.Dot || Peek().Type == TokenType.QuestionDot)
|
|
{
|
|
// Handle nested property access
|
|
Expression expr = propertyName == parameter.Name ? (Expression)parameter : Expression.Property(parameter, propertyName);
|
|
propertyExpression = ParseMemberAccess(expr, parameter);
|
|
|
|
// Get the last identifier token's value
|
|
var lastToken = tokens[position - 1];
|
|
if (lastToken.Type == TokenType.Identifier)
|
|
{
|
|
propertyName = lastToken.Value;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Expect(TokenType.Equals);
|
|
propertyExpression = ParseExpression(parameter);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
propertyExpression = ParseExpression(parameter);
|
|
|
|
if (propertyExpression is MemberExpression memberExpression)
|
|
{
|
|
propertyName = memberExpression.Member.Name;
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException($"Invalid anonymous type member expression at position {position}");
|
|
}
|
|
}
|
|
|
|
properties.Add((propertyName, propertyExpression));
|
|
|
|
if (Peek().Type == TokenType.Comma)
|
|
{
|
|
Advance(1);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
} while (Peek().Type != TokenType.CloseBrace);
|
|
}
|
|
|
|
Expect(TokenType.CloseBrace);
|
|
|
|
var propertyTypes = properties.Select(p => p.Expression.Type).ToArray();
|
|
var propertyNames = properties.Select(p => p.Name).ToArray();
|
|
var dynamicType = DynamicTypeFactory.CreateType(parameter.Type.Name, propertyNames, propertyTypes);
|
|
var bindings = properties.Select(p => Expression.Bind(dynamicType.GetProperty(p.Name)!, p.Expression));
|
|
return Expression.MemberInit(Expression.New(dynamicType), bindings);
|
|
}
|
|
else
|
|
{
|
|
Type? elementType = null;
|
|
|
|
if (TryParseQualifiedArrayType(out var parsedElementType))
|
|
{
|
|
elementType = parsedElementType;
|
|
}
|
|
|
|
Expect(TokenType.OpenBracket);
|
|
Expect(TokenType.CloseBracket);
|
|
Expect(TokenType.OpenBrace);
|
|
|
|
var elements = new List<Expression>();
|
|
if (Peek().Type != TokenType.CloseBrace)
|
|
{
|
|
do
|
|
{
|
|
elements.Add(ParseExpression(parameter));
|
|
if (Peek().Type == TokenType.Comma)
|
|
{
|
|
Advance(1);
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
} while (Peek().Type != TokenType.CloseBrace);
|
|
}
|
|
|
|
Expect(TokenType.CloseBrace);
|
|
|
|
if (elementType == null)
|
|
{
|
|
elementType = elements.Count > 0 ? elements[0].Type : typeof(object);
|
|
}
|
|
|
|
return Expression.NewArrayInit(elementType, elements.Select(e => ConvertIfNeeded(e, elementType)));
|
|
}
|
|
default:
|
|
throw new InvalidOperationException($"Unexpected token: {token.Type} at position {position}");
|
|
}
|
|
}
|
|
|
|
private bool TryParseCastExpression(ParameterExpression parameter, out Expression expression)
|
|
{
|
|
expression = null!;
|
|
|
|
var token = Peek();
|
|
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var typeName = new StringBuilder(token.Value);
|
|
var index = position + 1;
|
|
var typeCast = true;
|
|
var nullable = false;
|
|
|
|
while (index < tokens.Count)
|
|
{
|
|
token = tokens[index];
|
|
|
|
if (token.Type == TokenType.Dot)
|
|
{
|
|
index++;
|
|
if (index >= tokens.Count || tokens[index].Type != TokenType.Identifier)
|
|
{
|
|
typeCast = false;
|
|
break;
|
|
}
|
|
typeName.Append('.').Append(tokens[index].Value);
|
|
index++;
|
|
}
|
|
else if (token.Type == TokenType.QuestionMark)
|
|
{
|
|
nullable = true;
|
|
index++;
|
|
if (index >= tokens.Count || tokens[index].Type != TokenType.CloseParen)
|
|
{
|
|
typeCast = false;
|
|
break;
|
|
}
|
|
}
|
|
else if (token.Type == TokenType.CloseParen)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
typeCast = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (typeCast && index < tokens.Count && tokens[index].Type == TokenType.CloseParen)
|
|
{
|
|
var name = typeName.ToString();
|
|
|
|
var type = ResolveType(name) ?? throw new InvalidOperationException($"Could not resolve type: {typeName}");
|
|
|
|
if (nullable && type.IsValueType)
|
|
{
|
|
type = typeof(Nullable<>).MakeGenericType(type);
|
|
}
|
|
|
|
position = index;
|
|
|
|
Advance(1);
|
|
|
|
if (Peek().Type == TokenType.OpenParen && TryParseCastExpression(parameter, out var innerExpression))
|
|
{
|
|
expression = Expression.Convert(innerExpression, type);
|
|
}
|
|
else
|
|
{
|
|
var source = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression to cast at position {position}");
|
|
expression = Expression.Convert(source, type);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private Expression ParseStaticMemberAccess(Type type, ParameterExpression parameter)
|
|
{
|
|
Expect(TokenType.Dot);
|
|
|
|
var token = Expect(TokenType.Identifier);
|
|
|
|
if (Peek().Type == TokenType.OpenParen)
|
|
{
|
|
return ParseStaticInvocation(type, token.Value, parameter);
|
|
}
|
|
else
|
|
{
|
|
var member = (MemberInfo?)type.GetProperty(token.Value) ?? type.GetField(token.Value);
|
|
|
|
if (member == null)
|
|
{
|
|
throw new InvalidOperationException($"Member {token.Value} not found on type {type.Name}");
|
|
}
|
|
|
|
return Expression.MakeMemberAccess(null, member);
|
|
}
|
|
|
|
throw new InvalidOperationException($"Expected method invocation after {token.Value} at position {position}");
|
|
}
|
|
|
|
private Expression ParseStaticInvocation(Type type, string methodName, ParameterExpression parameter)
|
|
{
|
|
Advance(1);
|
|
|
|
var arguments = new List<Expression>();
|
|
|
|
if (Peek().Type != TokenType.CloseParen)
|
|
{
|
|
arguments.Add(ParseExpression(parameter));
|
|
|
|
while (Peek().Type == TokenType.Comma)
|
|
{
|
|
Advance(1);
|
|
arguments.Add(ParseExpression(parameter));
|
|
}
|
|
}
|
|
|
|
Expect(TokenType.CloseParen);
|
|
|
|
var method = type.GetMethod(methodName, [.. arguments.Select(a => a.Type)]) ?? throw new InvalidOperationException($"Method {methodName} not found on type {type.Name}");
|
|
|
|
return Expression.Call(null, method, arguments);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to parse a type reference (e.g. bool, int, System.DateTime, System.DateTime?) and returns the resolved type.
|
|
/// Advances position past the type name. Does not backtrack — expects the full token sequence to be a type.
|
|
/// Used for contexts like default(T) where the entire content is a type name.
|
|
/// </summary>
|
|
private bool TryParseTypeReference(out Type type)
|
|
{
|
|
type = null!;
|
|
|
|
var token = Peek();
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var startPosition = position;
|
|
var parts = new List<string> { token.Value };
|
|
Advance(1);
|
|
|
|
while (Peek().Type == TokenType.Dot)
|
|
{
|
|
Advance(1);
|
|
token = Peek();
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
position = startPosition;
|
|
return false;
|
|
}
|
|
parts.Add(token.Value);
|
|
Advance(1);
|
|
}
|
|
|
|
var nullable = false;
|
|
if (Peek().Type == TokenType.QuestionMark)
|
|
{
|
|
nullable = true;
|
|
Advance(1);
|
|
}
|
|
|
|
var typeName = string.Join(".", parts);
|
|
var resolvedType = ResolveType(typeName);
|
|
if (resolvedType != null)
|
|
{
|
|
if (nullable && resolvedType.IsValueType)
|
|
{
|
|
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
|
|
}
|
|
|
|
type = resolvedType;
|
|
return true;
|
|
}
|
|
|
|
position = startPosition;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to parse a qualified type name (e.g. System.DateTime or System.DateTime?) and returns the resolved type.
|
|
/// Advances position past the type name. The next token will be . for member access or [ for array.
|
|
/// Uses backtracking to find the longest resolvable type prefix (e.g. System.DateTime in System.DateTime.SpecifyKind).
|
|
/// </summary>
|
|
private bool TryParseQualifiedType(out Type type)
|
|
{
|
|
type = null!;
|
|
|
|
var token = Peek();
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var startPosition = position;
|
|
var parts = new List<string> { token.Value };
|
|
Advance(1);
|
|
|
|
while (Peek().Type == TokenType.Dot)
|
|
{
|
|
Advance(1);
|
|
token = Peek();
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
return false;
|
|
}
|
|
parts.Add(token.Value);
|
|
Advance(1);
|
|
}
|
|
|
|
var nullable = false;
|
|
if (Peek().Type == TokenType.QuestionMark)
|
|
{
|
|
nullable = true;
|
|
Advance(1);
|
|
}
|
|
|
|
for (var i = parts.Count; i >= 1; i--)
|
|
{
|
|
var typeName = string.Join(".", parts.Take(i));
|
|
var resolvedType = ResolveType(typeName);
|
|
if (resolvedType != null)
|
|
{
|
|
if (nullable && resolvedType.IsValueType)
|
|
{
|
|
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
|
|
}
|
|
|
|
position = startPosition;
|
|
var tokensToConsume = (i * 2) - 1 + (nullable ? 1 : 0);
|
|
for (var t = 0; t < tokensToConsume; t++)
|
|
{
|
|
Advance(1);
|
|
}
|
|
|
|
type = resolvedType;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
position = startPosition;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to parse a qualified array element type (e.g. System.DateTime? or DateTime) before [].
|
|
/// Advances position past the type name to the [. Returns the element type for the array.
|
|
/// </summary>
|
|
private bool TryParseQualifiedArrayType(out Type? elementType)
|
|
{
|
|
elementType = null;
|
|
|
|
var token = Peek();
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var startPosition = position;
|
|
var parts = new List<string> { token.Value };
|
|
Advance(1);
|
|
|
|
while (Peek().Type == TokenType.Dot)
|
|
{
|
|
Advance(1);
|
|
token = Peek();
|
|
if (token.Type != TokenType.Identifier)
|
|
{
|
|
position = startPosition;
|
|
return false;
|
|
}
|
|
parts.Add(token.Value);
|
|
Advance(1);
|
|
}
|
|
|
|
var nullable = false;
|
|
if (Peek().Type == TokenType.QuestionMark)
|
|
{
|
|
nullable = true;
|
|
Advance(1);
|
|
}
|
|
|
|
if (Peek().Type != TokenType.OpenBracket)
|
|
{
|
|
position = startPosition;
|
|
return false;
|
|
}
|
|
|
|
for (var i = parts.Count; i >= 1; i--)
|
|
{
|
|
var typeName = string.Join(".", parts.Take(i));
|
|
var resolvedType = ResolveType(typeName);
|
|
if (resolvedType != null)
|
|
{
|
|
if (nullable && resolvedType.IsValueType)
|
|
{
|
|
resolvedType = typeof(Nullable<>).MakeGenericType(resolvedType);
|
|
}
|
|
elementType = resolvedType;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
position = startPosition;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolves a type name using well-known types, the optional type resolver, or by searching loaded assemblies.
|
|
/// This allows any type from loaded assemblies to be resolved without hardcoding.
|
|
/// </summary>
|
|
private Type? ResolveType(string typeName)
|
|
{
|
|
return GetWellKnownType(typeName)
|
|
?? typeResolver?.Invoke(typeName)
|
|
?? ResolveTypeFromAssemblies(typeName);
|
|
}
|
|
|
|
private static Type? ResolveTypeFromAssemblies(string typeName)
|
|
{
|
|
return AppDomain.CurrentDomain.GetAssemblies()
|
|
.SelectMany(a => a.GetTypes())
|
|
.FirstOrDefault(t =>
|
|
{
|
|
var fullName = t.FullName;
|
|
return fullName != null && fullName.Replace("+", ".", StringComparison.Ordinal) == typeName;
|
|
});
|
|
}
|
|
|
|
private static Type? GetWellKnownType(string typeName)
|
|
{
|
|
return typeName switch
|
|
{
|
|
nameof(DateTime) => typeof(DateTime),
|
|
nameof(DateOnly) => typeof(DateOnly),
|
|
nameof(TimeOnly) => typeof(TimeOnly),
|
|
nameof(DateTimeOffset) => typeof(DateTimeOffset),
|
|
nameof(Guid) => typeof(Guid),
|
|
nameof(CultureInfo) => typeof(CultureInfo),
|
|
nameof(DateTimeStyles) => typeof(DateTimeStyles),
|
|
nameof(DateTimeKind) => typeof(DateTimeKind),
|
|
nameof(Double) or "double" => typeof(double),
|
|
nameof(Single) or "float" => typeof(float),
|
|
nameof(Int32) or "int" => typeof(int),
|
|
nameof(Int64) or "long" => typeof(long),
|
|
nameof(Int16) or "short" => typeof(short),
|
|
nameof(Byte) or "byte" => typeof(byte),
|
|
nameof(SByte) or "sbyte" => typeof(sbyte),
|
|
nameof(UInt32) or "uint" => typeof(uint),
|
|
nameof(UInt64) or "ulong" => typeof(ulong),
|
|
nameof(UInt16) or "ushort" => typeof(ushort),
|
|
nameof(Boolean) or "bool" => typeof(bool),
|
|
nameof(Char) or "char" => typeof(char),
|
|
nameof(Decimal) or "decimal" => typeof(decimal),
|
|
nameof(String) or "string" => typeof(string),
|
|
nameof(Math) => typeof(Math),
|
|
nameof(Convert) => typeof(Convert),
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private Expression ParseOr(ParameterExpression parameter)
|
|
{
|
|
var left = ParseMemberAccess(ParseAnd(parameter), parameter);
|
|
|
|
var token = Peek();
|
|
while (token.Type == TokenType.BarBar)
|
|
{
|
|
Advance(1);
|
|
var right = ParseMemberAccess(ParseAnd(parameter) ?? throw new InvalidOperationException($"Expected expression after || at position {position}"), parameter);
|
|
left = Expression.OrElse(left, right);
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseAnd(ParameterExpression parameter)
|
|
{
|
|
var left = ParseMemberAccess(ParseComparison(parameter), parameter);
|
|
|
|
var token = Peek();
|
|
while (token.Type == TokenType.AmpersandAmpersand)
|
|
{
|
|
Advance(1);
|
|
var right = ParseMemberAccess(ParseComparison(parameter) ?? throw new InvalidOperationException($"Expected expression after && at position {position}"), parameter);
|
|
left = Expression.AndAlso(left, right);
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseComparison(ParameterExpression parameter)
|
|
{
|
|
var left = ParseShift(parameter);
|
|
|
|
var token = Peek();
|
|
if (token.Type is TokenType.EqualsEquals or TokenType.NotEquals or TokenType.GreaterThan or TokenType.LessThan or TokenType.LessThanOrEqual or TokenType.GreaterThanOrEqual)
|
|
{
|
|
Advance(1);
|
|
var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
AlignBinaryOperandTypes(ref left, ref right);
|
|
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
|
}
|
|
|
|
return ParseBinaryAnd(left, parameter);
|
|
}
|
|
|
|
private Expression ParseBinaryAnd(Expression left, ParameterExpression parameter)
|
|
{
|
|
var token = Peek();
|
|
while (token.Type == TokenType.Ampersand)
|
|
{
|
|
Advance(1);
|
|
var right = ParseShift(parameter) ?? throw new InvalidOperationException($"Expected expression after & at position {position}");
|
|
left = Expression.MakeBinary(ExpressionType.And, left, ConvertIfNeeded(right, left.Type));
|
|
token = Peek();
|
|
}
|
|
|
|
return ParseBinaryXor(left, parameter);
|
|
}
|
|
|
|
private Expression ParseBinaryXor(Expression left, ParameterExpression parameter)
|
|
{
|
|
var token = Peek();
|
|
while (token.Type == TokenType.Caret)
|
|
{
|
|
Advance(1);
|
|
var right = ParseBinaryAnd(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after ^ at position {position}");
|
|
left = Expression.MakeBinary(ExpressionType.ExclusiveOr, left, ConvertIfNeeded(right, left.Type));
|
|
token = Peek();
|
|
}
|
|
|
|
return ParseBinaryOr(left, parameter);
|
|
}
|
|
|
|
private Expression ParseBinaryOr(Expression left, ParameterExpression parameter)
|
|
{
|
|
var token = Peek();
|
|
while (token.Type == TokenType.Bar)
|
|
{
|
|
Advance(1);
|
|
var right = ParseBinaryXor(ParseShift(parameter), parameter) ?? throw new InvalidOperationException($"Expected expression after | at position {position}");
|
|
left = Expression.MakeBinary(ExpressionType.Or, left, ConvertIfNeeded(right, left.Type));
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseShift(ParameterExpression parameter)
|
|
{
|
|
var left = ParseAdditive(parameter);
|
|
|
|
var token = Peek();
|
|
while (token.Type is TokenType.LessThanLessThan or TokenType.GreaterThanGreaterThan)
|
|
{
|
|
Advance(1);
|
|
var right = ParseAdditive(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseAdditive(ParameterExpression parameter)
|
|
{
|
|
var left = ParseMultiplicative(parameter);
|
|
|
|
var token = Peek();
|
|
while (token.Type is TokenType.Plus or TokenType.Minus)
|
|
{
|
|
Advance(1);
|
|
var right = ParseMultiplicative(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
|
|
if (token.Type == TokenType.Plus && left.Type == typeof(string))
|
|
{
|
|
left = Expression.Call(null, typeof(string).GetMethod(nameof(string.Concat), [typeof(string), typeof(string)])!, left, ConvertIfNeeded(right, typeof(string)));
|
|
}
|
|
else
|
|
{
|
|
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
|
}
|
|
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private Expression ParseMultiplicative(ParameterExpression parameter)
|
|
{
|
|
var left = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression at position {position}");
|
|
|
|
var token = Peek();
|
|
while (token.Type is TokenType.Star or TokenType.Slash)
|
|
{
|
|
Advance(1);
|
|
var right = ParseTerm(parameter) ?? throw new InvalidOperationException($"Expected expression after {token.Value} at position {position}");
|
|
left = Expression.MakeBinary(token.Type.ToExpressionType(), left, ConvertIfNeeded(right, left.Type));
|
|
token = Peek();
|
|
}
|
|
|
|
return left;
|
|
}
|
|
|
|
private static Expression ConvertIfNeeded(Expression expression, Type targetType)
|
|
{
|
|
if (expression is not LambdaExpression)
|
|
{
|
|
return expression.Type == targetType ? expression : Expression.Convert(expression, targetType);
|
|
}
|
|
|
|
return expression;
|
|
}
|
|
|
|
private static void AlignBinaryOperandTypes(ref Expression left, ref Expression right)
|
|
{
|
|
if (left.Type == right.Type)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var leftUnderlying = Nullable.GetUnderlyingType(left.Type);
|
|
var rightUnderlying = Nullable.GetUnderlyingType(right.Type);
|
|
|
|
if (leftUnderlying != null && rightUnderlying == null && leftUnderlying == right.Type)
|
|
{
|
|
right = Expression.Convert(right, left.Type);
|
|
}
|
|
else if (rightUnderlying != null && leftUnderlying == null && rightUnderlying == left.Type)
|
|
{
|
|
left = Expression.Convert(left, right.Type);
|
|
}
|
|
}
|
|
} |