Detect inherited fixtures/tests from a different MSTest version (MSTEST0082) - #10508
Detect inherited fixtures/tests from a different MSTest version (MSTEST0082)#10508Jakub Jareš (nohwnd) wants to merge 13 commits into
Conversation
…Test version Issue #10505: when a test class compiled against MSTest v4 inherits a [TestInitialize]/[TestMethod]/... from a base compiled against v3, the inherited member silently does not run and inherited test methods are not discovered. The framework assembly was renamed (Microsoft.VisualStudio.TestPlatform.TestFramework -> MSTest.TestFramework), so the attribute on the v3-compiled base is a different type identity than the v4 adapter matches, and there is no build or discovery error. InheritedMemberFromDifferentMSTestVersionAnalyzer walks a test class's base types and warns when an inherited lifecycle or test-method attribute resolves to a framework assembly whose name differs from the one the test project compiles against. One analyzer/rule covers all eight lifecycle and test-method attributes, parameterized by the attribute name. Includes unit tests reproducing the mismatch (inherited [TestInitialize] and [TestMethod] from a v3-named base assembly) plus the legitimate cases that must stay silent (same-version cross-assembly and intra-assembly inheritance). 🤖 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds MSTEST0082 to detect inherited MSTest fixtures or tests compiled against a differently named framework assembly.
Changes:
- Implements cross-version inherited-member analysis.
- Adds analyzer tests and release tracking.
- Adds localized diagnostic resources.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs |
Implements MSTEST0082. |
src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs |
Registers the rule ID. |
src/Analyzers/MSTest.Analyzers/AnalyzerReleases.Unshipped.md |
Tracks the new rule. |
src/Analyzers/MSTest.Analyzers/Resources.resx |
Adds diagnostic text. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf |
Adds Czech localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf |
Adds German localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf |
Adds Spanish localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf |
Adds French localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf |
Adds Italian localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf |
Adds Japanese localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf |
Adds Korean localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf |
Adds Polish localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf |
Adds Portuguese localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf |
Adds Russian localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf |
Adds Turkish localization units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hans.xlf |
Adds Simplified Chinese units. |
src/Analyzers/MSTest.Analyzers/xlf/Resources.zh-Hant.xlf |
Adds Traditional Chinese units. |
test/UnitTests/MSTest.Analyzers.UnitTests/InheritedMemberFromDifferentMSTestVersionAnalyzerTests.cs |
Tests core diagnostic scenarios. |
Amaury Levé (Evangelink)
left a comment
There was a problem hiding this comment.
Additional review notes after validating the analyzer against MSTest's custom-attribute extensibility. I am submitting these as comments only—not requesting changes—so the PR is not rejected or blocked while the author is away.
- Anchor the framework assembly on the canonical TestClassAttribute in the applied attribute's base chain, so a custom [TestClass] subclass no longer produces a false positive (Evangelink). - Match inherited custom lifecycle/test attribute subclasses by walking the applied attribute's base chain to the canonical MSTest attribute, so a v3-based [RetryTest : TestMethodAttribute] is caught too (Evangelink). - Only report members that would actually run/be discovered after recompiling: public instance test methods and instance fixtures (suppressing overridden ones), and public static class fixtures only when the attribute specifies InheritanceBehavior.BeforeEachDerivedClass. - Drop [AssemblyInitialize]/[AssemblyCleanup]: assembly fixtures are discovered per-assembly, never inherited, so recompiling the base cannot make them run. - Skip same-assembly base types; mention [DataTestMethod] in the description. - Add tests: custom [TestClass] (no diagnostic), custom [TestMethod] subclass, unreferenced/missing framework assembly graph, private and overridden members, class-fixture inheritance mode, and assembly fixture (no diagnostic). 🤖 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Addressed the automated review as well (3998b0c):
13 tests, all green. 🤖 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:190
- This check does not establish that the member would actually run after recompilation. It only checks accessibility/staticness, while MSTest also rejects instance fixtures with parameters or invalid return types and class fixtures without the required
TestContextshape (seeMethodInfoExtensions.cs:20-30,59-66); test methods with invalid return types are likewise skipped (TestMethodValidator.cs:52-73). Such members will now receive MSTEST0082 even though the prescribed recompilation cannot make them run. Please validate the complete per-kind signature before reporting.
private static bool WouldRunOrBeDiscoveredIfSameVersion(IMethodSymbol method, MSTestMemberKind kind, AttributeData attribute, INamedTypeSymbol testClass, INamedTypeSymbol declaringBaseType)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:197
- Checking only overrides misses members hidden with
new. MSTest suppresses an inherited initialize/cleanup whenever a valid method with the same name was seen in a more-derived type (TypeCache.ClassInfo.cs:314-316,341-343), and CLR method enumeration similarly hides a same-signature base test method. MSTEST0082 therefore warns even though recompiling the base cannot restore that member. Account for hidden declarations as well as override chains.
&& !IsOverriddenInDerivedType(method, testClass, declaringBaseType),
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:186
- The PR description says this rule covers all eight listed attributes, including
AssemblyInitializeandAssemblyCleanup, but this mapping explicitly excludes both and the new test asserts no diagnostic. Since the exclusion matches the adapter's per-assembly discovery model, update the PR description to describe the six supported inherited attributes rather than promising eight.
// AssemblyInitialize/AssemblyCleanup are intentionally excluded: they are discovered per-assembly, not
// inherited, so a base library's assembly fixture never runs on a derived test class even after recompilation.
test/UnitTests/MSTest.Analyzers.UnitTests/InheritedMemberFromDifferentMSTestVersionAnalyzerTests.cs:235
- This positive case uses an invalid class-initialize signature: MSTest requires exactly one
TestContextparameter (MethodInfoExtensions.cs:20-30). Even with a same-version attribute, this method would be rejected, so the test currently codifies the analyzer's false positive instead of proving the inheritance scenario. Define the legacyTestContexttype and pass it here.
public static void BaseClassInitialize() { }
- Validate the full per-kind signature before reporting: instance fixtures must be parameterless and return void/Task/ValueTask; class fixtures must be static with a valid (optional TestContext) parameter shape and a compatible return; test methods must be instance with a compatible return. - Suppress inherited members that are hidden with `new` in a more-derived type, not only overrides (MSTest suppresses by name). - Fix the class-initialize positive test to use a valid signature (TestContext parameter) so it demonstrates a real scenario, not an already-invalid member. - Add tests for a `new`-hidden member and an invalid-signature fixture. - Convert GetFrameworkAssembly and HasBeforeEachDerivedClassBehavior to LINQ to satisfy the code-quality analyzer. 🤖 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Second pass (8555a7f), addressing the follow-up review:
15 tests, all green; analyzer builds clean (0 warnings). 🤖 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:244
- Checking only the method name suppresses real diagnostics. For example, a derived
BaseInitialize(int)does not suppress a valid parameterless base initializer (the adapter only records same-name lifecycle methods with valid fixture signatures), and a differently-shaped test-method overload does not hide the inherited test. This helper nevertheless returnstruefor either case, so the mismatched base attribute is missed. Please apply the adapter's kind-specific hiding/signature rules rather than treating every same-name method as a replacement.
if (type.GetMembers(baseMethod.Name).Any(member => member is IMethodSymbol))
{
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:217
- This return-type check accepts
Task<T>/ValueTask<T>because Roslyn reports their names asTask/ValueTask, and it acceptsasync voidthroughReturnsVoid. The adapter rejects all three shapes, so this rule can warn even though recompiling the base would not make the member runnable. Please use exactTask/ValueTasksymbol equality and exclude asyncvoid, asFixtureUtils.HasValidTestMethodSignature/HasValidFixtureMethodSignaturealready do.
=> method.ReturnsVoid
|| (method.ReturnType.ContainingNamespace is { } containingNamespace
&& string.Equals(containingNamespace.ToDisplayString(), "System.Threading.Tasks", StringComparison.Ordinal)
&& (string.Equals(method.ReturnType.Name, "Task", StringComparison.Ordinal)
|| string.Equals(method.ReturnType.Name, "ValueTask", StringComparison.Ordinal)));
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:207
HasClassFixtureParameterShapeallows zero parameters for both class fixture kinds, but the adapter requires[ClassInitialize]to take exactly oneTestContext; only[ClassCleanup]permits zero or one. A zero-argument legacy class initializer therefore receives MSTEST0082 even though recompilation cannot make it run. Please retain whether the canonical attribute is initialize or cleanup and validate each parameter shape separately.
MSTestMemberKind.ClassFixture
=> method.IsStatic
&& HasClassFixtureParameterShape(method)
&& ReturnsVoidTaskOrValueTask(method)
&& HasBeforeEachDerivedClassBehavior(attribute),
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:181
- The PR description says MSTEST0082 covers all eight attributes, including
AssemblyInitializeandAssemblyCleanup, while this switch intentionally supports only the six inheritable class/member attributes. Please align the stated scope with the implementation, or add the intended assembly-fixture detection path if those two attributes are still required.
// AssemblyInitialize/AssemblyCleanup are intentionally excluded: they are discovered per-assembly, not
// inherited, so a base library's assembly fixture never runs on a derived test class even after recompilation.
_ => MSTestMemberKind.None,
This comment has been minimized.
This comment has been minimized.
Amaury Levé (Evangelink)
left a comment
There was a problem hiding this comment.
Fresh review of the latest head. The current automated review already captures the method-hiding, return-type, and ClassInitialize parameter-shape mismatches, so I am not duplicating those inline. This additional comment covers the remaining unresolved-metadata case. Comment-only review; no rejection or request-changes state.
Amaury Levé (Evangelink)
left a comment
There was a problem hiding this comment.
Fresh code + MSTest expert review (8555a7ff)
I ran two independent reviews of the complete latest diff. They converged on the same four open semantic gaps and found no additional correctness, concurrency, localization, API, or performance issues. The PR head has not changed since the previous review, so I am consolidating the result here rather than duplicating the existing inline threads.
-
Missing legacy PE metadata can hide inherited class fixtures.
HasBeforeEachDerivedClassBehaviordepends on decodedAttributeData.ConstructorArguments. In the real three-PE graph (legacy framework PE → base-library PE → consumer omitting the legacy framework), Roslyn retains the missing attribute identity but cannot bind its constructor, leaving the arguments empty. An explicitBeforeEachDerivedClassclass initialize/cleanup therefore escapes MSTEST0082. The current project-reference test covers only[TestInitialize]and preserves dependency symbols. Add an emitted-PE test withMetadataReference.CreateFromImage; complete support requires metadata-blob decoding, otherwise document/limit this scope. Existing thread:InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:228. -
Any same-name derived method suppresses a real warning.
IsHiddenOrOverriddenInDerivedTypetreats every derived method with the same name as a replacement. The adapter is kind-specific: an invalid/privateInitialize(int)does not suppress baseInitialize(), different-signature test overloads remain discoverable, and class fixtures have no ordinary name-based suppression. Make hiding signature- and member-kind-aware and add C#/VB coverage for each case. Existing current-head automated finding: line 244. -
Invalid return types pass analyzer validation. Name-only matching accepts
Task<T>andValueTask<T>, whileReturnsVoidalso acceptsasync void/VBAsync Sub. The adapter requires exact non-genericTask/ValueTaskand rejects asynchronous void. Resolve the task symbols at compilation start, compare withSymbolEqualityComparer.Default, and require!method.IsAsyncfor void. Existing current-head automated finding: line 217. -
ClassInitializeandClassCleanupparameter rules are conflated. The sharedClassFixturebranch allows zero or oneTestContext, but initialize requires exactly one while cleanup permits zero or one. Preserve distinct member kinds and add a no-diagnostic test for parameterless legacyClassInitialize. Existing current-head automated finding: line 207.
Previously reported custom [TestClass], custom TestMethodAttribute, assembly-fixture, [DataTestMethod], private/override, and PR-scope issues are fixed. CI is green, but it does not exercise the four cases above.
This is a comment-only review: no rejection or request-changes state.
Amaury's point: the project-reference "unreferenced framework" test preserves symbol info, so it is not equivalent to a real absent PE. In the true missing-PE case a class fixture's [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)] attribute cannot bind its constructor, ConstructorArguments is empty, and the inheritance mode cannot be read — so MSTEST0082 does not flag it. Instance fixtures and test methods carry no constructor arguments and are unaffected. Full attribute-blob decoding is not available to an analyzer, so: - Document the limitation on the analyzer (type remarks + HasBeforeEachDerivedClassBehavior). - Add real emitted-PE regression tests: instance fixture is still detected with the legacy framework PE absent; ClassInitialize and ClassCleanup are (documented as) not detected in that case. - Correct the misleading project-reference test name/comment so it no longer claims equivalence with missing PE metadata. 🤖 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:226
- This accepts
async void,Task<T>, andValueTask<T>because it checks onlyReturnsVoidor the return type's namespace/name. MSTest rejects all three (FixtureUtils.cs:74-76,140-143;TestMethodValidator.cs:55-67), so MSTEST0082 can claim that recompilation fixes a member that would remain invalid. Require non-asyncvoidand arity-zeroTask/ValueTask.
=> method.ReturnsVoid
|| (method.ReturnType.ContainingNamespace is { } containingNamespace
&& string.Equals(containingNamespace.ToDisplayString(), "System.Threading.Tasks", StringComparison.Ordinal)
&& (string.Equals(method.ReturnType.Name, "Task", StringComparison.Ordinal)
|| string.Equals(method.ReturnType.Name, "ValueTask", StringComparison.Ordinal)));
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:255
- This name-only suppression does not match MSTest and causes false negatives. Instance fixtures are suppressed only when the derived same-name method itself has a valid fixture signature (
TypeCache.ClassInfo.cs:312-317), class fixtures are accumulated independently (TypeCache.AssemblyInfo.cs:122-139), and test-method hiding is signature-based. Thus a private/static/parameterized same-name method—or any same-name class fixture—currently hides MSTEST0082 even though recompiling the base changes execution. Make this check member-kind-aware and mirror each adapter path's hiding rules.
if (type.GetMembers(baseMethod.Name).Any(member => member is IMethodSymbol))
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:214
ClassInitializeandClassCleanupcannot share this parameter check. MSTest requiresClassInitializeto take exactly oneTestContext(MethodInfoExtensions.cs:20-30andClassInitializeShouldBeValidAnalyzer.cs:37-41), whereas onlyClassCleanupmay be parameterless. As written, a parameterless legacy[ClassInitialize]receives MSTEST0082 even though recompiling it would still leave an invalid fixture. Preserve the canonical initialize/cleanup kind and apply their distinct parameter rules.
This issue also appears in the following locations of the same file:
- line 222
- line 255
&& HasClassFixtureParameterShape(method)
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Addresses the remaining three semantic gaps from Amaury's review: - Return types (#3): resolve Task/ValueTask at compilation start and compare by symbol, so Task<T>/ValueTask<T> are rejected; require non-async void. - ClassInitialize vs ClassCleanup params (#4): split the class-fixture kind so ClassInitialize requires exactly one TestContext while ClassCleanup allows zero or one; a parameterless legacy [ClassInitialize] is no longer flagged. - Hiding (#2): make suppression member-kind-aware and signature-based. Instance fixtures are suppressed only by a same-name derived method that is itself a valid fixture; test methods only by a same-signature method; class fixtures accumulate and are never name-suppressed. Overrides still always suppress. Adds C# tests for each case plus two Visual Basic tests confirming language parity. 🤖 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:169
- This visibility check accepts protected and protected-internal nested classes because
GetResultantVisibility()treats them as public, but the adapter rejects those classes even withDiscoverInternals. This produces MSTEST0082 for a class that discovery will never run. MirrorTypeValidator.TypeHasValidAccessibilityby requiring every containing type to be public, or exactly internal when internals discovery is enabled (asDependsOnShouldBeValidAnalyzer.HasValidAccessibilityalready does).
SymbolVisibility resultantVisibility = classSymbol.GetResultantVisibility();
return canDiscoverInternals
? resultantVisibility != SymbolVisibility.Private
: resultantVisibility == SymbolVisibility.Public;
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:362
- A same-signature
newdeclaration does not by itself suppress the base test.TypeEnumerator.GetTestsde-duplicates only after the derived method passesIsValidTestMethod; therefore the addedpublic new void Run()without[TestMethod]still leaves the recompiled base[TestMethod]discoverable. This branch currently suppresses MSTEST0082 for that case (and for private/otherwise non-test declarations). Require the derived declaration to be a discoverable current-version test before treating it as hiding the base test.
// Test-method hiding is signature-based: only a same-signature method shadows the inherited test;
// a different-arity, by-ref, or otherwise different-signature overload stays discoverable.
case MSTestMemberKind.TestMethod when HaveSameSignature(derivedMethod, baseMethod):
return true;
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:399
SymbolEqualityComparertreats method type parameters from separate declarations as different symbols, although their names are not part of the CLR signature. ConsequentlyRun<T>(T)and a derived current-version[TestMethod] Run<U>(U)are considered different here, and MSTEST0082 is reported even though the derived test hides the base test. Normalize method type parameters by ordinal when comparing parameter types;DependsOnShouldBeValidAnalyzer.BuildSignatureKeyalready handles this case.
// ref/out/in all share the same by-ref CLR signature, so compare by-value versus by-ref rather than the
// exact RefKind (a derived 'Run(out int)' still hides a base 'Run(ref int)').
if ((leftParameter.RefKind == RefKind.None) != (rightParameter.RefKind == RefKind.None)
|| !SymbolEqualityComparer.Default.Equals(leftParameter.Type, rightParameter.Type))
{
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Address CodeQL "missed opportunity to use Where": express the predicate-based foreach loops in AllGenericTypeParametersAreInferable and IsOrHasTypeParameter as LINQ All/Any, keeping behavior identical. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa3c2d45-4786-429c-8388-e3cec14952ea
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:368
- This comparison does not recognize alpha-equivalent generic signatures:
Run<T>(T)andRun<U>(U)hide one another in CLR metadata, but their parameter type symbols are unequal, so the analyzer reports the inherited method even though recompiling it would not make it discoverable.DependsOnShouldBeValidAnalyzer.BuildSignatureKeyalready normalizes method type parameters by ordinal (lines 833-907); extract/reuse that signature logic here and add this renamed-type-parameter case.
// ref/out/in all share the same by-ref CLR signature, so compare by-value versus by-ref rather than the
// exact RefKind (a derived 'Run(out int)' still hides a base 'Run(ref int)').
if ((leftParameter.RefKind == RefKind.None) != (rightParameter.RefKind == RefKind.None)
|| !SymbolEqualityComparer.Default.Equals(leftParameter.Type, rightParameter.Type))
{
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:169
GetResultantVisibility()treats nestedprotectedandprotected internaltypes as public, but MSTest discovery explicitly accepts only nested public types (or nested internal types withDiscoverInternals) and rejects those protected variants (TypeValidator.cs:165-178, covered byTypeValidatorTests.cs:353-370). This therefore emits MSTEST0082 for classes the adapter never discovers. Check the exact accessibility of the class and each containing type instead.
private static bool IsDiscoverableTestClassVisibility(INamedTypeSymbol classSymbol, bool canDiscoverInternals)
{
SymbolVisibility resultantVisibility = classSymbol.GetResultantVisibility();
return canDiscoverInternals
? resultantVisibility != SymbolVisibility.Private
: resultantVisibility == SymbolVisibility.Public;
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Amaury Levé (Evangelink)
left a comment
There was a problem hiding this comment.
Fresh review of the latest head (53457772) using independent code and MSTest-expert passes plus a direct CLR reflection probe. The existing current-head automated review already records the protected nested-class visibility issue, so this review adds only the new signature-hiding defect below. Comment-only; no rejection or request-changes state.
Amaury pointed out that the signature-only hiding check suppressed MSTEST0082 for base test methods that CLR reflection still exposes. MSTest's TypeEnumerator enumerates every runtime method, keeps only those that pass IsValidTestMethod, and de-duplicates by MethodInfo.ToString() (which includes the return type). So a derived `new int Run()` (different return), `new static void Run()` (static), or a `new void Run()` without a recognized current-version [TestMethod] is skipped by discovery, leaving the inherited legacy [TestMethod] silently undiscovered - exactly what the rule should report. Make the test-method replacement check mirror discovery: the derived method must itself be a discoverable current-version test method (public, instance, void/Task/ValueTask, inferable generics, current [TestMethod]/[DataTestMethod]), and the signature comparison now includes the return type and static/instance convention. Added diagnostic regression tests for the different-return-type, static, and no-[TestMethod] shadowing cases, and corrected the same-signature hiding no-diagnostic test to use a genuine current-version [TestMethod] override. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa3c2d45-4786-429c-8388-e3cec14952ea
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:363
- With
[assembly: DiscoverInternals], the adapter treats aninternal[TestMethod]as discoverable (TestMethodValidator.cs:52-58). This public-only check therefore misses a valid derivednew internaltest method that de-duplicates and replaces the inherited base test, causing MSTEST0082 to be reported even though recompiling the base would not change discovery. PasscanDiscoverInternalsinto this path and apply the same accessibility rule as the adapter.
private static bool IsDiscoverableTestMethod(IMethodSymbol method, IAssemblySymbol referenceAssembly, INamedTypeSymbol? taskSymbol, INamedTypeSymbol? valueTaskSymbol)
=> method is { DeclaredAccessibility: Accessibility.Public, IsStatic: false }
&& ReturnsVoidTaskOrValueTask(method, taskSymbol, valueTaskSymbol)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:392
- This compares parameter types by symbol identity, which is not equivalent to the CLR signature for generic methods. For example, derived
Run<U>(U)and baseRun<T>(T)have the same reflected signature and MSTest keeps the derived test, butTandUare distinct Roslyn symbols, so this returns false and emits a false-positive MSTEST0082. Normalize method type parameters by ordinal (and compare constructed types structurally), asDependsOnShouldBeValidAnalyzer.BuildSignatureKeyalready does.
// ref/out/in all share the same by-ref CLR signature, so compare by-value versus by-ref rather than the
// exact RefKind (a derived 'Run(out int)' still hides a base 'Run(ref int)').
if ((leftParameter.RefKind == RefKind.None) != (rightParameter.RefKind == RefKind.None)
|| !SymbolEqualityComparer.Default.Equals(leftParameter.Type, rightParameter.Type))
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…082 hiding Two more discovery-faithfulness gaps in the test-method hiding check: - Accessibility: the derived hider was required to be public, but with [assembly: DiscoverInternals] the adapter also discovers internal test methods. An internal derived [TestMethod] that legitimately hides the inherited base was therefore missed, producing a false positive. Thread canDiscoverInternals in and apply the adapter's accessibility rule (mirrors TestMethodShouldBeValidAnalyzer). - Generic signature: parameter types were compared by symbol identity, so 'Run<U>(U)' and base 'Run<T>(T)' looked different though they share a CLR signature, giving a false positive. Compare method type parameters by ordinal (recursing through arrays and constructed generics), like the derived test method still hides the base per reflection. Added regression tests for the internal-with/without-DiscoverInternals cases and the renamed-type-parameter generic hiding case. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa3c2d45-4786-429c-8388-e3cec14952ea
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:378
- With
DiscoverInternals,GetResultantVisibility() != Privatealso admitsprotected,protected internal, andprivate protectedmethods.TestMethodValidatoronly discovers methods that arepublicor exactlyinternal, so such a derived method cannot hide the inherited test; returning true here suppresses the MSTEST0082 warning even though the inherited legacy test remains missing. CheckDeclaredAccessibilityagainstPublic/Internaldirectly.
SymbolVisibility resultantVisibility = method.GetResultantVisibility();
return canDiscoverInternals
? resultantVisibility != SymbolVisibility.Private
: resultantVisibility == SymbolVisibility.Public && method.DeclaredAccessibility == Accessibility.Public;
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:169
- This visibility check treats nested
protected,protected internal, andprivate protectedclasses as discoverable becauseGetResultantVisibilitydoes not preserve those accessibilities. The adapter only accepts nested classes whose entire containing chain ispublic(orpublic/internalwithDiscoverInternals; seeTypeValidator.TypeHasValidAccessibility), so MSTEST0082 can warn on a class that will never be discovered. Mirror that exact containing-type accessibility check here.
This issue also appears on line 375 of the same file.
SymbolVisibility resultantVisibility = classSymbol.GetResultantVisibility();
return canDiscoverInternals
? resultantVisibility != SymbolVisibility.Private
: resultantVisibility == SymbolVisibility.Public;
This comment has been minimized.
This comment has been minimized.
GetResultantVisibility collapses protected to public, so the class- and method-level visibility checks were imprecise: - Class: nested protected/protected-internal/private-protected test classes were treated as discoverable, producing a false positive on a class the adapter never runs. Mirror TypeValidator.TypeHasValidAccessibility: the class and every containing type must be public (or public/internal with DiscoverInternals). - Method: a protected derived method was treated as a valid hider under DiscoverInternals, suppressing a real mismatch (false negative). Mirror TestMethodValidator: discoverable means exactly public, or exactly internal with DiscoverInternals. Both now compare DeclaredAccessibility directly against the adapter's rules. Added regression tests for the nested-protected class and the protected-method shadow cases. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa3c2d45-4786-429c-8388-e3cec14952ea
🧵 Parallel-safety audit — PR #10508Nothing audited here touches process-global state, shared filesystem paths, or The only change is a new test file, Audited Re-run with
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/Analyzers/MSTest.Analyzers/InheritedMemberFromDifferentMSTestVersionAnalyzer.cs:347
GetRuntimeMethodsdelegates toType.GetMethods(Everything)(ReflectionOperations.cs:68-69), which removes a base method hidden by a same-signature derived declaration beforeTestMethodValidatorexamines attributes. Requiring the derived method itself to be discoverable therefore produces a false MSTEST0082 for cases such aspublic new void Run()without[TestMethod]: the legacy baseRunis absent both before and after recompilation. Treat any same-signature derived method as hiding the base method, and update the correspondingWhenInheritedTestMethodShadowedByNonTestMethod_Diagnosticexpectation.
case MSTestMemberKind.TestMethod
when IsDiscoverableTestMethod(derivedMethod, referenceAssembly, canDiscoverInternals, taskSymbol, valueTaskSymbol)
&& HaveSameSignature(derivedMethod, baseMethod):
🧪 Expert test review — PR #10508The only test file changed in this PR is a newly-added file: Overall assessment: This is an exceptionally thorough and well-structured analyzer test suite. Each test:
No high-confidence actionable defects were found that warrant an inline suggestion — the suite is internally consistent, each assertion is specific (exact diagnostic arguments), and no test asserts a tautology or omits verification of its stated claim.
This advisory comment was generated automatically. Grades are heuristic and informational — they do not block merging. Re-run with
|
||||||||||||||||||||||||||||||||||||||||
Jan Krivanek (JanKrivanek)
left a comment
There was a problem hiding this comment.
Code looks good to go at head 824705c9; no major correctness, API, or test-coverage concerns found.
Fixes the silent v3/v4 inheritance breakage from #10505 by detecting it at build time.
The problem
When a
[TestClass]compiled against MSTest v4 inherits a[TestInitialize]/[TestMethod]/ … from a base class compiled against v3, the inherited member does not run and inherited test methods are not discovered — with no build error and no discovery error. The framework assembly was renamed:Microsoft.VisualStudio.TestPlatform.TestFrameworkMSTest.TestFrameworkso the attribute baked into the v3-compiled base is a different CLR type identity than the one the v4 adapter matches, and it is silently ignored.
The rule
InheritedMemberFromDifferentMSTestVersionAnalyzer(MSTEST0082, Usage, Warning). For a test class it walks the base types and warns when an inherited lifecycle or test-method attribute resolves to a framework assembly whose name differs from the one the test project compiles against. Assembly identity is matched by name, so a benign version bump of the same framework is not flagged — only the rename is.One rule covers the six inherited attributes (
TestInitialize,TestCleanup,ClassInitialize,ClassCleanup,TestMethod,DataTestMethod), with the attribute name passed as a message argument.AssemblyInitialize/AssemblyCleanupare excluded: assembly fixtures are discovered per-assembly, never inherited, so recompiling the base cannot make them run.It only warns when recompiling the base would actually fix the problem:
[TestClass]/[TestMethod]subclasses are handled — no false positive on a custom test-class attribute, and a custom[RetryTest : TestMethodAttribute]from a v3 base is still caught;TestContext-shaped class fixtures,void/Task/ValueTaskreturns), class fixtures only withInheritanceBehavior.BeforeEachDerivedClass, and members overridden or hidden withnewin a more-derived type are suppressed.It's a
SymbolKind.NamedTypeaction that early-outs viaIsTestClassand skips same-assembly base types.Tests
15 unit tests. Warns: inherited
[TestInitialize], inherited[TestMethod], a custom[RetryTest : TestMethodAttribute]subclass, an inheritable[ClassInitialize], and the realistic graph where the old framework is reached only through the base-library metadata (not a direct reference of the test project). Silent: same-version cross-assembly base, intra-assembly inheritance, custom[TestClass]with a same-version inherited fixture, private / overridden /new-hidden / invalid-signature members, non-inheritable[ClassInitialize],[AssemblyInitialize], and a non-test-class derived type.🤖