diff --git a/src/NetEvolve.Arguments.Analyser/ThrowIfCountCodeFixProvider.cs b/src/NetEvolve.Arguments.Analyser/ThrowIfCountCodeFixProvider.cs index 1df1ffa9..3b27c2f5 100644 --- a/src/NetEvolve.Arguments.Analyser/ThrowIfCountCodeFixProvider.cs +++ b/src/NetEvolve.Arguments.Analyser/ThrowIfCountCodeFixProvider.cs @@ -37,10 +37,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics[0]; var ifStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + if (ifStatement is null) + { + return; + } + if ( - ifStatement is null - || !ThrowIfCountAnalyzer.TryGetCountComparison(ifStatement.Condition, out var comparison) - || comparison is null + !ThrowIfCountAnalyzer.TryGetCountComparison(ifStatement.Condition, out var comparison) || comparison is null ) { return; diff --git a/src/NetEvolve.Arguments.Analyser/ThrowIfLengthCodeFixProvider.cs b/src/NetEvolve.Arguments.Analyser/ThrowIfLengthCodeFixProvider.cs index bdb0a4e0..8e393d72 100644 --- a/src/NetEvolve.Arguments.Analyser/ThrowIfLengthCodeFixProvider.cs +++ b/src/NetEvolve.Arguments.Analyser/ThrowIfLengthCodeFixProvider.cs @@ -37,9 +37,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics[0]; var ifStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + if (ifStatement is null) + { + return; + } + if ( - ifStatement is null - || !ThrowIfLengthAnalyzer.TryGetLengthComparison(ifStatement.Condition, out var comparison) + !ThrowIfLengthAnalyzer.TryGetLengthComparison(ifStatement.Condition, out var comparison) || comparison is null ) { diff --git a/src/NetEvolve.Arguments.Analyser/ThrowIfNullOrEmptyCodeFixProvider.cs b/src/NetEvolve.Arguments.Analyser/ThrowIfNullOrEmptyCodeFixProvider.cs index 4518e947..81398b70 100644 --- a/src/NetEvolve.Arguments.Analyser/ThrowIfNullOrEmptyCodeFixProvider.cs +++ b/src/NetEvolve.Arguments.Analyser/ThrowIfNullOrEmptyCodeFixProvider.cs @@ -36,9 +36,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics[0]; var ifStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + if (ifStatement is null) + { + return; + } + if ( - ifStatement is null - || !ThrowIfNullOrEmptyAnalyzer.TryGetStringCheck(ifStatement.Condition, out _, out var helperName) + !ThrowIfNullOrEmptyAnalyzer.TryGetStringCheck(ifStatement.Condition, out _, out var helperName) || helperName is null ) { diff --git a/src/NetEvolve.Arguments.Analyser/ThrowIfOutOfRangeCodeFixProvider.cs b/src/NetEvolve.Arguments.Analyser/ThrowIfOutOfRangeCodeFixProvider.cs index 140c7321..76e5311d 100644 --- a/src/NetEvolve.Arguments.Analyser/ThrowIfOutOfRangeCodeFixProvider.cs +++ b/src/NetEvolve.Arguments.Analyser/ThrowIfOutOfRangeCodeFixProvider.cs @@ -37,10 +37,13 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var diagnostic = context.Diagnostics[0]; var ifStatement = root.FindNode(diagnostic.Location.SourceSpan).FirstAncestorOrSelf(); + if (ifStatement is null) + { + return; + } + if ( - ifStatement is null - || !ThrowIfOutOfRangeAnalyzer.TryGetComparison(ifStatement.Condition, out var comparison) - || comparison is null + !ThrowIfOutOfRangeAnalyzer.TryGetComparison(ifStatement.Condition, out var comparison) || comparison is null ) { return; diff --git a/tests/NetEvolve.Arguments.Analyser.Tests.Unit/AnalyzerVerifier.cs b/tests/NetEvolve.Arguments.Analyser.Tests.Unit/AnalyzerVerifier.cs index 4a39a436..239b7ce6 100644 --- a/tests/NetEvolve.Arguments.Analyser.Tests.Unit/AnalyzerVerifier.cs +++ b/tests/NetEvolve.Arguments.Analyser.Tests.Unit/AnalyzerVerifier.cs @@ -52,6 +52,44 @@ public static Task ApplyFixAllAsync( CancellationToken cancellationToken = default ) => ApplyFixAllCoreAsync(analyzer, codeFix, source, useLegacyReferences, cancellationToken: cancellationToken); + /// + /// Raises a synthetic diagnostic at the first occurrence of and returns how many + /// code actions the provider registers for it (used to cover the defensive "pattern no longer matches" exits). + /// + public static async Task CountRegisteredFixesAsync( + CodeFixProvider codeFix, + string source, + string spanText, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + using var workspace = new AdhocWorkspace(); + var project = workspace + .AddProject("TestProject", LanguageNames.CSharp) + .WithMetadataReferences(ModernReferences); + var document = workspace.AddDocument(project.Id, "Test.cs", SourceText.From(source)); + var tree = (await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false))!; + + var descriptor = new DiagnosticDescriptor( + codeFix.FixableDiagnosticIds[0], + "t", + "t", + "t", + DiagnosticSeverity.Warning, + true + ); + var start = source.IndexOf(spanText, StringComparison.Ordinal); + var diagnostic = Diagnostic.Create(descriptor, Location.Create(tree, new TextSpan(start, spanText.Length))); + + var count = 0; + var fixContext = new CodeFixContext(document, diagnostic, (_, _) => count++, cancellationToken); + await codeFix.RegisterCodeFixesAsync(fixContext).ConfigureAwait(false); + + return count; + } + private static async Task> GetDiagnosticsCoreAsync( DiagnosticAnalyzer analyzer, string source, diff --git a/tests/NetEvolve.Arguments.Analyser.Tests.Unit/CodeFixProviderDefensiveTests.cs b/tests/NetEvolve.Arguments.Analyser.Tests.Unit/CodeFixProviderDefensiveTests.cs new file mode 100644 index 00000000..776c6303 --- /dev/null +++ b/tests/NetEvolve.Arguments.Analyser.Tests.Unit/CodeFixProviderDefensiveTests.cs @@ -0,0 +1,60 @@ +namespace NetEvolve.Arguments.Analyser.Tests.Unit; + +using System.Threading; +using Microsoft.CodeAnalysis.CodeFixes; + +public sealed class CodeFixProviderDefensiveTests +{ + private const string Source = """ + class C + { + void M(bool argument) + { + if (argument) { } + } + } + """; + + [Test] + [Arguments(typeof(ThrowIfCountCodeFixProvider))] + [Arguments(typeof(ThrowIfLengthCodeFixProvider))] + [Arguments(typeof(ThrowIfNullOrEmptyCodeFixProvider))] + [Arguments(typeof(ThrowIfOutOfRangeCodeFixProvider))] + public async Task RegisterCodeFixes_WhenDiagnosticIsOutsideIfStatement_RegistersNothing( + Type providerType, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + var provider = (CodeFixProvider)Activator.CreateInstance(providerType)!; + + var count = await AnalyzerVerifier.CountRegisteredFixesAsync(provider, Source, "void M", cancellationToken); + + _ = await Assert.That(count).IsEqualTo(0); + } + + [Test] + [Arguments(typeof(ThrowIfCountCodeFixProvider))] + [Arguments(typeof(ThrowIfLengthCodeFixProvider))] + [Arguments(typeof(ThrowIfNullOrEmptyCodeFixProvider))] + [Arguments(typeof(ThrowIfOutOfRangeCodeFixProvider))] + public async Task RegisterCodeFixes_WhenIfConditionDoesNotMatchPattern_RegistersNothing( + Type providerType, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + + var provider = (CodeFixProvider)Activator.CreateInstance(providerType)!; + + var count = await AnalyzerVerifier.CountRegisteredFixesAsync( + provider, + Source, + "if (argument)", + cancellationToken + ); + + _ = await Assert.That(count).IsEqualTo(0); + } +}