Upstream icsharpcode/ILSpy state of the touched files before 53822318d9

This commit is contained in:
upstream committed 2026-10-10 13:03:54 +02:00
1 parent 3f1ef717b6
commit 8e3e41fedb
4 files changed
+908 -14

No files matched your search

@@ -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<T>, so that form does not compile.
ByRefLikeStruct RefStructNotCoalescing(RefStructHolder holder)
{
return holder != null ? holder.Get() : default(ByRefLikeStruct);
}
#endif
}
}
@@ -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<T1, T2>
{
public GenericStruct<T1, T2> Other;
}
private struct GenericStruct<T1, T2>
{
public T1 Field1;
public T2 Field2;
public Container<T1, T2> 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 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<int> 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 t) where T : ITest
{
return t?.Int();
}
private static int? GenericClassConstraintInt<T>(T t) where T : class, ITest
{
return t?.Int();
}
private static int? GenericStructConstraintInt<T>(T? t) where T : struct, ITest
{
return t?.Int();
}
private static int? GenericRefUnconstrainedInt<T>(ref T t) where T : ITest
{
return t?.Int();
}
private static int? GenericRefClassConstraintInt<T>(ref T t) where T : class, ITest
{
return t?.Int();
}
private static int? GenericRefStructConstraintInt<T>(ref T? t) where T : struct, ITest
{
return t?.Int();
}
public int? Issue1709(object obj)
{
return (obj as ICollection)?.Count + (obj as ICollection<int>)?.Count;
}
private static void Issue1689(List<byte[]> 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
}
}
}
+4 -14
View File
@@ -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<IType>.Array;
var directionExpression = firstArgument.Expression as DirectionExpression;
@@ -1342,19 +1342,9 @@ 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;
}
@@ -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
{
/// <summary>
/// Decompiles query expressions.
/// Based on C# 4.0 spec, §7.16.2 Query expression translation
/// </summary>
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<QueryExpression>())
{
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<ILVariableResolveResult>()?.Variable;
ILVariable? rangeVariable = fromClause.Annotation<ILVariableResolveResult>()?.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<Identifier>())
{
var parent = identifier.Parent;
if (parent == null)
continue;
var variable = parent.Annotation<ILVariableResolveResult>()?.Variable;
if (variable == oldVariable)
{
context.Step("Combine query range variables", identifier);
parent.RemoveAnnotations<ILVariableResolveResult>();
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<IL.ILFunction>()!, projectionLambda.Annotation<IL.ILFunction>()!));
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<IL.ILFunction>()!, innerLambda.Annotation<IL.ILFunction>()!));
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<AstNode>
{
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
};
/// <summary>
/// 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
/// </summary>
Expression WrapExpressionInParenthesesIfNecessary(Expression expression, string parameterName)
{
if (expression is IdentifierExpression ident && parameterName.Equals(ident.Identifier, StringComparison.Ordinal))
return new ParenthesizedExpression(expression);
return expression;
}
/// <summary>
/// Ensure that all ThenBy's are correct, and that the list of ThenBy's is terminated by an 'OrderBy' invocation.
/// </summary>
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;
}
/// <summary>Matches simple lambdas of the form "a => b"</summary>
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;
}
}
}