feat(analyzers): suppress VSTHRD200 on test and hook methods#6123
Conversation
Adds a DiagnosticSuppressor that silences VSTHRD200 ("Use Async suffix
for async methods") on TUnit test methods (any attribute deriving from
BaseTestAttribute) and hook methods, matching the convention used by
MSTest and xUnit.
Closes #6121
There was a problem hiding this comment.
Review: Vsthrd200AsyncSuffixSuppressor
This is a clean, well-motivated addition. The implementation is consistent with existing suppressors and the tests cover all the important cases. A few observations and suggestions below.
Asymmetry with MarkMethodStaticSuppressor (potential coverage gap)
MarkMethodStaticSuppressor explicitly limits its hook-method suppression to IsStandardHookMethod where hookLevel is HookLevel.Test — it does not suppress CA1822 for [BeforeEvery] / [AfterEvery] hooks. The new Vsthrd200AsyncSuffixSuppressor uses IsHookMethod (which covers both standard and "every" hooks), so it suppresses VSTHRD200 on all hook levels.
That is probably the right call for VSTHRD200 — there is no reason any hook method should require an Async suffix — but it is worth noting explicitly so the intentional difference is documented and does not get "fixed" toward the narrower behaviour later.
If the intent is "all hooks", consider a short comment in ReportSuppressions like:
// All hook levels (Before/After/BeforeEvery/AfterEvery) are suppressed —
// hook methods intentionally omit the Async suffix regardless of scope.HasTestAttribute vs IsTestMethod — diverging helpers with similar names
IsTestMethod checks for exact TUnit.Core.TestAttribute, while HasTestAttribute walks the inheritance chain up from BaseTestAttribute. Both exist on MethodExtensions now, with the doc comment on HasTestAttribute explaining the distinction. However:
- The name gap is subtle. A future contributor may reach for
IsTestMethodwhen they actually want the broaderHasTestAttributecheck. Consider whetherHasTestAttributeshould supersedeIsTestMethodby havingIsTestMethoddelegate to it, or at least adding an XML<remarks>toIsTestMethodpointing toHasTestAttribute:
/// <remarks>
/// This performs an exact-match on <c>TUnit.Core.TestAttribute</c>.
/// Use <see cref="HasTestAttribute"/> if you need to match custom subclasses
/// (e.g. <c>[DynamicTestBuilder]</c>).
/// </remarks>
public static bool IsTestMethod(this IMethodSymbol methodSymbol, Compilation compilation)- Is
IsTestMethodcorrect for the existing 14 callers? They all use it for validation logic that presumably should also apply to[DynamicTestBuilder]tests. If so, they have a latent bug that this PR's new helper exposes but does not fix. That can be a separate issue/PR, but calling it out here is worthwhile.
SupportedSuppressions.First(...) in Suppress — unnecessary allocation per call
This is a pattern copied from the existing suppressors, so it is not introduced by this PR. But since this PR adds another instance, it is worth noting: SupportedSuppressions.First(s => s.SuppressedDiagnosticId == diagnostic.Id) performs a linear scan every time a suppression is reported. For a suppressor that only ever suppresses one diagnostic ID (VSTHRD200), a direct reference is simpler and allocation-free:
private void Suppress(SuppressionAnalysisContext context, Diagnostic diagnostic)
=> context.ReportSuppression(Suppression.Create(SupportedSuppressions[0], diagnostic));Or, since CreateDescriptor is private and only called once, just capture the descriptor in a static readonly field alongside SupportedSuppressions. Again — this is a pre-existing pattern issue, but this PR is a good opportunity to fix it in the new class at least.
Test: [Before(Test)] hook is missing from MarkMethodStaticSuppressorTests — intentional?
MarkMethodStaticSuppressorTests tests [BeforeEvery(Test)] and [AfterEvery(Test)]... actually looking again, it tests [Before(Test)] and [After(Test)] only. The new Vsthrd200AsyncSuffixSuppressorTests additionally tests [BeforeEvery(Test)] and [AfterEvery(Test)], which makes sense given the broader IsHookMethod check. This is a good incremental improvement.
Minor: #if NET8_0_OR_GREATER guard
The test file is correctly gated, matching the pattern in MarkMethodStaticSuppressorTests. No issue here — just confirming the guard is appropriate given the NET8_0_OR_GREATER requirement for the VSTHRD analyzer type used in tests.
Summary
Overall this is a well-executed feature: it follows existing patterns, has the right scope (test + all hook methods), and includes both positive and negative test cases. The main actionable suggestion is adding a <remarks> to IsTestMethod to signal its narrow semantics compared to HasTestAttribute, which will prevent future confusion as both helpers coexist.
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 9 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
…essor - Add <remarks> to IsTestMethod noting it is an exact-match check vs the inheritance-aware HasTestAttribute, preventing future confusion. - Document the intentional all-hook-levels suppression in the VSTHRD200 suppressor and reference the single descriptor directly instead of a per-diagnostic linear scan.
There was a problem hiding this comment.
Review: Vsthrd200AsyncSuffixSuppressor
This is a clean, well-motivated addition. The implementation is consistent with existing suppressors and the tests cover all important cases. Below are observations ranging from minor to more significant.
SupportedSuppressions[0] — improvement already applied
The new suppressor correctly uses SupportedSuppressions[0] rather than the .First(s => s.SuppressedDiagnosticId == diagnostic.Id) scan seen in MarkMethodStaticSuppressor and DynamicTestAwaitExpressionSuppressor. Since this suppressor handles exactly one diagnostic ID, the direct index access is both more efficient and clearer. This is a good practice improvement over the pre-existing pattern.
Latent bug: IsTestClass also relies on exact-match IsTestMethod
TypeExtensions.IsTestClass (line 42–47 in TypeExtensions.cs) determines whether a class is a test class by checking whether any of its methods pass IsTestMethod. Since IsTestMethod performs an exact match on TUnit.Core.TestAttribute, a class that only contains [DynamicTestBuilder] methods is not recognized as a test class by IsTestClass. This flows into at least 10 callsites (e.g. MatrixAnalyzer, TestDataAnalyzer, ClassParametersAnalyzer, etc.) that may quietly skip analysis for [DynamicTestBuilder]-only classes.
This is a pre-existing issue exposed by the documentation contrast between IsTestMethod (narrow) and HasTestAttribute (broad) that this PR introduces. It is worth creating a follow-up issue so it is not forgotten.
HasTestAttribute — allocation in the hot path
HasTestAttribute calls GetSelfAndBaseTypes() which yields types lazily, then passes the sequence to Contains(...) with an IEqualityComparer. The Contains extension from LINQ forces enumeration of the entire chain up to System.Object even after a match is found on the first type in the chain (because .Contains on IEnumerable<T> is a linear scan).
A more allocation-efficient pattern:
return methodSymbol.GetAttributes().Any(attribute =>
{
var cls = attribute.AttributeClass;
while (cls is not null && cls.SpecialType != SpecialType.System_Object)
{
if (SymbolEqualityComparer.Default.Equals(cls, baseTestAttribute))
return true;
cls = cls.BaseType;
}
return false;
});This is a minor concern for an analyzer (not a hot production path), but since GetSelfAndBaseTypes() is already implemented as a yield-based iterator, simply replacing the LINQ .Contains call with .Any(t => SymbolEqualityComparer.Default.Equals(t, baseTestAttribute)) would at least short-circuit correctly:
return methodSymbol.GetAttributes().Any(attribute =>
attribute.AttributeClass?.GetSelfAndBaseTypes()
.Any(t => SymbolEqualityComparer.Default.Equals(t, baseTestAttribute)) == true);The current Contains(baseTestAttribute, SymbolEqualityComparer.Default) calls the IEnumerable<T>.Contains overload which does not short-circuit the iterator — it walks to the end. Using .Any(...) instead would short-circuit on first match.
MarkMethodStaticSuppressor still uses narrow IsTestMethod
CA1822 ("mark member as static") is suppressed for [Test] methods but not for [DynamicTestBuilder] methods via the static suppressor. If a [DynamicTestBuilder] method is an instance method (the common case — it accesses test context), CA1822 would fire and not be suppressed. This is the same kind of oversight that VSTHRD200 had before this PR. This is not a new problem introduced here, but it is a natural companion fix.
Test coverage: missing [DynamicTestBuilder] case for HasTestAttribute
The WarningsOnTestAndHookMethodsAreSuppressed test covers [Test] and all four hook variants, but not [DynamicTestBuilder] — which is precisely the motivation for introducing HasTestAttribute over IsTestMethod. Adding a [Arguments("DynamicTestBuilder")] case would close this gap:
[Arguments("Test")]
[Arguments("DynamicTestBuilder")] // <-- add this
[Arguments("Before(Test)")]
...Minor: SupportedSuppressions as instance property backed by ImmutableArray.Create
SupportedSuppressions is declared as an instance property initialized with ImmutableArray.Create(...). Since the array does not vary per-instance, it could be static readonly:
public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions { get; } =
ImmutableArray.Create(CreateDescriptor("VSTHRD200"));This matches the pattern in MarkMethodStaticSuppressor, so it is already consistent — just noting that this is actually static state stored per-instance. A private static readonly backing field and a property returning it would be marginally more efficient (no per-instance allocation), but this is a nit given how infrequently suppressor instances are created.
Summary
The core feature is correct and well-structured. The main actionable items are:
- Add a
[DynamicTestBuilder]test case toWarningsOnTestAndHookMethodsAreSuppressedto validate the primary motivating scenario forHasTestAttribute. - Use
.Any(...)instead of.Contains(...)inHasTestAttributeto ensure short-circuit evaluation. - File a follow-up issue tracking the
IsTestClass/IsTestMethodgap for[DynamicTestBuilder]-only classes. - Consider a companion fix for
MarkMethodStaticSuppressorusingHasTestAttributeto cover[DynamicTestBuilder].
Items 3 and 4 are out-of-scope for this PR and can be addressed in follow-ups.
…ribute - Add a [DynamicTestBuilder] case to the VSTHRD200 suppressor tests — the motivating scenario for HasTestAttribute over the exact-match IsTestMethod. - Replace Contains(...) with Any(...) in HasTestAttribute so the base-type chain walk short-circuits on first match.
There was a problem hiding this comment.
Code Review: feat(analyzers): suppress VSTHRD200 on test and hook methods
This is a well-structured addition that follows the existing suppressor pattern. The design is sound and the core logic is correct. A few issues worth addressing:
Bug: — use ID-aware lookup instead
File: TUnit.Analyzers/Vsthrd200AsyncSuffixSuppressor.cs, line 45
// Current (fragile):
private void Suppress(SuppressionAnalysisContext context, Diagnostic diagnostic)
=> context.ReportSuppression(Suppression.Create(SupportedSuppressions[0], diagnostic));
// MarkMethodStaticSuppressor does this (correct pattern):
var suppression = SupportedSuppressions.First(s => s.SuppressedDiagnosticId == diagnostic.Id);
context.ReportSuppression(Suppression.Create(suppression, diagnostic));MarkMethodStaticSuppressor (line 41) uses SupportedSuppressions.First(s => s.SuppressedDiagnosticId == diagnostic.Id). The new suppressor hard-codes index [0] instead. Today this is harmless since there is only one descriptor — but it is inconsistent with the established codebase pattern and becomes a latent bug the moment a second descriptor is added to SupportedSuppressions: the wrong descriptor would be applied to diagnostics matching the second ID, silently mis-attributing the suppression.
Behavioral asymmetry: is suppressed for VSTHRD200 but not for CA1822
File: TUnit.Analyzers/Vsthrd200AsyncSuffixSuppressor.cs, line 34 vs MarkMethodStaticSuppressor.cs line 27
The new suppressor uses HasTestAttribute (inheritance-aware, covers [DynamicTestBuilder]). MarkMethodStaticSuppressor uses IsTestMethod (exact match, [DynamicTestBuilder] not covered). The result: the same [DynamicTestBuilder] async non-static method will have its VSTHRD200 suppressed by this PR, but will still get a CA1822 "make static" warning. This inconsistency will confuse users.
Either update MarkMethodStaticSuppressor to also use HasTestAttribute, or document why the two suppressors intentionally have different coverage.
Wrong code comment: Contains vs Any
File: TUnit.Analyzers/Extensions/MethodExtensions.cs, lines 36–37
// Use Any(...) rather than Contains(...): the latter walks the whole base-type chain even
// after a match, whereas Any short-circuits on the first matching type.This comment is factually incorrect. Enumerable.Contains<T> (LINQ extension on IEnumerable<T>) short-circuits on the first match — it is equivalent to Any(x => Equals(x, value)). There is no performance difference. A future maintainer reading this comment will draw false conclusions about LINQ semantics. The comment should either be removed or corrected to explain the actual reason Any was chosen (e.g., the predicate form is clearer, or there was a specific compat concern).
Minor: without CancellationToken
File: TUnit.Analyzers/Vsthrd200AsyncSuffixSuppressor.cs, line 19
SuppressionAnalysisContext exposes context.CancellationToken. SyntaxTree.GetRoot(CancellationToken) cooperates with IDE cancellation. The tokenless overload is used here (and in MarkMethodStaticSuppressor/DynamicTestAwaitExpressionSuppressor — pre-existing), but since the new PR touches this path, it would be a good opportunity to pass context.CancellationToken consistently.
Test gap: Assembly/Class-scope hooks not tested
File: TUnit.Analyzers.Tests/Vsthrd200AsyncSuffixSuppressorTests.cs, line 13
The parameterized test covers Before(Test), After(Test), BeforeEvery(Test), AfterEvery(Test). The suppressor comment says suppression is deliberate for all hook levels. Before(Assembly), Before(Class), and their every-variants are not covered. A future narrowing of IsHookMethod to only Test-level scope (matching MarkMethodStaticSuppressor's pattern) would silently regress Assembly/Class hooks with no test catching it. Worth adding a few extra [Arguments] entries.
Overall
The core design — using HasTestAttribute for inheritance-aware detection and delegating to IsHookMethod for hooks — is correct and clean. The SupportedSuppressions[0] → ID-aware lookup fix is the most important change since it aligns with the pattern used everywhere else in the codebase. The [DynamicTestBuilder] CA1822 asymmetry is worth a conscious decision one way or the other.
There was a problem hiding this comment.
Code Review: feat(analyzers): suppress VSTHRD200 on test and hook methods
This is a well-structured addition that follows the existing suppressor pattern. The design is sound and the core logic is correct. A few issues worth addressing:
Bug: SupportedSuppressions[0] — use ID-aware lookup instead
File: TUnit.Analyzers/Vsthrd200AsyncSuffixSuppressor.cs, line 45
MarkMethodStaticSuppressor (line 41) uses SupportedSuppressions.First(s => s.SuppressedDiagnosticId == diagnostic.Id). The new suppressor hard-codes index [0] instead. Today this is harmless since there is only one descriptor, but it is inconsistent with the established codebase pattern and becomes a latent bug the moment a second descriptor is added: the wrong descriptor would be applied to diagnostics matching the second ID, silently mis-attributing the suppression.
Recommended fix — match the existing pattern:
private void Suppress(SuppressionAnalysisContext context, Diagnostic diagnostic)
{
var descriptor = SupportedSuppressions.First(s => s.SuppressedDiagnosticId == diagnostic.Id);
context.ReportSuppression(Suppression.Create(descriptor, diagnostic));
}Behavioral asymmetry: [DynamicTestBuilder] suppressed for VSTHRD200 but not CA1822
File: TUnit.Analyzers/Vsthrd200AsyncSuffixSuppressor.cs line 34 vs MarkMethodStaticSuppressor.cs line 27
The new suppressor uses HasTestAttribute (inheritance-aware, covers [DynamicTestBuilder]). MarkMethodStaticSuppressor uses IsTestMethod (exact match — [DynamicTestBuilder] not covered). The result: a [DynamicTestBuilder] async non-static method will have VSTHRD200 suppressed but will still get a CA1822 "make static" warning from the other suppressor. This inconsistency will confuse users who annotate methods with [DynamicTestBuilder].
Either update MarkMethodStaticSuppressor to also use HasTestAttribute, or add a comment explaining why the two suppressors intentionally have different coverage.
Wrong code comment: Contains vs Any
File: TUnit.Analyzers/Extensions/MethodExtensions.cs, lines 36–37
The comment reads: "the latter walks the whole base-type chain even after a match, whereas Any short-circuits on the first matching type."
This is factually incorrect. Enumerable.Contains<T> (LINQ extension on IEnumerable<T>) short-circuits on the first match — it is functionally equivalent to Any(x => Equals(x, value)). There is no performance difference between the two. A future maintainer will draw false conclusions about LINQ semantics from this comment. Remove it or replace it with the actual reason Any was chosen (e.g., the predicate form is structurally necessary because the equality check is non-trivial).
Minor: GetRoot() without CancellationToken
File: TUnit.Analyzers/Vsthrd200AsyncSuffixSuppressor.cs, line 19
SuppressionAnalysisContext exposes context.CancellationToken. SyntaxTree.GetRoot(CancellationToken) cooperates with IDE cancellation. The tokenless overload is used here and in the two other pre-existing suppressors, but since this PR adds new code to the same pattern, it would be a good opportunity to use diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken) consistently.
Test gap: Assembly/Class-scope hooks not covered
File: TUnit.Analyzers.Tests/Vsthrd200AsyncSuffixSuppressorTests.cs, line 13
The parameterized test covers Before(Test), After(Test), BeforeEvery(Test), AfterEvery(Test). The suppressor comment claims all hook scopes are covered, but Before(Assembly), Before(Class), and their every-variants are not tested. A future narrowing of IsHookMethod to only Test-level scope (paralleling MarkMethodStaticSuppressor) would silently stop suppressing Assembly/Class hooks with no failing test.
Overall
The core design is correct and clean. The SupportedSuppressions[0] → ID-aware lookup fix and the [DynamicTestBuilder] / CA1822 asymmetry are the most important things to address — both for consistency with the existing codebase and to avoid surprising users.
- Suppress(): use ID-aware SupportedSuppressions.First lookup, matching the established suppressor pattern. - Pass context.CancellationToken to GetRoot for IDE cancellation cooperation. - Expand the asymmetry comment: HasTestAttribute covers [DynamicTestBuilder] while CA1822's IsTestMethod does not — intentional, documented. - Revert HasTestAttribute to Contains (LINQ Contains already short-circuits; the prior comment was incorrect) and drop the misleading comment. - Cover Class/Assembly-scope hooks in the suppressor tests.
There was a problem hiding this comment.
Code Review
This is a well-motivated, cleanly executed PR. Suppressing VSTHRD200 on test/hook methods is the right call — the convention that async test methods should be named MyTestAsync is pure noise, and this mirrors what MSTest and xUnit already do. The implementation follows the existing suppressor pattern faithfully and is in good shape overall.
Good things worth calling out
HasTestAttributeis a cleaner, more reusable primitive thanIsTestMethodand the PR correctly leavesIsTestMethodalone to avoid touching 14 call sites. The new method handles theBaseTestAttributesubclass case ([DynamicTestBuilder], custom user attributes) that the old one misses.- Using
GetRoot(context.CancellationToken)is a real improvement over the existing suppressors (MarkMethodStaticSuppressor,DynamicTestAwaitExpressionSuppressor) which both callGetRoot()without the cancellation token. This is the more correct Roslyn pattern. - The decision to use
IsHookMethodfor all hook levels (rather than justHookLevel.TestasMarkMethodStaticSuppressordoes) is sound — the async-naming rule is irrelevant at every hook scope, not just test-level hooks. The inline comment explaining this divergence is helpful. - Test coverage for all 10 hook attribute variants is thorough.
Issues
1. Missing test for custom BaseTestAttribute subclass (most significant gap)
The whole point of HasTestAttribute over IsTestMethod is inheritance-aware detection. But the tests only exercise built-in TUnit attributes. A test that defines:
[AttributeUsage(AttributeTargets.Method)]
public class MyCustomTestAttribute : TUnit.Core.BaseTestAttribute { }
public class MyTests
{
[MyCustomTest]
public async Task {|#0:Foo|}() { await Task.CompletedTask; }
}...and asserts VSTHRD200 is suppressed would validate the core new behavior. Without it, a regression that broke inheritance walking would go undetected.
2. Suppress() should be private static
Suppress() doesn't access this, so it should be private static void Suppress(...). The same issue exists in MarkMethodStaticSuppressor and DynamicTestAwaitExpressionSuppressor — but it's worth fixing consistently here since this is new code.
// current
private void Suppress(SuppressionAnalysisContext context, Diagnostic diagnostic) { ... }
// better
private static void Suppress(SuppressionAnalysisContext context, Diagnostic diagnostic) { ... }3. SupportedSuppressions.First() should use index access
SupportedSuppressions is an ImmutableArray<SuppressionDescriptor> with exactly one element, constructed at class instantiation. Inside the suppression hot path:
// current — allocates an enumerator every call
var descriptor = SupportedSuppressions.First(s => s.SuppressedDiagnosticId == diagnostic.Id);
// better — direct index access, no allocation
var descriptor = SupportedSuppressions[0];Given only one descriptor is registered and it's already been matched to diagnostic.Id by the Roslyn framework before this method is called, the First() predicate is redundant. (Same issue exists in MarkMethodStaticSuppressor.)
4. Pre-existing: IsTestMethod null-forgiveness vs. HasTestAttribute null guard
IsTestMethod uses ! null-forgiveness on compilation.GetTypeByMetadataName(...)! while the new HasTestAttribute correctly null-guards it. This inconsistency pre-exists and is out of scope here, but worth tracking — if TUnit.Core isn't referenced in the compilation, IsTestMethod will throw.
Summary
The core feature and design are solid. The main actionable items are: add a test for a custom BaseTestAttribute subclass (validates the actual new behavior), make Suppress() static, and use [0] instead of .First(). Items 2 and 3 are minor polish.
- Hoist the single VSTHRD200 descriptor into a static readonly field so Suppress() can be static and reference it directly (no per-call scan). - Settled the [0] vs First() churn with a documenting comment. Did not add the suggested 'custom BaseTestAttribute subclass' test: its constructor is internal, so external code cannot derive from it. The inheritance-aware HasTestAttribute path is already covered by the [DynamicTestBuilder] case (a real BaseTestAttribute subclass).
|
Addressed in the latest commit:
Declined:
|
There was a problem hiding this comment.
Review: feat(analyzers): suppress VSTHRD200 on test and hook methods
Overall this is a clean, well-motivated addition that follows established patterns — and in a few places improves on them. A couple of things worth discussing:
Good improvements over the existing pattern
Null-guard in HasTestAttribute (MethodExtensions.cs)
The existing IsTestMethod uses ! (null-forgiving) after GetTypeByMetadataName. HasTestAttribute does the right thing instead:
if (baseTestAttribute is null) return false;Strictly better, and worth noting as the preferred pattern going forward.
CancellationToken passed to GetRoot (Vsthrd200AsyncSuffixSuppressor.cs:20)
diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken)MarkMethodStaticSuppressor and DynamicTestAwaitExpressionSuppressor both call GetRoot() without a token — this is a quiet improvement. No action needed here, but it highlights a minor gap in the existing suppressors.
Minor: Contains with SymbolEqualityComparer.Default — type inference subtlety
GetSelfAndBaseTypes() returns IEnumerable<INamedTypeSymbol>, and SymbolEqualityComparer.Default is IEqualityComparer<ISymbol>. These are compatible only because IEnumerable<T> is covariant, so the compiler resolves TSource as ISymbol. It compiles and behaves correctly, but it's a subtle inference.
The Any form from an earlier commit is slightly more explicit about what's happening:
.Any(t => SymbolEqualityComparer.Default.Equals(t, baseTestAttribute))Both short-circuit identically. The commit notes say Contains "already short-circuits" and reverted from Any — this is correct. It's a style preference, not a correctness issue.
Design note: intentional asymmetry with MarkMethodStaticSuppressor
The PR comment documents this well: MarkMethodStaticSuppressor (CA1822) uses exact-match IsTestMethod + only Test-level hooks, while this suppressor uses inheritance-aware HasTestAttribute + all hook levels. The reasoning is sound — CA1822 "make static" has narrower applicability than "drop Async suffix". The comment in the code explains this clearly enough to not be surprising to future maintainers.
Test coverage
Comprehensive — covers all hook scopes (Test/Class/Assembly), [DynamicTestBuilder], and verifies non-test methods are not suppressed. The #if NET8_0_OR_GREATER guard correctly includes all targeted frameworks. The package reference setup with Analyzer="false" is the right approach to make the VSTHRD200 analyzer type loadable without applying it to the test project itself.
Verdict: Ready to merge. The only real discussion point is Contains vs Any on the IEnumerable<INamedTypeSymbol>, which is purely stylistic — both are correct and both short-circuit. Everything else is solid.
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.45.29 to 1.48.6. <details> <summary>Release notes</summary> _Sourced from [TUnit's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.48.6 <!-- Release notes generated using configuration in .github/release.yml at v1.48.6 --> ## What's Changed ### Other Changes * fix(sourcegen): fully-qualify Linq calls in params array binding (#6140) by @thomhurst in thomhurst/TUnit#6141 ### Dependencies * chore(deps): update tunit to 1.48.0 by @thomhurst in thomhurst/TUnit#6135 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6137 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6138 * chore(deps): update verify to 31.19.0 by @thomhurst in thomhurst/TUnit#6139 **Full Changelog**: thomhurst/TUnit@v1.48.0...v1.48.6 ## 1.48.0 <!-- Release notes generated using configuration in .github/release.yml at v1.48.0 --> ## What's Changed ### Other Changes * feat(html-report): baked-in C# syntax highlighting on Source tab by @slang25 in thomhurst/TUnit#6132 * feat(analyzers): suppress VSTHRD200 on test and hook methods by @thomhurst in thomhurst/TUnit#6123 * fix(source-gen): correct source location for cross-project inherited tests by @slang25 in thomhurst/TUnit#6133 * feat(assertions): add WasCalled to tunit mocks assertions by @robertcoltheart in thomhurst/TUnit#6126 * feat(arguments): bind array values to a single array test parameter by @thomhurst in thomhurst/TUnit#6122 * fix: populate retry/flaky attempt history in HTML report (#6119) by @thomhurst in thomhurst/TUnit#6124 ### Dependencies * chore(deps): update tunit to 1.47.0 by @thomhurst in thomhurst/TUnit#6115 * chore(deps): update dependency microsoft.visualstudio.threading.analyzers to 17.14.15 by @thomhurst in thomhurst/TUnit#6134 **Full Changelog**: thomhurst/TUnit@v1.47.0...v1.48.0 ## 1.47.0 <!-- Release notes generated using configuration in .github/release.yml at v1.47.0 --> ## What's Changed ### Other Changes * perf(engine): hoist GetParameters and dict-dedup AfterTestDiscovery hooks by @thomhurst in thomhurst/TUnit#6062 * perf(engine): hoist GetParameters and drop LINQ in reflection discovery by @thomhurst in thomhurst/TUnit#6063 * perf(engine): cache treenode filter path on TestMetadata by @thomhurst in thomhurst/TUnit#6064 * perf: use is T pattern in ReflectionExtensions.HasAttribute fallback (#6060) by @thomhurst in thomhurst/TUnit#6066 * perf: replace OrderBy().ToArray() with Array.Sort in ConstraintKeyScheduler by @thomhurst in thomhurst/TUnit#6067 * perf: pool HashSet in WaitingTestIndex.GetCandidatesForReleasedKeys by @thomhurst in thomhurst/TUnit#6069 * perf: collapse OfType chains in JUnitXmlWriter (#6052) by @thomhurst in thomhurst/TUnit#6070 * perf(engine): avoid closure allocation in AfterHookPairTracker.GetOrCreateAfterAssemblyTask (#6041) by @thomhurst in thomhurst/TUnit#6071 * perf: avoid closure allocation in BeforeHookTaskCache.GetOrCreateBeforeAssemblyTask (#6040) by @thomhurst in thomhurst/TUnit#6073 * perf: use TryAdd in TestDependencyResolver dependency dedupe by @thomhurst in thomhurst/TUnit#6068 * perf: replace LINQ Any with foreach in TestGenericTypeResolver (#6044) by @thomhurst in thomhurst/TUnit#6072 * perf: avoid Cast<object>().FirstOrDefault() iterator alloc in CastHelper (#6029) by @thomhurst in thomhurst/TUnit#6074 * perf(engine): avoid string round-trip when building nested type names (#6049) by @thomhurst in thomhurst/TUnit#6075 * perf(engine): replace Select+ToArray with manual Type[] build (#6043) by @thomhurst in thomhurst/TUnit#6076 * perf(core): replace OfType().FirstOrDefault()/.Any() with foreach in ClassConstructorHelper by @thomhurst in thomhurst/TUnit#6078 * perf(engine): avoid FirstOrDefault iterator alloc in TestGenericTypeResolver by @thomhurst in thomhurst/TUnit#6079 * perf(engine): use SearchValues<char> for reporter filename sanitization by @thomhurst in thomhurst/TUnit#6090 * perf: dedupe TestDataFormatter.FormatArguments with pooled StringBuilder by @thomhurst in thomhurst/TUnit#6088 * perf(engine): use MemoryExtensions.Split for path parsing in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6085 * perf(engine): use CollectionsMarshal.GetValueRefOrAddDefault for dictionary index builds by @thomhurst in thomhurst/TUnit#6086 * perf(engine): replace LINQ Where closure with inline filter in MetadataDependencyExpander BFS by @thomhurst in thomhurst/TUnit#6084 * perf(engine): pool StringBuilder in DisplayNameBuilder.FormatArguments by @thomhurst in thomhurst/TUnit#6082 * Preserve specialized chaining after null assertions by @thomhurst in thomhurst/TUnit#6008 * perf: use EnumerateLines for line splitting in HtmlReportGenerator by @thomhurst in thomhurst/TUnit#6089 * perf: collapse Replace chain in TestNameFormatter.BuildTestId by @thomhurst in thomhurst/TUnit#6083 * perf: use OrdinalIgnoreCase Contains in HtmlReportGenerator span mapping by @thomhurst in thomhurst/TUnit#6093 * perf(assertions): avoid eager interpolated-string alloc in assertion source ctors by @thomhurst in thomhurst/TUnit#6091 * perf: optimize TestNameFormatter argument and bool formatting by @thomhurst in thomhurst/TUnit#6095 * perf: use FrozenSet/FrozenDictionary for read-only static lookups by @thomhurst in thomhurst/TUnit#6099 * perf: avoid GetCustomAttributes() + LINQ chain for per-property attribute scans by @thomhurst in thomhurst/TUnit#6098 * perf(engine): replace magic-string RequiredAttribute match with type check in ConstructorHelper by @thomhurst in thomhurst/TUnit#6087 * perf(core): replace Select+Func factory chain in DataSourceHelpers by @thomhurst in thomhurst/TUnit#6081 * perf: replace LINQ dependency extraction with manual loop by @thomhurst in thomhurst/TUnit#6096 * perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments by @thomhurst in thomhurst/TUnit#6080 * perf: use [GeneratedRegex] in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6094 * perf: dedupe GetSimpleTypeName into shared TypeNameFormatter by @thomhurst in thomhurst/TUnit#6097 * fix: remove GitVersion MSBuild task, pin local builds to 99.99.99 (#6077) by @thomhurst in thomhurst/TUnit#6101 * HTML Report: source link + code snippet on Source tab (#5993) by @thomhurst in thomhurst/TUnit#6100 * perf(sourcegen): Single-pass attribute classification by @thomhurst in thomhurst/TUnit#6111 * perf(core): eliminate per-test allocations in TestDetails/HookMethod by @thomhurst in thomhurst/TUnit#6109 * perf: hoist char[] alloc in FsCheckPropertyTestExecutor to static SearchValues by @thomhurst in thomhurst/TUnit#6108 * perf(core): de-LINQ data-source expansion by @thomhurst in thomhurst/TUnit#6110 * perf: avoid LINQ chains in TestDependency equality and MethodDataSourceAttribute method matching by @thomhurst in thomhurst/TUnit#6092 * perf(engine): reduce allocations in reflection-mode discovery/execution by @thomhurst in thomhurst/TUnit#6113 * perf(assertions): allocation-free passing path (TUnit.Assertions) by @thomhurst in thomhurst/TUnit#6112 ### Dependencies ... (truncated) ## 1.46.0 <!-- Release notes generated using configuration in .github/release.yml at v1.46.0 --> ## What's Changed ### Other Changes * docs: add Rider VSTest conflict troubleshooting by @smolchanovsky in thomhurst/TUnit#5989 * Populate generated test metadata with full source spans by @Copilot in thomhurst/TUnit#5991 * Add devcontainer configuration by @Copilot in thomhurst/TUnit#5995 * fix: treenode filter pre-filter rejects parenthesised segments (#6026) by @thomhurst in thomhurst/TUnit#6027 * fix(engine): isolate per-session state under MTP server-mode concurrency (#6001) by @thomhurst in thomhurst/TUnit#6025 ### Dependencies * chore(deps): update dependency stackexchange.redis to 2.13.10 by @thomhurst in thomhurst/TUnit#5985 * chore(deps): update tunit to 1.45.29 by @thomhurst in thomhurst/TUnit#5986 * chore(deps): update dependency mockolate to 3.2.1 by @thomhurst in thomhurst/TUnit#5987 * chore(deps): update dependency microsoft.playwright to 1.60.0 by @thomhurst in thomhurst/TUnit#5988 * chore(deps): update dependency messagepack to 3.1.6 by @thomhurst in thomhurst/TUnit#5992 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5998 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5997 * chore(deps): update verify to 31.17.0 by @thomhurst in thomhurst/TUnit#6000 * chore(deps): update verify to 31.18.0 by @thomhurst in thomhurst/TUnit#6013 * chore(deps): update dependency microsoft.net.test.sdk to 18.6.0 by @thomhurst in thomhurst/TUnit#6016 * chore(deps): update dependency dompurify to v3.4.6 by @thomhurst in thomhurst/TUnit#6015 * chore(deps): update dependency dompurify to v3.4.7 by @thomhurst in thomhurst/TUnit#6019 * chore(deps): update dependency npgsql to 10.0.3 by @thomhurst in thomhurst/TUnit#6020 * chore(deps): update dependency stackexchange.redis to 2.13.17 by @thomhurst in thomhurst/TUnit#6021 * chore(deps): update dependency npgsql.entityframeworkcore.postgresql to 10.0.2 by @thomhurst in thomhurst/TUnit#6022 ## New Contributors * @smolchanovsky made their first contribution in thomhurst/TUnit#5989 **Full Changelog**: thomhurst/TUnit@v1.45.29...v1.46.0 Commits viewable in [compare view](thomhurst/TUnit@v1.45.29...v1.48.6). </details> Updated [TUnit.AspNetCore](https://github.com/thomhurst/TUnit) from 1.45.29 to 1.48.6. <details> <summary>Release notes</summary> _Sourced from [TUnit.AspNetCore's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.48.6 <!-- Release notes generated using configuration in .github/release.yml at v1.48.6 --> ## What's Changed ### Other Changes * fix(sourcegen): fully-qualify Linq calls in params array binding (#6140) by @thomhurst in thomhurst/TUnit#6141 ### Dependencies * chore(deps): update tunit to 1.48.0 by @thomhurst in thomhurst/TUnit#6135 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6137 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6138 * chore(deps): update verify to 31.19.0 by @thomhurst in thomhurst/TUnit#6139 **Full Changelog**: thomhurst/TUnit@v1.48.0...v1.48.6 ## 1.48.0 <!-- Release notes generated using configuration in .github/release.yml at v1.48.0 --> ## What's Changed ### Other Changes * feat(html-report): baked-in C# syntax highlighting on Source tab by @slang25 in thomhurst/TUnit#6132 * feat(analyzers): suppress VSTHRD200 on test and hook methods by @thomhurst in thomhurst/TUnit#6123 * fix(source-gen): correct source location for cross-project inherited tests by @slang25 in thomhurst/TUnit#6133 * feat(assertions): add WasCalled to tunit mocks assertions by @robertcoltheart in thomhurst/TUnit#6126 * feat(arguments): bind array values to a single array test parameter by @thomhurst in thomhurst/TUnit#6122 * fix: populate retry/flaky attempt history in HTML report (#6119) by @thomhurst in thomhurst/TUnit#6124 ### Dependencies * chore(deps): update tunit to 1.47.0 by @thomhurst in thomhurst/TUnit#6115 * chore(deps): update dependency microsoft.visualstudio.threading.analyzers to 17.14.15 by @thomhurst in thomhurst/TUnit#6134 **Full Changelog**: thomhurst/TUnit@v1.47.0...v1.48.0 ## 1.47.0 <!-- Release notes generated using configuration in .github/release.yml at v1.47.0 --> ## What's Changed ### Other Changes * perf(engine): hoist GetParameters and dict-dedup AfterTestDiscovery hooks by @thomhurst in thomhurst/TUnit#6062 * perf(engine): hoist GetParameters and drop LINQ in reflection discovery by @thomhurst in thomhurst/TUnit#6063 * perf(engine): cache treenode filter path on TestMetadata by @thomhurst in thomhurst/TUnit#6064 * perf: use is T pattern in ReflectionExtensions.HasAttribute fallback (#6060) by @thomhurst in thomhurst/TUnit#6066 * perf: replace OrderBy().ToArray() with Array.Sort in ConstraintKeyScheduler by @thomhurst in thomhurst/TUnit#6067 * perf: pool HashSet in WaitingTestIndex.GetCandidatesForReleasedKeys by @thomhurst in thomhurst/TUnit#6069 * perf: collapse OfType chains in JUnitXmlWriter (#6052) by @thomhurst in thomhurst/TUnit#6070 * perf(engine): avoid closure allocation in AfterHookPairTracker.GetOrCreateAfterAssemblyTask (#6041) by @thomhurst in thomhurst/TUnit#6071 * perf: avoid closure allocation in BeforeHookTaskCache.GetOrCreateBeforeAssemblyTask (#6040) by @thomhurst in thomhurst/TUnit#6073 * perf: use TryAdd in TestDependencyResolver dependency dedupe by @thomhurst in thomhurst/TUnit#6068 * perf: replace LINQ Any with foreach in TestGenericTypeResolver (#6044) by @thomhurst in thomhurst/TUnit#6072 * perf: avoid Cast<object>().FirstOrDefault() iterator alloc in CastHelper (#6029) by @thomhurst in thomhurst/TUnit#6074 * perf(engine): avoid string round-trip when building nested type names (#6049) by @thomhurst in thomhurst/TUnit#6075 * perf(engine): replace Select+ToArray with manual Type[] build (#6043) by @thomhurst in thomhurst/TUnit#6076 * perf(core): replace OfType().FirstOrDefault()/.Any() with foreach in ClassConstructorHelper by @thomhurst in thomhurst/TUnit#6078 * perf(engine): avoid FirstOrDefault iterator alloc in TestGenericTypeResolver by @thomhurst in thomhurst/TUnit#6079 * perf(engine): use SearchValues<char> for reporter filename sanitization by @thomhurst in thomhurst/TUnit#6090 * perf: dedupe TestDataFormatter.FormatArguments with pooled StringBuilder by @thomhurst in thomhurst/TUnit#6088 * perf(engine): use MemoryExtensions.Split for path parsing in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6085 * perf(engine): use CollectionsMarshal.GetValueRefOrAddDefault for dictionary index builds by @thomhurst in thomhurst/TUnit#6086 * perf(engine): replace LINQ Where closure with inline filter in MetadataDependencyExpander BFS by @thomhurst in thomhurst/TUnit#6084 * perf(engine): pool StringBuilder in DisplayNameBuilder.FormatArguments by @thomhurst in thomhurst/TUnit#6082 * Preserve specialized chaining after null assertions by @thomhurst in thomhurst/TUnit#6008 * perf: use EnumerateLines for line splitting in HtmlReportGenerator by @thomhurst in thomhurst/TUnit#6089 * perf: collapse Replace chain in TestNameFormatter.BuildTestId by @thomhurst in thomhurst/TUnit#6083 * perf: use OrdinalIgnoreCase Contains in HtmlReportGenerator span mapping by @thomhurst in thomhurst/TUnit#6093 * perf(assertions): avoid eager interpolated-string alloc in assertion source ctors by @thomhurst in thomhurst/TUnit#6091 * perf: optimize TestNameFormatter argument and bool formatting by @thomhurst in thomhurst/TUnit#6095 * perf: use FrozenSet/FrozenDictionary for read-only static lookups by @thomhurst in thomhurst/TUnit#6099 * perf: avoid GetCustomAttributes() + LINQ chain for per-property attribute scans by @thomhurst in thomhurst/TUnit#6098 * perf(engine): replace magic-string RequiredAttribute match with type check in ConstructorHelper by @thomhurst in thomhurst/TUnit#6087 * perf(core): replace Select+Func factory chain in DataSourceHelpers by @thomhurst in thomhurst/TUnit#6081 * perf: replace LINQ dependency extraction with manual loop by @thomhurst in thomhurst/TUnit#6096 * perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments by @thomhurst in thomhurst/TUnit#6080 * perf: use [GeneratedRegex] in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6094 * perf: dedupe GetSimpleTypeName into shared TypeNameFormatter by @thomhurst in thomhurst/TUnit#6097 * fix: remove GitVersion MSBuild task, pin local builds to 99.99.99 (#6077) by @thomhurst in thomhurst/TUnit#6101 * HTML Report: source link + code snippet on Source tab (#5993) by @thomhurst in thomhurst/TUnit#6100 * perf(sourcegen): Single-pass attribute classification by @thomhurst in thomhurst/TUnit#6111 * perf(core): eliminate per-test allocations in TestDetails/HookMethod by @thomhurst in thomhurst/TUnit#6109 * perf: hoist char[] alloc in FsCheckPropertyTestExecutor to static SearchValues by @thomhurst in thomhurst/TUnit#6108 * perf(core): de-LINQ data-source expansion by @thomhurst in thomhurst/TUnit#6110 * perf: avoid LINQ chains in TestDependency equality and MethodDataSourceAttribute method matching by @thomhurst in thomhurst/TUnit#6092 * perf(engine): reduce allocations in reflection-mode discovery/execution by @thomhurst in thomhurst/TUnit#6113 * perf(assertions): allocation-free passing path (TUnit.Assertions) by @thomhurst in thomhurst/TUnit#6112 ### Dependencies ... (truncated) ## 1.46.0 <!-- Release notes generated using configuration in .github/release.yml at v1.46.0 --> ## What's Changed ### Other Changes * docs: add Rider VSTest conflict troubleshooting by @smolchanovsky in thomhurst/TUnit#5989 * Populate generated test metadata with full source spans by @Copilot in thomhurst/TUnit#5991 * Add devcontainer configuration by @Copilot in thomhurst/TUnit#5995 * fix: treenode filter pre-filter rejects parenthesised segments (#6026) by @thomhurst in thomhurst/TUnit#6027 * fix(engine): isolate per-session state under MTP server-mode concurrency (#6001) by @thomhurst in thomhurst/TUnit#6025 ### Dependencies * chore(deps): update dependency stackexchange.redis to 2.13.10 by @thomhurst in thomhurst/TUnit#5985 * chore(deps): update tunit to 1.45.29 by @thomhurst in thomhurst/TUnit#5986 * chore(deps): update dependency mockolate to 3.2.1 by @thomhurst in thomhurst/TUnit#5987 * chore(deps): update dependency microsoft.playwright to 1.60.0 by @thomhurst in thomhurst/TUnit#5988 * chore(deps): update dependency messagepack to 3.1.6 by @thomhurst in thomhurst/TUnit#5992 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5998 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5997 * chore(deps): update verify to 31.17.0 by @thomhurst in thomhurst/TUnit#6000 * chore(deps): update verify to 31.18.0 by @thomhurst in thomhurst/TUnit#6013 * chore(deps): update dependency microsoft.net.test.sdk to 18.6.0 by @thomhurst in thomhurst/TUnit#6016 * chore(deps): update dependency dompurify to v3.4.6 by @thomhurst in thomhurst/TUnit#6015 * chore(deps): update dependency dompurify to v3.4.7 by @thomhurst in thomhurst/TUnit#6019 * chore(deps): update dependency npgsql to 10.0.3 by @thomhurst in thomhurst/TUnit#6020 * chore(deps): update dependency stackexchange.redis to 2.13.17 by @thomhurst in thomhurst/TUnit#6021 * chore(deps): update dependency npgsql.entityframeworkcore.postgresql to 10.0.2 by @thomhurst in thomhurst/TUnit#6022 ## New Contributors * @smolchanovsky made their first contribution in thomhurst/TUnit#5989 **Full Changelog**: thomhurst/TUnit@v1.45.29...v1.46.0 Commits viewable in [compare view](thomhurst/TUnit@v1.45.29...v1.48.6). </details> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.45.29 to 1.48.6. <details> <summary>Release notes</summary> _Sourced from [TUnit.Core's releases](https://github.com/thomhurst/TUnit/releases)._ ## 1.48.6 <!-- Release notes generated using configuration in .github/release.yml at v1.48.6 --> ## What's Changed ### Other Changes * fix(sourcegen): fully-qualify Linq calls in params array binding (#6140) by @thomhurst in thomhurst/TUnit#6141 ### Dependencies * chore(deps): update tunit to 1.48.0 by @thomhurst in thomhurst/TUnit#6135 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6137 * chore(deps): update dependency polyfill to 10.7.1 by @thomhurst in thomhurst/TUnit#6138 * chore(deps): update verify to 31.19.0 by @thomhurst in thomhurst/TUnit#6139 **Full Changelog**: thomhurst/TUnit@v1.48.0...v1.48.6 ## 1.48.0 <!-- Release notes generated using configuration in .github/release.yml at v1.48.0 --> ## What's Changed ### Other Changes * feat(html-report): baked-in C# syntax highlighting on Source tab by @slang25 in thomhurst/TUnit#6132 * feat(analyzers): suppress VSTHRD200 on test and hook methods by @thomhurst in thomhurst/TUnit#6123 * fix(source-gen): correct source location for cross-project inherited tests by @slang25 in thomhurst/TUnit#6133 * feat(assertions): add WasCalled to tunit mocks assertions by @robertcoltheart in thomhurst/TUnit#6126 * feat(arguments): bind array values to a single array test parameter by @thomhurst in thomhurst/TUnit#6122 * fix: populate retry/flaky attempt history in HTML report (#6119) by @thomhurst in thomhurst/TUnit#6124 ### Dependencies * chore(deps): update tunit to 1.47.0 by @thomhurst in thomhurst/TUnit#6115 * chore(deps): update dependency microsoft.visualstudio.threading.analyzers to 17.14.15 by @thomhurst in thomhurst/TUnit#6134 **Full Changelog**: thomhurst/TUnit@v1.47.0...v1.48.0 ## 1.47.0 <!-- Release notes generated using configuration in .github/release.yml at v1.47.0 --> ## What's Changed ### Other Changes * perf(engine): hoist GetParameters and dict-dedup AfterTestDiscovery hooks by @thomhurst in thomhurst/TUnit#6062 * perf(engine): hoist GetParameters and drop LINQ in reflection discovery by @thomhurst in thomhurst/TUnit#6063 * perf(engine): cache treenode filter path on TestMetadata by @thomhurst in thomhurst/TUnit#6064 * perf: use is T pattern in ReflectionExtensions.HasAttribute fallback (#6060) by @thomhurst in thomhurst/TUnit#6066 * perf: replace OrderBy().ToArray() with Array.Sort in ConstraintKeyScheduler by @thomhurst in thomhurst/TUnit#6067 * perf: pool HashSet in WaitingTestIndex.GetCandidatesForReleasedKeys by @thomhurst in thomhurst/TUnit#6069 * perf: collapse OfType chains in JUnitXmlWriter (#6052) by @thomhurst in thomhurst/TUnit#6070 * perf(engine): avoid closure allocation in AfterHookPairTracker.GetOrCreateAfterAssemblyTask (#6041) by @thomhurst in thomhurst/TUnit#6071 * perf: avoid closure allocation in BeforeHookTaskCache.GetOrCreateBeforeAssemblyTask (#6040) by @thomhurst in thomhurst/TUnit#6073 * perf: use TryAdd in TestDependencyResolver dependency dedupe by @thomhurst in thomhurst/TUnit#6068 * perf: replace LINQ Any with foreach in TestGenericTypeResolver (#6044) by @thomhurst in thomhurst/TUnit#6072 * perf: avoid Cast<object>().FirstOrDefault() iterator alloc in CastHelper (#6029) by @thomhurst in thomhurst/TUnit#6074 * perf(engine): avoid string round-trip when building nested type names (#6049) by @thomhurst in thomhurst/TUnit#6075 * perf(engine): replace Select+ToArray with manual Type[] build (#6043) by @thomhurst in thomhurst/TUnit#6076 * perf(core): replace OfType().FirstOrDefault()/.Any() with foreach in ClassConstructorHelper by @thomhurst in thomhurst/TUnit#6078 * perf(engine): avoid FirstOrDefault iterator alloc in TestGenericTypeResolver by @thomhurst in thomhurst/TUnit#6079 * perf(engine): use SearchValues<char> for reporter filename sanitization by @thomhurst in thomhurst/TUnit#6090 * perf: dedupe TestDataFormatter.FormatArguments with pooled StringBuilder by @thomhurst in thomhurst/TUnit#6088 * perf(engine): use MemoryExtensions.Split for path parsing in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6085 * perf(engine): use CollectionsMarshal.GetValueRefOrAddDefault for dictionary index builds by @thomhurst in thomhurst/TUnit#6086 * perf(engine): replace LINQ Where closure with inline filter in MetadataDependencyExpander BFS by @thomhurst in thomhurst/TUnit#6084 * perf(engine): pool StringBuilder in DisplayNameBuilder.FormatArguments by @thomhurst in thomhurst/TUnit#6082 * Preserve specialized chaining after null assertions by @thomhurst in thomhurst/TUnit#6008 * perf: use EnumerateLines for line splitting in HtmlReportGenerator by @thomhurst in thomhurst/TUnit#6089 * perf: collapse Replace chain in TestNameFormatter.BuildTestId by @thomhurst in thomhurst/TUnit#6083 * perf: use OrdinalIgnoreCase Contains in HtmlReportGenerator span mapping by @thomhurst in thomhurst/TUnit#6093 * perf(assertions): avoid eager interpolated-string alloc in assertion source ctors by @thomhurst in thomhurst/TUnit#6091 * perf: optimize TestNameFormatter argument and bool formatting by @thomhurst in thomhurst/TUnit#6095 * perf: use FrozenSet/FrozenDictionary for read-only static lookups by @thomhurst in thomhurst/TUnit#6099 * perf: avoid GetCustomAttributes() + LINQ chain for per-property attribute scans by @thomhurst in thomhurst/TUnit#6098 * perf(engine): replace magic-string RequiredAttribute match with type check in ConstructorHelper by @thomhurst in thomhurst/TUnit#6087 * perf(core): replace Select+Func factory chain in DataSourceHelpers by @thomhurst in thomhurst/TUnit#6081 * perf: replace LINQ dependency extraction with manual loop by @thomhurst in thomhurst/TUnit#6096 * perf(core): avoid string[] alloc in ArgumentFormatter.FormatArguments by @thomhurst in thomhurst/TUnit#6080 * perf: use [GeneratedRegex] in MetadataFilterMatcher by @thomhurst in thomhurst/TUnit#6094 * perf: dedupe GetSimpleTypeName into shared TypeNameFormatter by @thomhurst in thomhurst/TUnit#6097 * fix: remove GitVersion MSBuild task, pin local builds to 99.99.99 (#6077) by @thomhurst in thomhurst/TUnit#6101 * HTML Report: source link + code snippet on Source tab (#5993) by @thomhurst in thomhurst/TUnit#6100 * perf(sourcegen): Single-pass attribute classification by @thomhurst in thomhurst/TUnit#6111 * perf(core): eliminate per-test allocations in TestDetails/HookMethod by @thomhurst in thomhurst/TUnit#6109 * perf: hoist char[] alloc in FsCheckPropertyTestExecutor to static SearchValues by @thomhurst in thomhurst/TUnit#6108 * perf(core): de-LINQ data-source expansion by @thomhurst in thomhurst/TUnit#6110 * perf: avoid LINQ chains in TestDependency equality and MethodDataSourceAttribute method matching by @thomhurst in thomhurst/TUnit#6092 * perf(engine): reduce allocations in reflection-mode discovery/execution by @thomhurst in thomhurst/TUnit#6113 * perf(assertions): allocation-free passing path (TUnit.Assertions) by @thomhurst in thomhurst/TUnit#6112 ### Dependencies ... (truncated) ## 1.46.0 <!-- Release notes generated using configuration in .github/release.yml at v1.46.0 --> ## What's Changed ### Other Changes * docs: add Rider VSTest conflict troubleshooting by @smolchanovsky in thomhurst/TUnit#5989 * Populate generated test metadata with full source spans by @Copilot in thomhurst/TUnit#5991 * Add devcontainer configuration by @Copilot in thomhurst/TUnit#5995 * fix: treenode filter pre-filter rejects parenthesised segments (#6026) by @thomhurst in thomhurst/TUnit#6027 * fix(engine): isolate per-session state under MTP server-mode concurrency (#6001) by @thomhurst in thomhurst/TUnit#6025 ### Dependencies * chore(deps): update dependency stackexchange.redis to 2.13.10 by @thomhurst in thomhurst/TUnit#5985 * chore(deps): update tunit to 1.45.29 by @thomhurst in thomhurst/TUnit#5986 * chore(deps): update dependency mockolate to 3.2.1 by @thomhurst in thomhurst/TUnit#5987 * chore(deps): update dependency microsoft.playwright to 1.60.0 by @thomhurst in thomhurst/TUnit#5988 * chore(deps): update dependency messagepack to 3.1.6 by @thomhurst in thomhurst/TUnit#5992 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5998 * chore(deps): update dependency polyfill to 10.7.0 by @thomhurst in thomhurst/TUnit#5997 * chore(deps): update verify to 31.17.0 by @thomhurst in thomhurst/TUnit#6000 * chore(deps): update verify to 31.18.0 by @thomhurst in thomhurst/TUnit#6013 * chore(deps): update dependency microsoft.net.test.sdk to 18.6.0 by @thomhurst in thomhurst/TUnit#6016 * chore(deps): update dependency dompurify to v3.4.6 by @thomhurst in thomhurst/TUnit#6015 * chore(deps): update dependency dompurify to v3.4.7 by @thomhurst in thomhurst/TUnit#6019 * chore(deps): update dependency npgsql to 10.0.3 by @thomhurst in thomhurst/TUnit#6020 * chore(deps): update dependency stackexchange.redis to 2.13.17 by @thomhurst in thomhurst/TUnit#6021 * chore(deps): update dependency npgsql.entityframeworkcore.postgresql to 10.0.2 by @thomhurst in thomhurst/TUnit#6022 ## New Contributors * @smolchanovsky made their first contribution in thomhurst/TUnit#5989 **Full Changelog**: thomhurst/TUnit@v1.45.29...v1.46.0 Commits viewable in [compare view](thomhurst/TUnit@v1.45.29...v1.48.6). </details> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
What
Adds
Vsthrd200AsyncSuffixSuppressor, a RoslynDiagnosticSuppressorthat silences VSTHRD200 ("UseAsyncsuffix for async methods") on:TUnit.Core.BaseTestAttribute(covers[Test],[DynamicTestBuilder], and any future subclass).[Before]/[After]/[BeforeEvery]/[AfterEvery].VSTHRD200 is useful for production code but noisy on tests:
[Test] async Task MyTest()shouldn't be forced toMyTestAsync. This mirrors the convention MSTest and xUnit already ship.How
MarkMethodStaticSuppressor(CA1822) pattern.MethodExtensions.HasTestAttributewalks the attribute inheritance chain via the existingGetSelfAndBaseTypes()helper. Left the existing exact-matchIsTestMethoduntouched to avoid changing behavior of the ~14 analyzers that call it.Microsoft.VisualStudio.Threading.Analyzers(analyzer dll,Analyzer="false") so the VSTHRD200 analyzer type is loadable in suppressor tests.No
AnalyzerReleases.*.mdchange — suppressors are not tracked there.Tests
Vsthrd200AsyncSuffixSuppressorTests— VSTHRD200 suppressed on[Test]+ 4 hook variants; not suppressed on a plain async non-test method. All pass.Closes #6121