diff --git a/Directory.Build.props b/Directory.Build.props index 3be9ff13..9bec28d2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ AgileObjects Ltd AgileObjects.ReadableExpressions Steve Wilkes - Copyright © AgileObjects Ltd 2023 + Copyright © AgileObjects Ltd 2024 en git https://github.com/AgileObjects/ReadableExpressions diff --git a/NuGet/AgileObjects.ReadableExpressions.4.1.2.nupkg b/NuGet/AgileObjects.ReadableExpressions.4.1.2.nupkg new file mode 100644 index 00000000..10cfae7b Binary files /dev/null and b/NuGet/AgileObjects.ReadableExpressions.4.1.2.nupkg differ diff --git a/NuGet/AgileObjects.ReadableExpressions.4.1.2.snupkg b/NuGet/AgileObjects.ReadableExpressions.4.1.2.snupkg new file mode 100644 index 00000000..feb63794 Binary files /dev/null and b/NuGet/AgileObjects.ReadableExpressions.4.1.2.snupkg differ diff --git a/src/ReadableExpressions.sln b/src/ReadableExpressions.sln index e867a962..4cdef28f 100644 --- a/src/ReadableExpressions.sln +++ b/src/ReadableExpressions.sln @@ -91,6 +91,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadableExpressions.Visuali EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadableExpressions.Visualizers.Vs17.6.ObjectSource", "Visualizers\ReadableExpressions.Visualizers.Vs17.6.ObjectSource\ReadableExpressions.Visualizers.Vs17.6.ObjectSource.csproj", "{EFF649F7-ABD2-49E6-BA1A-07BC34F89AA2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReadableExpressions.UnitTests.Net8", "Tests\ReadableExpressions.UnitTests.Net8\ReadableExpressions.UnitTests.Net8.csproj", "{E40E25FE-C7FA-4455-87A3-15D41784706E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -354,6 +356,14 @@ Global {EFF649F7-ABD2-49E6-BA1A-07BC34F89AA2}.Release|Any CPU.Build.0 = Release|Any CPU {EFF649F7-ABD2-49E6-BA1A-07BC34F89AA2}.Release|x86.ActiveCfg = Release|Any CPU {EFF649F7-ABD2-49E6-BA1A-07BC34F89AA2}.Release|x86.Build.0 = Release|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Debug|x86.ActiveCfg = Debug|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Debug|x86.Build.0 = Debug|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Release|Any CPU.Build.0 = Release|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Release|x86.ActiveCfg = Release|Any CPU + {E40E25FE-C7FA-4455-87A3-15D41784706E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -394,6 +404,7 @@ Global {53A69151-1721-4ED5-BB65-6A750D46B133} = {E2401C71-C5F2-46FB-B5A3-E6EFB85106B9} {CAC4A53F-D4B9-4AD5-864A-F1141ECE91DA} = {4A12EE3E-81ED-4842-A69F-9D15413DC46D} {EFF649F7-ABD2-49E6-BA1A-07BC34F89AA2} = {4A12EE3E-81ED-4842-A69F-9D15413DC46D} + {E40E25FE-C7FA-4455-87A3-15D41784706E} = {E2401C71-C5F2-46FB-B5A3-E6EFB85106B9} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7EFE121E-7A84-43A4-8C76-7EE70DF2736A} diff --git a/src/ReadableExpressions.sln.DotSettings b/src/ReadableExpressions.sln.DotSettings index 5bd4690c..8321d9ab 100644 --- a/src/ReadableExpressions.sln.DotSettings +++ b/src/ReadableExpressions.sln.DotSettings @@ -4,6 +4,10 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> @@ -45,4 +49,5 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - DO_NOTHING \ No newline at end of file + DO_NOTHING + True \ No newline at end of file diff --git a/src/ReadableExpressions/Extensions/InternalEnumerableExtensions.cs b/src/ReadableExpressions/Extensions/InternalEnumerableExtensions.cs index 00eaa0bb..24c20569 100644 --- a/src/ReadableExpressions/Extensions/InternalEnumerableExtensions.cs +++ b/src/ReadableExpressions/Extensions/InternalEnumerableExtensions.cs @@ -35,7 +35,7 @@ public static ReadOnlyCollection ToReadOnlyCollection( this IList items) { return items.Count != 0 - ? new ReadOnlyCollection(items) + ? new(items) : Enumerable.EmptyReadOnlyCollection; } diff --git a/src/ReadableExpressions/Extensions/InternalExpressionExtensions.cs b/src/ReadableExpressions/Extensions/InternalExpressionExtensions.cs index da0567f3..e9db841c 100644 --- a/src/ReadableExpressions/Extensions/InternalExpressionExtensions.cs +++ b/src/ReadableExpressions/Extensions/InternalExpressionExtensions.cs @@ -9,6 +9,7 @@ #endif using System.Reflection; using NetStandardPolyfills; +using Translations; #if NET35 using static Microsoft.Scripting.Ast.ExpressionType; #else @@ -69,13 +70,10 @@ public static bool IsReturnable(this Expression expression) public static bool IsReturnable(this BlockExpression block) => block.HasReturnType() && block.Result.IsReturnable(); - public static bool IsCapturedValue( - this Expression expression, - out object capturedValue, - out bool isStatic) + public static bool IsCapture(this Expression expression, out Capture capture) { - capturedValue = null; - isStatic = false; + capture = new(); + var capturedMemberAccesses = new List(); while (true) @@ -102,14 +100,15 @@ public static bool IsCapturedValue( return false; } - var declaringType = capturedMemberAccesses.LastOrDefault()?.DeclaringType; + var declaringType = capturedMemberAccesses + .LastOrDefault()?.DeclaringType; if (captureConstant.Type != declaringType) { return false; } - capturedValue = captureConstant.Value; + capture.Object = captureConstant.Value; break; case Convert: @@ -117,7 +116,7 @@ public static bool IsCapturedValue( continue; case null: - isStatic = true; + capture.IsStatic = true; break; default: @@ -131,9 +130,10 @@ public static bool IsCapturedValue( for (var i = capturedMemberAccesses.Count - 1; i >= 0; --i) { - capturedValue = capturedMemberAccesses[i].GetValue(capturedValue); + capture.Object = capturedMemberAccesses[i].GetValue(capture.Object); } + capture.Type = capturedMemberAccesses[0].GetMemberInfoType(); return true; } } diff --git a/src/ReadableExpressions/Extensions/InternalReflectionExtensions.cs b/src/ReadableExpressions/Extensions/InternalReflectionExtensions.cs index 318e8359..fa866eaa 100644 --- a/src/ReadableExpressions/Extensions/InternalReflectionExtensions.cs +++ b/src/ReadableExpressions/Extensions/InternalReflectionExtensions.cs @@ -1,9 +1,8 @@ namespace AgileObjects.ReadableExpressions.Extensions { -#if FEATURE_VALUE_TUPLE using System; -#endif using System.Collections.Generic; + using System.Linq; using System.Reflection; using NetStandardPolyfills; using Translations.Reflection; @@ -49,18 +48,105 @@ public static string GetKeywordOrNull(this IType type) } public static object GetValue(this MemberInfo member, object subject) - { - var hasSubject = subject != null; + => member.TryGetValue(subject, out var value) ? value : null; + public static Type GetMemberInfoType(this MemberInfo member) + { return member switch { - FieldInfo field when hasSubject || field.IsStatic => field.GetValue(subject), - PropertyInfo property when hasSubject || property.IsStatic() => property.GetValue(subject, - Enumerable.EmptyArray), + FieldInfo field => field.FieldType, + PropertyInfo property => property.PropertyType, + MethodInfo method => method.ReturnType, _ => null }; } + public static bool TryGetValue( + this MemberInfo member, + object subject, + out object value) + { + var hasSubject = subject != null; + + switch (member) + { + case FieldInfo field when hasSubject || field.IsStatic: + value = field.GetValue(subject); + break; + + case PropertyInfo property when hasSubject || property.IsStatic(): + value = property.GetValue(subject, Enumerable.EmptyArray); + break; + + case MethodInfo method when method.IsCallable(subject, out var parameters): + value = method.Invoke(subject, parameters); + break; + + default: + value = null; + return false; + } + + return true; + } + + private static bool IsCallable( + this MethodInfo method, + object subject, + out object[] parameters) + { + if (!method.IsPure()) + { + parameters = null; + return false; + } + + var parameterCount = method.GetParameters().Length; + var isParameterless = parameterCount == 0; + + if (!method.IsStatic) + { + parameters = Enumerable.EmptyArray; + return isParameterless && subject != null; + } + + if (isParameterless) + { + parameters = Enumerable.EmptyArray; + return true; + } + + if (parameterCount == 1 && subject != null && + method.IsExtensionMethod()) + { + parameters = new[] { subject }; + return true; + } + + parameters = null; + return false; + } + + private static bool IsPure(this MethodInfo method) + { + if (method.DeclaringType == typeof(Enumerable)) + { + return method.Name switch + { + nameof(Enumerable.Any) => true, + nameof(Enumerable.First) => true, + nameof(Enumerable.FirstOrDefault) => true, + nameof(Enumerable.Last) => true, + nameof(Enumerable.LastOrDefault) => true, + _ => false + }; + } + + return method + .GetCustomAttributes(inherit: false) + .Any(attr => attr.GetType().Name == "PureAttribute"); + } + #if FEATURE_VALUE_TUPLE public static bool IsValueTuple(this Type type) { diff --git a/src/ReadableExpressions/ReadableExpressions.csproj b/src/ReadableExpressions/ReadableExpressions.csproj index b7f16600..59718c99 100644 --- a/src/ReadableExpressions/ReadableExpressions.csproj +++ b/src/ReadableExpressions/ReadableExpressions.csproj @@ -14,10 +14,10 @@ 1.6.1 $(PackageTargetFallback);dnxcore50 - 4.1.1.0 - 4.1.1.0 - 4.1.1 - 4.1.1 + 4.1.2.0 + 4.1.2.0 + 4.1.2 + 4.1.2 AgileObjects.ReadableExpressions AgileObjects.ReadableExpressions @@ -26,8 +26,10 @@ ./Icon.png ExpressionTrees Debugging DebuggerVisualizers Linq DLR https://github.com/AgileObjects/ReadableExpressions - - Fixing static method access when showing captured values re: #129 -- Translating enum comparisons as enum constants + - Fixing nullable enum translation, re: #134 +- Improving ShowCapturedValues capabilities to include captured Linq calls, re: #133 +- Fixing parameterless Value Tuple translation, re: #135 +- Fixing non-equality enum comparisons, re: #136 README.md ../../NuGet diff --git a/src/ReadableExpressions/Translations/BinaryTranslation.cs b/src/ReadableExpressions/Translations/BinaryTranslation.cs index 8d155237..39a3057b 100644 --- a/src/ReadableExpressions/Translations/BinaryTranslation.cs +++ b/src/ReadableExpressions/Translations/BinaryTranslation.cs @@ -25,14 +25,25 @@ internal class BinaryTranslation : private readonly INodeTranslation _rightOperandTranslation; private bool _suppressParentheses; - private BinaryTranslation(BinaryExpression binary, ITranslationContext context) : - base(IsChecked(binary.NodeType), "(", ")") + private BinaryTranslation( + BinaryExpression binary, + ITranslationContext context) : + this(binary.Left, binary.NodeType, binary.Right, context) + { + } + + private BinaryTranslation( + Expression leftOperand, + ExpressionType nodeType, + Expression rightOperand, + ITranslationContext context) : + base(IsChecked(nodeType), "(", ")") { _context = context; - NodeType = binary.NodeType; - _leftOperandTranslation = context.GetTranslationFor(binary.Left); + NodeType = nodeType; + _leftOperandTranslation = context.GetTranslationFor(leftOperand); _operator = GetOperator(NodeType); - _rightOperandTranslation = context.GetTranslationFor(binary.Right); + _rightOperandTranslation = context.GetTranslationFor(rightOperand); if (_leftOperandTranslation is BinaryTranslation leftNestedBinary && HasComplimentaryOperator(leftNestedBinary)) @@ -138,32 +149,45 @@ public static INodeTranslation For( goto default; default: - TryGetEnumComparisonExpression(ref binary); + var isEnumComparison = TryGetEnumComparisonExpression( + binary, + out var leftOperand, + out var rightOperand); + + if (isEnumComparison) + { + return new BinaryTranslation( + leftOperand, + binary.NodeType, + rightOperand, + context); + } + break; } return new BinaryTranslation(binary, context); } - public static void TryGetEnumComparisonExpression( - ref BinaryExpression comparison) + private static bool TryGetEnumComparisonExpression( + BinaryExpression comparison, + out Expression leftOperand, + out Expression rightOperand) { - var leftOperandIsEnum = - IsEnumType(comparison.Left, out var leftExpression); - - var rightOperandIsEnum = - IsEnumType(comparison.Right, out var rightExpression); + var leftOperandIsEnum = IsEnumType(comparison.Left, out leftOperand); + var rightOperandIsEnum = IsEnumType(comparison.Right, out rightOperand); - if (leftOperandIsEnum || rightOperandIsEnum) + if (!(leftOperandIsEnum || rightOperandIsEnum)) { - var enumType = leftOperandIsEnum - ? leftExpression.Type : rightExpression.Type; - - comparison = comparison.Update( - GetEnumValue(leftExpression, enumType), - comparison.Conversion, - GetEnumValue(rightExpression, enumType)); + return false; } + + var enumType = leftOperandIsEnum + ? leftOperand.Type : rightOperand.Type; + + leftOperand = GetEnumValue(leftOperand, enumType); + rightOperand = GetEnumValue(rightOperand, enumType); + return true; } private static bool IsEnumType( @@ -187,7 +211,7 @@ private static bool IsEnumType( private static Expression GetEnumValue( Expression expression, - Type enumType) + Type enumValueType) { if (expression.NodeType != Constant) { @@ -195,8 +219,9 @@ private static Expression GetEnumValue( } var value = ((ConstantExpression)expression).Value; + var enumType = enumValueType.GetNonNullableType(); var enumValue = Enum.Parse(enumType, value.ToString()); - return Expression.Constant(enumValue, enumType); + return Expression.Constant(enumValue, enumValueType); } #endregion diff --git a/src/ReadableExpressions/Translations/Capture.cs b/src/ReadableExpressions/Translations/Capture.cs new file mode 100644 index 00000000..81729d77 --- /dev/null +++ b/src/ReadableExpressions/Translations/Capture.cs @@ -0,0 +1,12 @@ +namespace AgileObjects.ReadableExpressions.Translations; + +using System; + +internal class Capture +{ + public object Object { get; set; } + + public Type Type { get; set; } + + public bool IsStatic { get; set; } +} \ No newline at end of file diff --git a/src/ReadableExpressions/Translations/ConstantTranslation.cs b/src/ReadableExpressions/Translations/ConstantTranslation.cs index c33a653b..e582c511 100644 --- a/src/ReadableExpressions/Translations/ConstantTranslation.cs +++ b/src/ReadableExpressions/Translations/ConstantTranslation.cs @@ -28,32 +28,75 @@ internal static class ConstantTranslation public static INodeTranslation For( ConstantExpression constant, ITranslationContext context) + { + var isValueConstant = TryCreateValueTranslation( + constant, + context, + out var translation, + out var constantValueType); + + if (isValueConstant) + { + return translation; + } + + return context.GetTranslationFor(constantValueType).WithNodeType(Constant); + } + + public static bool TryCreateValueTranslation( + object constant, + Type valueType, + ITranslationContext context, + out INodeTranslation valueTranslation) + { + return TryCreateValueTranslation( + Expression.Constant(constant, valueType), + context, + out valueTranslation, + out _); + } + + private static bool TryCreateValueTranslation( + ConstantExpression constant, + ITranslationContext context, + out INodeTranslation valueTranslation, + out Type constantValueType) { if (constant.Value == null) { - return DefaultValueTranslation.For(constant, context); + valueTranslation = DefaultValueTranslation.For(constant, context); + constantValueType = null; + return true; } - if (constant.Type.IsEnum()) + var constantType = constant.Type.GetNonNullableType(); + + if (constantType.IsEnum()) { - return constant.Type.HasAttribute() - ? new FlagsEnumConstantTranslation(constant, context) - : new EnumConstantTranslation(constant, context); + valueTranslation = constantType.HasAttribute() + ? new FlagsEnumConstantTranslation(constant, constantType, context) + : new EnumConstantTranslation(constant, constantType, context); + + constantValueType = null; + return true; } - if (TryTranslateFromTypeCode(constant, context, out var translation)) + if (TryTranslateFromTypeCode(constant, context, out valueTranslation)) { - return translation; + constantValueType = null; + return true; } - var valueType = constant.Value.GetType(); + constantValueType = constant.Value.GetType(); - if (valueType.IsPrimitive() || valueType.IsValueType()) + if (constantValueType.IsPrimitive() || constantValueType.IsValueType()) { - return FixedValueTranslation(constant.Value); + valueTranslation = FixedValueTranslation(constant.Value); + return true; } - return context.GetTranslationFor(valueType).WithNodeType(Constant); + valueTranslation = null; + return false; } private static INodeTranslation FixedValueTranslation( @@ -337,9 +380,9 @@ private class FlagsEnumConstantTranslation : INodeTranslation public FlagsEnumConstantTranslation( ConstantExpression constant, + Type enumType, ITranslationContext context) { - var enumType = constant.Type; _typeNameTranslation = context.GetTranslationFor(enumType); var enumValue = constant.Value; @@ -416,9 +459,10 @@ private class EnumConstantTranslation : INodeTranslation public EnumConstantTranslation( ConstantExpression constant, + Type constantType, ITranslationContext context) { - _typeNameTranslation = context.GetTranslationFor(constant.Type); + _typeNameTranslation = context.GetTranslationFor(constantType); _enumMemberName = constant.Value.ToString(); } diff --git a/src/ReadableExpressions/Translations/MemberAccessTranslation.cs b/src/ReadableExpressions/Translations/MemberAccessTranslation.cs index 43524d6c..403122fb 100644 --- a/src/ReadableExpressions/Translations/MemberAccessTranslation.cs +++ b/src/ReadableExpressions/Translations/MemberAccessTranslation.cs @@ -33,15 +33,19 @@ public static INodeTranslation For( { bool translateSubject; - if (memberAccess.IsCapturedValue(out var capturedValue, out var isStatic)) + if (memberAccess.IsCapture(out var capture)) { - if (context.Settings.ShowCapturedValues) + if (context.Settings.ShowCapturedValues && + ConstantTranslation.TryCreateValueTranslation( + capture.Object, + capture.Type, + context, + out var valueTranslation)) { - return context.GetTranslationFor( - Expression.Constant(capturedValue, memberAccess.Type)); + return valueTranslation; } - translateSubject = isStatic; + translateSubject = capture.IsStatic; } else { @@ -91,7 +95,7 @@ private static bool IsIndexedPropertyAccess( { return Expression.Constant(p.DefaultValue, p.ParameterType); } - + return (Expression)Expression.Default(p.ParameterType); }); diff --git a/src/ReadableExpressions/Translations/MethodCallTranslation.cs b/src/ReadableExpressions/Translations/MethodCallTranslation.cs index 629735c2..aeccf3e6 100644 --- a/src/ReadableExpressions/Translations/MethodCallTranslation.cs +++ b/src/ReadableExpressions/Translations/MethodCallTranslation.cs @@ -102,19 +102,22 @@ public static INodeTranslation For( context.GetTranslationFor(methodCall.Method.ReturnType)); } - var methodCallTranslation = new StandardMethodCallTranslation( - Call, - methodCall.GetSubjectTranslation(context), - method, - parameters, - context); + var isCapturedCallResultValue = TryGetCapturedCallResultValue( + methodCall, + context, + out var callResultTranslation); - if (context.Analysis.IsPartOfMethodCallChain(methodCall)) + if (isCapturedCallResultValue) { - methodCallTranslation.AsPartOfMethodCallChain(); + return callResultTranslation; } - return methodCallTranslation; + return new StandardMethodCallTranslation( + Call, + methodCall, + method, + parameters, + context); } /// @@ -141,6 +144,28 @@ private static bool IsDefaultIndexedPropertyAccess(MethodCallExpression methodCa methodCall.Method.GetProperty()?.GetIndexParameters().Any() == true; } + private static bool TryGetCapturedCallResultValue( + MethodCallExpression methodCall, + ITranslationContext context, + out INodeTranslation resultTranslation) + { + var subject = methodCall.GetSubject(); + + if (subject.IsCapture(out var capturedSubject) && + methodCall.Method.TryGetValue(capturedSubject.Object, out var callResult) && + ConstantTranslation.TryCreateValueTranslation( + callResult, + methodCall.Type, + context, + out resultTranslation)) + { + return true; + } + + resultTranslation = null; + return false; + } + /// /// Creates an for the given . /// @@ -256,7 +281,26 @@ private class StandardMethodCallTranslation : INodeTranslation private readonly ITranslationContext _context; private readonly INodeTranslation _subjectTranslation; private readonly MethodInvocationTranslation _methodInvocationTranslation; - private bool _isPartOfMethodCallChain; + private readonly bool _isPartOfMethodCallChain; + + public StandardMethodCallTranslation( + ExpressionType nodeType, + MethodCallExpression methodCall, + IMethod method, + ParameterSetTranslation parameters, + ITranslationContext context) : + this( + nodeType, + methodCall.GetSubjectTranslation(context), + method, + parameters, + context) + { + if (context.Analysis.IsPartOfMethodCallChain(methodCall)) + { + _isPartOfMethodCallChain = true; + } + } public StandardMethodCallTranslation( ExpressionType nodeType, @@ -268,9 +312,7 @@ public StandardMethodCallTranslation( _context = context; NodeType = nodeType; _subjectTranslation = subjectTranslation; - - _methodInvocationTranslation = - new MethodInvocationTranslation(method, parameters, context); + _methodInvocationTranslation = new(method, parameters, context); } public ExpressionType NodeType { get; } @@ -280,8 +322,6 @@ public StandardMethodCallTranslation( ".".Length + _methodInvocationTranslation.TranslationLength; - public void AsPartOfMethodCallChain() => _isPartOfMethodCallChain = true; - public void WriteTo(TranslationWriter writer) { _subjectTranslation.WriteInParenthesesIfRequired(writer, _context); diff --git a/src/ReadableExpressions/Translations/NewingTranslation.cs b/src/ReadableExpressions/Translations/NewingTranslation.cs index b93ca7f8..194fa095 100644 --- a/src/ReadableExpressions/Translations/NewingTranslation.cs +++ b/src/ReadableExpressions/Translations/NewingTranslation.cs @@ -43,7 +43,7 @@ public static INodeTranslation For( } #if FEATURE_VALUE_TUPLE - if (newing.Type.IsValueTuple()) + if (newing.Type.IsValueTuple() && newing.Arguments.Any()) { return new ValueTupleNewingTranslation(newing, context); } diff --git a/src/Tests/ReadableExpressions.UnitTests.Common/OddNumber.cs b/src/Tests/ReadableExpressions.UnitTests.Common/OddNumber.cs index b116d949..dbcf9ed3 100644 --- a/src/Tests/ReadableExpressions.UnitTests.Common/OddNumber.cs +++ b/src/Tests/ReadableExpressions.UnitTests.Common/OddNumber.cs @@ -2,6 +2,7 @@ { public enum OddNumber { - One = 1 + One = 1, + Three = 3 } } \ No newline at end of file diff --git a/src/Tests/ReadableExpressions.UnitTests.Net35/ReadableExpressions.UnitTests.Net35.csproj b/src/Tests/ReadableExpressions.UnitTests.Net35/ReadableExpressions.UnitTests.Net35.csproj index 9c96f7b5..b2aace3e 100644 --- a/src/Tests/ReadableExpressions.UnitTests.Net35/ReadableExpressions.UnitTests.Net35.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.Net35/ReadableExpressions.UnitTests.Net35.csproj @@ -7,6 +7,7 @@ true false + true diff --git a/src/Tests/ReadableExpressions.UnitTests.Net5/ReadableExpressions.UnitTests.Net5.csproj b/src/Tests/ReadableExpressions.UnitTests.Net5/ReadableExpressions.UnitTests.Net5.csproj index 6f2616a7..9c305465 100644 --- a/src/Tests/ReadableExpressions.UnitTests.Net5/ReadableExpressions.UnitTests.Net5.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.Net5/ReadableExpressions.UnitTests.Net5.csproj @@ -11,6 +11,7 @@ false false false + true diff --git a/src/Tests/ReadableExpressions.UnitTests.Net6/ReadableExpressions.UnitTests.Net6.csproj b/src/Tests/ReadableExpressions.UnitTests.Net6/ReadableExpressions.UnitTests.Net6.csproj index 1c9ea5f1..eabdd02d 100644 --- a/src/Tests/ReadableExpressions.UnitTests.Net6/ReadableExpressions.UnitTests.Net6.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.Net6/ReadableExpressions.UnitTests.Net6.csproj @@ -11,6 +11,7 @@ false false false + true diff --git a/src/Tests/ReadableExpressions.UnitTests.Net7/ReadableExpressions.UnitTests.Net7.csproj b/src/Tests/ReadableExpressions.UnitTests.Net7/ReadableExpressions.UnitTests.Net7.csproj index d04908ac..f6553971 100644 --- a/src/Tests/ReadableExpressions.UnitTests.Net7/ReadableExpressions.UnitTests.Net7.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.Net7/ReadableExpressions.UnitTests.Net7.csproj @@ -11,6 +11,7 @@ false false false + true diff --git a/src/Tests/ReadableExpressions.UnitTests.Net8/ReadableExpressions.UnitTests.Net8.csproj b/src/Tests/ReadableExpressions.UnitTests.Net8/ReadableExpressions.UnitTests.Net8.csproj new file mode 100644 index 00000000..18a7bb42 --- /dev/null +++ b/src/Tests/ReadableExpressions.UnitTests.Net8/ReadableExpressions.UnitTests.Net8.csproj @@ -0,0 +1,50 @@ + + + + net8.0 + AgileObjects.ReadableExpressions.UnitTests.Net7 + AgileObjects.ReadableExpressions.UnitTests + true + + true + false + false + false + false + true + + + + $(DefineConstants);TRACE;FEATURE_VALUE_TUPLE + + + + + Properties\ClsCompliant.cs + + + + + + %(RecursiveDir)%(Filename)%(Extension) + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + diff --git a/src/Tests/ReadableExpressions.UnitTests.NetCore1/ReadableExpressions.UnitTests.NetCore1.csproj b/src/Tests/ReadableExpressions.UnitTests.NetCore1/ReadableExpressions.UnitTests.NetCore1.csproj index 34493cee..dd0b8d2c 100644 --- a/src/Tests/ReadableExpressions.UnitTests.NetCore1/ReadableExpressions.UnitTests.NetCore1.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.NetCore1/ReadableExpressions.UnitTests.NetCore1.csproj @@ -13,6 +13,7 @@ false false false + true diff --git a/src/Tests/ReadableExpressions.UnitTests.NetCore2/ReadableExpressions.UnitTests.NetCore2.csproj b/src/Tests/ReadableExpressions.UnitTests.NetCore2/ReadableExpressions.UnitTests.NetCore2.csproj index 0b4c979a..39718ed1 100644 --- a/src/Tests/ReadableExpressions.UnitTests.NetCore2/ReadableExpressions.UnitTests.NetCore2.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.NetCore2/ReadableExpressions.UnitTests.NetCore2.csproj @@ -11,6 +11,7 @@ false false false + true diff --git a/src/Tests/ReadableExpressions.UnitTests.NetCore3/ReadableExpressions.UnitTests.NetCore3.csproj b/src/Tests/ReadableExpressions.UnitTests.NetCore3/ReadableExpressions.UnitTests.NetCore3.csproj index d05145c4..528c42b0 100644 --- a/src/Tests/ReadableExpressions.UnitTests.NetCore3/ReadableExpressions.UnitTests.NetCore3.csproj +++ b/src/Tests/ReadableExpressions.UnitTests.NetCore3/ReadableExpressions.UnitTests.NetCore3.csproj @@ -11,6 +11,7 @@ false false false + true diff --git a/src/Tests/ReadableExpressions.UnitTests/ReadableExpressions.UnitTests.csproj b/src/Tests/ReadableExpressions.UnitTests/ReadableExpressions.UnitTests.csproj index 097283e1..1e3c6498 100644 --- a/src/Tests/ReadableExpressions.UnitTests/ReadableExpressions.UnitTests.csproj +++ b/src/Tests/ReadableExpressions.UnitTests/ReadableExpressions.UnitTests.csproj @@ -7,6 +7,7 @@ true false + true diff --git a/src/Tests/ReadableExpressions.UnitTests/WhenShowingCapturedValues.cs b/src/Tests/ReadableExpressions.UnitTests/WhenShowingCapturedValues.cs new file mode 100644 index 00000000..86b09427 --- /dev/null +++ b/src/Tests/ReadableExpressions.UnitTests/WhenShowingCapturedValues.cs @@ -0,0 +1,188 @@ +namespace AgileObjects.ReadableExpressions.UnitTests; + +using System.Collections.Generic; +using System.Linq; +using Common; +#if !NET35 +using Xunit; +#else +using Fact = NUnit.Framework.TestAttribute; + +[NUnit.Framework.TestFixture] +#endif +public class WhenShowingCapturedValues : TestClassBase +{ + // See https://github.com/agileobjects/ReadableExpressions/issues/86 + [Fact] + public void ShouldIncludeCapturedLocalVariableValues() + { + var value = int.Parse("123"); + + var capturedLocalVariableLamda = CreateLambda( + (PropertiesHelper helper) => helper.PublicInstance == value); + + var translated = capturedLocalVariableLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.PublicInstance == 123"); + } + + [Fact] + public void ShouldIncludeCapturedInstanceFieldValues() + { + var capture = new FieldsHelper { PublicInstance = 7238 }; + + var capturedInstanceFieldLamda = CreateLambda( + (PropertiesHelper helper) => helper.PublicInstance == capture.PublicInstance); + + var translated = capturedInstanceFieldLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.PublicInstance == 7238"); + } + + [Fact] + public void ShouldIncludeCapturedInstancePropertyValues() + { + var capture = new PropertiesHelper { PublicInstance = 999 }; + + var capturedInstancePropertyLamda = CreateLambda( + (PropertiesHelper helper) => helper.PublicInstance == capture.PublicInstance); + + var translated = capturedInstancePropertyLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.PublicInstance == 999"); + } + + [Fact] + public void ShouldIncludeCapturedStaticFieldValues() + { + FieldsHelper.PublicStatic = 90210; + + var capturedStaticFieldLamda = CreateLambda( + (FieldsHelper helper) => helper.PublicInstance == FieldsHelper.PublicStatic); + + var translated = capturedStaticFieldLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.PublicInstance == 90210"); + } + + [Fact] + public void ShouldIncludeCapturedStaticBclFieldValues() + { + var capturedStaticFieldLamda = CreateLambda( + (FieldsHelper helper) => helper.PublicInstance.ToString() == string.Empty); + + var translated = capturedStaticFieldLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.PublicInstance.ToString() == \"\""); + } + + [Fact] + public void ShouldIncludeCapturedStaticPropertyValues() + { + PropertiesHelper.PublicStatic = 456; + + var capturedStaticPropertyLamda = CreateLambda( + (PropertiesHelper helper) => helper.PublicInstance == PropertiesHelper.PublicStatic); + + var translated = capturedStaticPropertyLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.PublicInstance == 456"); + } + + // See https://github.com/agileobjects/ReadableExpressions/issues/129 + // See https://github.com/agileobjects/ReadableExpressions/issues/133 + [Fact] + public void ShouldHandleCapturedNestedEnumCollectionLinqFirstAccess() + { + var source = new ValueWrapper>> + { + Value = new() { new() { Value = OddNumber.One } } + }; + + var capturedLocalVariableLamda = CreateLambda( + (List> helpers) => + helpers.Any(h => h.Value == source.Value.First().Value)); + + var translated = capturedLocalVariableLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helpers.Any(h => h.Value == OddNumber.One)"); + } + + [Fact] + public void ShouldHandleCapturedNestedEnumCollectionLinqFirstOrDefaultAccess() + { + var source = new ValueWrapper>> + { + Value = new() { new() { Value = OddNumber.Three } } + }; + + var capturedLocalVariableLamda = CreateLambda( + (IEnumerable> helpers) => + helpers.Any(h => h.Value == source.Value.FirstOrDefault().Value)); + + var translated = capturedLocalVariableLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helpers.Any(h => h.Value == OddNumber.Three)"); + } + + [Fact] + public void ShouldHandleCapturedNestedEnumCollectionLinqLastAccess() + { + var source = new ValueWrapper>> + { + Value = new() { new() { Value = OddNumber.Three } } + }; + + var capturedLocalVariableLamda = CreateLambda( + (IEnumerable> helpers) => + helpers.Any(h => h.Value == source.Value.Last().Value)); + + var translated = capturedLocalVariableLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helpers.Any(h => h.Value == OddNumber.Three)"); + } + + [Fact] + public void ShouldHandleCapturedNestedEnumCollectionLinqLastOrDefaultAccess() + { + var source = new ValueWrapper>> + { + Value = new() { new() { Value = OddNumber.One } } + }; + + var capturedLocalVariableLamda = CreateLambda( + (ICollection> helpers) => + helpers.Any(h => h.Value == source.Value.LastOrDefault().Value)); + + var translated = capturedLocalVariableLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helpers.Any(h => h.Value == OddNumber.One)"); + } + + [Fact] + public void ShouldHandleCapturedNestedIntCollectionLinqAnyAccess() + { + var source = new ValueWrapper> + { + Value = new() { 1, 2, 3 } + }; + + var capturedLocalVariableLamda = CreateLambda( + (ValueWrapper helper) => helper.Value == source.Value.Any()); + + var translated = capturedLocalVariableLamda.Body + .ToReadableString(stgs => stgs.ShowCapturedValues); + + translated.ShouldBe("helper.Value == true"); + } +} \ No newline at end of file diff --git a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingComparisons.cs b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingComparisons.cs index 9d85bc82..4068e381 100644 --- a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingComparisons.cs +++ b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingComparisons.cs @@ -22,6 +22,17 @@ public void ShouldTranslateAnEqualityExpression() translated.ShouldBe("(i1, i2) => i1 == i2"); } + [Fact] + public void ShouldTranslateAnEnumEqualityExpression() + { + var enumEqualsOne = + CreateLambda((OddNumber number) => number == OddNumber.One); + + var translated = enumEqualsOne.ToReadableString(); + + translated.ShouldBe("number => number == OddNumber.One"); + } + [Fact] public void ShouldTranslateALessThanExpression() { @@ -72,6 +83,30 @@ public void ShouldTranslateAnInequalityExpression() translated.ShouldBe("(i1, i2) => i1 != i2"); } + // See https://github.com/agileobjects/ReadableExpressions/issues/134 + [Fact] + public void ShouldTranslateANullableEnumInequalityExpression() + { + var enumDoesNotEqualOne = + CreateLambda((OddNumber? number) => number != OddNumber.One); + + var translated = enumDoesNotEqualOne.ToReadableString(); + + translated.ShouldBe("number => number != OddNumber.One"); + } + + // See https://github.com/agileobjects/ReadableExpressions/issues/136 + [Fact] + public void ShouldTranslateAnEnumLessThanExpression() + { + var enumLessThanThree = CreateLambda( + (ValueWrapper number) => number.Value < OddNumber.Three); + + var translated = enumLessThanThree.Body.ToReadableString(); + + translated.ShouldBe("number.Value < OddNumber.Three"); + } + [Fact] public void ShouldAbbreviateBooleanTrueComparisons() { diff --git a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingConstants.cs b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingConstants.cs index f50edc0f..20aad378 100644 --- a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingConstants.cs +++ b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingConstants.cs @@ -234,6 +234,26 @@ public void ShouldTranslateAnEnumMember() translated.ShouldBe("OddNumber.One"); } + [Fact] + public void ShouldTranslateANullableEnumMember() + { + var enumConstant = Constant(OddNumber.One, typeof(OddNumber?)); + + var translated = enumConstant.ToReadableString(); + + translated.ShouldBe("OddNumber.One"); + } + + [Fact] + public void ShouldTranslateANullNullableEnumMember() + { + var nullEnumConstant = Constant(default(OddNumber?)); + + var translated = nullEnumConstant.ToReadableString(); + + translated.ShouldBe("null"); + } + [Fact] public void ShouldTranslateADefaultFlagsEnumMember() { @@ -592,7 +612,7 @@ public void ShouldTranslateALambdaConstant() var translated = lambdaConstant.ToReadableString(); - const string EXPECTED = @"num => Enumerable.Range(num, 10).Select(i => new { Index = i }).Sum(d => d.Index)"; + const string EXPECTED = "num => Enumerable.Range(num, 10).Select(i => new { Index = i }).Sum(d => d.Index)"; translated.ShouldBe(EXPECTED.TrimStart()); } diff --git a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingMemberAccesses.cs b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingMemberAccesses.cs index ba938f9d..50dc6c87 100644 --- a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingMemberAccesses.cs +++ b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingMemberAccesses.cs @@ -577,103 +577,6 @@ public void ShouldNotIncludeCapturedInstanceNames() translated.ShouldBe("_i == comparator"); } - // See https://github.com/agileobjects/ReadableExpressions/issues/86 - [Fact] - public void ShouldIncludeCapturedLocalVariableValues() - { - var value = int.Parse("123"); - - var capturedLocalVariableLamda = CreateLambda( - (PropertiesHelper helper) => helper.PublicInstance == value); - - var translated = capturedLocalVariableLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helper.PublicInstance == 123"); - } - - [Fact] - public void ShouldIncludeCapturedInstanceFieldValues() - { - var capture = new FieldsHelper { PublicInstance = 7238 }; - - var capturedInstanceFieldLamda = CreateLambda( - (PropertiesHelper helper) => helper.PublicInstance == capture.PublicInstance); - - var translated = capturedInstanceFieldLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helper.PublicInstance == 7238"); - } - - [Fact] - public void ShouldIncludeCapturedInstancePropertyValues() - { - var capture = new PropertiesHelper { PublicInstance = 999 }; - - var capturedInstancePropertyLamda = CreateLambda( - (PropertiesHelper helper) => helper.PublicInstance == capture.PublicInstance); - - var translated = capturedInstancePropertyLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helper.PublicInstance == 999"); - } - - [Fact] - public void ShouldIncludeCapturedStaticFieldValues() - { - FieldsHelper.PublicStatic = 90210; - - var capturedStaticFieldLamda = CreateLambda( - (FieldsHelper helper) => helper.PublicInstance == FieldsHelper.PublicStatic); - - var translated = capturedStaticFieldLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helper.PublicInstance == 90210"); - } - - [Fact] - public void ShouldIncludeCapturedStaticBclFieldValues() - { - var capturedStaticFieldLamda = CreateLambda( - (FieldsHelper helper) => helper.PublicInstance.ToString() == string.Empty); - - var translated = capturedStaticFieldLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helper.PublicInstance.ToString() == \"\""); - } - - [Fact] - public void ShouldIncludeCapturedStaticPropertyValues() - { - PropertiesHelper.PublicStatic = 456; - - var capturedStaticPropertyLamda = CreateLambda( - (PropertiesHelper helper) => helper.PublicInstance == PropertiesHelper.PublicStatic); - - var translated = capturedStaticPropertyLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helper.PublicInstance == 456"); - } - - // See https://github.com/agileobjects/ReadableExpressions/issues/129 - [Fact] - public void ShouldHandleComplexTypeCollectionAccessWithCapturedValues() - { - var capturedLocalVariableLamda = CreateLambda( - (IEnumerable helpers) => - helpers.First().PublicEnumInstance == OddNumber.One); - - var translated = capturedLocalVariableLamda.Body - .ToReadableString(stgs => stgs.ShowCapturedValues); - - translated.ShouldBe("helpers.First().PublicEnumInstance == OddNumber.One"); - } - [Fact] public void ShouldIncludeOutParameterKeywords() { @@ -1112,6 +1015,11 @@ public static void DoSomething() } } +internal class ValueWrapper +{ + public T Value { get; set; } +} + internal class CustomAdder : IEnumerable { public static implicit operator string(CustomAdder adder) => adder.ToString(); diff --git a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingObjectCreations.cs b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingObjectCreations.cs index 3d0e0a13..d8f2ce91 100644 --- a/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingObjectCreations.cs +++ b/src/Tests/ReadableExpressions.UnitTests/WhenTranslatingObjectCreations.cs @@ -492,6 +492,18 @@ public void ShouldTranslateAValueTupleNewExpression() translated.ShouldBe("intStringValueTuple = (123, string.Empty)"); } + + // See https://github.com/agileobjects/ReadableExpressions/issues/135 + [Fact] + public void ShouldTranslateAnExplicitParameterlessTripleValueTupleNewExpression() + { + Expression>> newTripleTuple = + () => new(); + + var translated = newTripleTuple.ToReadableString(); + + translated.ShouldBe("() => new ValueTuple()"); + } #endif }