Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking - #10283

Merged
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix
Jul 28, 2026
Merged

Add [TestClass] code fix for MSTEST0041 and document condition attribute stacking#10283
Amaury Levé (Evangelink) merged 6 commits into
mainfrom
dev/amauryleve/mstest0041-code-fix

Conversation

@Evangelink

Copy link
Copy Markdown
Member

Addresses the actionable, non-speculative parts of #10271.

What changed

Task 1 (High) — code fix for MSTEST0041

UseConditionBaseWithTestClassAnalyzer (MSTEST0041) fires when a ConditionBaseAttribute-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] — and AddTestClassFixer already does exactly that for MSTEST0004 and MSTEST0030, so MSTEST0041 is now registered there rather than duplicating the logic in a new fixer file.

// before[OSCondition(OperatingSystems.Windows)]publicclassMyClass{}// after[OSCondition(OperatingSystems.Windows)][TestClass]publicclassMyClass{}

While wiring this up, AddTestClassFixer was hardened for the new entry point:

  • OfType<TypeDeclarationSyntax>().First()FirstOrDefault() with a null check. A custom condition attribute can redeclare its own AttributeUsage to target a type kind that has no TypeDeclarationSyntax (an enum, for instance), and First() would have thrown.
  • Interfaces are skipped, since [TestClass] is meaningless there.

Task 4 (Low) — document the AllowMultiple behaviour

The issue suspected that ConditionBaseAttribute "silently allows AllowMultiple = true via inheritance". That is not what the code does — every derived attribute sets the value explicitly, and the split is deliberate:

AttributeAllowMultipleRationale
MemberConditionAttribute, ExecutableConditionAttributetrueEach usage names a different member/command, producing a distinct GroupName, so stacked usages AND together.
OSConditionAttribute, ArchitectureConditionAttributefalseThey take a flags enum — use OperatingSystems.Windows | OperatingSystems.Linux instead of stacking.
CIConditionAttributefalseOnly carries a ConditionMode; there is nothing to combine.

Rather than change observable attribute behaviour, the ConditionBaseAttribute XML 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 (MSTEST0078UseArchitectureConditionAttributeInsteadOfRuntimeCheck and MSTEST0079UseCIConditionAttributeInsteadOfEnvironmentCheck) 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 how CIConditionAttribute actually detects CI via CIEnvironmentDetector, so it needs design agreement before implementation. Those are better served by dedicated PRs, so #10271 is intentionally left open.

Testing

UseConditionBaseWithTestClassAnalyzerTests now verifies against AddTestClassFixer instead of EmptyCodeFixProvider. The existing diagnostic tests were converted to VerifyCodeFixAsync, and coverage was added for record classes, nested classes and generic classes with constraints.

  • MSTest.Analyzers.UnitTests: 1547 passed, 0 failed (net8.0)
  • TestFramework builds clean across net462/netstandard2.0/net8.0/net9.0 with 0 warnings

…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
CopilotAI review requested due to automatic review settings July 28, 2026 07:56
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7a096980-cbb4-4078-9880-ad61c1a84e23

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MSTEST0041 code fix and clarifies condition-attribute stacking behavior.

Changes:

  • Registers MSTEST0041 with AddTestClassFixer and hardens unsupported-type handling.
  • Expands analyzer code-fix tests.
  • Documents condition grouping and updates the changelog.
Show a summary per file
FileDescription
AddTestClassFixer.csRegisters and hardens the MSTEST0041 fix.
UseConditionBaseWithTestClassAnalyzerTests.csAdds code-fix coverage.
ConditionBaseAttribute.csDocuments stacking and grouping behavior.
docs/Changelog.mdRecords the new code fix.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 4
  • Review effort level: Medium

@github-actionsgithub-actionsBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

This comment has been minimized.

@EvangelinkAmaury Levé (Evangelink) added the state/needs-review Awaiting review from the team. label Jul 28, 2026
@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 08:38

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute is also visible, it still returns true and the generated [TestClass] is ambiguous; a visible type named TestClass can similarly take precedence during attribute binding. Use the short form only when there is no exact TestClass symbol and TestClassAttribute resolves 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

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

