diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs index baf5e6184..1d808ec6a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/NullableRefTypes.cs @@ -19,6 +19,18 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty private string[]?[] field_array; private Dictionary<(string, string?), (int, string[]?, string?[])> field_complex; + public (string A, dynamic? B) PropertyNamedTuple { + get { + throw new NotImplementedException(); + } + } + + public (string A, dynamic? B) this[(dynamic? C, string D) weirdIndexer] { + get { + throw new NotImplementedException(); + } + } + public int GetLength1(string[] arr) { return field_string.Length + arr.Length; diff --git a/ICSharpCode.Decompiler/TypeSystem/Accessibility.cs b/ICSharpCode.Decompiler/TypeSystem/Accessibility.cs index 9cee2d55e..25091ddc8 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Accessibility.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Accessibility.cs @@ -115,5 +115,18 @@ namespace ICSharpCode.Decompiler.TypeSystem return b; } } + + /// + /// Gets the effective accessibility of the entity. + /// For example, a public method in an internal class returns "internal". + /// + public static Accessibility EffectiveAccessibility(this IEntity entity) + { + Accessibility accessibility = entity.Accessibility; + for (ITypeDefinition typeDef = entity.DeclaringTypeDefinition; typeDef != null; typeDef = typeDef.DeclaringTypeDefinition) { + accessibility = Intersect(accessibility, typeDef.Accessibility); + } + return accessibility; + } } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs index 872f31af0..0e7b33a7b 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs @@ -41,6 +41,7 @@ namespace ICSharpCode.Decompiler.TypeSystem TupleElementNames, Nullable, NullableContext, + NullablePublicOnly, Conditional, Obsolete, IsReadOnly, @@ -111,6 +112,7 @@ namespace ICSharpCode.Decompiler.TypeSystem new TopLevelTypeName("System.Runtime.CompilerServices", nameof(TupleElementNamesAttribute)), new TopLevelTypeName("System.Runtime.CompilerServices", "NullableAttribute"), new TopLevelTypeName("System.Runtime.CompilerServices", "NullableContextAttribute"), + new TopLevelTypeName("System.Runtime.CompilerServices", "NullablePublicOnlyAttribute"), new TopLevelTypeName("System.Diagnostics", nameof(ConditionalAttribute)), new TopLevelTypeName("System", nameof(ObsoleteAttribute)), new TopLevelTypeName("System.Runtime.CompilerServices", "IsReadOnlyAttribute"), diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataEvent.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataEvent.cs index e85a559d3..cd59290b7 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataEvent.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataEvent.cs @@ -82,7 +82,10 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation var declaringTypeDef = DeclaringTypeDefinition; var context = new GenericContext(declaringTypeDef?.TypeParameters); var nullableContext = declaringTypeDef?.NullableContext ?? Nullability.Oblivious; - returnType = module.ResolveType(ev.Type, context, ev.GetCustomAttributes(), nullableContext); + // The event does not have explicit accessibility in metadata, so use its + // containing type to determine whether nullability applies to this type. + var typeOptions = module.OptionsForEntity(declaringTypeDef); + returnType = module.ResolveType(ev.Type, context, typeOptions, ev.GetCustomAttributes(), nullableContext); return LazyInit.GetOrSet(ref this.returnType, returnType); } } diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataField.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataField.cs index 18f513939..0e24e62c2 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataField.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataField.cs @@ -186,7 +186,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation ty = mod.ElementType; } ty = ApplyAttributeTypeVisitor.ApplyAttributesToType(ty, Compilation, - fieldDef.GetCustomAttributes(), metadata, module.TypeSystemOptions, + fieldDef.GetCustomAttributes(), metadata, module.OptionsForEntity(this), DeclaringTypeDefinition?.NullableContext ?? Nullability.Oblivious); } catch (BadImageFormatException) { ty = SpecialType.UnknownType; diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs index 87f7eb4c9..ab02b8252 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataMethod.cs @@ -166,7 +166,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation try { var nullableContext = methodDef.GetCustomAttributes().GetNullableContext(module.metadata) ?? DeclaringTypeDefinition.NullableContext; var signature = methodDef.DecodeSignature(module.TypeProvider, genericContext); - (returnType, parameters) = DecodeSignature(module, this, signature, methodDef.GetParameters(), nullableContext); + (returnType, parameters) = DecodeSignature(module, this, signature, methodDef.GetParameters(), nullableContext, module.OptionsForEntity(this)); } catch (BadImageFormatException) { returnType = SpecialType.UnknownType; parameters = Empty.Array; @@ -175,11 +175,13 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation LazyInit.GetOrSet(ref this.parameters, parameters); } - internal static (IType, IParameter[]) DecodeSignature(MetadataModule module, IParameterizedMember owner, MethodSignature signature, ParameterHandleCollection? parameterHandles, Nullability nullableContext) + internal static (IType, IParameter[]) DecodeSignature(MetadataModule module, IParameterizedMember owner, + MethodSignature signature, ParameterHandleCollection? parameterHandles, + Nullability nullableContext, TypeSystemOptions typeSystemOptions, + CustomAttributeHandleCollection? returnTypeAttributes = null) { var metadata = module.metadata; int i = 0; - CustomAttributeHandleCollection? returnTypeAttributes = null; IParameter[] parameters = new IParameter[signature.RequiredParameterCount + (signature.Header.CallingConvention == SignatureCallingConvention.VarArgs ? 1 : 0)]; IType parameterType; @@ -187,8 +189,14 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation foreach (var parameterHandle in parameterHandles) { var par = metadata.GetParameter(parameterHandle); if (par.SequenceNumber == 0) { - // "parameter" holds return type attributes - returnTypeAttributes = par.GetCustomAttributes(); + // "parameter" holds return type attributes. + // Note: for properties, the attributes normally stored on a method's return type + // are instead stored as normal attributes on the property. + // So MetadataProperty provides a non-null value for returnTypeAttributes, + // which then should be preferred over the attributes on the accessor's parameters. + if (returnTypeAttributes == null) { + returnTypeAttributes = par.GetCustomAttributes(); + } } else if (par.SequenceNumber > 0 && i < signature.RequiredParameterCount) { // "Successive rows of the Param table that are owned by the same method shall be // ordered by increasing Sequence value - although gaps in the sequence are allowed" @@ -196,14 +204,14 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation // Fill gaps in the sequence with non-metadata parameters: while (i < par.SequenceNumber - 1) { parameterType = ApplyAttributeTypeVisitor.ApplyAttributesToType( - signature.ParameterTypes[i], module.Compilation, null, metadata, module.TypeSystemOptions, nullableContext); + signature.ParameterTypes[i], module.Compilation, null, metadata, typeSystemOptions, nullableContext); parameters[i] = new DefaultParameter(parameterType, name: string.Empty, owner, referenceKind: parameterType.Kind == TypeKind.ByReference ? ReferenceKind.Ref : ReferenceKind.None); i++; } parameterType = ApplyAttributeTypeVisitor.ApplyAttributesToType( signature.ParameterTypes[i], module.Compilation, - par.GetCustomAttributes(), metadata, module.TypeSystemOptions, nullableContext); + par.GetCustomAttributes(), metadata, typeSystemOptions, nullableContext); parameters[i] = new MetadataParameter(module, owner, parameterType, parameterHandle); i++; } @@ -211,7 +219,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation } while (i < signature.RequiredParameterCount) { parameterType = ApplyAttributeTypeVisitor.ApplyAttributesToType( - signature.ParameterTypes[i], module.Compilation, null, metadata, module.TypeSystemOptions, nullableContext); + signature.ParameterTypes[i], module.Compilation, null, metadata, typeSystemOptions, nullableContext); parameters[i] = new DefaultParameter(parameterType, name: string.Empty, owner, referenceKind: parameterType.Kind == TypeKind.ByReference ? ReferenceKind.Ref : ReferenceKind.None); i++; @@ -222,7 +230,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation } Debug.Assert(i == parameters.Length); var returnType = ApplyAttributeTypeVisitor.ApplyAttributesToType(signature.ReturnType, - module.Compilation, returnTypeAttributes, metadata, module.TypeSystemOptions, nullableContext); + module.Compilation, returnTypeAttributes, metadata, typeSystemOptions, nullableContext); return (returnType, parameters); } #endregion diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataProperty.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataProperty.cs index 7ba165859..83596cb51 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataProperty.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataProperty.cs @@ -129,23 +129,33 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation try { var signature = propertyDef.DecodeSignature(module.TypeProvider, genericContext); var accessors = propertyDef.GetAccessors(); + var declTypeDef = this.DeclaringTypeDefinition; ParameterHandleCollection? parameterHandles; Nullability nullableContext; if (!accessors.Getter.IsNil) { var getter = module.metadata.GetMethodDefinition(accessors.Getter); parameterHandles = getter.GetParameters(); nullableContext = getter.GetCustomAttributes().GetNullableContext(module.metadata) - ?? DeclaringTypeDefinition?.NullableContext ?? Nullability.Oblivious; + ?? declTypeDef?.NullableContext ?? Nullability.Oblivious; } else if (!accessors.Setter.IsNil) { var setter = module.metadata.GetMethodDefinition(accessors.Setter); parameterHandles = setter.GetParameters(); nullableContext = setter.GetCustomAttributes().GetNullableContext(module.metadata) - ?? DeclaringTypeDefinition?.NullableContext ?? Nullability.Oblivious; + ?? declTypeDef?.NullableContext ?? Nullability.Oblivious; } else { parameterHandles = null; - nullableContext = DeclaringTypeDefinition?.NullableContext ?? Nullability.Oblivious; + nullableContext = declTypeDef?.NullableContext ?? Nullability.Oblivious; } - (returnType, parameters) = MetadataMethod.DecodeSignature(module, this, signature, parameterHandles, nullableContext); + // We call OptionsForEntity() for the declaring type, not the property itself, + // because the property's accessibilty isn't stored in metadata but computed. + // Otherwise we'd get infinite recursion, because computing the accessibility + // requires decoding the signature for the GetBaseMembers() call. + // Roslyn uses the same workaround (see the NullableTypeDecoder.TransformType + // call in PEPropertySymbol). + var typeOptions = module.OptionsForEntity(declTypeDef); + (returnType, parameters) = MetadataMethod.DecodeSignature(module, this, signature, + parameterHandles, nullableContext, typeOptions, + returnTypeAttributes: propertyDef.GetCustomAttributes()); } catch (BadImageFormatException) { returnType = SpecialType.UnknownType; parameters = Empty.Array; diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeParameter.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeParameter.cs index 73b72d147..aef3b2db4 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeParameter.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataTypeParameter.cs @@ -145,7 +145,7 @@ namespace ICSharpCode.Decompiler.TypeSystem.Implementation Nullability LoadNullabilityConstraint() { - if ((module.TypeSystemOptions & TypeSystemOptions.NullabilityAnnotations) == 0) + if (!module.ShouldDecodeNullableAttributes(Owner)) return Nullability.Oblivious; var metadata = module.metadata; diff --git a/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs b/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs index 1fb75d51d..5d8d4bd3a 100644 --- a/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs +++ b/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs @@ -67,7 +67,9 @@ namespace ICSharpCode.Decompiler.TypeSystem this.AssemblyName = metadata.GetString(moddef.Name); this.FullAssemblyName = this.AssemblyName; } - this.NullableContext = metadata.GetModuleDefinition().GetCustomAttributes().GetNullableContext(metadata) ?? Nullability.Oblivious; + var customAttrs = metadata.GetModuleDefinition().GetCustomAttributes(); + this.NullableContext = customAttrs.GetNullableContext(metadata) ?? Nullability.Oblivious; + this.minAccessibilityForNRT = FindMinimumAccessibilityForNRT(metadata, customAttrs); this.rootNamespace = new MetadataNamespace(this, null, string.Empty, metadata.GetNamespaceDefinitionRoot()); if (!options.HasFlag(TypeSystemOptions.Uncached)) { @@ -739,5 +741,51 @@ namespace ICSharpCode.Decompiler.TypeSystem || att == MethodAttributes.FamORAssem; } #endregion + + #region Nullability Reference Type Support + readonly Accessibility minAccessibilityForNRT; + + static Accessibility FindMinimumAccessibilityForNRT(MetadataReader metadata, CustomAttributeHandleCollection customAttributes) + { + // Determine the minimum effective accessibility an entity must have, so that the metadata stores the nullability for its type. + foreach (var handle in customAttributes) { + var customAttribute = metadata.GetCustomAttribute(handle); + if (customAttribute.IsKnownAttribute(metadata, KnownAttribute.NullablePublicOnly)) { + CustomAttributeValue value; + try { + value = customAttribute.DecodeValue(Metadata.MetadataExtensions.MinimalAttributeTypeProvider); + } catch (BadImageFormatException) { + continue; + } catch (EnumUnderlyingTypeResolveException) { + continue; + } + if (value.FixedArguments.Length == 1 && value.FixedArguments[0].Value is bool includesInternals) { + return includesInternals ? Accessibility.ProtectedAndInternal : Accessibility.Protected; + } + } + } + return Accessibility.None; + } + + internal bool ShouldDecodeNullableAttributes(IEntity entity) + { + if ((options & TypeSystemOptions.NullabilityAnnotations) == 0) + return false; + if (minAccessibilityForNRT == Accessibility.None || entity == null) + return true; + return minAccessibilityForNRT.LessThanOrEqual(entity.EffectiveAccessibility()); + } + + internal TypeSystemOptions OptionsForEntity(IEntity entity) + { + var opt = this.options; + if ((opt & TypeSystemOptions.NullabilityAnnotations) != 0) { + if (!ShouldDecodeNullableAttributes(entity)) { + opt &= ~TypeSystemOptions.NullabilityAnnotations; + } + } + return opt; + } + #endregion } }