Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283
Conversation
…ute stacking MSTEST0041 (UseConditionBaseWithTestClass) reported that a ConditionBaseAttribute-derived attribute was applied to a type that is not a [TestClass], but offered no code fix. The mechanical fix is to add [TestClass] to the type, which AddTestClassFixer already implements for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there too. While doing so, harden AddTestClassFixer's node lookup: a custom condition attribute can redeclare its AttributeUsage to target a type kind without a TypeDeclarationSyntax (an enum, for example), which made the previous First() call throw, and [TestClass] is meaningless on an interface. Also document on ConditionBaseAttribute which derived condition attributes allow stacking and what to use instead when they don't. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23
There was a problem hiding this comment.
Pull request overview
Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.
Changes:
- Registers MSTEST0041 with
AddTestClassFixerand hardens unsupported-type handling. - Expands analyzer code-fix tests.
- Documents condition grouping and updates the changelog.
Show a summary per file
| File | Description |
|---|---|
AddTestClassFixer.cs |
Registers and hardens the MSTEST0041 fix. |
UseConditionBaseWithTestClassAnalyzerTests.cs |
Adds code-fix coverage. |
ConditionBaseAttribute.cs |
Documents stacking and grouping behavior. |
docs/Changelog.md |
Records the new code fix. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 4
- Review effort level: Medium
There was a problem hiding this comment.
Note
🤖 Automated review by GitHub Copilot. Generated by the Expert Code Review workflow. To request a follow-up action, reply by tagging @copilot directly.
Clean PR. The code fix reuse is well done — registering MSTEST0041 in the existing AddTestClassFixer is the right call, and the defensive null/interface guard is correct. The new tests cover record classes, nested types, and generics nicely.
One minor doc nit flagged inline: ArchitectureConditionAttribute uses <c> tags where <see cref> would be consistent with the rest of the block and provide IDE navigation.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- AddTestClassFixer now emits the fully qualified Microsoft.VisualStudio.TestTools.UnitTesting.TestClass attribute when TestClassAttribute is not in scope at the type declaration, so fixing a fully qualified condition attribute in a file without the using no longer leaves the document with CS0246. Attribute construction is centralized in one helper shared by the class, struct and record struct paths. - Add tests covering the no-using fully qualified case, plus enum and interface targets that exercise the null and interface early-return guards. Verified by mutation: restoring First() makes the enum test fail with 'Sequence contains no elements', and dropping the interface guard makes the interface test fail. - Correct the ConditionBaseAttribute remarks: condition attributes are grouped by GroupName value regardless of attribute type, so distinct attribute types do not guarantee a logical AND. - Resave UseConditionBaseWithTestClassAnalyzerTests.cs as UTF-8 with BOM per .editorconfig. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (1)
src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:186
- This lookup only proves that the MSTest symbol is one candidate. If another imported
TestClassAttributeis also visible, it still returns true and the generated[TestClass]is ambiguous; a visible type namedTestClasscan similarly take precedence during attribute binding. Use the short form only when there is no exactTestClasssymbol andTestClassAttributeresolves uniquely to the MSTest attribute; otherwise keep the qualified form.
INamedTypeSymbol? testClassAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute);
return testClassAttributeSymbol is not null
&& semanticModel.LookupNamespacesAndTypes(position, name: $"{TestClassAttributeName}Attribute")
.Any(symbol => SymbolEqualityComparer.Default.Equals(symbol, testClassAttributeSymbol));
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Medium
This comment has been minimized.
This comment has been minimized.
❌ Build Failure AnalysisRoot cause: This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release). ErrorFixThe PR modified dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlfThis updates all locale files under
|
Correcting the record: this PR does not touch The failure is pre-existing on
Running |
This comment has been minimized.
This comment has been minimized.
The OneLocBuild localized file check-in (866ed18, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal '{0}' placeholder. XliffTasks 11.0.0-beta.26377.3 flags the unit as out-of-date with Resources.resx, failing every build leg: 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' This branch inherited the failure when main was merged in. Restoring the blank line clears the check while preserving the French translation. Running /t:UpdateXlf instead would also fix the build, but destructively: it resets the unit to state='new' with the English source text, discarding the translation. Verified locally: with the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error; with this fix a full solution build in the same mode succeeds, and no other xlf file is stale. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23
|
Following up on my earlier comment: I said the XLF fix belonged on The root cause was not a stale regeneration. The OneLocBuild check-in ( I deliberately did not run Verification:
This should also unblock |
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (2)
src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185
- Checking only whether
TestClassAttributeis in scope does not prove that the shortened[TestClass]is unambiguous. If another type namedTestClassis also in scope, C# reports CS1614 betweenTestClassandTestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit aglobal::-qualified attribute and let Roslyn simplify it only when safe.
INamedTypeSymbol? testClassAttributeSymbol = semanticModel.Compilation.GetTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingTestClassAttribute);
return testClassAttributeSymbol is not null
&& semanticModel.LookupNamespacesAndTypes(position, name: $"{TestClassAttributeName}Attribute")
.Any(symbol => SymbolEqualityComparer.Default.Equals(symbol, testClassAttributeSymbol));
src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:37
- Registering MSTEST0041 also sends condition-decorated structs through the existing struct-to-class fix. A custom condition declared with
[AttributeUsage(AttributeTargets.Struct)]is valid on the original struct, but after this fix converts it to a class the condition attribute itself becomes invalid (CS0592). Please either suppress the MSTEST0041 action for struct/record-struct declarations, as for enums/interfaces, or only offer conversion when the applied condition attribute also permits class targets; add coverage for this entry point.
DiagnosticIds.UseConditionBaseWithTestClassRuleId);
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Medium
This comment has been minimized.
This comment has been minimized.
Registering MSTEST0041 on AddTestClassFixer also routed condition-decorated structs into the existing struct-to-class conversion. A condition attribute can only be applied to a struct if its own AttributeUsage permits struct targets, so rewriting the struct as a class strands the attribute on a target it doesn't allow and the fixed code no longer compiles (CS0592). Skip the conversion for the MSTEST0041 entry point only. MSTEST0004 and MSTEST0030 always ask for a test class, where converting the struct is the intended fix, so their behavior and tests are unchanged. Reproduced first: both new tests failed with the fixer rewriting 'public struct MyStruct' to 'public class MyStruct'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23
There was a problem hiding this comment.
Review details
Comments suppressed due to low confidence (2)
src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185
- This check only verifies that the MSTest symbol is one visible
TestClassAttributecandidate. With another importedTestClassAttribute, the emitted[TestClass]is ambiguous; with a visible attribute or alias namedTestClass, it can bind to the wrong type. Generate the attribute from the metadata symbol/fully qualified name and let Roslyn simplify it safely, or require the MSTest candidate to be unique and ensure the short name has no competing attribute binding.
string attributeName = semanticModel is not null && IsTestClassAttributeInScope(semanticModel, position)
? TestClassAttributeName
: FullyQualifiedTestClassAttributeName;
src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:73
- This blanket guard also suppresses a valid fix when a custom condition declares
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]. In that case converting the struct to a class keeps the condition attribute valid, but MSTEST0041 now offers no action. Please inspect the offending condition attribute's effectiveAttributeUsageand skip only whenClassis not allowed; add class-and-struct coverage for both struct forms.
// MSTEST0041 fires on whatever target the condition attribute allows. When that target is a struct, the
// attribute only got there because its own AttributeUsage permits structs, so turning the struct into a
// class would strand the attribute on a target it doesn't allow (CS0592). The other rules only ever ask for
// a test class, where the conversion is the intended fix.
if (isStruct && diagnostic.Id == DiagnosticIds.UseConditionBaseWithTestClassRuleId)
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Medium
🧪 Test quality grade — PR #10283
This advisory comment was generated automatically. Grades are heuristic
|
Addresses the actionable, non-speculative parts of #10271.
What changed
Task 1 (High) — code fix for MSTEST0041
UseConditionBaseWithTestClassAnalyzer(MSTEST0041) fires when aConditionBaseAttribute-derived attribute is applied to a type that is not decorated with[TestClass], but it shipped without a code fix. The fix is purely mechanical — add[TestClass]— andAddTestClassFixeralready does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.While wiring this up,
AddTestClassFixerwas hardened for the new entry point:OfType<TypeDeclarationSyntax>().First()→FirstOrDefault()with a null check. A custom condition attribute can redeclare its ownAttributeUsageto target a type kind that has noTypeDeclarationSyntax(an enum, for instance), andFirst()would have thrown.[TestClass]is meaningless there.Task 4 (Low) — document the
AllowMultiplebehaviourThe issue suspected that
ConditionBaseAttribute"silently allowsAllowMultiple = truevia inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:AllowMultipleMemberConditionAttribute,ExecutableConditionAttributetrueGroupName, so stacked usages AND together.OSConditionAttribute,ArchitectureConditionAttributefalseOperatingSystems.Windows | OperatingSystems.Linuxinstead of stacking.CIConditionAttributefalseConditionMode; there is nothing to combine.Rather than change observable attribute behaviour, the
ConditionBaseAttributeXML docs now spell this out so users hitting the compiler error know what to write instead. No public API surface changed.Not included
Tasks 2 and 3 (
MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheckandMSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) introduce brand-new public diagnostic IDs that need their own learn.microsoft.com documentation pages and a product decision on the detection heuristics — in particular MSTEST0079's proposal to hard-code CI environment-variable names (CI,TF_BUILD, …) diverges from howCIConditionAttributeactually detects CI viaCIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.Testing
UseConditionBaseWithTestClassAnalyzerTestsnow verifies againstAddTestClassFixerinstead ofEmptyCodeFixProvider. The existing diagnostic tests were converted toVerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)TestFrameworkbuilds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings