diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs index 618f0d5af..669c9c4e4 100644 --- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs +++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/CSharpOutputVisitor.cs @@ -1112,6 +1112,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor var opSymbol = UnaryOperatorExpression.GetOperatorRole(opType); if (opType == UnaryOperatorType.Await) { WriteKeyword(opSymbol); + Space(); } else if (!IsPostfixOperator(opType) && opSymbol != null) { WriteToken(opSymbol); } @@ -1863,6 +1864,9 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor public virtual void VisitUsingStatement(UsingStatement usingStatement) { StartNode(usingStatement); + if (usingStatement.IsAsync) { + WriteKeyword(UsingStatement.AwaitRole); + } WriteKeyword(UsingStatement.UsingKeywordRole); Space(policy.SpaceBeforeUsingParentheses); LPar(); diff --git a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs index 06ea73131..735c97f5a 100644 --- a/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/StatementBuilder.cs @@ -411,14 +411,29 @@ namespace ICSharpCode.Decompiler.CSharp return transformed; AstNode usingInit = resource; var var = inst.Variable; - if (!inst.ResourceExpression.MatchLdNull() && !NullableType.GetUnderlyingType(var.Type).GetAllBaseTypes().Any(b => b.IsKnownType(KnownTypeCode.IDisposable))) { + KnownTypeCode knownTypeCode; + IType disposeType; + string disposeTypeMethodName; + if (inst.IsAsync) { + knownTypeCode = KnownTypeCode.IAsyncDisposable; + disposeType = exprBuilder.compilation.FindType(KnownTypeCode.IAsyncDisposable); + disposeTypeMethodName = "DisposeAsync"; + } else { + knownTypeCode = KnownTypeCode.IDisposable; + disposeType = exprBuilder.compilation.FindType(KnownTypeCode.IDisposable); + disposeTypeMethodName = "Dispose"; + } + if (!inst.ResourceExpression.MatchLdNull() && !NullableType.GetUnderlyingType(var.Type).GetAllBaseTypes().Any(b => b.IsKnownType(knownTypeCode))) { Debug.Assert(var.Kind == VariableKind.UsingLocal); var.Kind = VariableKind.Local; - var disposeType = exprBuilder.compilation.FindType(KnownTypeCode.IDisposable); var disposeVariable = currentFunction.RegisterVariable( VariableKind.Local, disposeType, AssignVariableNames.GenerateVariableName(currentFunction, disposeType) ); + Expression disposeInvocation = new InvocationExpression(new MemberReferenceExpression(exprBuilder.ConvertVariable(disposeVariable).Expression, disposeTypeMethodName)); + if (inst.IsAsync) { + disposeInvocation = new UnaryOperatorExpression { Expression = disposeInvocation, Operator = UnaryOperatorType.Await }; + } return new BlockStatement { new ExpressionStatement(new AssignmentExpression(exprBuilder.ConvertVariable(var).Expression, resource.Detach())), new TryCatchStatement { @@ -427,7 +442,7 @@ namespace ICSharpCode.Decompiler.CSharp new ExpressionStatement(new AssignmentExpression(exprBuilder.ConvertVariable(disposeVariable).Expression, new AsExpression(exprBuilder.ConvertVariable(var).Expression, exprBuilder.ConvertType(disposeType)))), new IfElseStatement { Condition = new BinaryOperatorExpression(exprBuilder.ConvertVariable(disposeVariable), BinaryOperatorType.InEquality, new NullReferenceExpression()), - TrueStatement = new ExpressionStatement(new InvocationExpression(new MemberReferenceExpression(exprBuilder.ConvertVariable(disposeVariable).Expression, "Dispose"))) + TrueStatement = new ExpressionStatement(disposeInvocation) } } }, @@ -441,6 +456,7 @@ namespace ICSharpCode.Decompiler.CSharp } return new UsingStatement { ResourceAcquisition = usingInit, + IsAsync = inst.IsAsync, EmbeddedStatement = ConvertAsBlock(inst.Body) }; } diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs index 814819e8d..b8ad576a8 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs @@ -28,57 +28,67 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax { /// - /// using (ResourceAcquisition) EmbeddedStatement + /// [ await ] using (ResourceAcquisition) EmbeddedStatement /// public class UsingStatement : Statement { - public static readonly TokenRole UsingKeywordRole = new TokenRole ("using"); + public static readonly TokenRole UsingKeywordRole = new TokenRole("using"); + public static readonly TokenRole AwaitRole = UnaryOperatorExpression.AwaitRole; public static readonly Role ResourceAcquisitionRole = new Role("ResourceAcquisition", AstNode.Null); - + public CSharpTokenNode UsingToken { - get { return GetChildByRole (UsingKeywordRole); } + get { return GetChildByRole(UsingKeywordRole); } + } + + public CSharpTokenNode AwaitToken { + get { return GetChildByRole(AwaitRole); } } - + + public bool IsAsync { + get { return !GetChildByRole(AwaitRole).IsNull; } + set { SetChildByRole(AwaitRole, value ? new CSharpTokenNode(TextLocation.Empty, null) : null); } + } + public CSharpTokenNode LParToken { - get { return GetChildByRole (Roles.LPar); } + get { return GetChildByRole(Roles.LPar); } } - + /// /// Either a VariableDeclarationStatement, or an Expression. /// public AstNode ResourceAcquisition { - get { return GetChildByRole (ResourceAcquisitionRole); } - set { SetChildByRole (ResourceAcquisitionRole, value); } + get { return GetChildByRole(ResourceAcquisitionRole); } + set { SetChildByRole(ResourceAcquisitionRole, value); } } - + public CSharpTokenNode RParToken { - get { return GetChildByRole (Roles.RPar); } + get { return GetChildByRole(Roles.RPar); } } - + public Statement EmbeddedStatement { - get { return GetChildByRole (Roles.EmbeddedStatement); } - set { SetChildByRole (Roles.EmbeddedStatement, value); } + get { return GetChildByRole(Roles.EmbeddedStatement); } + set { SetChildByRole(Roles.EmbeddedStatement, value); } } - - public override void AcceptVisitor (IAstVisitor visitor) + + public override void AcceptVisitor(IAstVisitor visitor) { - visitor.VisitUsingStatement (this); + visitor.VisitUsingStatement(this); } - - public override T AcceptVisitor (IAstVisitor visitor) + + public override T AcceptVisitor(IAstVisitor visitor) { - return visitor.VisitUsingStatement (this); + return visitor.VisitUsingStatement(this); } - - public override S AcceptVisitor (IAstVisitor visitor, T data) + + public override S AcceptVisitor(IAstVisitor visitor, T data) { - return visitor.VisitUsingStatement (this, data); + return visitor.VisitUsingStatement(this, data); } - + protected internal override bool DoMatch(AstNode other, PatternMatching.Match match) { UsingStatement o = other as UsingStatement; - return o != null && this.ResourceAcquisition.DoMatch(o.ResourceAcquisition, match) && this.EmbeddedStatement.DoMatch(o.EmbeddedStatement, match); + return o != null && this.IsAsync == o.IsAsync && this.ResourceAcquisition.DoMatch(o.ResourceAcquisition, match) && this.EmbeddedStatement.DoMatch(o.EmbeddedStatement, match); } } } diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index febdbd6c1..fed45dec5 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -108,13 +108,14 @@ namespace ICSharpCode.Decompiler if (languageVersion < CSharp.LanguageVersion.CSharp8_0) { nullableReferenceTypes = false; readOnlyMethods = false; + asyncUsingAndForEachStatement = false; asyncEnumerator = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator) + if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator || asyncUsingAndForEachStatement) return CSharp.LanguageVersion.CSharp8_0; if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers) return CSharp.LanguageVersion.CSharp7_3; @@ -876,6 +877,20 @@ namespace ICSharpCode.Decompiler } } + bool asyncUsingAndForEachStatement = true; + + [Category("C# 8.0 / VS 2019")] + [Description("DecompilerSettings.DetectAsyncUsingAndForeachStatements")] + public bool AsyncUsingAndForEachStatement { + get { return asyncUsingAndForEachStatement; } + set { + if (asyncUsingAndForEachStatement != value) { + asyncUsingAndForEachStatement = value; + OnPropertyChanged(); + } + } + } + bool introduceUnmanagedConstraint = true; /// diff --git a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs index 61913e619..f44d30459 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/UsingInstruction.cs @@ -36,10 +36,16 @@ namespace ICSharpCode.Decompiler.IL /// partial class UsingInstruction { + public bool IsAsync { get; set; } + public override void WriteTo(ITextOutput output, ILAstWritingOptions options) { WriteILRange(output, options); - output.Write("using ("); + output.Write("using"); + if (IsAsync) { + output.Write(".async"); + } + output.Write(" ("); Variable.WriteTo(output); output.Write(" = "); ResourceExpression.WriteTo(output, options); diff --git a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs index 86e0cecf2..31c8c91d5 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/UsingTransform.cs @@ -34,7 +34,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (!context.Settings.UsingStatement) return; this.context = context; for (int i = block.Instructions.Count - 1; i >= 0; i--) { - if (!TransformUsing(block, i) && !TransformUsingVB(block, i)) + if (!TransformUsing(block, i) && !TransformUsingVB(block, i) && !TransformAsyncUsing(block, i)) continue; // This happens in some cases: // Use correct index after transformation. @@ -167,7 +167,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } - bool MatchDisposeBlock(BlockContainer container, ILVariable objVar, bool usingNull) + bool MatchDisposeBlock(BlockContainer container, ILVariable objVar, bool usingNull, in string disposeMethodFullName = "System.IDisposable.Dispose", KnownTypeCode disposeTypeCode = KnownTypeCode.IDisposable) { var entryPoint = container.EntryPoint; if (entryPoint.Instructions.Count < 2 || entryPoint.Instructions.Count > 3 || entryPoint.IncomingEdgeCount != 1) @@ -180,17 +180,17 @@ namespace ICSharpCode.Decompiler.IL.Transforms if (castIndex > -1) { if (!entryPoint.Instructions[castIndex].MatchStLoc(out var tempVar, out var isinst)) return false; - if (!isinst.MatchIsInst(out var load, out var disposableType) || !load.MatchLdLoc(objVar) || !disposableType.IsKnownType(KnownTypeCode.IDisposable)) + if (!isinst.MatchIsInst(out var load, out var disposableType) || !load.MatchLdLoc(objVar) || !disposableType.IsKnownType(disposeTypeCode)) return false; if (!tempVar.IsSingleDefinition) return false; isReference = true; - if (!MatchDisposeCheck(tempVar, checkInst, isReference, usingNull, out int numObjVarLoadsInCheck)) + if (!MatchDisposeCheck(tempVar, checkInst, isReference, usingNull, out int numObjVarLoadsInCheck, disposeMethodFullName, disposeTypeCode)) return false; if (tempVar.LoadCount != numObjVarLoadsInCheck) return false; } else { - if (!MatchDisposeCheck(objVar, checkInst, isReference, usingNull, out _)) + if (!MatchDisposeCheck(objVar, checkInst, isReference, usingNull, out _, disposeMethodFullName, disposeTypeCode)) return false; } if (!entryPoint.Instructions[leaveIndex].MatchLeave(container, out var returnValue) || !returnValue.MatchNop()) @@ -198,9 +198,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms return true; } - bool MatchDisposeCheck(ILVariable objVar, ILInstruction checkInst, bool isReference, bool usingNull, out int numObjVarLoadsInCheck) + bool MatchDisposeCheck(ILVariable objVar, ILInstruction checkInst, bool isReference, bool usingNull, out int numObjVarLoadsInCheck, in string disposeMethodFullName, KnownTypeCode disposeTypeCode) { numObjVarLoadsInCheck = 2; + ILInstruction disposeInvocation; CallVirt callVirt; if (objVar.Type.IsKnownType(KnownTypeCode.NullableOfT)) { if (checkInst.MatchIfInstruction(out var condition, out var disposeInst)) { @@ -208,22 +209,27 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; if (!(disposeInst is Block disposeBlock) || disposeBlock.Instructions.Count != 1) return false; - callVirt = disposeBlock.Instructions[0] as CallVirt; + disposeInvocation = disposeBlock.Instructions[0]; } else if (checkInst.MatchNullableRewrap(out disposeInst)) { - callVirt = disposeInst as CallVirt; + disposeInvocation = disposeInst; } else { return false; } + if (disposeTypeCode == KnownTypeCode.IAsyncDisposable) { + if (!UnwrapAwait(ref disposeInvocation)) + return false; + } + callVirt = disposeInvocation as CallVirt; if (callVirt == null) return false; - if (callVirt.Method.FullName != "System.IDisposable.Dispose") + if (callVirt.Method.FullName != disposeMethodFullName) return false; if (callVirt.Method.Parameters.Count > 0) return false; if (callVirt.Arguments.Count != 1) return false; var firstArg = callVirt.Arguments.FirstOrDefault(); - if (!(firstArg.MatchUnboxAny(out var innerArg1, out var unboxType) && unboxType.IsKnownType(KnownTypeCode.IDisposable))) { + if (!(firstArg.MatchUnboxAny(out var innerArg1, out var unboxType) && unboxType.IsKnownType(disposeTypeCode))) { if (!firstArg.MatchAddressOf(out var innerArg2, out _)) return false; return NullableLiftingTransform.MatchGetValueOrDefault(innerArg2, objVar) @@ -255,7 +261,12 @@ namespace ICSharpCode.Decompiler.IL.Transforms return false; if (!(disposeInst is Block disposeBlock) || disposeBlock.Instructions.Count != 1) return false; - if (!(disposeBlock.Instructions[0] is CallVirt cv)) + disposeInvocation = disposeBlock.Instructions[0]; + if (disposeTypeCode == KnownTypeCode.IAsyncDisposable) { + if (!UnwrapAwait(ref disposeInvocation)) + return false; + } + if (!(disposeInvocation is CallVirt cv)) return false; target = cv.Arguments.FirstOrDefault(); if (target == null) @@ -264,6 +275,10 @@ namespace ICSharpCode.Decompiler.IL.Transforms target = newTarget; callVirt = cv; } else { + if (disposeTypeCode == KnownTypeCode.IAsyncDisposable) { + if (!UnwrapAwait(ref checkInst)) + return false; + } if (!(checkInst is CallVirt cv)) return false; target = cv.Arguments.FirstOrDefault(); @@ -275,7 +290,7 @@ namespace ICSharpCode.Decompiler.IL.Transforms } callVirt = cv; } - if (callVirt.Method.FullName != "System.IDisposable.Dispose") + if (callVirt.Method.FullName != disposeMethodFullName) return false; if (callVirt.Method.Parameters.Count > 0) return false; @@ -286,8 +301,74 @@ namespace ICSharpCode.Decompiler.IL.Transforms || (usingNull && callVirt.Arguments[0].MatchLdNull()) || (isReference && checkInst is NullableRewrap && target.MatchIsInst(out var arg, out var type2) - && arg.MatchLdLoc(objVar) && type2.IsKnownType(KnownTypeCode.IDisposable)); + && arg.MatchLdLoc(objVar) && type2.IsKnownType(disposeTypeCode)); } } + + /// + /// stloc test(resourceExpression) + /// .try BlockContainer { + /// Block IL_002b (incoming: 1) { + /// call Use(ldloc test) + /// leave IL_002b (nop) + /// } + /// + /// } finally BlockContainer { + /// Block IL_0045 (incoming: 1) { + /// if (comp.o(ldloc test == ldnull)) leave IL_0045 (nop) + /// br IL_00ae + /// } + /// + /// Block IL_00ae (incoming: 1) { + /// await(addressof System.Threading.Tasks.ValueTask(callvirt DisposeAsync(ldloc test))) + /// leave IL_0045 (nop) + /// } + /// + /// } + /// + private bool TransformAsyncUsing(Block block, int i) + { + if (i < 1 || !context.Settings.AsyncUsingAndForEachStatement) return false; + if (!(block.Instructions[i] is TryFinally tryFinally) || !(block.Instructions[i - 1] is StLoc storeInst)) + return false; + if (!CheckAsyncResourceType(storeInst.Variable.Type)) + return false; + if (storeInst.Variable.Kind != VariableKind.Local) + return false; + if (storeInst.Variable.LoadInstructions.Any(ld => !ld.IsDescendantOf(tryFinally))) + return false; + if (storeInst.Variable.AddressInstructions.Any(la => !la.IsDescendantOf(tryFinally) || (la.IsDescendantOf(tryFinally.TryBlock) && !ILInlining.IsUsedAsThisPointerInCall(la)))) + return false; + if (storeInst.Variable.StoreInstructions.Count > 1) + return false; + if (!(tryFinally.FinallyBlock is BlockContainer container) || !MatchDisposeBlock(container, storeInst.Variable, usingNull: false, "System.IAsyncDisposable.DisposeAsync", KnownTypeCode.IAsyncDisposable)) + return false; + context.Step("AsyncUsingTransform", tryFinally); + storeInst.Variable.Kind = VariableKind.UsingLocal; + block.Instructions.RemoveAt(i); + block.Instructions[i - 1] = new UsingInstruction(storeInst.Variable, storeInst.Value, tryFinally.TryBlock) { IsAsync = true } + .WithILRange(storeInst); + return true; + } + + bool CheckAsyncResourceType(IType type) + { + if (NullableType.GetUnderlyingType(type).GetAllBaseTypes().Any(b => b.IsKnownType(KnownTypeCode.IAsyncDisposable))) + return true; + return false; + } + + bool UnwrapAwait(ref ILInstruction awaitInstruction) + { + if (awaitInstruction == null) + return false; + if (!awaitInstruction.MatchAwait(out var arg)) + return false; + if (!arg.MatchAddressOf(out awaitInstruction, out var type)) + return false; + if (!type.IsKnownType(KnownTypeCode.ValueTask)) + return false; + return true; + } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs b/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs index e9c100d31..805f95b14 100644 --- a/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs +++ b/ICSharpCode.Decompiler/TypeSystem/KnownTypeReference.cs @@ -115,10 +115,16 @@ namespace ICSharpCode.Decompiler.TypeSystem Task, /// System.Threading.Tasks.Task{T} TaskOfT, + /// System.Threading.Tasks.ValueTask + ValueTask, + /// System.Threading.Tasks.ValueTask{T} + ValueTaskOfT, /// System.Nullable{T} NullableOfT, /// System.IDisposable IDisposable, + /// System.IAsyncDisposable + IAsyncDisposable, /// System.Runtime.CompilerServices.INotifyCompletion INotifyCompletion, /// System.Runtime.CompilerServices.ICriticalNotifyCompletion @@ -193,10 +199,13 @@ namespace ICSharpCode.Decompiler.TypeSystem new KnownTypeReference(KnownTypeCode.IReadOnlyCollectionOfT, TypeKind.Interface, "System.Collections.Generic", "IReadOnlyCollection", 1), new KnownTypeReference(KnownTypeCode.IReadOnlyListOfT, TypeKind.Interface, "System.Collections.Generic", "IReadOnlyList", 1), - new KnownTypeReference(KnownTypeCode.Task, TypeKind.Class, "System.Threading.Tasks", "Task"), - new KnownTypeReference(KnownTypeCode.TaskOfT, TypeKind.Class, "System.Threading.Tasks", "Task", 1, baseType: KnownTypeCode.Task), + new KnownTypeReference(KnownTypeCode.Task, TypeKind.Class, "System.Threading.Tasks", "Task"), + new KnownTypeReference(KnownTypeCode.TaskOfT, TypeKind.Class, "System.Threading.Tasks", "Task", 1, baseType: KnownTypeCode.Task), + new KnownTypeReference(KnownTypeCode.ValueTask, TypeKind.Struct, "System.Threading.Tasks", "ValueTask"), + new KnownTypeReference(KnownTypeCode.ValueTaskOfT, TypeKind.Struct, "System.Threading.Tasks", "ValueTask", 1), new KnownTypeReference(KnownTypeCode.NullableOfT, TypeKind.Struct, "System", "Nullable", 1), new KnownTypeReference(KnownTypeCode.IDisposable, TypeKind.Interface, "System", "IDisposable"), + new KnownTypeReference(KnownTypeCode.IAsyncDisposable, TypeKind.Interface, "System", "IAsyncDisposable"), new KnownTypeReference(KnownTypeCode.INotifyCompletion, TypeKind.Interface, "System.Runtime.CompilerServices", "INotifyCompletion"), new KnownTypeReference(KnownTypeCode.ICriticalNotifyCompletion, TypeKind.Interface, "System.Runtime.CompilerServices", "ICriticalNotifyCompletion"), diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index d51ba0b1f..dd8e2a829 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -673,6 +673,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Detect awaited using and foreach statements. + /// + public static string DecompilerSettings_DetectAsyncUsingAndForeachStatements { + get { + return ResourceManager.GetString("DecompilerSettings.DetectAsyncUsingAndForeachStatements", resourceCulture); + } + } + /// /// Looks up a localized string similar to Detect foreach statements. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index dbe93bffd..7aa7db1df 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -447,9 +447,6 @@ Version - - Culture - Public Key Token @@ -775,4 +772,7 @@ Are you sure you want to continue? Read-only methods + + Detect awaited using and foreach statements + \ No newline at end of file