diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs new file mode 100644 index 0000000..90a83bd --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Correctness/NullPropagation.cs @@ -0,0 +1,63 @@ +using System; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Correctness +{ + class NullPropagation + { + static void Main() + { + new NullPropagation().TestNotCoalescing(); +#if CS72 + new NullPropagation().TestRefStructNotCoalescing(); +#endif + } + + class MyClass + { + public string Text; + } + + void TestNotCoalescing() + { + Console.WriteLine("TestNotCoalescing:"); + Console.WriteLine(NotCoalescing(null)); + Console.WriteLine(NotCoalescing(new MyClass())); + } + + string NotCoalescing(MyClass c) + { + return c != null ? c.Text : "Hello"; + } + +#if CS72 + ref struct ByRefLikeStruct + { + public int Value; + } + + class RefStructHolder + { + public ByRefLikeStruct Get() + { + ByRefLikeStruct result = default(ByRefLikeStruct); + result.Value = 42; + return result; + } + } + + void TestRefStructNotCoalescing() + { + Console.WriteLine("TestRefStructNotCoalescing:"); + Console.WriteLine(RefStructNotCoalescing(null).Value); + Console.WriteLine(RefStructNotCoalescing(new RefStructHolder()).Value); + } + + // A by-ref-like return type cannot be turned into a ?. / ?? null-propagation: a ref struct + // cannot be wrapped in Nullable, so that form does not compile. + ByRefLikeStruct RefStructNotCoalescing(RefStructHolder holder) + { + return holder != null ? holder.Get() : default(ByRefLikeStruct); + } +#endif + } +} diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs new file mode 100644 index 0000000..063dab6 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullPropagation.cs @@ -0,0 +1,384 @@ +// Copyright (c) 2018 Daniel Grunwald +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty +{ + internal class NullPropagation + { + private class MyClass + { + public int IntVal; + public readonly int ReadonlyIntVal; + public MyStruct StructField; + public readonly MyStruct ReadonlyStructField; + public string Text; + public MyClass Field; + public MyClass Property { get; set; } + public MyClass this[int index] => null; + public MyClass Method(int arg) + { + return null; + } + + public bool GetBool() + { + return false; + } + + public void Done() + { + } + } + + private struct MyStruct + { + public int IntVal; + public readonly int ReadonlyIntVal; + public MyClass Field; + public MyStruct? Property1 => null; +#if CS71 + public MyStruct Property2 => default; +#else + public MyStruct Property2 => default(MyStruct); +#endif + public MyStruct? this[int index] => null; + public MyStruct? Method1(int arg) + { + return null; + } + public MyStruct Method2(int arg) + { +#if CS71 + return default; +#else + return default(MyStruct); +#endif + } + + public bool GetBool() + { + return false; + } + + public void Done() + { + } + } + + private class Container + { + public GenericStruct Other; + } + + private struct GenericStruct + { + public T1 Field1; + public T2 Field2; + public Container Other; + + public override string ToString() + { + return "(" + Field1?.ToString() + ", " + Field2?.ToString() + ")"; + } + + public int? GetTextLength() + { + return Field1?.ToString().Length + Field2?.ToString().Length + 4; + } + + public string Chain1() + { + return Other?.Other.Other?.Other.Field1?.ToString(); + } + + public string Chain2() + { + return Other?.Other.Other?.Other.Field1?.ToString()?.GetType().Name; + } + + public int? Test2() + { + return Field1?.ToString().Length ?? 42; + } + + public int? GetTextLengthNRE() + { + return (Field1?.ToString()).Length; + } + } + + public interface ITest + { + int Int(); + ITest Next(); + } + + private int GetInt() + { + return 9; + } + + private string GetString() + { + return null; + } + + private MyClass GetMyClass() + { + return null; + } + + private MyStruct? GetMyStruct() + { + return null; + } + + public string Substring() + { + return GetString()?.Substring(GetInt()); + } + + public void CallSubstringAndIgnoreResult() + { + GetString()?.Substring(GetInt()); + } + + private void Use(T t) + { + } + + public void CallDone() + { + GetMyClass()?.Done(); + GetMyClass()?.Field?.Done(); + GetMyClass()?.Field.Done(); + GetMyClass()?.Property?.Done(); + GetMyClass()?.Property.Done(); + GetMyClass()?.Method(GetInt())?.Done(); + GetMyClass()?.Method(GetInt()).Done(); + GetMyClass()?[GetInt()]?.Done(); + GetMyClass()?[GetInt()].Done(); + } + + public void CallGetBool() + { + GetMyClass()?.GetBool(); + GetMyClass()?.Field?.GetBool(); + GetMyClass()?.Field.GetBool(); + GetMyClass()?.Property?.GetBool(); + GetMyClass()?.Property.GetBool(); + GetMyClass()?.Method(GetInt())?.GetBool(); + GetMyClass()?.Method(GetInt()).GetBool(); + GetMyClass()?[GetInt()]?.GetBool(); + GetMyClass()?[GetInt()].GetBool(); + } + + public void CallDoneStruct() + { + GetMyStruct()?.Done(); + GetMyStruct()?.Field?.Done(); + GetMyStruct()?.Field.Done(); + GetMyStruct()?.Property1?.Done(); + GetMyStruct()?.Property2.Done(); + GetMyStruct()?.Method1(GetInt())?.Done(); + GetMyStruct()?.Method2(GetInt()).Done(); + GetMyStruct()?[GetInt()]?.Done(); + } + + public void CallGetBoolStruct() + { + GetMyStruct()?.GetBool(); + GetMyStruct()?.Field?.GetBool(); + GetMyStruct()?.Field.GetBool(); + GetMyStruct()?.Property1?.GetBool(); + GetMyStruct()?.Property2.GetBool(); + GetMyStruct()?.Method1(GetInt())?.GetBool(); + GetMyStruct()?.Method2(GetInt()).GetBool(); + GetMyStruct()?[GetInt()]?.GetBool(); + } + + public void RequiredParentheses() + { + (GetMyClass()?.Field).Done(); + (GetMyClass()?.Method(GetInt())).Done(); + (GetMyStruct()?.Property2)?.Done(); + } + + public int?[] ChainsOnClass() + { + return new int?[9] { + GetMyClass()?.IntVal, + GetMyClass()?.Field.IntVal, + GetMyClass()?.Field?.IntVal, + GetMyClass()?.Property.IntVal, + GetMyClass()?.Property?.IntVal, + GetMyClass()?.Method(GetInt()).IntVal, + GetMyClass()?.Method(GetInt())?.IntVal, + GetMyClass()?[GetInt()].IntVal, + GetMyClass()?[GetInt()]?.IntVal + }; + } + + public int?[] ChainsStruct() + { + return new int?[8] { + GetMyStruct()?.IntVal, + GetMyStruct()?.Field.IntVal, + GetMyStruct()?.Field?.IntVal, + GetMyStruct()?.Property2.IntVal, + GetMyStruct()?.Property1?.IntVal, + GetMyStruct()?.Method2(GetInt()).IntVal, + GetMyStruct()?.Method1(GetInt())?.IntVal, + GetMyStruct()?[GetInt()]?.IntVal + }; + } + + public int CoalescingReturn() + { + return GetMyClass()?.IntVal ?? 1; + } + + public void Coalescing() + { + Use(GetMyClass()?.IntVal ?? 1); + } + + public void CoalescingString() + { + Use(GetMyClass()?.Text ?? "Hello"); + } + + public void CallOnValueTypeField() + { + Use(GetMyClass()?.IntVal.ToString()); + Use(GetMyStruct()?.IntVal.ToString()); + Use(GetMyClass()?.ReadonlyIntVal.ToString()); + Use(GetMyStruct()?.ReadonlyIntVal.ToString()); + GetMyClass()?.StructField.Done(); + GetMyClass()?.ReadonlyStructField.Done(); + } + + public void InvokeDelegate(EventHandler eh) + { + eh?.Invoke(null, EventArgs.Empty); + } + + public int? InvokeDelegate(Func f) + { + return f?.Invoke(); + } + + private void NotNullPropagation(MyClass c) + { + // don't decompile this to "(c?.IntVal ?? 0) != 0" + if (c != null && c.IntVal != 0) + { + Console.WriteLine("non-zero"); + } + if (c == null || c.IntVal == 0) + { + Console.WriteLine("null or zero"); + } + Console.WriteLine("end of method"); + } + + private void Setter(MyClass c) + { + if (c != null) + { + c.IntVal = 1; + } + Console.WriteLine(); + if (c != null) + { + c.Property = null; + } + } + + private static int? GenericUnconstrainedInt(T t) where T : ITest + { + return t?.Int(); + } + + private static int? GenericClassConstraintInt(T t) where T : class, ITest + { + return t?.Int(); + } + + private static int? GenericStructConstraintInt(T? t) where T : struct, ITest + { + return t?.Int(); + } + + private static int? GenericRefUnconstrainedInt(ref T t) where T : ITest + { + return t?.Int(); + } + + private static int? GenericRefClassConstraintInt(ref T t) where T : class, ITest + { + return t?.Int(); + } + + private static int? GenericRefStructConstraintInt(ref T? t) where T : struct, ITest + { + return t?.Int(); + } + + public int? Issue1709(object obj) + { + return (obj as ICollection)?.Count + (obj as ICollection)?.Count; + } + + private static void Issue1689(List setsOfNumbers) + { + Console.WriteLine(setsOfNumbers?[0]?[1].ToString() == "2"); + Console.WriteLine(setsOfNumbers?[1]?[1].ToString() == null); + } + + private static byte[] GetBytes() + { + return null; + } + + private static void ArrayLengthWithFallback() + { + Console.WriteLine(GetBytes()?.Length ?? 0); + Console.WriteLine(GetBytes()?.LongLength ?? 0); + } + + private static dynamic DynamicNullProp(dynamic a) + { + return a?.b.c(1)?.d[10]; + } + + private static string Issue3181() + { +#if EXPECTED_OUTPUT + return ((int?)null)?.ToString(); +#else + int? x = null; + return x?.ToString(); +#endif + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs index 1797541..bad908d 100644 --- a/ICSharpCode.Decompiler/CSharp/CallBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/CallBuilder.cs @@ -1318,8 +1318,8 @@ namespace ICSharpCode.Decompiler.CSharp return false; var firstArgument = argumentList.Arguments[0]; - bool canWriteTypeArguments = !settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType()); - bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0 && canWriteTypeArguments; + bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0 + && (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType())); IType[] typeArguments = writeTypeArguments ? method.TypeArguments.ToArray() : Empty.Array; var directionExpression = firstArgument.Expression as DirectionExpression; @@ -1342,20 +1342,10 @@ namespace ICSharpCode.Decompiler.CSharp string[]? remainingNames = argumentNames?.Take(actualArgumentCount).Skip(1).ToArray(); if (remainingNames != null && remainingNames.All(name => name == null)) remainingNames = null; - var remainingArguments = argumentList.GetArgumentResolveResults(skipCount: 1).ToArray(); - if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames)) + if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, + argumentList.GetArgumentResolveResults(skipCount: 1).ToArray(), remainingNames)) { - // 'a?.M()' has no static call form to fall back to. Spelled-out type arguments rule out - // extension methods of a different arity, which NullPropagationTransform relies on. - if (writeTypeArguments || !canWriteTypeArguments || method.TypeArguments.Count == 0 - || !IsNullConditional(firstArgument.Expression)) - { - return false; - } - writeTypeArguments = true; - typeArguments = method.TypeArguments.ToArray(); - if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames)) - return false; + return false; } Expression targetExpression; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs new file mode 100644 index 0000000..c9c5064 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/Transforms/IntroduceQueryExpressions.cs @@ -0,0 +1,457 @@ +// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.IL; +using ICSharpCode.Decompiler.Semantics; + +namespace ICSharpCode.Decompiler.CSharp.Transforms +{ + /// + /// Decompiles query expressions. + /// Based on C# 4.0 spec, ยง7.16.2 Query expression translation + /// + public class IntroduceQueryExpressions : IAstTransform + { + [AllowNull] + TransformContext context; + + public void Run(AstNode rootNode, TransformContext context) + { + if (!context.Settings.QueryExpressions) + return; + this.context = context; + DecompileQueries(rootNode); + // After all queries were decompiled, detect degenerate queries (queries not property terminated with 'select' or 'group') + // and fix them, either by adding a degenerate select, or by combining them with another query. + foreach (QueryExpression query in rootNode.Descendants.OfType()) + { + QueryFromClause fromClause = (QueryFromClause)query.Clauses.First(); + if (IsDegenerateQuery(query)) + { + // introduce select for degenerate query + context.Step("Add degenerate query select clause", query); + query.Clauses.Add(new QuerySelectClause { Expression = new IdentifierExpression(fromClause.Identifier).CopyAnnotationsFrom(fromClause) }); + } + // See if the data source of this query is a degenerate query, + // and combine the queries if possible. + QueryExpression? innerQuery = fromClause.Expression as QueryExpression; + while (innerQuery != null && IsDegenerateQuery(innerQuery)) + { + QueryFromClause innerFromClause = (QueryFromClause)innerQuery.Clauses.First(); + ILVariable? innerVariable = innerFromClause.Annotation()?.Variable; + ILVariable? rangeVariable = fromClause.Annotation()?.Variable; + context.Step("Combine nested query clauses", fromClause); + // Replace the fromClause with all clauses from the inner query + fromClause.Remove(); + QueryClause? insertionPos = null; + foreach (var clause in innerQuery.Clauses) + { + CombineRangeVariables(clause, innerVariable, rangeVariable); + query.Clauses.InsertAfter(insertionPos, insertionPos = clause.Detach()); + } + context.EndStep(innerFromClause); + fromClause = innerFromClause; + innerQuery = fromClause.Expression as QueryExpression; + } + } + } + + private void CombineRangeVariables(QueryClause clause, ILVariable? oldVariable, ILVariable? newVariable) + { + if (oldVariable == null || newVariable == null) + return; + foreach (var identifier in clause.DescendantNodes().OfType()) + { + var parent = identifier.Parent; + if (parent == null) + continue; + var variable = parent.Annotation()?.Variable; + if (variable == oldVariable) + { + context.Step("Combine query range variables", identifier); + parent.RemoveAnnotations(); + parent.AddAnnotation(new ILVariableResolveResult(newVariable)); + var newIdentifier = Identifier.Create(newVariable.Name!); + identifier.ReplaceWith(newIdentifier); + context.EndStep(newIdentifier); + } + } + } + + bool IsDegenerateQuery(QueryExpression? query) + { + if (query == null) + return false; + var lastClause = query.Clauses.LastOrDefault(); + return !(lastClause is QuerySelectClause || lastClause is QueryGroupClause); + } + + void DecompileQueries(AstNode node) + { + Expression? query = DecompileQuery(node as InvocationExpression); + if (query != null) + { + if (node.Parent is ExpressionStatement && CanUseDiscardAssignment()) + query = new AssignmentExpression(new IdentifierExpression("_"), query); + node.ReplaceWith(query); + context.EndStep(query); + } + + AstNode? next; + for (AstNode? child = (query ?? node).FirstChild; child != null; child = next) + { + // store reference to next child before transformation + next = child.NextSibling; + DecompileQueries(child); + } + } + + bool CanUseDiscardAssignment() + { + // TODO : check whether there exists a variable named '_' in scope. + return context.Settings.Discards; + } + + QueryExpression? DecompileQuery(InvocationExpression? invocation) + { + if (invocation == null) + return null; + MemberReferenceExpression? mre = invocation.Target as MemberReferenceExpression; + if (mre == null || IsNullConditional(mre.Target)) + return null; + if (mre.TypeArguments.Count > 0) + return null; + switch (mre.MemberName) + { + case "Select": + { + if (invocation.Arguments.Count != 1) + return null; + if (!IsComplexQuery(mre)) + return null; + Expression expr = invocation.Arguments.Single(); + if (MatchSimpleLambda(expr, out var parameter, out var body)) + { + context.Step("Build select query", invocation); + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); + query.Clauses.Add(new QuerySelectClause { Expression = WrapExpressionInParenthesesIfNecessary(body.Detach(), parameter.Name!) }.CopyAnnotationsFrom(expr)); + return query; + } + return null; + } + case "GroupBy": + { + if (invocation.Arguments.Count == 2) + { + Expression keyLambda = invocation.Arguments.ElementAt(0); + Expression projectionLambda = invocation.Arguments.ElementAt(1); + if (MatchSimpleLambda(keyLambda, out var parameter1, out var keySelector) + && MatchSimpleLambda(projectionLambda, out var parameter2, out var elementSelector) + && parameter1.Name == parameter2.Name) + { + context.Step("Build group query", invocation); + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(parameter1, mre.Target.Detach())); + var queryGroupClause = new QueryGroupClause { + Projection = elementSelector.Detach(), + Key = keySelector.Detach() + }; + // The matched group-by lambdas always carry their ILFunction annotation. + queryGroupClause.AddAnnotation(new QueryGroupClauseAnnotation(keyLambda.Annotation()!, projectionLambda.Annotation()!)); + query.Clauses.Add(queryGroupClause); + return query; + } + } + else if (invocation.Arguments.Count == 1) + { + Expression lambda = invocation.Arguments.Single(); + if (MatchSimpleLambda(lambda, out var parameter, out var keySelector)) + { + context.Step("Build group query", invocation); + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); + query.Clauses.Add(new QueryGroupClause { Projection = new IdentifierExpression(parameter.Name!).CopyAnnotationsFrom(parameter), Key = keySelector.Detach() }); + return query; + } + } + return null; + } + case "SelectMany": + { + if (invocation.Arguments.Count != 2) + return null; + var fromExpressionLambda = invocation.Arguments.ElementAt(0); + if (!MatchSimpleLambda(fromExpressionLambda, out var parameter, out var collectionSelector)) + return null; + if (IsNullConditional(collectionSelector)) + return null; + LambdaExpression? lambda = invocation.Arguments.ElementAt(1) as LambdaExpression; + if (lambda != null && lambda.Parameters.Count == 2 && lambda.Body is Expression) + { + ParameterDeclaration p1 = lambda.Parameters.ElementAt(0); + ParameterDeclaration p2 = lambda.Parameters.ElementAt(1); + if (p1.Name == parameter.Name) + { + context.Step("Build select-many query", invocation); + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(p1, mre.Target.Detach())); + query.Clauses.Add(MakeFromClause(p2, collectionSelector.Detach()).CopyAnnotationsFrom(fromExpressionLambda)); + query.Clauses.Add(new QuerySelectClause { Expression = WrapExpressionInParenthesesIfNecessary(((Expression)lambda.Body).Detach(), parameter.Name!) }); + return query; + } + } + return null; + } + case "Where": + { + if (invocation.Arguments.Count != 1) + return null; + if (!IsComplexQuery(mre)) + return null; + Expression expr = invocation.Arguments.Single(); + if (MatchSimpleLambda(expr, out var parameter, out var body)) + { + context.Step("Build where query", invocation); + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); + query.Clauses.Add(new QueryWhereClause { Condition = body.Detach() }.CopyAnnotationsFrom(expr)); + return query; + } + return null; + } + case "OrderBy": + case "OrderByDescending": + case "ThenBy": + case "ThenByDescending": + { + if (invocation.Arguments.Count != 1) + return null; + if (!IsComplexQuery(mre)) + return null; + var lambda = invocation.Arguments.Single(); + if (MatchSimpleLambda(lambda, out var parameter, out var orderExpression)) + { + if (ValidateThenByChain(invocation, parameter.Name!)) + { + context.Step("Build order query", invocation); + QueryOrderClause orderClause = new QueryOrderClause(); + while (mre.MemberName == "ThenBy" || mre.MemberName == "ThenByDescending") + { + // insert new ordering at beginning + orderClause.Orderings.InsertAfter( + null, new QueryOrdering { + Expression = orderExpression.Detach(), + Direction = (mre.MemberName == "ThenBy" ? QueryOrderingDirection.None : QueryOrderingDirection.Descending) + }.CopyAnnotationsFrom(lambda)); + + InvocationExpression tmp = (InvocationExpression)mre.Target; + mre = (MemberReferenceExpression)tmp.Target; + lambda = tmp.Arguments.Single(); + // ValidateThenByChain already confirmed every clause in the chain is a simple + // lambda, so this match always succeeds. + bool matched = MatchSimpleLambda(lambda, out parameter, out orderExpression); + Debug.Assert(matched); + Debug.Assert(orderExpression != null); + } + // The chain was validated and every iteration re-matched a simple lambda, + // so the final range-variable parameter is set. + Debug.Assert(parameter != null); + // insert new ordering at beginning + orderClause.Orderings.InsertAfter( + null, new QueryOrdering { + Expression = orderExpression.Detach(), + Direction = (mre.MemberName == "OrderBy" ? QueryOrderingDirection.None : QueryOrderingDirection.Descending) + }.CopyAnnotationsFrom(lambda)); + + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(parameter, mre.Target.Detach())); + query.Clauses.Add(orderClause); + return query; + } + } + return null; + } + case "Join": + case "GroupJoin": + { + if (invocation.Arguments.Count != 4) + return null; + Expression source1 = mre.Target; + Expression source2 = invocation.Arguments.ElementAt(0); + if (IsNullConditional(source2)) + return null; + Expression outerLambda = invocation.Arguments.ElementAt(1); + if (!MatchSimpleLambda(outerLambda, out var element1, out var key1)) + return null; + Expression innerLambda = invocation.Arguments.ElementAt(2); + if (!MatchSimpleLambda(innerLambda, out var element2, out var key2)) + return null; + LambdaExpression? lambda = invocation.Arguments.ElementAt(3) as LambdaExpression; + if (lambda != null && lambda.Parameters.Count == 2 && lambda.Body is Expression) + { + ParameterDeclaration p1 = lambda.Parameters.ElementAt(0); + ParameterDeclaration p2 = lambda.Parameters.ElementAt(1); + if (ValidateParameter(p1) && ValidateParameter(p2) + && p1.Name == element1.Name && (p2.Name == element2.Name || mre.MemberName == "GroupJoin")) + { + context.Step(mre.MemberName == "GroupJoin" ? "Build group join query" : "Build join query", invocation); + QueryExpression query = new QueryExpression(); + query.Clauses.Add(MakeFromClause(element1, source1.Detach())); + QueryJoinClause joinClause = new QueryJoinClause(); + joinClause.JoinIdentifier = element2.Name!; // join elementName2 + joinClause.JoinIdentifierToken.CopyAnnotationsFrom(element2); + joinClause.InExpression = source2.Detach(); // in source2 + joinClause.OnExpression = key1.Detach(); // on key1 + joinClause.EqualsExpression = key2.Detach(); // equals key2 + if (mre.MemberName == "GroupJoin") + { + joinClause.IntoIdentifier = p2.Name; // into p2.Name + joinClause.IntoIdentifierToken!.CopyAnnotationsFrom(p2); + } + // The matched join key-selector lambdas always carry their ILFunction annotation. + joinClause.AddAnnotation(new QueryJoinClauseAnnotation(outerLambda.Annotation()!, innerLambda.Annotation()!)); + query.Clauses.Add(joinClause); + query.Clauses.Add(new QuerySelectClause { Expression = ((Expression)lambda.Body).Detach() }.CopyAnnotationsFrom(lambda)); + return query; + } + } + return null; + } + default: + return null; + } + } + + static bool IsComplexQuery(MemberReferenceExpression mre) + { + return (mre.Target is InvocationExpression && mre.Parent is InvocationExpression) || mre.Parent?.Parent is QueryClause; + } + + QueryFromClause MakeFromClause(ParameterDeclaration parameter, Expression body) + { + QueryFromClause fromClause = new QueryFromClause { + Identifier = parameter.Name!, + Expression = body + }; + fromClause.CopyAnnotationsFrom(parameter); + return fromClause; + } + + class ApplyAnnotationVisitor : DepthFirstAstVisitor + { + private LetIdentifierAnnotation annotation; + private string identifier; + + public ApplyAnnotationVisitor(LetIdentifierAnnotation annotation, string identifier) + { + this.annotation = annotation; + this.identifier = identifier; + } + + public override AstNode VisitIdentifier(Identifier identifier) + { + if (identifier.Name == this.identifier) + identifier.AddAnnotation(annotation); + return identifier; + } + } + + bool IsNullConditional(Expression target) => target switch { + UnaryOperatorExpression { Operator: UnaryOperatorType.NullConditional } => true, + MemberReferenceExpression member => IsNullConditional(member.Target), + InvocationExpression invocation => IsNullConditional(invocation.Target), + IndexerExpression { Target: { } indexerTarget } => IsNullConditional(indexerTarget), + _ => false + }; + + /// + /// This fixes #437: Decompilation of query expression loses material parentheses + /// We wrap the expression in parentheses if: + /// - the Select-call is explicit (see caller(s)) + /// - the expression is a plain identifier matching the parameter name + /// + Expression WrapExpressionInParenthesesIfNecessary(Expression expression, string parameterName) + { + if (expression is IdentifierExpression ident && parameterName.Equals(ident.Identifier, StringComparison.Ordinal)) + return new ParenthesizedExpression(expression); + return expression; + } + + /// + /// Ensure that all ThenBy's are correct, and that the list of ThenBy's is terminated by an 'OrderBy' invocation. + /// + bool ValidateThenByChain(InvocationExpression? invocation, string expectedParameterName) + { + if (invocation == null || invocation.Arguments.Count != 1) + return false; + if (!(invocation.Target is MemberReferenceExpression mre)) + return false; + if (!MatchSimpleLambda(invocation.Arguments.Single(), out var parameter, out _)) + return false; + if (parameter.Name != expectedParameterName) + return false; + if (mre.TypeArguments.Count > 0) + return false; + + if (mre.MemberName == "OrderBy" || mre.MemberName == "OrderByDescending") + return !IsNullConditional(mre.Target); + else if (mre.MemberName == "ThenBy" || mre.MemberName == "ThenByDescending") + return ValidateThenByChain(mre.Target as InvocationExpression, expectedParameterName); + else + return false; + } + + /// Matches simple lambdas of the form "a => b" + bool MatchSimpleLambda(Expression expr, [NotNullWhen(true)] out ParameterDeclaration? parameter, [NotNullWhen(true)] out Expression? body) + { + if (expr is LambdaExpression lambda && lambda.Parameters.Count == 1 && lambda.Body is Expression) + { + if (lambda.GetResolveResult() is DecompiledLambdaResolveResult { AttemptedConversionWithTypeMismatch: true }) + { + parameter = null; + body = null; + return false; + } + ParameterDeclaration p = lambda.Parameters.Single(); + if (ValidateParameter(p)) + { + parameter = p; + body = (Expression)lambda.Body; + return true; + } + } + parameter = null; + body = null; + return false; + } + + private static bool ValidateParameter(ParameterDeclaration p) + { + return p.ParameterModifier == Decompiler.TypeSystem.ReferenceKind.None && p.Attributes.Count == 0; + } + } +}