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/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/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 1c98eb476..ad1c177d6 100644 --- a/ICSharpCode.Decompiler/TypeSystem/IMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/IMethod.cs @@ -41,7 +41,8 @@ namespace ICSharpCode.Decompiler.TypeSystem bool ReturnTypeIsRefReadOnly { get; } /// - /// Gets whether the method is readonly (C# 8): accepts the 'this' reference as ref readonly + /// 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; } 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/MetadataMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs index 57e096823..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,9 +425,24 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation return hasReadOnlyAttr; } } - #endregion - public bool ThisIsRefReadOnly => DeclaringTypeDefinition?.IsReadOnly ?? false; + 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);