diff --git a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs index 250b2f1ef..f17a64a52 100644 --- a/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs +++ b/ICSharpCode.Decompiler.Tests/Helpers/Tester.cs @@ -224,6 +224,7 @@ namespace ICSharpCode.Decompiler.Tests.Helpers preprocessorSymbols.Add("CS71"); preprocessorSymbols.Add("CS72"); preprocessorSymbols.Add("CS73"); + preprocessorSymbols.Add("CS80"); preprocessorSymbols.Add("VB11"); preprocessorSymbols.Add("VB14"); preprocessorSymbols.Add("VB15"); diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index dad38279b..0d2436bf4 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -42,8 +42,8 @@ - - + + diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs index 665be1a32..a78bf77d3 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/RefLocalsAndReturns.cs @@ -63,9 +63,57 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty { private readonly int dummy; + public int Property { + get { + return 1; + } + set { + } + } + +#if CS80 + public readonly int ReadOnlyProperty { + get { + return 1; + } + set { + } + } + + public int PropertyWithReadOnlyGetter { + readonly get { + return 1; + } + set { + } + } + + public int PropertyWithReadOnlySetter { + get { + return 1; + } + readonly set { + } + } + + public event EventHandler NormalEvent; + + public readonly event EventHandler ReadOnlyEvent { + add { + } + remove { + } + } +#endif public void Method() { } + +#if CS80 + public readonly void ReadOnlyMethod() + { + } +#endif } public readonly struct ReadOnlyStruct diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs index 589f1042c..f39442bd8 100644 --- a/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs +++ b/ICSharpCode.Decompiler/CSharp/Syntax/TypeSystemAstBuilder.cs @@ -1522,6 +1522,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax Accessor decl = new Accessor(); if (this.ShowAccessibility && accessor.Accessibility != ownerAccessibility) decl.Modifiers = ModifierFromAccessibility(accessor.Accessibility); + if (accessor.ThisIsRefReadOnly && accessor.DeclaringTypeDefinition?.IsReadOnly == false) + decl.Modifiers |= Modifiers.Readonly; if (ShowAttributes) { decl.Attributes.AddRange(ConvertAttributes(accessor.GetAttributes())); decl.Attributes.AddRange(ConvertAttributes(accessor.GetReturnTypeAttributes(), "return")); @@ -1551,9 +1553,18 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax decl.Getter = ConvertAccessor(property.Getter, property.Accessibility, false); decl.Setter = ConvertAccessor(property.Setter, property.Accessibility, true); decl.PrivateImplementationType = GetExplicitInterfaceType (property); + MergeReadOnlyModifiers(decl, decl.Getter, decl.Setter); return decl; } + static void MergeReadOnlyModifiers(EntityDeclaration decl, Accessor accessor1, Accessor accessor2) + { + if (accessor1.HasModifier(Modifiers.Readonly) && accessor2.HasModifier(Modifiers.Readonly)) { + accessor1.Modifiers &= ~Modifiers.Readonly; + accessor2.Modifiers &= ~Modifiers.Readonly; + decl.Modifiers |= Modifiers.Readonly; + } + } IndexerDeclaration ConvertIndexer(IProperty indexer) { IndexerDeclaration decl = new IndexerDeclaration(); @@ -1571,6 +1582,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax decl.Getter = ConvertAccessor(indexer.Getter, indexer.Accessibility, false); decl.Setter = ConvertAccessor(indexer.Setter, indexer.Accessibility, true); decl.PrivateImplementationType = GetExplicitInterfaceType (indexer); + MergeReadOnlyModifiers(decl, decl.Getter, decl.Setter); return decl; } @@ -1590,6 +1602,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax decl.AddAccessor = ConvertAccessor(ev.AddAccessor, ev.Accessibility, true); decl.RemoveAccessor = ConvertAccessor(ev.RemoveAccessor, ev.Accessibility, true); decl.PrivateImplementationType = GetExplicitInterfaceType (ev); + MergeReadOnlyModifiers(decl, decl.AddAccessor, decl.RemoveAccessor); return decl; } else { EventDeclaration decl = new EventDeclaration(); @@ -1759,6 +1772,8 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax m |= Modifiers.Virtual; if (member.IsSealed) m |= Modifiers.Sealed; + if (member is IMethod method && method.ThisIsRefReadOnly && method.DeclaringTypeDefinition?.IsReadOnly == false) + m |= Modifiers.Readonly; } } return m; diff --git a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs index faa6a3163..8692bf678 100644 --- a/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs +++ b/ICSharpCode.Decompiler/CSharp/Transforms/PatternStatementTransform.cs @@ -571,11 +571,14 @@ namespace ICSharpCode.Decompiler.CSharp.Transforms } if (field == null) return null; + if (propertyDeclaration.Setter.HasModifier(Modifiers.Readonly)) + return null; if (field.IsCompilerGenerated() && field.DeclaringTypeDefinition == property.DeclaringTypeDefinition) { RemoveCompilerGeneratedAttribute(propertyDeclaration.Getter.Attributes); RemoveCompilerGeneratedAttribute(propertyDeclaration.Setter.Attributes); propertyDeclaration.Getter.Body = null; propertyDeclaration.Setter.Body = null; + propertyDeclaration.Getter.Modifiers &= ~Modifiers.Readonly; // Add C# 7.3 attributes on backing field: var attributes = field.GetAttributes() diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 167961528..40aa62e4a 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -107,12 +107,13 @@ namespace ICSharpCode.Decompiler } if (languageVersion < CSharp.LanguageVersion.CSharp8_0) { nullableReferenceTypes = false; + readOnlyMethods = false; } } public CSharp.LanguageVersion GetMinimumRequiredVersion() { - if (nullableReferenceTypes) + if (nullableReferenceTypes || readOnlyMethods) return CSharp.LanguageVersion.CSharp8_0; if (introduceUnmanagedConstraint || tupleComparisons || stackAllocInitializers) return CSharp.LanguageVersion.CSharp7_3; @@ -842,6 +843,20 @@ namespace ICSharpCode.Decompiler } } + bool readOnlyMethods = true; + + [Category("C# 8.0 / VS 2019")] + [Description("DecompilerSettings.IsReadOnlyAttributeShouldBeReplacedWithReadonlyInModifiersOnStructsParameters")] + public bool ReadOnlyMethods { + get { return readOnlyMethods; } + set { + if (readOnlyMethods != value) { + readOnlyMethods = value; + OnPropertyChanged(); + } + } + } + bool introduceUnmanagedConstraint = true; /// diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index cef0c023a..8cbb8a1ff 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -191,7 +191,7 @@ namespace ICSharpCode.Decompiler.IL declaringType = new ParameterizedType(declaringType, declaringType.TypeParameters); } ILVariable ilVar = CreateILVariable(-1, declaringType, "this"); - ilVar.IsRefReadOnly = declaringType.GetDefinition()?.IsReadOnly == true; + ilVar.IsRefReadOnly = method.ThisIsRefReadOnly; parameterVariables[paramIndex++] = ilVar; } while (paramIndex < parameterVariables.Length) { diff --git a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs index c1a5de160..840c77e9b 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ILInlining.cs @@ -272,8 +272,8 @@ namespace ICSharpCode.Decompiler.IL.Transforms var type = method.DeclaringType; if (type.IsReferenceType == true) return false; // reference types are never implicitly copied - if (type.GetDefinition()?.IsReadOnly == true) - return false; // readonly structs are never implicitly copied + if (method.ThisIsRefReadOnly) + return false; // no copies for calls on readonly structs return true; } diff --git a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs index 04ce3ef8f..4ba6622bf 100644 --- a/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs +++ b/ICSharpCode.Decompiler/TypeSystem/DecompilerTypeSystem.cs @@ -84,7 +84,8 @@ namespace ICSharpCode.Decompiler.TypeSystem /// KeepModifiers = 0x40, /// - /// If this option is active, [IsReadOnlyAttribute] is removed and parameters are marked as in, structs as readonly. + /// If this option is active, [IsReadOnlyAttribute] on parameters+structs is removed + /// and parameters are marked as in, structs as readonly. /// Otherwise, the attribute is preserved but the parameters and structs are not marked. /// ReadOnlyStructsAndParameters = 0x80, @@ -104,10 +105,15 @@ namespace ICSharpCode.Decompiler.TypeSystem /// NullabilityAnnotations = 0x400, /// + /// If this option is active, [IsReadOnlyAttribute] on methods is removed + /// and the method marked as ThisIsRefReadOnly. + /// + ReadOnlyMethods = 0x800, + /// /// Default settings: typical options for the decompiler, with all C# languages features enabled. /// Default = Dynamic | Tuple | ExtensionMethods | DecimalConstants | ReadOnlyStructsAndParameters - | RefStructs | UnmanagedConstraints | NullabilityAnnotations + | RefStructs | UnmanagedConstraints | NullabilityAnnotations | ReadOnlyMethods } /// @@ -137,6 +143,8 @@ namespace ICSharpCode.Decompiler.TypeSystem typeSystemOptions |= TypeSystemOptions.UnmanagedConstraints; if (settings.NullableReferenceTypes) typeSystemOptions |= TypeSystemOptions.NullabilityAnnotations; + if (settings.ReadOnlyMethods) + typeSystemOptions |= TypeSystemOptions.ReadOnlyMethods; return typeSystemOptions; } diff --git a/ICSharpCode.Decompiler/TypeSystem/IMethod.cs b/ICSharpCode.Decompiler/TypeSystem/IMethod.cs index c53d1cc60..ad1c177d6 100644 --- a/ICSharpCode.Decompiler/TypeSystem/IMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/IMethod.cs @@ -40,6 +40,12 @@ namespace ICSharpCode.Decompiler.TypeSystem /// bool ReturnTypeIsRefReadOnly { get; } + /// + /// Gets whether the method accepts the 'this' reference as ref readonly. + /// This can be either because the method is C# 8.0 'readonly', or because it is within a C# 7.2 'readonly struct' + /// + bool ThisIsRefReadOnly { get; } + /// /// Gets the type parameters of this method; or an empty list if the method is not generic. /// diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs index 1bc076ad9..920106c37 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/AttributeListBuilder.cs @@ -201,7 +201,16 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation case "DecimalConstantAttribute": return (options & TypeSystemOptions.DecimalConstants) != 0 && (target == SymbolKind.Field || target == SymbolKind.Parameter); case "IsReadOnlyAttribute": - return (options & TypeSystemOptions.ReadOnlyStructsAndParameters) != 0; + switch (target) { + case SymbolKind.TypeDefinition: + case SymbolKind.Parameter: + return (options & TypeSystemOptions.ReadOnlyStructsAndParameters) != 0; + case SymbolKind.Method: + case SymbolKind.Accessor: + return (options & TypeSystemOptions.ReadOnlyMethods) != 0; + default: + return false; + } case "IsByRefLikeAttribute": return (options & TypeSystemOptions.RefStructs) != 0 && target == SymbolKind.TypeDefinition; case "IsUnmanagedAttribute": diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/FakeMember.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/FakeMember.cs index 02d439f84..2a11babee 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/FakeMember.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/FakeMember.cs @@ -133,6 +133,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation IEnumerable IMethod.GetReturnTypeAttributes() => EmptyList.Instance; bool IMethod.ReturnTypeIsRefReadOnly => false; + bool IMethod.ThisIsRefReadOnly => false; public IReadOnlyList TypeParameters { get; set; } = EmptyList.Instance; diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/LocalFunctionMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/LocalFunctionMethod.cs index 6a42b4364..cb53d51a6 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/LocalFunctionMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/LocalFunctionMethod.cs @@ -117,6 +117,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation IEnumerable IEntity.GetAttributes() => baseMethod.GetAttributes(); IEnumerable IMethod.GetReturnTypeAttributes() => baseMethod.GetReturnTypeAttributes(); bool IMethod.ReturnTypeIsRefReadOnly => baseMethod.ReturnTypeIsRefReadOnly; + bool IMethod.ThisIsRefReadOnly => baseMethod.ThisIsRefReadOnly; /// /// We consider local functions as always static, because they do not have a "this parameter". /// Even local functions in instance methods capture this. diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs index d97382984..881933acd 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs @@ -49,6 +49,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation IParameter[] parameters; IType returnType; byte returnTypeIsRefReadonly = ThreeState.Unknown; + byte thisIsRefReadonly = ThreeState.Unknown; internal MetadataMethod(MetadataModule module, MethodDefinitionHandle handle) { @@ -424,6 +425,23 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return hasReadOnlyAttr; } } + + public bool ThisIsRefReadOnly { + get { + if (thisIsRefReadonly != ThreeState.Unknown) { + return thisIsRefReadonly == ThreeState.True; + } + var metadata = module.metadata; + var methodDefinition = metadata.GetMethodDefinition(handle); + bool hasReadOnlyAttr = DeclaringTypeDefinition?.IsReadOnly ?? false; + if ((module.TypeSystemOptions & TypeSystemOptions.ReadOnlyMethods) != 0) { + hasReadOnlyAttr |= methodDefinition.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.IsReadOnly); + } + this.thisIsRefReadonly = ThreeState.From(hasReadOnlyAttr); + return hasReadOnlyAttr; + } + } + #endregion public Accessibility Accessibility => GetAccessibility(attributes); diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedMethod.cs index 311395fc4..c25ba6013 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/SpecializedMethod.cs @@ -97,6 +97,8 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation public IEnumerable GetReturnTypeAttributes() => methodDefinition.GetReturnTypeAttributes(); public bool ReturnTypeIsRefReadOnly => methodDefinition.ReturnTypeIsRefReadOnly; + bool IMethod.ThisIsRefReadOnly => methodDefinition.ThisIsRefReadOnly; + public IReadOnlyList TypeParameters { get { return specializedTypeParameters ?? methodDefinition.TypeParameters; diff --git a/ICSharpCode.Decompiler/TypeSystem/VarArgInstanceMethod.cs b/ICSharpCode.Decompiler/TypeSystem/VarArgInstanceMethod.cs index a151d1464..ecc0cbf22 100644 --- a/ICSharpCode.Decompiler/TypeSystem/VarArgInstanceMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/VarArgInstanceMethod.cs @@ -114,6 +114,7 @@ namespace ICSharpCode.Decompiler.TypeSystem IEnumerable IEntity.GetAttributes() => baseMethod.GetAttributes(); IEnumerable IMethod.GetReturnTypeAttributes() => baseMethod.GetReturnTypeAttributes(); bool IMethod.ReturnTypeIsRefReadOnly => baseMethod.ReturnTypeIsRefReadOnly; + bool IMethod.ThisIsRefReadOnly => baseMethod.ThisIsRefReadOnly; public IReadOnlyList TypeParameters { get { return baseMethod.TypeParameters; } diff --git a/ILSpy.Tests/ILSpy.Tests.csproj b/ILSpy.Tests/ILSpy.Tests.csproj index e2543cffc..32c81dd25 100644 --- a/ILSpy.Tests/ILSpy.Tests.csproj +++ b/ILSpy.Tests/ILSpy.Tests.csproj @@ -42,8 +42,8 @@ - - + +