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 number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.SourceGeneration.Helpers;
Expand Down Expand Up @@ -58,6 +58,7 @@ public static string EmitSupportTypes()
using (sb.Block("internal sealed class TestMethodReflectionInfo"))
{
sb.AppendLine("public string Name { get; set; } = string.Empty;");
sb.AppendLine("public Type DeclaringType { get; set; } = null!;");
sb.AppendLine("public bool IsTestMethod { get; set; }");
sb.AppendLine("public bool IsStatic { get; set; }");
sb.AppendLine("public bool IsAsync { get; set; }");
Expand Down Expand Up @@ -243,6 +244,7 @@ private static void EmitMethods(IndentedStringBuilder sb, string fqn, TestClassM
using (sb.Block(null))
{
sb.AppendLine($"Name = \"{Escape(method.Name)}\",");
sb.AppendLine($"DeclaringType = typeof({method.DeclaringTypeFullyQualifiedName}),");
sb.AppendLine($"IsTestMethod = {Bool(method.IsTestMethod)},");
sb.AppendLine($"IsStatic = {Bool(method.IsStatic)},");
sb.AppendLine($"IsAsync = {Bool(method.IsAsync)},");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList<T
// reflection for that one method) rather than throwing out of the [ModuleInitializer],
// which would fault registration for the whole assembly.
sb.AppendLine("availableMethods ??= type.GetMethods(memberFlags);");
sb.AppendLine("MethodInfo? methodInfo = ResolveMethod(availableMethods, method.Name, method.ParameterTypes);");
sb.AppendLine("MethodInfo? methodInfo = ResolveMethod(availableMethods, method.DeclaringType, method.Name, method.ParameterTypes);");
using (sb.Block("if (methodInfo is not null)"))
{
sb.AppendLine("methodInvokers[methodInfo] = method.Invoke;");
Expand Down Expand Up @@ -280,12 +280,12 @@ private static void EmitInitializeBody(IndentedStringBuilder sb, IReadOnlyList<T

private static void EmitResolveMethodHelper(IndentedStringBuilder sb)
{
sb.AppendLine("private static MethodInfo? ResolveMethod(MethodInfo[] availableMethods, string name, Type[] parameterTypes)");
sb.AppendLine("private static MethodInfo? ResolveMethod(MethodInfo[] availableMethods, Type declaringType, string name, Type[] parameterTypes)");
using (sb.Block(null))
{
using (sb.Block("foreach (MethodInfo candidate in availableMethods)"))
{
using (sb.Block("if (candidate.Name != name)"))
using (sb.Block("if (candidate.DeclaringType != declaringType || candidate.Name != name)"))
{
sb.AppendLine("continue;");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;
Expand Down Expand Up @@ -41,11 +41,15 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List<DiagnosticI
// [TestMethod], the [TestContext] setter, … — are visible to the consumer
// without runtime reflection.
//
// Iteration order is derived-first so that an override or `new`-shadowed member
// on the derived type wins over the base declaration with the same signature.
// Iteration order is derived-first. Members declared at a nearer inheritance level
// hide same-name ancestor members according to C# lookup rules, while overloads
// declared together on the same type are preserved. Indexers are excluded because
// their metadata name is not used in C# member access.
// Constructors are NEVER inherited and are taken only from the leaf type.
var methodsByKey = new Dictionary<string, TestMethodModel>(StringComparer.Ordinal);
var propertiesByName = new Dictionary<string, TestPropertyModel>(StringComparer.Ordinal);
var seenPropertyNames = new HashSet<string>(StringComparer.Ordinal);
var methodNamesInDerivedTypes = new HashSet<string>(StringComparer.Ordinal);
var nonMethodNamesInDerivedTypes = new HashSet<string>(StringComparer.Ordinal);
var methodsInDerivedTypes = new List<IMethodSymbol>();
ImmutableArray<TestMethodModel>.Builder methods = ImmutableArray.CreateBuilder<TestMethodModel>();
ImmutableArray<TestPropertyModel>.Builder properties = ImmutableArray.CreateBuilder<TestPropertyModel>();
ImmutableArray<TestConstructorModel>.Builder ctors = ImmutableArray.CreateBuilder<TestConstructorModel>();
Expand All @@ -66,6 +70,7 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List<DiagnosticI
{
bool isLeaf = SymbolEqualityComparer.Default.Equals(current, typeSymbol);
hasPartialTypeInHierarchy |= IsPartial(current);
ImmutableArray<ISymbol> currentMembers = current.GetMembers();

// Capture each closed, referenceable base type so the runtime registration can root
// its members (e.g. base-declared [ClassInitialize]/[TestContext]) via [DynamicDependency]
Expand All @@ -76,14 +81,35 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List<DiagnosticI
baseTypes.Add(current.ToDisplayString(SymbolDisplayFormats.FullyQualified));
}

foreach (ISymbol member in current.GetMembers())
foreach (ISymbol member in currentMembers)
{
switch (member)
{
case IMethodSymbol { MethodKind: MethodKind.Ordinary } method:
ImmutableArray<AttributeData> inheritedAttributes = AttributeMaterializationHelper.CollectInheritedAttributes(method);
bool isTestMethod = TestMemberValidationHelper.IsTestMethodAttributePresent(inheritedAttributes);
if (!TestMemberValidationHelper.IsAccessibleFromConsumer(method))
bool hiddenByNonMethod = nonMethodNamesInDerivedTypes.Contains(method.Name);
bool hiddenByMethodGroup = methodNamesInDerivedTypes.Contains(method.Name);
bool isAccessible = TestMemberValidationHelper.IsAccessibleFromConsumer(method, consumingAssembly);
if ((hiddenByNonMethod || hiddenByMethodGroup)
&& isAccessible
&& TestMemberValidationHelper.TryReportUnsupportedMethod(method, leafFqn, diagnostics))
{
hasUnsupportedTestMethod |= isTestMethod;
break;
}

if (hiddenByNonMethod || hiddenByMethodGroup)
{
hasUnsupportedTestMethod |= isTestMethod
&& (hiddenByNonMethod
|| !methodsInDerivedTypes.Any(derivedMethod =>
ReplacesInheritedRuntimeTest(derivedMethod)
&& TestMemberValidationHelper.HaveSameRuntimeDiscoverySignature(derivedMethod, method)));
break;
}

if (!isAccessible)
{
hasUnsupportedTestMethod |= isTestMethod;
break;
Expand All @@ -98,25 +124,23 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List<DiagnosticI
break;
}

string key = TestMemberValidationHelper.BuildMethodSignatureKey(method);
if (!methodsByKey.ContainsKey(key))
{
TestMethodModel model = BuildMethod(method, consumingAssembly, inheritedAttributes, isTestMethod);
methodsByKey[key] = model;
methods.Add(model);
}
methods.Add(BuildMethod(method, consumingAssembly, inheritedAttributes, isTestMethod));

break;
case IPropertySymbol property:
hasUnsupportedTestMethod |= HasTestMethodAttribute(property.GetMethod)
|| HasTestMethodAttribute(property.SetMethod);
if (methodNamesInDerivedTypes.Contains(property.Name)
|| nonMethodNamesInDerivedTypes.Contains(property.Name))
{
break;
}

if (!property.IsIndexer
&& TestMemberValidationHelper.IsAccessibleFromConsumer(property)
&& !propertiesByName.ContainsKey(property.Name))
&& seenPropertyNames.Add(property.Name)
&& TestMemberValidationHelper.IsAccessibleFromConsumer(property, consumingAssembly))
{
TestPropertyModel model = BuildProperty(property, consumingAssembly);
propertiesByName[property.Name] = model;
properties.Add(model);
properties.Add(BuildProperty(property, consumingAssembly));
}

break;
Expand Down Expand Up @@ -150,6 +174,24 @@ public static TestClassModel Build(INamedTypeSymbol typeSymbol, List<DiagnosticI
break;
}
}

foreach (ISymbol member in currentMembers)
{
switch (member)
{
case IMethodSymbol { MethodKind: MethodKind.Ordinary } method:
methodNamesInDerivedTypes.Add(method.Name);
methodsInDerivedTypes.Add(method);
break;

case IPropertySymbol { IsIndexer: false }:
case IFieldSymbol:
case IEventSymbol:
case INamedTypeSymbol:
Comment thread
Evangelink marked this conversation as resolved.
nonMethodNamesInDerivedTypes.Add(member.Name);
break;
}
}
}

AttributeMaterializationHelper.AttributeMaterializationResult classAttributes =
Expand Down Expand Up @@ -204,6 +246,11 @@ private static bool HasTestMethodAttribute(IMethodSymbol? method)
=> method is not null
&& TestMemberValidationHelper.IsTestMethodAttributePresent(AttributeMaterializationHelper.CollectInheritedAttributes(method));

private static bool ReplacesInheritedRuntimeTest(IMethodSymbol method)
=> method.OverriddenMethod is not null
|| (method is { DeclaredAccessibility: Accessibility.Public, IsStatic: false }
&& HasTestMethodAttribute(method));

private static TestMethodModel BuildMethod(
IMethodSymbol method,
IAssemblySymbol consumingAssembly,
Expand Down Expand Up @@ -237,6 +284,7 @@ private static TestMethodModel BuildMethod(

return new TestMethodModel(
Name: method.Name,
DeclaringTypeFullyQualifiedName: method.ContainingType.ToDisplayString(SymbolDisplayFormats.FullyQualified),
IsStatic: method.IsStatic,
IsAsync: method.IsAsync,
ReturnsTask: returnsTask,
Expand Down Expand Up @@ -282,15 +330,11 @@ private static TestPropertyModel BuildProperty(IPropertySymbol property, IAssemb
FullyQualifiedType: property.Type.ToDisplayString(SymbolDisplayFormats.FullyQualified),
IsStatic: property.IsStatic,

// The generated registry lives in the consuming assembly, so a getter is reachable
// when it is public, internal, or protected-internal. private / protected getters
// cannot be read from the generated (non-derived) call site.
HasGettableValue: property.GetMethod is
{
DeclaredAccessibility: Accessibility.Public
or Accessibility.Internal
or Accessibility.ProtectedOrInternal,
},
HasGettableValue: property.GetMethod is { } getter
&& SymbolReferenceabilityHelper.IsMemberAccessibleFrom(
getter.DeclaredAccessibility,
getter.ContainingType,
consumingAssembly),
// An init-only setter has public DeclaredAccessibility but cannot be assigned outside an
// object initializer, so emitting `instance.Prop = value` would not compile (CS8852);
// treat it as non-settable so the adapter falls back to reflection (PropertyInfo.SetValue).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Immutable;
Expand All @@ -19,13 +19,13 @@ internal static class TestMemberValidationHelper
// Restricted to accessibilities the emitted helper class (a separate static type
// declared in MSTest.SourceGenerated, not a derived type) can legally call.
// 'protected' and 'private protected' members require the caller to be a derived
// type, so they are excluded; 'protected internal' is included because the internal
// half is satisfied (the generated helper lives in the same assembly).
internal static bool IsAccessibleFromConsumer(ISymbol symbol)
=> symbol.DeclaredAccessibility is
Accessibility.Public
or Accessibility.Internal
or Accessibility.ProtectedOrInternal;
// type, so they are excluded. Internal access is available only for members declared
// in the consuming assembly.
internal static bool IsAccessibleFromConsumer(ISymbol symbol, IAssemblySymbol consumingAssembly)
=> SymbolReferenceabilityHelper.IsMemberAccessibleFrom(
symbol.DeclaredAccessibility,
symbol.ContainingAssembly,
consumingAssembly);
Comment thread
Evangelink marked this conversation as resolved.

internal static bool IsTestMethodAttributePresent(ImmutableArray<AttributeData> attributes)
{
Expand Down Expand Up @@ -85,44 +85,79 @@ internal static bool IsSupportedTestClassConstructor(IMethodSymbol constructor)
&& parameters[0].Type.ToDisplayString(SymbolDisplayFormats.FullyQualified) == "global::" + MSTestAttributeNames.UnitTestingNamespace + ".TestContext");
}

internal static string BuildMethodSignatureKey(IMethodSymbol method)
// Mirrors TypeEnumerator's MethodInfo.ToString()-based discovery identity. In particular,
// generic parameter names remain significant because reflection formats them into that string.
internal static bool HaveSameRuntimeDiscoverySignature(IMethodSymbol left, IMethodSymbol right)
{
var sb = new StringBuilder();
sb.Append(method.IsStatic ? "S:" : "I:");
sb.Append(method.Name);
if (method.Arity > 0)
if (!string.Equals(left.Name, right.Name, StringComparison.Ordinal)
|| left.Arity != right.Arity
|| left.Parameters.Length != right.Parameters.Length
|| (left.IsStatic && !right.IsStatic)
|| !AreSignatureTypesEquivalent(left.ReturnType, right.ReturnType))
Comment thread
Evangelink marked this conversation as resolved.
{
sb.Append('`');
sb.Append(method.Arity);
return false;
}

sb.Append('(');
bool first = true;
foreach (IParameterSymbol p in method.Parameters)
for (int index = 0; index < left.Parameters.Length; index++)
{
if (!first)
IParameterSymbol leftParameter = left.Parameters[index];
IParameterSymbol rightParameter = right.Parameters[index];
if ((leftParameter.RefKind == RefKind.None) != (rightParameter.RefKind == RefKind.None)
|| !AreSignatureTypesEquivalent(leftParameter.Type, rightParameter.Type))
{
sb.Append(',');
return false;
}
}

return true;
}

private static bool AreSignatureTypesEquivalent(ITypeSymbol left, ITypeSymbol right)
{
if (left is IDynamicTypeSymbol)
{
return right is IDynamicTypeSymbol || right.SpecialType == SpecialType.System_Object;
}

if (right is IDynamicTypeSymbol)
{
return left.SpecialType == SpecialType.System_Object;
}

if (left is ITypeParameterSymbol leftTypeParameter && right is ITypeParameterSymbol rightTypeParameter)
{
return leftTypeParameter.TypeParameterKind == rightTypeParameter.TypeParameterKind
&& string.Equals(leftTypeParameter.Name, rightTypeParameter.Name, StringComparison.Ordinal);
Comment thread
Evangelink marked this conversation as resolved.
}

if (left is IArrayTypeSymbol leftArray && right is IArrayTypeSymbol rightArray)
{
return leftArray.Rank == rightArray.Rank
&& AreSignatureTypesEquivalent(leftArray.ElementType, rightArray.ElementType);
}

first = false;
switch (p.RefKind)
if (left is INamedTypeSymbol leftNamed && right is INamedTypeSymbol rightNamed)
{
if (leftNamed.TypeArguments.Length != rightNamed.TypeArguments.Length
|| !SymbolEqualityComparer.Default.Equals(leftNamed.OriginalDefinition, rightNamed.OriginalDefinition)
|| (leftNamed.ContainingType is null) != (rightNamed.ContainingType is null)
|| (leftNamed.ContainingType is not null
&& !AreSignatureTypesEquivalent(leftNamed.ContainingType, rightNamed.ContainingType!)))
{
case RefKind.Ref:
sb.Append("ref ");
break;
case RefKind.Out:
sb.Append("out ");
break;
case RefKind.In:
sb.Append("in ");
break;
return false;
}

for (int index = 0; index < leftNamed.TypeArguments.Length; index++)
{
if (!AreSignatureTypesEquivalent(leftNamed.TypeArguments[index], rightNamed.TypeArguments[index]))
{
return false;
}
}

sb.Append(p.Type.ToDisplayString(SymbolDisplayFormats.FullyQualified));
return true;
}

sb.Append(')');
return sb.ToString();
return SymbolEqualityComparer.Default.Equals(left, right);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using MSTest.Analyzers.Shared;
Expand Down Expand Up @@ -65,6 +65,7 @@ internal sealed record DynamicDataSourceModel(

internal sealed record TestMethodModel(
string Name,
string DeclaringTypeFullyQualifiedName,
bool IsStatic,
bool IsAsync,
bool ReturnsTask,
Expand Down
Loading
Loading