❌ Build Failure Analysis

Root cause:xlf/Resources.fr.xlf (and other .xlf localization files) are out of date with Resources.resx in src/Analyzers/MSTest.Analyzers.

This failure reproduces on all build legs (Linux Debug/Release, macOS Debug/Release, Windows Debug/Release).

Error

'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx'. Run `msbuild /t:UpdateXlf` to update .xlf files
Project: src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj
Target: _UpdateXlf

Fix

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files. Run the following locally and commit the updated files:

dotnet msbuild src/Analyzers/MSTest.Analyzers/MSTest.Analyzers.csproj /t:UpdateXlf

This updates all locale files under src/Analyzers/MSTest.Analyzers/xlf/ (Resources.fr.xlf, Resources.de.xlf, Resources.es.xlf, etc.).

🤖 Automated content by GitHub Copilot. Generated by the Build Failure Analysis workflow. · sonnet46 51.3 AIC · ⌖ 5.26 AIC · ⊞ 8K · [◷]( · )

@Evangelink

Copy link
Copy Markdown
MemberAuthor

The PR modified Resources.resx (adding resource strings for the new MSTEST0041 code fix) but did not regenerate the .xlf translation files.

Correcting the record: this PR does not touch Resources.resx or any .xlf file. git diff --name-only origin/main...HEAD returns exactly four files: docs/Changelog.md, AddTestClassFixer.cs, ConditionBaseAttribute.cs, and UseConditionBaseWithTestClassAnalyzerTests.cs. The MSTEST0041 code fix reuses the existing CodeFixResources.AddTestClassFix string, so no new resources were added.

