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.