diff --git a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
index 0c67e1035..3702f04d2 100644
--- a/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/CSharpDecompiler.cs
@@ -305,6 +305,8 @@ namespace ICSharpCode.Decompiler.CSharp
return true;
if (settings.AsyncAwait && AsyncAwaitDecompiler.IsCompilerGeneratedStateMachine(typeHandle, metadata))
return true;
+ if (settings.AsyncEnumerator && AsyncAwaitDecompiler.IsCompilerGeneratorAsyncEnumerator(typeHandle, metadata))
+ return true;
if (settings.FixedBuffers && name.StartsWith("<", StringComparison.Ordinal) && name.Contains("__FixedBuffer"))
return true;
} else if (type.IsCompilerGenerated(metadata)) {
diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs
index 40aa62e4a..febdbd6c1 100644
--- a/ICSharpCode.Decompiler/DecompilerSettings.cs
+++ b/ICSharpCode.Decompiler/DecompilerSettings.cs
@@ -108,12 +108,13 @@ namespace ICSharpCode.Decompiler
if (languageVersion < CSharp.LanguageVersion.CSharp8_0) {
nullableReferenceTypes = false;
readOnlyMethods = false;
+ asyncEnumerator = false;
}
}
public CSharp.LanguageVersion GetMinimumRequiredVersion()
{
- if (nullableReferenceTypes || readOnlyMethods)
+ if (nullableReferenceTypes || readOnlyMethods || asyncEnumerator)
return CSharp.LanguageVersion.CSharp8_0;
if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers)
return CSharp.LanguageVersion.CSharp7_3;
@@ -273,6 +274,24 @@ namespace ICSharpCode.Decompiler
}
}
+ bool asyncEnumerator = true;
+
+ ///
+ /// Decompile IAsyncEnumerator/IAsyncEnumerable.
+ /// Only has an effect if is enabled.
+ ///
+ [Category("C# 8.0 / VS 2019")]
+ [Description("DecompilerSettings.AsyncEnumerator")]
+ public bool AsyncEnumerator {
+ get { return asyncEnumerator; }
+ set {
+ if (asyncEnumerator != value) {
+ asyncEnumerator = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
bool decimalConstants = true;
///
@@ -846,7 +865,7 @@ namespace ICSharpCode.Decompiler
bool readOnlyMethods = true;
[Category("C# 8.0 / VS 2019")]
- [Description("DecompilerSettings.IsReadOnlyAttributeShouldBeReplacedWithReadonlyInModifiersOnStructsParameters")]
+ [Description("DecompilerSettings.ReadOnlyMethods")]
public bool ReadOnlyMethods {
get { return readOnlyMethods; }
set {
diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
index 63610c57e..1919ec106 100644
--- a/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
+++ b/ICSharpCode.Decompiler/IL/ControlFlow/AsyncAwaitDecompiler.cs
@@ -62,14 +62,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
{
Void,
Task,
- TaskOfT
+ TaskOfT,
+ AsyncEnumerator,
+ AsyncEnumerable
}
ILTransformContext context;
- // These fields are set by MatchTaskCreationPattern()
- IType taskType; // return type of the async method
- IType underlyingReturnType; // return type of the method (only the "T" for Task{T})
+ // These fields are set by MatchTaskCreationPattern() or MatchEnumeratorCreationNewObj()
+ IType taskType; // return type of the async method; or IAsyncEnumerable{T}/IAsyncEnumerator{T}
+ IType underlyingReturnType; // return type of the method (only the "T" for Task{T}), for async enumerators this is the type being yielded
AsyncMethodType methodType;
ITypeDefinition stateMachineType;
ITypeDefinition builderType;
@@ -84,9 +86,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
ILVariable cachedStateVar; // variable in MoveNext that caches the stateField.
TryCatch mainTryCatch;
Block setResultAndExitBlock; // block that is jumped to for return statements
+ // Note: for async enumerators, a jump to setResultAndExitBlock is a 'yield break;'
int finalState; // final state after the setResultAndExitBlock
bool finalStateKnown;
ILVariable resultVar; // the variable that gets returned by the setResultAndExitBlock
+ Block setResultYieldBlock; // block that is jumped to for 'yield return' statements
ILVariable doFinallyBodies;
// These fields are set by AnalyzeStateMachine():
@@ -110,7 +114,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
awaitBlocks.Clear();
awaitDebugInfos.Clear();
moveNextLeaves.Clear();
- if (!MatchTaskCreationPattern(function))
+ if (!MatchTaskCreationPattern(function) && !MatchAsyncEnumeratorCreationPattern(function))
return;
try {
AnalyzeMoveNext();
@@ -133,7 +137,11 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
TranslateCachedFieldsToLocals();
FinalizeInlineMoveNext(function);
- ((BlockContainer)function.Body).ExpectedResultType = underlyingReturnType.GetStackType();
+ if (methodType == AsyncMethodType.AsyncEnumerable || methodType == AsyncMethodType.AsyncEnumerator) {
+ ((BlockContainer)function.Body).ExpectedResultType = StackType.Void;
+ } else {
+ ((BlockContainer)function.Body).ExpectedResultType = underlyingReturnType.GetStackType();
+ }
// Re-run control flow simplification over the newly constructed set of gotos,
// and inlining because TranslateFieldsToLocalAccess() might have opened up new inlining opportunities.
@@ -337,6 +345,118 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
#endregion
+ #region MatchAsyncEnumeratorCreationPattern
+ private bool MatchAsyncEnumeratorCreationPattern(ILFunction function)
+ {
+ if (!context.Settings.AsyncEnumerator)
+ return false;
+ taskType = function.ReturnType;
+ if (taskType.IsKnownType(KnownTypeCode.IAsyncEnumeratorOfT)) {
+ methodType = AsyncMethodType.AsyncEnumerator;
+ } else if (taskType.IsKnownType(KnownTypeCode.IAsyncEnumerableOfT)) {
+ methodType = AsyncMethodType.AsyncEnumerable;
+ } else {
+ return false;
+ }
+ underlyingReturnType = taskType.TypeArguments.Single();
+ if (!(function.Body is BlockContainer blockContainer))
+ return false;
+ if (blockContainer.Blocks.Count != 1)
+ return false;
+ var body = blockContainer.EntryPoint;
+ if (body.Instructions.Count == 1) {
+ // No parameters passed to enumerator (not even 'this'):
+ // ret(newobj(...))
+ if (!body.Instructions[0].MatchReturn(out var newObj))
+ return false;
+ if (MatchEnumeratorCreationNewObj(newObj, context, out initialState, out stateMachineType)) {
+ // HACK: the normal async/await logic expects 'initialState' to be the 'in progress' state
+ initialState = -1;
+ try {
+ AnalyzeEnumeratorCtor(((NewObj)newObj).Method, context, out builderField, out stateField);
+ } catch (SymbolicAnalysisFailedException) {
+ return false;
+ }
+ builderType = builderField.Type.GetDefinition();
+ if (builderType == null)
+ return false;
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // TODO: enumerator creation with parameters
+ return false;
+ }
+
+ static bool MatchEnumeratorCreationNewObj(ILInstruction inst, ILTransformContext context,
+ out int initialState, out ITypeDefinition stateMachineType)
+ {
+ initialState = default;
+ stateMachineType = default;
+ // newobj(CurrentType/...::.ctor, ldc.i4(-2))
+ if (!(inst is NewObj newObj))
+ return false;
+ if (newObj.Arguments.Count != 1)
+ return false;
+ if (!newObj.Arguments[0].MatchLdcI4(out initialState))
+ return false;
+ stateMachineType = newObj.Method.DeclaringTypeDefinition;
+ if (stateMachineType == null)
+ return false;
+ if (stateMachineType.DeclaringTypeDefinition != context.Function.Method.DeclaringTypeDefinition)
+ return false;
+ return IsCompilerGeneratorAsyncEnumerator(
+ (TypeDefinitionHandle)stateMachineType.MetadataToken,
+ context.TypeSystem.MainModule.metadata);
+ }
+
+ public static bool IsCompilerGeneratorAsyncEnumerator(TypeDefinitionHandle type, MetadataReader metadata)
+ {
+ TypeDefinition td;
+ if (type.IsNil || !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(metadata) || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil)
+ return false;
+ foreach (var i in td.GetInterfaceImplementations()) {
+ var tr = metadata.GetInterfaceImplementation(i).Interface.GetFullTypeName(metadata);
+ if (!tr.IsNested && tr.TopLevelTypeName.Namespace == "System.Collections.Generic" && tr.TopLevelTypeName.Name == "IAsyncEnumerator" && tr.TopLevelTypeName.TypeParameterCount == 1)
+ return true;
+ }
+ return false;
+ }
+
+ static void AnalyzeEnumeratorCtor(IMethod ctor, ILTransformContext context, out IField builderField, out IField stateField)
+ {
+ builderField = null;
+ stateField = null;
+
+ var ctorHandle = (MethodDefinitionHandle)ctor.MetadataToken;
+ Block body = YieldReturnDecompiler.SingleBlock(YieldReturnDecompiler.CreateILAst(ctorHandle, context).Body);
+ if (body == null)
+ throw new SymbolicAnalysisFailedException("Missing enumeratorCtor.Body");
+ // Block IL_0000 (incoming: 1) {
+ // call Object..ctor(ldloc this)
+ // stfld <>1__state(ldloc this, ldloc <>1__state)
+ // stfld <>t__builder(ldloc this, call Create())
+ // leave IL_0000 (nop)
+ // }
+ if (body.Instructions.ElementAtOrDefault(1).MatchStFld(out var target, out var field, out var value)
+ && target.MatchLdThis()
+ && value.MatchLdLoc(out var arg)
+ && arg.Kind == VariableKind.Parameter && arg.Index == 0) {
+ stateField = (IField)field.MemberDefinition;
+ } else {
+ throw new SymbolicAnalysisFailedException("Could not find stateField");
+ }
+ if (body.Instructions.ElementAtOrDefault(2).MatchStFld(out target, out field, out value)
+ && target.MatchLdThis()
+ && value is Call call && call.Method.Name == "Create") {
+ builderField = (IField)field.MemberDefinition;
+ } else {
+ throw new SymbolicAnalysisFailedException("Could not find builderField");
+ }
+ }
+ #endregion
+
#region AnalyzeMoveNext
///
/// First peek into MoveNext(); analyzes everything outside the big try-catch.
@@ -353,8 +473,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
moveNextFunction = YieldReturnDecompiler.CreateILAst(moveNextMethod, context);
if (!(moveNextFunction.Body is BlockContainer blockContainer))
throw new SymbolicAnalysisFailedException();
- if (blockContainer.Blocks.Count != 2 && blockContainer.Blocks.Count != 1)
- throw new SymbolicAnalysisFailedException();
if (blockContainer.EntryPoint.IncomingEdgeCount != 1)
throw new SymbolicAnalysisFailedException();
cachedStateVar = null;
@@ -385,12 +503,49 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
if (((BlockContainer)mainTryCatch.TryBlock).EntryPoint.Instructions[0] is StLoc initDoFinallyBodies
&& initDoFinallyBodies.Variable.Kind == VariableKind.Local
&& initDoFinallyBodies.Variable.Type.IsKnownType(KnownTypeCode.Boolean)
- && initDoFinallyBodies.Value.MatchLdcI4(1))
- {
+ && initDoFinallyBodies.Value.MatchLdcI4(1)) {
doFinallyBodies = initDoFinallyBodies.Variable;
}
- setResultAndExitBlock = blockContainer.Blocks.ElementAtOrDefault(1);
+ Debug.Assert(blockContainer.Blocks[0] == blockContainer.EntryPoint); // already checked this block
+ pos = 1;
+ if (MatchYieldBlock(blockContainer, pos)) {
+ setResultYieldBlock = blockContainer.Blocks[pos];
+ pos++;
+ } else {
+ setResultYieldBlock = null;
+ }
+
+ setResultAndExitBlock = blockContainer.Blocks.ElementAtOrDefault(pos);
+ CheckSetResultAndExitBlock(blockContainer);
+
+ if (pos + 1 < blockContainer.Blocks.Count)
+ throw new SymbolicAnalysisFailedException("too many blocks");
+ }
+
+ private bool IsAsyncEnumerator => methodType == AsyncMethodType.AsyncEnumerable || methodType == AsyncMethodType.AsyncEnumerator;
+
+ bool MatchYieldBlock(BlockContainer blockContainer, int pos)
+ {
+ if (!IsAsyncEnumerator)
+ return false;
+ var block = blockContainer.Blocks.ElementAtOrDefault(pos);
+ // call SetResult(ldflda <>v__promiseOfValueOrEnd(ldloc this), ldc.i4 1)
+ // leave IL_0000(nop)
+ if (block.Instructions.Count != 2)
+ return false;
+ if (!MatchCall(block.Instructions[0], "SetResult", out var args))
+ return false;
+ if (args.Count != 2)
+ return false;
+ if (!IsBuilderOrPromiseFieldOnThis(args[0]))
+ return false;
+ if (!args[1].MatchLdcI4(1))
+ return false;
+ return block.Instructions[1].MatchLeave(blockContainer);
+ }
+ void CheckSetResultAndExitBlock(BlockContainer blockContainer)
+ {
if (setResultAndExitBlock == null) {
// This block can be absent if the function never exits normally,
// but always throws an exception/loops infinitely.
@@ -408,17 +563,29 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
finalStateKnown = true;
if (!MatchCall(setResultAndExitBlock.Instructions[1], "SetResult", out var args))
throw new SymbolicAnalysisFailedException();
- if (!IsBuilderFieldOnThis(args[0]))
+ if (!IsBuilderOrPromiseFieldOnThis(args[0]))
throw new SymbolicAnalysisFailedException();
- if (methodType == AsyncMethodType.TaskOfT) {
- if (args.Count != 2)
- throw new SymbolicAnalysisFailedException();
- if (!args[1].MatchLdLoc(out resultVar))
- throw new SymbolicAnalysisFailedException();
- } else {
- resultVar = null;
- if (args.Count != 1)
- throw new SymbolicAnalysisFailedException();
+ switch (methodType) {
+ case AsyncMethodType.TaskOfT:
+ if (args.Count != 2)
+ throw new SymbolicAnalysisFailedException();
+ if (!args[1].MatchLdLoc(out resultVar))
+ throw new SymbolicAnalysisFailedException();
+ break;
+ case AsyncMethodType.Task:
+ case AsyncMethodType.Void:
+ resultVar = null;
+ if (args.Count != 1)
+ throw new SymbolicAnalysisFailedException();
+ break;
+ case AsyncMethodType.AsyncEnumerable:
+ case AsyncMethodType.AsyncEnumerator:
+ resultVar = null;
+ if (args.Count != 2)
+ throw new SymbolicAnalysisFailedException();
+ if (!args[1].MatchLdcI4(0))
+ throw new SymbolicAnalysisFailedException();
+ break;
}
if (!setResultAndExitBlock.Instructions[2].MatchLeave(blockContainer))
throw new SymbolicAnalysisFailedException();
@@ -465,7 +632,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
throw new SymbolicAnalysisFailedException();
if (args.Count != 2)
throw new SymbolicAnalysisFailedException();
- if (!IsBuilderFieldOnThis(args[0]))
+ if (!IsBuilderOrPromiseFieldOnThis(args[0]))
throw new SymbolicAnalysisFailedException();
if (!args[1].MatchLdLoc(stloc.Variable))
throw new SymbolicAnalysisFailedException();
@@ -490,13 +657,22 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
return target.MatchLdThis() && field.MemberDefinition == builderField;
}
+ bool IsBuilderOrPromiseFieldOnThis(ILInstruction inst)
+ {
+ if (methodType == AsyncMethodType.AsyncEnumerable || methodType == AsyncMethodType.AsyncEnumerator) {
+ return true; // TODO: check against uses of promise fields in other methods?
+ } else {
+ return IsBuilderFieldOnThis(inst);
+ }
+ }
+
bool MatchStateAssignment(ILInstruction inst, out int newState)
{
// stfld(StateMachine::<>1__state, ldloc(this), ldc.i4(stateId))
if (inst.MatchStFld(out var target, out var field, out var value)
- && target.MatchLdThis()
+ && StackSlotValue(target).MatchLdThis()
&& field.MemberDefinition == stateField
- && value.MatchLdcI4(out newState))
+ && StackSlotValue(value).MatchLdcI4(out newState))
{
return true;
}
@@ -520,6 +696,10 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
branch.ReplaceWith(new Leave((BlockContainer)function.Body, resultVar == null ? null : new LdLoc(resultVar)).WithILRange(branch));
}
}
+ if (setResultYieldBlock != null) {
+ // We still might have branches to this block; and we can't quite yet get rid of it.
+ ((BlockContainer)function.Body).Blocks.Add(setResultYieldBlock);
+ }
foreach (var leave in function.Descendants.OfType()) {
if (leave.TargetContainer == moveNextFunction.Body) {
leave.TargetContainer = (BlockContainer)function.Body;
@@ -591,6 +771,19 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
smallestAwaiterVarIndex = awaiterVar.Index.Value;
}
}
+ } else if (block.Instructions.Last().MatchBranch(setResultYieldBlock)) {
+ // This is a 'yield' in an async enumerator.
+ if (AnalyzeYieldReturn(block, out var yieldValue, out int state)) {
+ block.Instructions.Add(new YieldReturn(yieldValue));
+ Block targetBlock = stateToBlockMap.GetOrDefault(state);
+ if (targetBlock != null) {
+ block.Instructions.Add(new Branch(targetBlock));
+ } else {
+ block.Instructions.Add(new InvalidBranch("Could not find block for state " + state));
+ }
+ } else {
+ block.Instructions.Add(new InvalidBranch("Could not detect 'yield return'"));
+ }
}
}
// Skip the state dispatcher and directly jump to the initial state
@@ -606,6 +799,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
}
+
bool AnalyzeAwaitBlock(Block block, out ILVariable awaiter, out IField awaiterField, out int state, out int yieldOffset)
{
awaiter = null;
@@ -705,6 +899,49 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
}
return inst;
}
+
+ private bool AnalyzeYieldReturn(Block block, out ILInstruction yieldValue, out int newState)
+ {
+ yieldValue = default;
+ newState = default;
+ Debug.Assert(block.Instructions.Last().MatchBranch(setResultYieldBlock));
+ // stfld current(ldloc this, ldstr "yieldValue")
+ // stloc S_45(ldloc this)
+ // stloc S_46(ldc.i4 -5)
+ // stloc V_0(ldloc S_46)
+ // stfld stateField(ldloc S_45, ldloc S_46)
+ // br setResultYieldBlock
+
+ int pos = block.Instructions.Count - 2;
+ // Immediately before the 'yield return', there should be a state assignment:
+ if (pos < 0 || !MatchStateAssignment(block.Instructions[pos], out newState))
+ return false;
+ pos--;
+
+ if (pos >= 0 && block.Instructions[pos].MatchStLoc(cachedStateVar, out var cachedStateNewValue)) {
+ if (StackSlotValue(cachedStateNewValue).MatchLdcI4(newState)) {
+ pos--; // OK, ignore V_0 store
+ } else {
+ return false;
+ }
+ }
+
+ while (pos >= 0 && block.Instructions[pos] is StLoc stloc) {
+ if (stloc.Variable.Kind != VariableKind.StackSlot)
+ return false;
+ if (!SemanticHelper.IsPure(stloc.Value.Flags))
+ return false;
+ }
+
+ if (pos < 0 || !block.Instructions[pos].MatchStFld(out var target, out var field, out yieldValue))
+ return false;
+ if (!StackSlotValue(target).MatchLdThis())
+ return false;
+ // TODO: check that we are accessing the current field (compare with get_Current)
+
+ block.Instructions.RemoveRange(pos, block.Instructions.Count - pos);
+ return true;
+ }
#endregion
#region DetectAwaitPattern
diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs
index bd1c76287..7df129a49 100644
--- a/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs
+++ b/ICSharpCode.Decompiler/IL/ControlFlow/AwaitInCatchTransform.cs
@@ -197,6 +197,8 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
{
public static void Run(ILFunction function, ILTransformContext context)
{
+ if (!context.Settings.AwaitInCatchFinally)
+ return;
HashSet changedContainers = new HashSet();
// analyze all try-catch statements in the function
diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs
index bcd9e047b..a4e90674b 100644
--- a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs
+++ b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs
@@ -305,6 +305,16 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow
///
bool MatchEnumeratorCreationNewObj(ILInstruction inst)
{
+ return MatchEnumeratorCreationNewObj(inst, metadata, currentType,
+ out enumeratorCtor, out enumeratorType);
+ }
+
+ internal static bool MatchEnumeratorCreationNewObj(ILInstruction inst,
+ MetadataReader metadata, TypeDefinitionHandle currentType,
+ out MethodDefinitionHandle enumeratorCtor, out TypeDefinitionHandle enumeratorType)
+ {
+ enumeratorCtor = default;
+ enumeratorType = default;
// newobj(CurrentType/...::.ctor, ldc.i4(-2))
if (!(inst is NewObj newObj))
return false;
diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs
index 3283a7103..d51ba0b1f 100644
--- a/ILSpy/Properties/Resources.Designer.cs
+++ b/ILSpy/Properties/Resources.Designer.cs
@@ -556,6 +556,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Decompile async IAsyncEnumerator methods.
+ ///
+ public static string DecompilerSettings_AsyncEnumerator {
+ get {
+ return ResourceManager.GetString("DecompilerSettings.AsyncEnumerator", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Decompile ?. and ?[] operators.
///
@@ -810,6 +819,15 @@ namespace ICSharpCode.ILSpy.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Read-only methods.
+ ///
+ public static string DecompilerSettings_ReadOnlyMethods {
+ get {
+ return ResourceManager.GetString("DecompilerSettings.ReadOnlyMethods", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Remove dead and side effect free code (use with caution!).
///
diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx
index eb20f2e6e..dbe93bffd 100644
--- a/ILSpy/Properties/Resources.resx
+++ b/ILSpy/Properties/Resources.resx
@@ -769,4 +769,10 @@ Are you sure you want to continue?
Decompile to new tab
+
+ Decompile async IAsyncEnumerator methods
+
+
+ Read-only methods
+
\ No newline at end of file