diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs index e98991a378..1554bb069f 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs @@ -196,6 +196,87 @@ public void DynamicParameterHasNoAttributes() Assert.That(m1.Parameters[0].GetAttributes().Count(), Is.EqualTo(0)); } + [Test] + public void LifetimeAnnotations() + { + ITypeDefinition type = GetTypeDefinition(typeof(LifetimeAnnotationTests)); + + IParameter explicitScopedRef = type.Methods.Single(m => m.Name == "ExplicitScopedRef").Parameters.Single(); + Assert.That(explicitScopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(explicitScopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(explicitScopedRef.Lifetime.HasUnscopedRefAttribute, Is.False); + Assert.That(explicitScopedRef.Lifetime.ScopedRef, Is.True); + + IParameter explicitScopedValue = type.Methods.Single(m => m.Name == "ExplicitScopedValue").Parameters.Single(); + Assert.That(explicitScopedValue.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.ScopedValue)); + Assert.That(explicitScopedValue.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedValue)); + Assert.That(explicitScopedValue.Lifetime.HasUnscopedRefAttribute, Is.False); + Assert.That(explicitScopedValue.Lifetime.ScopedRef, Is.True); + + IParameter implicitScopedRef = type.Methods.Single(m => m.Name == "ImplicitScopedRef").Parameters.Single(); + Assert.That(implicitScopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.None)); + Assert.That(implicitScopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(implicitScopedRef.Lifetime.HasUnscopedRefAttribute, Is.False); + Assert.That(implicitScopedRef.Lifetime.ScopedRef, Is.False); + + IParameter unscopedRef = type.Methods.Single(m => m.Name == "UnscopedRef").Parameters.Single(); + Assert.That(unscopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedRef.Lifetime.HasUnscopedRefAttribute, Is.True); + Assert.That(unscopedRef.GetAttributes().Any(a => + a.AttributeType.FullName == "System.Diagnostics.CodeAnalysis.UnscopedRefAttribute"), Is.True); + + var paramsRefStruct = new DefaultParameter(type, "values", isParams: true); + Assert.That(paramsRefStruct.GetEffectiveScope(), Is.EqualTo(ScopedKind.ScopedValue)); + + IMethod instanceMethod = type.Methods.Single(m => m.Name == "InstanceMethod"); + Assert.That(instanceMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.ScopedRef)); + Assert.That(instanceMethod.HasAttribute(KnownAttribute.UnscopedRef), Is.False); + + IMethod unscopedMethod = type.Methods.Single(m => m.Name == "UnscopedMethod"); + Assert.That(unscopedMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedMethod.HasAttribute(KnownAttribute.UnscopedRef), Is.True); + + IProperty unscopedProperty = type.Properties.Single(p => p.Name == "UnscopedProperty"); + Assert.That(unscopedProperty.Getter.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(unscopedProperty.HasAttribute(KnownAttribute.UnscopedRef), Is.True); + + IProperty accessorUnscopedProperty = type.Properties.Single(p => p.Name == "AccessorUnscopedProperty"); + Assert.That(accessorUnscopedProperty.Getter.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(accessorUnscopedProperty.Getter.HasAttribute(KnownAttribute.UnscopedRef), Is.True); + + IMethod staticMethod = type.Methods.Single(m => m.Name == "ExplicitScopedRef"); + Assert.That(staticMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + + IMethod classMethod = GetTypeDefinition(typeof(SimplePublicClass)).Methods.Single(m => m.Name == "Method"); + Assert.That(classMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + + // The legacy mscorlib fixture has no RefSafetyRulesAttribute, so out is not implicitly scoped. + ITypeDefinition int32 = compilation.FindType(KnownTypeCode.Int32).GetDefinition(); + IMethod legacyTryParse = int32.Methods.Single(m => m.Name == "TryParse" + && m.Parameters.Count == 2 + && m.Parameters[1].ReferenceKind == ReferenceKind.Out); + Assert.That(legacyTryParse.Parameters[1].GetEffectiveScope(), Is.EqualTo(ScopedKind.None)); + } + + [Test] + public void LifetimeAnnotationsCanBeDisabled() + { + var compilationWithoutLifetimeAnnotations = new SimpleCompilation( + TestAssembly.WithOptions(TypeSystemOptions.Default & ~TypeSystemOptions.ScopedRef), + Mscorlib.WithOptions(TypeSystemOptions.Default | TypeSystemOptions.OnlyPublicAPI)); + ITypeDefinition type = compilationWithoutLifetimeAnnotations.FindType(typeof(LifetimeAnnotationTests)).GetDefinition(); + + IParameter explicitScopedRef = type.Methods.Single(m => m.Name == "ExplicitScopedRef").Parameters.Single(); + Assert.That(explicitScopedRef.Lifetime.DeclaredScope, Is.EqualTo(ScopedKind.None)); + Assert.That(explicitScopedRef.GetEffectiveScope(), Is.EqualTo(ScopedKind.None)); + Assert.That(explicitScopedRef.GetAttributes().Any(a => + a.AttributeType.FullName == "System.Runtime.CompilerServices.ScopedRefAttribute"), Is.True); + + IMethod instanceMethod = type.Methods.Single(m => m.Name == "InstanceMethod"); + Assert.That(instanceMethod.GetThisParameterScope(), Is.EqualTo(ScopedKind.None)); + } + [Test] public void AssemblyAttribute() { diff --git a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs index e3135e43e4..5213cb5084 100644 --- a/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs +++ b/ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs @@ -18,6 +18,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -219,6 +220,34 @@ public void MethodWithOptionalDecimalParameter(decimal x = 1) { } public void VarArgsMethod(__arglist) { } } + public ref struct LifetimeAnnotationTests + { + private int value; + + public static void ExplicitScopedRef(scoped ref int value) { } + public static void ExplicitScopedValue(scoped LifetimeAnnotationTests value) { } + public static void ImplicitScopedRef(out int value) { value = 0; } + public static void UnscopedRef([UnscopedRef] out int value) { value = 0; } + + public void InstanceMethod() { } + + [UnscopedRef] + public ref int UnscopedMethod() + { + return ref value; + } + + [UnscopedRef] + public ref int UnscopedProperty => ref value; + + public ref int AccessorUnscopedProperty { + [UnscopedRef] + get { + return ref value; + } + } + } + public class VarArgsCtor { public VarArgsCtor(__arglist) { } diff --git a/ICSharpCode.Decompiler/TypeSystem/IParameter.cs b/ICSharpCode.Decompiler/TypeSystem/IParameter.cs index 8ab231679d..7937affc4c 100644 --- a/ICSharpCode.Decompiler/TypeSystem/IParameter.cs +++ b/ICSharpCode.Decompiler/TypeSystem/IParameter.cs @@ -18,7 +18,6 @@ #nullable enable -using System; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -36,23 +35,36 @@ public enum ReferenceKind : byte RefReadOnly, } + /// + /// Describes which part of a parameter or local is restricted to the current scope. + /// + public enum ScopedKind : byte + { + None, + ScopedRef, + ScopedValue, + } + public struct LifetimeAnnotation { + /// + /// Gets or sets the scope explicitly encoded by ScopedRefAttribute. + /// Implicit and redundant scopedness is excluded. + /// + public ScopedKind DeclaredScope { get; set; } + /// /// C# 11 scoped annotation: "scoped ref" (ScopedRefAttribute) /// public bool ScopedRef { -#pragma warning disable 618 - get { return RefScoped; } - set { RefScoped = value; } -#pragma warning restore 618 + get { return DeclaredScope != ScopedKind.None; } + set { DeclaredScope = value ? ScopedKind.ScopedRef : ScopedKind.None; } } - [Obsolete("Use ScopedRef property instead of directly accessing this field")] - public bool RefScoped; - - [Obsolete("C# 11 preview: \"ref scoped\" no longer supported")] - public bool ValueScoped; + /// + /// Gets or sets whether UnscopedRefAttribute is present. + /// + public bool HasUnscopedRefAttribute { get; set; } } public interface IParameter : IVariable diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs index 52a20c3375..5f31844936 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/KnownAttributes.cs @@ -114,6 +114,8 @@ public enum KnownAttribute // C# 11 attributes: Required, + UnscopedRef, + RefSafetyRules, // C# 12 attributes: InlineArray, @@ -196,6 +198,8 @@ public static class KnownAttributes new TopLevelTypeName("System.Runtime.InteropServices", "UnmanagedCallersOnlyAttribute"), // C# 11 attributes: new TopLevelTypeName("System.Runtime.CompilerServices", "RequiredMemberAttribute"), + new TopLevelTypeName("System.Diagnostics.CodeAnalysis", "UnscopedRefAttribute"), + new TopLevelTypeName("System.Runtime.CompilerServices", "RefSafetyRulesAttribute"), // C# 12 attributes: new TopLevelTypeName("System.Runtime.CompilerServices", "InlineArrayAttribute"), // C# 14 attributes: diff --git a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs index c62438cb98..238c9ed2e5 100644 --- a/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs +++ b/ICSharpCode.Decompiler/TypeSystem/Implementation/MetadataParameter.cs @@ -125,17 +125,29 @@ public LifetimeAnnotation Lifetime { var metadata = module.metadata; var parameterDef = metadata.GetParameter(handle); - if ((module.TypeSystemOptions & TypeSystemOptions.ParamsCollections) != 0 - && parameterDef.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.ParamCollection)) + var attributes = parameterDef.GetCustomAttributes(); + bool hasUnscopedRefAttribute = attributes.HasKnownAttribute(metadata, KnownAttribute.UnscopedRef); + bool isParamsCollection = (module.TypeSystemOptions & TypeSystemOptions.ParamsCollections) != 0 + && attributes.HasKnownAttribute(metadata, KnownAttribute.ParamCollection); + ScopedKind declaredScope = ScopedKind.None; + if (!hasUnscopedRefAttribute + && !isParamsCollection + && attributes.HasKnownAttribute(metadata, KnownAttribute.ScopedRef)) { - // params collections are implicitly scoped - return default; - } - if (parameterDef.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.ScopedRef)) - { - return new LifetimeAnnotation { ScopedRef = true }; + if (ReferenceKind != ReferenceKind.None) + { + declaredScope = ScopedKind.ScopedRef; + } + else if (Type.IsRefLikeOrAllowsRefLikeType()) + { + declaredScope = ScopedKind.ScopedValue; + } } - return default; + + return new LifetimeAnnotation { + DeclaredScope = declaredScope, + HasUnscopedRefAttribute = hasUnscopedRefAttribute + }; } } diff --git a/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs b/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs index 9701bba081..0002b5d0a0 100644 --- a/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs +++ b/ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs @@ -91,6 +91,7 @@ internal MetadataModule(ICompilation compilation, MetadataFile peFile, TypeSyste var customAttrs = metadata.GetModuleDefinition().GetCustomAttributes(); this.NullableContext = customAttrs.GetNullableContext(metadata) ?? Nullability.Oblivious; this.minAccessibilityForNRT = FindMinimumAccessibilityForNRT(metadata, customAttrs); + this.UseUpdatedEscapeRules = HasRefSafetyRulesVersion(customAttrs, 11); this.rootNamespace = new MetadataNamespace(this, null, string.Empty, metadata.GetNamespaceDefinitionRoot()); if (!options.HasFlag(TypeSystemOptions.Uncached)) @@ -112,6 +113,43 @@ internal string GetString(StringHandle name) public TypeSystemOptions TypeSystemOptions => options; + /// + /// Gets whether the module declares the C# 11 ref-safety rules. + /// + internal bool UseUpdatedEscapeRules { get; } + + bool HasRefSafetyRulesVersion(CustomAttributeHandleCollection attributes, int expectedVersion) + { + foreach (var attributeHandle in attributes) + { + var attribute = metadata.GetCustomAttribute(attributeHandle); + if (!attribute.IsKnownAttribute(metadata, KnownAttribute.RefSafetyRules)) + continue; + + CustomAttributeValue value; + try + { + value = attribute.DecodeValue(Metadata.MetadataExtensions.MinimalAttributeTypeProvider); + } + catch (BadImageFormatException) + { + continue; + } + catch (Metadata.EnumUnderlyingTypeResolveException) + { + continue; + } + if (value.FixedArguments.Length == 1 + && value.FixedArguments[0].Value is int version) + { + // Roslyn recognizes exactly version 11; missing and unknown versions use legacy rules. + return version == expectedVersion; + } + } + + return false; + } + #region IModule interface public MetadataFile MetadataFile { get; } diff --git a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs index 90bf02ce64..a42b17d2b1 100644 --- a/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs +++ b/ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs @@ -658,6 +658,65 @@ public static IAttribute GetAttribute(this IParameter parameter, KnownAttribute } #endregion + #region Lifetime annotations + internal static bool IsRefLikeOrAllowsRefLikeType(this IType type) + { + return type.IsByRefLike + || type is ITypeParameter { AllowsRefLikeType: true }; + } + + /// + /// Gets the parameter's effective scope after applying implicit scope rules and + /// UnscopedRefAttribute. + /// + internal static ScopedKind GetEffectiveScope(this IParameter parameter) + { + LifetimeAnnotation lifetime = parameter.Lifetime; + if (lifetime.HasUnscopedRefAttribute) + return ScopedKind.None; + + if (lifetime.DeclaredScope != ScopedKind.None) + return lifetime.DeclaredScope; + + if (parameter.Owner?.ParentModule is MetadataModule module) + { + if ((module.TypeSystemOptions & TypeSystemOptions.ScopedRef) == 0) + return ScopedKind.None; + + if (module.UseUpdatedEscapeRules && parameter.ReferenceKind == ReferenceKind.Out) + return ScopedKind.ScopedRef; + } + + if (parameter.IsParams && parameter.Type.IsRefLikeOrAllowsRefLikeType()) + return ScopedKind.ScopedValue; + + return ScopedKind.None; + } + + /// + /// Gets the effective scope of the implicit this parameter. + /// + internal static ScopedKind GetThisParameterScope(this IMethod method) + { + if (method.IsStatic || method.DeclaringTypeDefinition?.Kind != TypeKind.Struct) + return ScopedKind.None; + + if (method.ParentModule is MetadataModule module) + { + if ((module.TypeSystemOptions & TypeSystemOptions.ScopedRef) == 0) + return ScopedKind.None; + + bool hasUnscopedRefAttribute = method.HasAttribute(KnownAttribute.UnscopedRef) + || method.AccessorOwner is IProperty property + && property.HasAttribute(KnownAttribute.UnscopedRef); + if (hasUnscopedRefAttribute && module.UseUpdatedEscapeRules) + return ScopedKind.None; + } + + return ScopedKind.ScopedRef; + } + #endregion + #region IParameter.IsDefaultValueAssignmentAllowed /// /// Checks if the parameter is allowed to be assigned a default value.