Skip to content

Commit 55c5ce2

Browse files
committed
Improve reflection for readonly, dynamic, extension attributes
Refactored attribute handling to use new constants and generic methods for DynamicAttribute and ExtensionAttribute. Enhanced parameter and property metadata logic, including IsDynamic and IsGeneric support, and clarified readonly/ref/in/out semantics. Removed redundant readonly property logic and updated init-only detection. Improved attribute lookup performance and code clarity with switch expressions and better documentation.
1 parent 1dae9c0 commit 55c5ce2

9 files changed

Lines changed: 47 additions & 64 deletions

File tree

‎src/BionicCode.Utilities.Net.Common/ReflectionConstants.cs‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
namespace BionicCode.Utilities.Net;
2-
3-
using System.ComponentModel;
4-
using System.Runtime.InteropServices;
5-
62
/// <summary>
73
/// Provides a set of constant string values that are commonly used in reflection scenarios, particularly when working with properties, indexers, events, delegates, and operator methods in .NET. These constants can be used to ensure consistency and avoid hardcoding string literals throughout the codebase when performing reflection operations or generating code dynamically.
84
/// </summary>
@@ -89,4 +85,6 @@ public static class ReflectionConstants
8985
public const string RequiresLocationAttributeFullName = "System.Runtime.CompilerServices.RequiresLocationAttribute";
9086
public const string InAttributeFullName = "System.Runtime.InteropServices.InAttribute";
9187
public const string IsExternalInitFullName = "System.Runtime.CompilerServices.IsExternalInit";
88+
public const string DynamicAttributeFullName = "System.Runtime.CompilerServices.DynamicAttribute";
89+
public const string ExtensionAttributeFullName = "System.Runtime.CompilerServices.ExtensionAttribute";
9290
}

‎src/BionicCode.Utilities.Reflection/IParameterDataView.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ public interface IParameterDataView : ISymbolInfoDataView
1010
RuntimeTypeHandle DeclaringTypeHandle { get; }
1111
object? DefaultValue { get; }
1212
bool IsByRef { get; }
13+
bool IsDynamic { get; }
14+
bool IsGeneric { get; }
1315
bool IsGenericMethodParameter { get; }
1416
bool IsGenericTypeParameter { get; }
1517
bool IsIn { get; }

‎src/BionicCode.Utilities.Reflection/MethodData.cs‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ internal sealed partial class MethodData : ParameterizedMemberData, IMethodDataI
7676
private RuntimeTypeHandle? _implementingTypeHandle;
7777
private bool? _isExplicitInterfaceImplementation;
7878
private IMethodDataView? _methodDataView;
79-
private bool? _isReadonly;
8079

8180
internal MethodData(SymbolReflectionInfoCacheKeyInternal symbolReflectionInfoCacheKey)
8281
: base(symbolReflectionInfoCacheKey)
@@ -1164,12 +1163,9 @@ internal bool IsAwaitableGenericTask
11641163
}
11651164
}
11661165

1167-
internal bool IsReadonly => _isReadonly
1168-
??= DeclaringTypeData.IsStruct && HasCompilerAttribute<IsReadOnlyAttribute>(ReflectionConstants.IsReadOnlyAttributeFullName);
1169-
11701166
internal bool IsOverride => _isOverride ??= MethodData.IsMethodOverride(this);
11711167

1172-
internal bool IsReturnValueReadOnly => _isReturnValueReadOnly ??= ReturnParameterData.IsRefReadOnly;
1168+
internal bool IsReturnValueRefReadOnly => _isReturnValueReadOnly ??= ReturnParameterData.IsRefReadOnly;
11731169

11741170
internal bool IsReturnValueByRef => _isReturnValueByRef ??= ReturnTypeData.IsByRef;
11751171

@@ -1239,7 +1235,7 @@ private static bool IsMethodExtensionMethod(MethodData methodData)
12391235
return false;
12401236
}
12411237

1242-
bool hasExtensionMethodMarker = methodData.HasCompilerAttribute(ReflectionHelperExtensions.ExtensionAttributeType, ReflectionHelperExtensions.ExtensionAttributeType.FullName);
1238+
bool hasExtensionMethodMarker = methodData.HasCompilerAttribute<ExtensionAttribute>(ReflectionConstants.ExtensionAttributeFullName);
12431239
if (!hasExtensionMethodMarker)
12441240
{
12451241
return false;

‎src/BionicCode.Utilities.Reflection/ParameterData.cs‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ internal sealed class ParameterData : SymbolInfoData
4646
private bool? _isMethodReturnParameter;
4747
private TypeList? _requiredModifiers;
4848
private TypeList? _optionalModifiers;
49+
private bool? _isDynamic;
4950

5051
internal ParameterData(SymbolReflectionInfoCacheKeyInternal symbolInfoDataCacheKey)
5152
: base(symbolInfoDataCacheKey.ParameterDescriptor.ParameterName, SymbolKind.Parameter, symbolInfoDataCacheKey)
@@ -62,6 +63,16 @@ internal ParameterData(SymbolReflectionInfoCacheKeyInternal symbolInfoDataCacheK
6263

6364
internal RuntimeTypeHandle ParameterTypeHandle => _propertyTypeHandle ??= ParameterInfo.ParameterType.TypeHandle;
6465

66+
internal bool IsDynamic => _isDynamic ??= HasCompilerAttribute<DynamicAttribute>(ReflectionConstants.DynamicAttributeFullName);
67+
68+
/// <summary>
69+
/// Gets a value indicating whether the current type represents a generic type or method parameter.
70+
/// </summary>
71+
/// <remarks>This property returns <see langword="true"/> if the type is either a generic type parameter
72+
/// or a generic method parameter. Use this property to determine whether the type is a placeholder for a generic
73+
/// argument in type or method definitions.</remarks>
74+
internal bool IsGeneric => IsGenericTypeParameter || IsGenericMethodParameter;
75+
6576
/// <summary>
6677
/// Gets a value indicating whether the current type is passed by reference using the <see langword="ref"/> keyword.
6778
/// </summary>
@@ -182,7 +193,7 @@ internal ParameterData(SymbolReflectionInfoCacheKeyInternal symbolInfoDataCacheK
182193
internal override IList<CustomAttributeData> AttributeData => _attributeData ??= [.. ParameterInfo.GetCustomAttributesData()];
183194

184195
/// <summary>
185-
/// Gets a value indicating whether the parameter is passed by reference.
196+
/// Gets a value indicating whether the parameter is passed by reference (<see langword="in"/>, <see langword="out"/>, <see langword="ref"/> or <see langword="ref"/><see langword="readonly"/>).
186197
/// </summary>
187198
internal bool IsByRef => _isByRef ??= ParameterTypeData.Type.IsByRef;
188199

@@ -284,7 +295,9 @@ internal bool HasRequiredCustomModifier(string modifierName)
284295
/// <returns></returns>
285296
private static bool IsRefInternal(ParameterData parameterData)
286297
{
287-
if (!parameterData.IsByRef || parameterData.IsOut)
298+
if (parameterData.IsMethodReturnParameter
299+
|| !parameterData.IsByRef
300+
|| parameterData.IsOut)
288301
{
289302
return false;
290303
}
@@ -308,8 +321,9 @@ private static bool IsRefReadOnlyInternal(ParameterData parameterData)
308321

309322
if (parameterData.IsMethodReturnParameter)
310323
{
311-
return parameterData.HasOptionalCustomModifier(ReflectionConstants.InAttributeFullName)
312-
|| parameterData.HasCompilerAttribute<IsReadOnlyAttribute>(ReflectionConstants.IsReadOnlyAttributeFullName);
324+
return parameterData.HasRequiredCustomModifier(ReflectionConstants.InAttributeFullName)
325+
|| parameterData.HasCompilerAttribute<IsReadOnlyAttribute>(ReflectionConstants.IsReadOnlyAttributeFullName)
326+
|| parameterData.HasRequiredCustomModifier(ReflectionConstants.IsReadOnlyAttributeFullName);
313327
}
314328
else
315329
{
@@ -319,11 +333,15 @@ private static bool IsRefReadOnlyInternal(ParameterData parameterData)
319333
}
320334
}
321335

322-
private static bool IsOutParameter(ParameterData parameterData) => parameterData.IsByRef && parameterData.ParameterInfo.IsOut;
336+
private static bool IsOutParameter(ParameterData parameterData) => !parameterData.IsMethodReturnParameter
337+
&& parameterData.IsByRef
338+
&& parameterData.ParameterInfo.IsOut;
323339

324340
private static bool IsInParameter(ParameterData parameterData)
325341
{
326-
if (!parameterData.ParameterTypeData.IsByRef || parameterData.IsOut)
342+
if (parameterData.IsMethodReturnParameter
343+
|| !parameterData.ParameterTypeData.IsByRef
344+
|| parameterData.IsOut)
327345
{
328346
return false;
329347
}

‎src/BionicCode.Utilities.Reflection/PropertyData.cs‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ namespace BionicCode.Utilities.Net.Reflection;
33

44
using System;
55
using System.Collections.Concurrent;
6-
using System.Linq;
76
using System.Reflection;
87
using System.Runtime.CompilerServices;
9-
using Microsoft.CodeAnalysis;
108

119
internal sealed class PropertyData : MemberData, IPropertyDataInvoker
1210
{
@@ -49,8 +47,6 @@ internal sealed class PropertyData : MemberData, IPropertyDataInvoker
4947
private readonly ConcurrentDictionary<RuntimeTypeHandle, Delegate> _invokerTable;
5048
private string? _assemblyName;
5149
private SymbolComponentInfo? _symbolComponentInfo;
52-
private bool? _isReadOnlySetMethodOnStruct;
53-
private bool? _isReadOnlyGetMethodOnStruct;
5450
private readonly WellKnownPropertyDescriptor _descriptor;
5551
private RuntimeTypeHandle? _declaringTypeHandle;
5652
private RuntimeTypeHandle? _implementingTypeHandle;
@@ -923,21 +919,6 @@ internal AccessModifier GetAccessorAccessModifier
923919
/// <value><see langword="true"/> if the property does not have a setter and is therefore read-only; otherwise, <see langword="false"/>.</value>
924920
internal bool IsReadOnly => !CanWrite;
925921

926-
/// <summary>
927-
/// Gets whether the property’s set accessor is a <see langword="readonly"/> instance member (struct-only), meaning it doesn’t modify the struct’s instance state.
928-
/// </summary>
929-
internal bool IsReadOnlySetMethodOnStruct => _isReadOnlySetMethodOnStruct ??= CanWrite && (PropertySetMethodData.IsReadonly || IsReadOnlyPropertyOnStruct);
930-
931-
/// <summary>
932-
/// Gets whether the property’s get accessor is a <see langword="readonly"/> instance member (struct-only), meaning it doesn’t modify the struct’s instance state.
933-
/// </summary>
934-
internal bool IsReadOnlyGetMethodOnStruct => _isReadOnlyGetMethodOnStruct ??= CanRead && (PropertyGetMethodData.IsReadonly || IsReadOnlyPropertyOnStruct);
935-
936-
/// <summary>
937-
/// Gets whether the property itself is a <see langword="readonly"/> instance member on a struct type, meaning it doesn’t modify the struct’s instance state. This is determined by checking if the property has the <see cref="IsReadOnlyAttribute"/> applied, which is used by the C# compiler to indicate that a struct member is readonly. If the property does not have this attribute, this property returns <see langword="false"/>. Note that for reference types, this property will always return <see langword="false"/>, as the concept of a readonly instance member only applies to value types (structs).
938-
/// </summary>
939-
internal bool IsReadOnlyPropertyOnStruct => HasCompilerAttribute<IsReadOnlyAttribute>(ReflectionConstants.IsReadOnlyAttributeFullName);
940-
941922
/// <summary>
942923
/// Returns whether the property is an init-only property.
943924
/// </summary>
@@ -1099,8 +1080,8 @@ private static SymbolAttributes GetAttributesInternal(PropertyData propertyData)
10991080
return propertyAttributes;
11001081
}
11011082

1102-
private static bool IsPropertyInit(PropertyData propertyData) => propertyData.CanWrite
1103-
&& propertyData.PropertySetMethodData.ReturnParameterData.HasCompilerAttribute(typeof(IsExternalInit), ReflectionConstants.IsExternalInitFullName);
1083+
private static bool IsPropertyInit(PropertyData propertyData) => propertyData.CanWrite
1084+
&& propertyData.PropertySetMethodData.ReturnParameterData.HasRequiredCustomModifier(ReflectionConstants.IsExternalInitFullName);
11041085

11051086
private static (AccessModifier PropertyModifier, AccessModifier GetMethodModifier, AccessModifier SetMethodModifier) GetPropertyAccessModifier(MethodData? getMethodData, MethodData? setMethodData)
11061087
{

‎src/BionicCode.Utilities.Reflection/ReflectionHelperExtensions.cs‎

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@ namespace BionicCode.Utilities.Net.Reflection;
66
using System.Collections;
77
using System.Collections.Generic;
88
using System.Collections.Immutable;
9-
using System.ComponentModel;
109
using System.Globalization;
1110
using System.Linq;
1211
using System.Reflection;
1312
using System.Runtime.CompilerServices;
14-
using System.Text;
1513
using Microsoft.CSharp;
1614

1715
/// <summary>
@@ -40,7 +38,6 @@ public static partial class ReflectionHelperExtensions
4038
internal const BindingFlags AllMembersFullHierarchyFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
4139

4240
internal static CSharpCodeProvider CodeProvider { get; } = new CSharpCodeProvider();
43-
internal static Type ExtensionAttributeType { get; } = typeof(ExtensionAttribute);
4441
internal static Type IsReadOnlyAttributeType { get; } = typeof(IsReadOnlyAttribute);
4542

4643
internal static PooledStringBuilder AppendCustomAttributes(this PooledStringBuilder nameBuilder, IEnumerable<CustomAttributeData> attributes, bool isAppendNewLineEnabled)
@@ -110,23 +107,16 @@ internal static string ToArgumentDisplayValue(this object value)
110107
{
111108
ArgumentNullExceptionAdvanced.ThrowIfNull(value);
112109

113-
switch (value)
114-
{
115-
case string stringValue:
116-
return $"\"{stringValue}\"";
117-
case char charValue:
118-
return $"'{charValue}'";
119-
case double doubleValue:
120-
return string.Format(CultureInfo.InvariantCulture, "{0}", doubleValue);
121-
case Enum enumValue:
122-
return $"{value.GetType().ToDisplayName()}.{enumValue.ToString()}";
123-
case Type type:
124-
return $"typeof({type.ToDisplayName()})";
125-
case IEnumerable enumerableValue:
126-
return $"new[] {{ {string.Join(", ", enumerableValue.OfType<object>().Select(val => val.ToArgumentDisplayValue()))} }}";
127-
default:
128-
return value.ToString();
129-
}
110+
return value switch
111+
{
112+
string stringValue => $"\"{stringValue}\"",
113+
char charValue => $"'{charValue}'",
114+
double doubleValue => string.Format(CultureInfo.InvariantCulture, "{0}", doubleValue),
115+
Enum enumValue => $"{value.GetType().ToDisplayName()}.{enumValue.ToString()}",
116+
Type type => $"typeof({type.ToDisplayName()})",
117+
IEnumerable enumerableValue => $"new[] {{ {string.Join(", ", enumerableValue.OfType<object>().Select(val => val.ToArgumentDisplayValue()))} }}",
118+
_ => value.ToString(),
119+
};
130120
}
131121

132122
internal static PooledStringBuilder AppendDisplayNameInternal(this PooledStringBuilder nameBuilder, TypeData typeData, bool isFullyQualifiedName, bool isGenericTypeParameterIncluded)

‎src/BionicCode.Utilities.Reflection/SymbolInfoData.cs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ internal bool HasCompilerAttribute(Type? attributeType, string? attributeName =
166166
internal bool HasCompilerAttribute(string attributeName, bool inherit = false)
167167
{
168168
ArgumentNullExceptionAdvanced.ThrowIfNullOrWhiteSpace(attributeName);
169-
return AttributeData
170-
.Any(attribute => attribute.AttributeType.FullName?.Equals(attributeName, StringComparison.Ordinal) ?? false);
169+
return AttributeDataLookup.Contains(attributeName);
171170
}
172171
}

‎src/BionicCode.Utilities.Reflection/SymbolSignatureGenerator.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ internal static SymbolComponentInfo ToSignatureComponentsInternal(MethodData met
146146
symbolComponents.AddModifier("ref");
147147
}
148148

149-
if (methodData.IsReturnValueReadOnly)
149+
if (methodData.IsReturnValueRefReadOnly)
150150
{
151151
symbolComponents.AddModifier("readonly");
152152
}
@@ -2027,7 +2027,7 @@ internal static string ToSignatureNameInternal(MethodData methodData, bool isFul
20272027
.Append(' ');
20282028
}
20292029

2030-
if (methodData.IsReturnValueReadOnly)
2030+
if (methodData.IsReturnValueRefReadOnly)
20312031
{
20322032
_ = signatureNameBuilder
20332033
.Append("readonly")

‎src/BionicCode.Utilities.Reflection/TypeData.cs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,8 +1067,7 @@ private static bool CanDeclareExtensionMethods(TypeData typeData)
10671067
return false;
10681068
}
10691069

1070-
Attribute? typeExtensionAttribute = typeInfo.GetCustomAttribute(ReflectionHelperExtensions.ExtensionAttributeType, false);
1071-
return typeExtensionAttribute != null;
1070+
return typeData.HasCompilerAttribute<ExtensionAttribute>(ReflectionConstants.ExtensionAttributeFullName);
10721071
}
10731072

10741073
/// <summary>
@@ -1174,7 +1173,7 @@ private static bool IsTypeDelegate(Type type)
11741173
&& s_delegateType.IsAssignableFrom(type);
11751174
}
11761175

1177-
private static bool IsReadOnlyStructInternal(TypeData typeData) => typeData.IsStruct && typeData.Type.GetCustomAttribute(ReflectionHelperExtensions.IsReadOnlyAttributeType) != null;
1176+
private static bool IsReadOnlyStructInternal(TypeData typeData) => typeData.IsStruct && typeData.HasCompilerAttribute<IsReadOnlyAttribute>(ReflectionConstants.IsReadOnlyAttributeFullName);
11781177

11791178
/// <summary>
11801179
/// Checks if the provided <see cref="MethodInfo"/> belongs to an asynchronous/awaitable method.

0 commit comments

Comments
 (0)