Skip to content
Merged
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
Original file line numberDiff line numberDiff line change
Expand Up@@ -222,7 +222,7 @@ private static SourceGeneratedReflectionDataProvider BuildMergedSnapshot(IReadOn
};
}

private static void MergeInto<TKey, TValue>(Dictionary<TKey, TValue> target, Dictionary<TKey, TValue> source)
private static void MergeInto<TKey, TValue>(Dictionary<TKey, TValue> target, IReadOnlyDictionary<TKey, TValue> source)
where TKey : notnull
{
foreach (KeyValuePair<TKey, TValue> kvp in source)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -52,8 +52,8 @@ public static class ReflectionMetadataHook
/// <param name="types">All types directly annotated with <c>[TestClass]</c> in the assembly.</param>
/// <param name="testMethods">
/// A map from each test class to its <c>[TestMethod]</c>-annotated <see cref="MethodInfo"/>
/// set. The dictionary and arrays are copied defensively; the caller may mutate the inputs
/// after the call.
/// set. Ownership transfers to the adapter: the source generator hands over freshly-built
/// collections and must not mutate them after the call (see the remarks on the full overload).
/// </param>
/// <remarks>
/// Do not call this method from hand-written code; it is meant to be invoked exclusively from
Expand DownExpand Up@@ -120,8 +120,21 @@ public static void Register(
/// assigns it directly.
/// </param>
/// <remarks>
/// <para>
/// Do not call this method from hand-written code; it is meant to be invoked exclusively from
/// the <c>[ModuleInitializer]</c> emitted by the MSTest source generator.
/// </para>
/// <para>
/// <b>Ownership transfer.</b> The adapter takes ownership of every collection passed in
/// (the <paramref name="types"/> array, the <paramref name="assemblyAttributes"/> array, and
/// each dictionary with its value arrays) and stores them without cloning; the read-only
/// dictionaries are held as-is behind <see cref="IReadOnlyDictionary{TKey, TValue}"/>. Callers
/// MUST hand over freshly-built collections and MUST NOT mutate them after the call returns;
/// the source generator (the only intended caller) already emits fresh, throwaway collections
/// that satisfy this. This is a contract about ownership and mutation, not caller identity: it
/// trades the previous defensive copies for zero-copy startup on the understanding that the
/// inputs are the adapter's to keep.
/// </para>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public static void Register(
Expand DownExpand Up@@ -174,29 +187,14 @@ public static void Register(
throw new ArgumentNullException(nameof(propertySetters));
}

var typesCopy = (Type[])types.Clone();

var testMethodsCopy = new Dictionary<Type, MethodInfo[]>(testMethods.Count);
foreach (KeyValuePair<Type, MethodInfo[]> kvp in testMethods)
{
testMethodsCopy[kvp.Key] = (MethodInfo[])kvp.Value.Clone();
}

var typeAttributesCopy = new Dictionary<Type, Attribute[]>(typeAttributes.Count);
foreach (KeyValuePair<Type, Attribute[]> kvp in typeAttributes)
{
typeAttributesCopy[kvp.Key] = (Attribute[])kvp.Value.Clone();
}

object[] assemblyAttributesCopy = (object[])assemblyAttributes.Clone();

var methodInvokersCopy = new Dictionary<MethodInfo, Func<object?, object?[]?, object?>>(methodInvokers.Count);
foreach (KeyValuePair<MethodInfo, Func<object?, object?[]?, object?>> kvp in methodInvokers)
{
methodInvokersCopy[kvp.Key] = kvp.Value;
}

var constructorInvokersCopy = new Dictionary<Type, SourceGeneratedReflectionDataProvider.ConstructorInvoker[]>(constructorInvokers.Count);
// Ownership transfer (see the remarks on this method): the source generator hands over
// freshly-built, throwaway collections and never mutates them after the call, so we store
// the passed arrays and read-only dictionaries directly instead of copying them.
Comment thread
Evangelink marked this conversation as resolved.
//
// ConstructorInvokerInfo (public struct) still has to be projected onto the adapter's
// internal ConstructorInvoker type; this is a representation change, not a defensive copy,
// and the parameter-type arrays are taken by reference.
var constructorInvokersMap = new Dictionary<Type, SourceGeneratedReflectionDataProvider.ConstructorInvoker[]>(constructorInvokers.Count);
foreach (KeyValuePair<Type, ConstructorInvokerInfo[]> kvp in constructorInvokers)
{
var invokers = new SourceGeneratedReflectionDataProvider.ConstructorInvoker[kvp.Value.Length];
Expand All@@ -205,26 +203,20 @@ public static void Register(
ConstructorInvokerInfo info = kvp.Value[i];
invokers[i] = new SourceGeneratedReflectionDataProvider.ConstructorInvoker
{
Parameters = (Type[])info.ParameterTypes.Clone(),
Parameters = info.ParameterTypes,
Invoker = info.Invoker,
};
}

constructorInvokersCopy[kvp.Key] = invokers;
}

var propertySettersCopy = new Dictionary<PropertyInfo, Action<object?, object?>>(propertySetters.Count);
foreach (KeyValuePair<PropertyInfo, Action<object?, object?>> kvp in propertySetters)
{
propertySettersCopy[kvp.Key] = kvp.Value;
constructorInvokersMap[kvp.Key] = invokers;
}

// TypesByName must always match Type.FullName at runtime (see comment in the source
// generator emitter): compute it on the runtime side from typeof(T).FullName so the
// generator emits less code and the same FullName conventions are honored for nested
// and generic types.
var typesByName = new Dictionary<string, Type>(typesCopy.Length, StringComparer.Ordinal);
foreach (Type type in typesCopy)
var typesByName = new Dictionary<string, Type>(types.Length, StringComparer.Ordinal);
foreach (Type type in types)
{
if (type.FullName is { } fullName)
{
Expand All@@ -236,14 +228,14 @@ public static void Register(
{
Assembly = assembly,
AssemblyName = assembly.GetName().Name ?? string.Empty,
Types = typesCopy,
Types = types,
TypesByName = typesByName,
TypeMethods = testMethodsCopy,
TypeAttributes = typeAttributesCopy,
AssemblyAttributes = assemblyAttributesCopy,
TypeMethodInvokers = methodInvokersCopy,
TypeConstructorsInvoker = constructorInvokersCopy,
TypePropertySetters = propertySettersCopy,
TypeMethods = testMethods,
TypeAttributes = typeAttributes,
AssemblyAttributes = assemblyAttributes,
TypeMethodInvokers = methodInvokers,
TypeConstructorsInvoker = constructorInvokersMap,
TypePropertySetters = propertySetters,
};

lock (Lock)
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -33,13 +33,13 @@ internal class SourceGeneratedReflectionDataProvider
/// <summary>
/// Gets a lookup of types by full name.
/// </summary>
public Dictionary<string, Type> TypesByName { get; init; } = [];
public IReadOnlyDictionary<string, Type> TypesByName { get; init; } = new Dictionary<string, Type>();

/// <summary>
/// Gets attributes declared on each type. The array contains attribute instances
/// already inflated by the source generator so no reflection call is required to read them.
/// </summary>
public Dictionary<Type, Attribute[]> TypeAttributes { get; init; } = [];
public IReadOnlyDictionary<Type, Attribute[]> TypeAttributes { get; init; } = new Dictionary<Type, Attribute[]>();

/// <summary>
/// Gets attribute instances declared at the assembly level.
Expand All@@ -50,7 +50,7 @@ internal class SourceGeneratedReflectionDataProvider
/// Gets the properties declared on each type that MSTest may inspect (for example
/// <c>TestContext</c> properties or properties referenced by <c>DynamicData</c>).
/// </summary>
public Dictionary<Type, PropertyInfo[]> TypeProperties { get; init; } = [];
public IReadOnlyDictionary<Type, PropertyInfo[]> TypeProperties { get; init; } = new Dictionary<Type, PropertyInfo[]>();

/// <summary>
/// Gets the methods declared on each type that the source generator was able to surface
Expand All@@ -62,36 +62,36 @@ internal class SourceGeneratedReflectionDataProvider
/// source for <c>BindingFlags.DeclaredOnly</c>-style enumerations; the reflection-backed
/// fallback is responsible for completeness.
/// </remarks>
public Dictionary<Type, MethodInfo[]> TypeMethods { get; init; } = [];
public IReadOnlyDictionary<Type, MethodInfo[]> TypeMethods { get; init; } = new Dictionary<Type, MethodInfo[]>();

/// <summary>
/// Gets source-location data for each type's methods so navigation in the IDE works
/// without a PDB round-trip.
/// </summary>
public Dictionary<string, TypeLocation> TypeMethodLocations { get; init; } = [];
public IReadOnlyDictionary<string, TypeLocation> TypeMethodLocations { get; init; } = new Dictionary<string, TypeLocation>();

/// <summary>
/// Gets attributes declared on each method, keyed by the <see cref="MethodInfo"/> instance
/// that the source-generator resolved at startup. Keying by <see cref="MethodInfo"/>
/// (rather than method name) preserves the ability to distinguish overloaded methods.
/// </summary>
public Dictionary<MethodInfo, Attribute[]> TypeMethodAttributes { get; init; } = [];
public IReadOnlyDictionary<MethodInfo, Attribute[]> TypeMethodAttributes { get; init; } = new Dictionary<MethodInfo, Attribute[]>();

/// <summary>
/// Gets constructors declared on each type. These are returned by
/// <c>GetDeclaredConstructors</c>.
/// </summary>
public Dictionary<Type, ConstructorInfo[]> TypeConstructors { get; init; } = [];
public IReadOnlyDictionary<Type, ConstructorInfo[]> TypeConstructors { get; init; } = new Dictionary<Type, ConstructorInfo[]>();

/// <summary>
/// Gets a lookup of properties on a type by property name.
/// </summary>
public Dictionary<Type, Dictionary<string, PropertyInfo>> TypePropertiesByName { get; init; } = [];
public IReadOnlyDictionary<Type, Dictionary<string, PropertyInfo>> TypePropertiesByName { get; init; } = new Dictionary<Type, Dictionary<string, PropertyInfo>>();

/// <summary>
/// Gets the constructor invokers that allow instantiating types without reflection.
/// </summary>
public Dictionary<Type, ConstructorInvoker[]> TypeConstructorsInvoker { get; init; } = [];
public IReadOnlyDictionary<Type, ConstructorInvoker[]> TypeConstructorsInvoker { get; init; } = new Dictionary<Type, ConstructorInvoker[]>();

/// <summary>
/// Gets the delegate-based invokers for test methods and fixtures, keyed by the
Expand All@@ -102,14 +102,14 @@ internal class SourceGeneratedReflectionDataProvider
/// a <see cref="System.Threading.Tasks.ValueTask"/> is converted with <c>AsTask()</c>, and any
/// return value is discarded — so callers can simply await the result.
/// </summary>
public Dictionary<MethodInfo, Func<object?, object?[]?, object?>> TypeMethodInvokers { get; init; } = [];
public IReadOnlyDictionary<MethodInfo, Func<object?, object?[]?, object?>> TypeMethodInvokers { get; init; } = new Dictionary<MethodInfo, Func<object?, object?[]?, object?>>();

/// <summary>
/// Gets the delegate-based property setters, keyed by the <see cref="PropertyInfo"/> the
/// adapter holds (today: the <c>TestContext</c> property). Each delegate assigns the value
/// directly instead of calling <see cref="PropertyInfo.SetValue(object, object)"/>.
/// </summary>
public Dictionary<PropertyInfo, Action<object?, object?>> TypePropertySetters { get; init; } = [];
public IReadOnlyDictionary<PropertyInfo, Action<object?, object?>> TypePropertySetters { get; init; } = new Dictionary<PropertyInfo, Action<object?, object?>>();

/// <summary>
/// Returns the snapshot of merged metadata that callers should read. Single-assembly
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,7 @@ public void GetTestMethodInvoker_ReturnsRegisteredDelegate_AndInvokesWithoutRefl

var provider = new SourceGeneratedReflectionDataProvider
{
TypeMethodInvokers = { [add] = invoker },
TypeMethodInvokers = new Dictionary<MethodInfo, Func<object?, object?[]?, object?>> { [add] = invoker },
};
var operations = new SourceGeneratedReflectionOperations(provider);

Expand DownExpand Up@@ -67,7 +67,7 @@ public void GetConstructorInvoker_CreatesInstance_WithoutActivator()
];
var provider = new SourceGeneratedReflectionDataProvider
{
TypeConstructorsInvoker = { [typeof(Sample)] = invokers },
TypeConstructorsInvoker = new Dictionary<Type, SourceGeneratedReflectionDataProvider.ConstructorInvoker[]> { [typeof(Sample)] = invokers },
};
var operations = new SourceGeneratedReflectionOperations(provider);

Expand DownExpand Up@@ -99,7 +99,7 @@ public void GetConstructorInvoker_FallsBackToReflection_WhenArgumentsDoNotMatchR
];
var provider = new SourceGeneratedReflectionDataProvider
{
TypeConstructorsInvoker = { [typeof(Sample)] = invokers },
TypeConstructorsInvoker = new Dictionary<Type, SourceGeneratedReflectionDataProvider.ConstructorInvoker[]> { [typeof(Sample)] = invokers },
};
var operations = new SourceGeneratedReflectionOperations(provider);

Expand All@@ -119,7 +119,7 @@ public void GetPropertySetter_AssignsValue_WithoutSetValue()

var provider = new SourceGeneratedReflectionDataProvider
{
TypePropertySetters = { [property] = setter },
TypePropertySetters = new Dictionary<PropertyInfo, Action<object?, object?>> { [property] = setter },
};
var operations = new SourceGeneratedReflectionOperations(provider);

Expand Down