Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
29 changes: 29 additions & 0 deletions ICSharpCode.Decompiler.Tests/TypeSystem/TypeSystemTestCase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

Expand Down Expand Up @@ -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) { }
Expand Down
32 changes: 22 additions & 10 deletions ICSharpCode.Decompiler/TypeSystem/IParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

#nullable enable

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

Expand All @@ -36,23 +35,36 @@ public enum ReferenceKind : byte
RefReadOnly,
}

/// <summary>
/// Describes which part of a parameter or local is restricted to the current scope.
/// </summary>
public enum ScopedKind : byte
{
None,
ScopedRef,
ScopedValue,
}

public struct LifetimeAnnotation
{
/// <summary>
/// Gets or sets the scope explicitly encoded by <c>ScopedRefAttribute</c>.
/// Implicit and redundant scopedness is excluded.
/// </summary>
public ScopedKind DeclaredScope { get; set; }

/// <summary>
/// C# 11 scoped annotation: "scoped ref" (ScopedRefAttribute)
/// </summary>
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;
/// <summary>
/// Gets or sets whether <c>UnscopedRefAttribute</c> is present.
/// </summary>
public bool HasUnscopedRefAttribute { get; set; }
}

public interface IParameter : IVariable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ public enum KnownAttribute

// C# 11 attributes:
Required,
UnscopedRef,
RefSafetyRules,

// C# 12 attributes:
InlineArray,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
}
}

Expand Down
38 changes: 38 additions & 0 deletions ICSharpCode.Decompiler/TypeSystem/MetadataModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -112,6 +113,43 @@ internal string GetString(StringHandle name)

public TypeSystemOptions TypeSystemOptions => options;

/// <summary>
/// Gets whether the module declares the C# 11 ref-safety rules.
/// </summary>
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<IType> 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; }

Expand Down
59 changes: 59 additions & 0 deletions ICSharpCode.Decompiler/TypeSystem/TypeSystemExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}

/// <summary>
/// Gets the parameter's effective scope after applying implicit scope rules and
/// <c>UnscopedRefAttribute</c>.
/// </summary>
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;
}

/// <summary>
/// Gets the effective scope of the implicit <c>this</c> parameter.
/// </summary>
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
/// <summary>
/// Checks if the parameter is allowed to be assigned a default value.
Expand Down
Loading