The failure is pre-existing on main, and this PR only inherits it because PR builds validate the merge with main:

  • main build 1529393 at commit 0b55734 fails with the identical 'xlf/Resources.fr.xlf' is out-of-date with 'Resources.resx' error.
  • This branch forks from 68c9411. Since then main gained 866ed183 (OneLocBuild localized file check-in, Localized file check-in by OneLocBuild Task: Build definition ID 1218: Build ID 3033187 #10268), which rewrote 8 MSTest.Analyzers.xlf files.
  • In Resources.fr.xlf that check-in added a UTF-8 BOM to the XML declaration and replaced a blank line inside a translated string with a literal {0} placeholder.

Running /t:UpdateXlf on this branch produces zero changes, so the suggested fix would be an empty diff here. The fix belongs on main (correct or revert the OneLocBuild check-in), which will unblock every PR rather than just this one.

CopilotAI review requested due to automatic review settings July 28, 2026 10:48

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Medium

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 10:58
@Evangelink

Copy link
Copy Markdown
MemberAuthor

Following up on my earlier comment: I said the XLF fix belonged on main rather than here. That was right at the time, but main has since been merged into this branch, so the breakage is now carried by this PR and had to be dealt with here. 5c6a3c6 does that.

The root cause was not a stale regeneration. The OneLocBuild check-in (866ed183, #10268) replaced a blank line inside the GlobalTestFixtureShouldBeValidDescription French target with a literal {0}, which is what XliffTasks flags as out-of-date with Resources.resx. The fix restores the blank line, a single line change.

I deliberately did not run /t:UpdateXlf, even though that is what the failure message and the bot suggested. It clears the error destructively: it resets that unit to state="new" with the English source text, throwing away the French translation. Restoring the blank line clears the same check and keeps the translation.

Verification:

  • With the corrupted file, a CI-mode build (/p:UpdateXlfOnBuild=false) reproduces the exact error locally, so the check is genuinely being exercised.
  • With the fix, a full solution build in that same mode succeeds and no other .xlf in the repo is stale.

This should also unblock main once it lands, since main is currently red for the same reason.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Comments suppressed due to low confidence (2)

src/Analyzers/MSTest.Analyzers.CodeFixes/AddTestClassFixer.cs:185

  • Checking only whether TestClassAttribute is in scope does not prove that the shortened [TestClass] is unambiguous. If another type named TestClass is also in scope, C# reports CS1614 between TestClass and TestClassAttribute, so applying this fix introduces a compiler error. Account for the unsuffixed name as well, or emit a global::-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

@github-actions

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
CopilotAI review requested due to automatic review settings July 28, 2026 11:11

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TestClassAttribute candidate. With another imported TestClassAttribute, the emitted [TestClass] is ambiguous; with a visible attribute or alias named TestClass, 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 effective AttributeUsage and skip only when Class is 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

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test quality grade — PR #10283

GradeTestMutationNotesHow to improve
B (80–89)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCIConditionAttribute_
Diagnostic
3/4 killedTests same analyzer+fixer path as OSCondition variant; no branch unique to CICondition is exercised.Fold into a DataRow-driven test over both attribute types to eliminate overlap, or add an assertion unique to CICondition behavior.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenAbstractNonTestClassHasConditionAttribute_
Diagnostic
3/3 killedConfirms abstract-class exemption is intentionally absent and fix correctly adds [TestClass] to abstract types.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenEnumHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedOnly test covering the enum bail-out path (no TypeDeclarationSyntax → no fix offered) in AddTestClassFixer.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenGenericNonTestClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies that type-parameter list and constraint clauses are preserved after AddAttributeLists mutation.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenInterfaceHasCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers the InterfaceDeclarationSyntax early-return guard that prevents offering a code fix.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNestedNonTestClassHasConditionAttribute_
FixAddsTestClassToNestedTypeOnly
3/3 killedConfirms fix is scoped to the inner type and does not duplicate [TestClass] on the outer class.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasCustomConditionAttribute_
Diagnostic
4/4 killedVerifies the Inherits() walk works for a custom ConditionBaseAttribute subclass and fix adds [TestClass].
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasFullyQualifiedOSConditionAttributeWithoutUsing_
FixAddsFullyQualifiedTestClass
3/3 killedOnly test exercising IsTestClassAttributeInScope=false; kills mutations that always use the short attribute name.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasMultipleConditionAttributes_
SingleDiagnostic
4/4 killedVerifies FirstOrDefault single-diagnostic behavior and that the fix appends [TestClass] after both condition attrs.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestClassHasOSConditionAttribute_
Diagnostic
4/4 killedPrimary scenario: verifies diagnostic location, argument, and exact fixed code for the basic class case.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenNonTestRecordClassHasConditionAttribute_
FixAddsTestClass
3/3 killedVerifies record class takes AddTestClassAttributeAsync path and preserves the record keyword.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenRecordStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers record struct variant of the struct+MSTEST0041 no-fix guard via ClassOrStructKeyword check.
A (90–100)new UseConditionBaseWithTestClassAnalyzerTests.
WhenStructHasStructOnlyCustomConditionAttribute_
DiagnosticHasNoCodeFix
3/3 killedCovers plain-struct bail-out guard with a clear CS0592 rationale; prevents regression in the isStruct branch.
A (90–100)mod UseConditionBaseWithTestClassAnalyzerTests.
WhenTwoLevelDerivedConditionAttributeOnNonTestClass_
Diagnostic
4/4 killedTests the recursive Inherits() walk at depth 2; would catch a non-recursive implementation.

This advisory comment was generated automatically. Grades are heuristic
and informational — they do not block merging. Re-run with
/grade-tests.

🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 85.7 AIC · ⌖ 7.34 AIC · ⊞ 10.3K · [◷]( · )

@Evangelink
Amaury Levé (Evangelink) merged commit c1cb853 into mainJul 28, 2026
32 checks passed
@Evangelink
Amaury Levé (Evangelink) deleted the dev/amauryleve/mstest0041-code-fix branch July 28, 2026 12:20
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

state/needs-reviewAwaiting review from the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@Evangelink@0101