From 30e94e7ec44e2ac3207eab6882ece8d23fce443e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 29 Oct 2026 04:23:40 +0100 Subject: [PATCH 1/2] ci(mutation): disable Stryker mutators on static initializer tables Stryker 5.0.0 with the Microsoft Testing Platform runner (preview) leaves mutants in static field initializers as Survived: the mutant is switched on in a warm test host after the type initializer already ran, so no test can fail. In the latest run 202 of 491 survivors were static mutants (CSharpKeywords 127, CollectionExpressionTargetTypes 46, AllowedCollectionMetadataNames 11, SupportedAsyncReturnTypeMetadataNames 5, others), even though tests such as ArrayList_ImmediateClear_Reports would fail if the entry were dropped. Scope the disable/restore pragmas to the individual static field declarations so the surrounding logic stays mutated. Remove once the MTP runner handles static mutants. --- .../Documentation/UseLangwordSuppressor.cs | 2 ++ src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs | 10 ++++++++++ .../Helpers/CancellationTokenCallSites.cs | 2 ++ .../Helpers/TargetFrameworkMonikers.cs | 2 ++ .../AvoidRedundantCollectionClearAnalyzer.cs | 2 ++ .../Style/RequireNumericLiteralSuffixSuppressor.cs | 2 ++ .../Usage/RequireCancellationCheckCodeFixProvider.cs | 2 ++ 7 files changed, 22 insertions(+) diff --git a/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs b/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs index 492bab3..a72a21e 100644 --- a/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs +++ b/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs @@ -19,12 +19,14 @@ [DiagnosticAnalyzer(LanguageNames.CSharp)] internal sealed class UseLangwordSuppressor : NetEvolveSuppressorBase { + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) internal static readonly SuppressionDescriptor Suppression = new( id: DiagnosticIds.NES0001, suppressedDiagnosticId: "MA0154", justification: "NE0007 already reports this / keyword usage; suppressed to avoid a duplicate " + "diagnostic from Meziantou.Analyzer." ); + // Stryker restore all /// public override ImmutableArray SupportedSuppressions { get; } = diff --git a/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs b/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs index 494d440..6a11102 100644 --- a/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs +++ b/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs @@ -19,6 +19,7 @@ internal static class CSharpKeywords /// denotes the absence of a return type rather than an actual type, and is commonly referenced the /// same way as //. /// + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) public static readonly ImmutableHashSet ReservedKeywords = ImmutableHashSet.Create( StringComparer.Ordinal, "abstract", @@ -84,6 +85,7 @@ internal static class CSharpKeywords "volatile", "while" ); + // Stryker restore all /// /// Keywords that carry special meaning only in specific syntactic contexts (query clauses, patterns, @@ -91,6 +93,7 @@ internal static class CSharpKeywords /// (, ) are intentionally excluded for the same reason as in /// . /// + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) public static readonly ImmutableHashSet ContextualKeywords = ImmutableHashSet.Create( StringComparer.Ordinal, "add", @@ -135,6 +138,7 @@ internal static class CSharpKeywords "with", "yield" ); + // Stryker restore all /// /// Native/predefined C# type names, deliberately excluded from and @@ -142,6 +146,7 @@ internal static class CSharpKeywords /// absence of a return type rather than an actual type, and stays classified under /// . /// + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) public static readonly ImmutableHashSet NativeTypeKeywords = ImmutableHashSet.Create( StringComparer.Ordinal, "bool", @@ -163,6 +168,7 @@ internal static class CSharpKeywords "nint", "nuint" ); + // Stryker restore all /// /// Common BCL value types that, like , are frequently misdocumented as @@ -172,6 +178,7 @@ internal static class CSharpKeywords /// see — since they only exist on the target frameworks /// that ship them. /// + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) public static readonly ImmutableHashSet WellKnownBclTypeNames = ImmutableHashSet.Create( StringComparer.Ordinal, "Guid", @@ -179,6 +186,7 @@ internal static class CSharpKeywords "DateTimeOffset", "TimeSpan" ); + // Stryker restore all /// /// BCL value types that, unlike , are not available on every target @@ -190,8 +198,10 @@ internal static class CSharpKeywords /// unconditionally. Different entries are free to name different minimum versions — nothing here assumes /// they all shipped together. /// + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) public static readonly ImmutableDictionary ConditionalBclTypeMinimumVersions = ImmutableDictionary .Create(StringComparer.Ordinal) .Add("DateOnly", 6) .Add("TimeOnly", 6); + // Stryker restore all } diff --git a/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs b/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs index 5fbd1ca..5b2a1d4 100644 --- a/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs +++ b/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs @@ -174,6 +174,7 @@ LocalFunctionStatementSyntax localFunction return HasUsableCancellationToken(semanticModel, localFunction.Body, localFunction.ExpressionBody?.Expression); } + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) private static readonly string[] SupportedAsyncReturnTypeMetadataNames = { "System.Threading.Tasks.Task", @@ -182,6 +183,7 @@ LocalFunctionStatementSyntax localFunction "System.Threading.Tasks.ValueTask`1", "System.Collections.Generic.IAsyncEnumerable`1", }; + // Stryker restore all private static bool IsSupportedAsyncReturnType(ITypeSymbol returnType) { diff --git a/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs b/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs index ba3bf80..d051cdc 100644 --- a/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs +++ b/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs @@ -17,11 +17,13 @@ internal static class TargetFrameworkMonikers // — ".NETFramework" ("net472", "net48"), ".NETStandard" ("netstandard2.0", "netstandard2.1"), and // ".NETCoreApp" ("netcoreapp3.1") — never match: each has a non-digit character glued right after "net", // where this pattern requires a digit. + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) private static readonly Regex UnifiedTfmPattern = new( @"^net(?\d+)\.\d+", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1) ); + // Stryker restore all /// /// Splits a comma-separated target framework list — BuildProperty.TargetFrameworks's re-joined diff --git a/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs b/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs index 8c8d8f3..e8cdcce 100644 --- a/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs +++ b/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs @@ -225,6 +225,7 @@ private static bool IsDeferredScope(SyntaxNode node) => // Exact-type match, not interface implementation: a subclass overriding 'ClearItems()' also implements // 'ICollection' but its 'Clear()' can carry side effects this rule must not delete. + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) private static readonly string[] AllowedCollectionMetadataNames = new[] { "System.Collections.ArrayList", @@ -239,6 +240,7 @@ private static bool IsDeferredScope(SyntaxNode node) => "System.Collections.ObjectModel.Collection`1", "System.Collections.ObjectModel.ObservableCollection`1", }; + // Stryker restore all private static bool IsAllowedCollectionType(ITypeSymbol type, Compilation compilation) => AllowedCollectionMetadataNames diff --git a/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs b/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs index 966198b..6f3baba 100644 --- a/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs +++ b/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs @@ -22,12 +22,14 @@ namespace NetEvolve.Analyzer.Style; [DiagnosticAnalyzer(LanguageNames.CSharp)] internal sealed class RequireNumericLiteralSuffixSuppressor : NetEvolveSuppressorBase { + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) internal static readonly SuppressionDescriptor Suppression = new( id: DiagnosticIds.NES0002, suppressedDiagnosticId: "S818", justification: "NE0012 already reports this numeric literal; suppressed to avoid a duplicate, " + "conflicting diagnostic from SonarAnalyzer.CSharp." ); + // Stryker restore all /// public override ImmutableArray SupportedSuppressions { get; } = diff --git a/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs b/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs index 473ad19..f96ef3d 100644 --- a/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs +++ b/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs @@ -311,6 +311,7 @@ CancellationToken cancellationToken // Span/ReadOnlySpan. Array types are handled separately (any element type, any rank). This is a // pragmatic subset, not the full collection-expression conversion spec (custom [CollectionBuilder] types are // not recognized), but covers what a guard-clause return is realistically going to need. + // Stryker disable all : static initializer, not killable under the MTP runner (Stryker 5.0.0) private static readonly ImmutableHashSet<(string Namespace, string Name)> CollectionExpressionTargetTypes = ImmutableHashSet.Create( ("System.Collections.Generic", "List"), @@ -337,6 +338,7 @@ CancellationToken cancellationToken ("System", "Span"), ("System", "ReadOnlySpan") ); + // Stryker restore all // 'yield break;' is required (and the only legal early-exit) in an iterator method — 'return value;' does // not compile there at all, regardless of return type. 'return;' is legal for a void-returning method, and From 785b989f4dc6a4e71fd707dcc79e86676a74a577 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20St=C3=BChmer?= Date: Thu, 29 Oct 2026 05:01:48 +0100 Subject: [PATCH 2/2] style: format Stryker pragma placement with csharpier --- src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs | 1 + src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs | 4 ++++ src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs | 1 + src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs | 1 + .../Maintainability/AvoidRedundantCollectionClearAnalyzer.cs | 1 + .../Style/RequireNumericLiteralSuffixSuppressor.cs | 1 + .../Usage/RequireCancellationCheckCodeFixProvider.cs | 1 + 7 files changed, 10 insertions(+) diff --git a/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs b/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs index a72a21e..2cdc71f 100644 --- a/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs +++ b/src/NetEvolve.Analyzer/Documentation/UseLangwordSuppressor.cs @@ -26,6 +26,7 @@ internal sealed class UseLangwordSuppressor : NetEvolveSuppressorBase justification: "NE0007 already reports this / keyword usage; suppressed to avoid a duplicate " + "diagnostic from Meziantou.Analyzer." ); + // Stryker restore all /// diff --git a/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs b/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs index 6a11102..a90491d 100644 --- a/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs +++ b/src/NetEvolve.Analyzer/Helpers/CSharpKeywords.cs @@ -85,6 +85,7 @@ internal static class CSharpKeywords "volatile", "while" ); + // Stryker restore all /// @@ -138,6 +139,7 @@ internal static class CSharpKeywords "with", "yield" ); + // Stryker restore all /// @@ -168,6 +170,7 @@ internal static class CSharpKeywords "nint", "nuint" ); + // Stryker restore all /// @@ -186,6 +189,7 @@ internal static class CSharpKeywords "DateTimeOffset", "TimeSpan" ); + // Stryker restore all /// diff --git a/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs b/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs index 5b2a1d4..5d03ad5 100644 --- a/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs +++ b/src/NetEvolve.Analyzer/Helpers/CancellationTokenCallSites.cs @@ -183,6 +183,7 @@ LocalFunctionStatementSyntax localFunction "System.Threading.Tasks.ValueTask`1", "System.Collections.Generic.IAsyncEnumerable`1", }; + // Stryker restore all private static bool IsSupportedAsyncReturnType(ITypeSymbol returnType) diff --git a/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs b/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs index d051cdc..d9a8e13 100644 --- a/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs +++ b/src/NetEvolve.Analyzer/Helpers/TargetFrameworkMonikers.cs @@ -23,6 +23,7 @@ internal static class TargetFrameworkMonikers RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1) ); + // Stryker restore all /// diff --git a/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs b/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs index e8cdcce..dbc354f 100644 --- a/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs +++ b/src/NetEvolve.Analyzer/Maintainability/AvoidRedundantCollectionClearAnalyzer.cs @@ -240,6 +240,7 @@ private static bool IsDeferredScope(SyntaxNode node) => "System.Collections.ObjectModel.Collection`1", "System.Collections.ObjectModel.ObservableCollection`1", }; + // Stryker restore all private static bool IsAllowedCollectionType(ITypeSymbol type, Compilation compilation) => diff --git a/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs b/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs index 6f3baba..e2c349e 100644 --- a/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs +++ b/src/NetEvolve.Analyzer/Style/RequireNumericLiteralSuffixSuppressor.cs @@ -29,6 +29,7 @@ internal sealed class RequireNumericLiteralSuffixSuppressor : NetEvolveSuppresso justification: "NE0012 already reports this numeric literal; suppressed to avoid a duplicate, " + "conflicting diagnostic from SonarAnalyzer.CSharp." ); + // Stryker restore all /// diff --git a/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs b/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs index f96ef3d..eef233e 100644 --- a/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs +++ b/src/NetEvolve.Analyzer/Usage/RequireCancellationCheckCodeFixProvider.cs @@ -338,6 +338,7 @@ CancellationToken cancellationToken ("System", "Span"), ("System", "ReadOnlySpan") ); + // Stryker restore all // 'yield break;' is required (and the only legal early-exit) in an iterator method — 'return value;' does