From 0eb41fb9177a1eb0ec925f87cf970cb92f743c49 Mon Sep 17 00:00:00 2001 From: el kampu <11272320+elkampu@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:04:10 +0200 Subject: [PATCH 1/2] fix(RCS1249): inspect nullable flow before suppression --- CHANGELOG.md | 1 + ...nnecessaryNullForgivingOperatorAnalyzer.cs | 19 +++ ...49UnnecessaryNullForgivingOperatorTests.cs | 111 ++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9beca678ad..f0b1ba0999 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix analyzer [RCS1249](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1249) to report null-forgiving operators on expressions that are already known to be non-null - [CLI] Reference the compiled output of referenced projects that cannot be loaded into the workspace (for example F# projects), so their types are no longer reported as missing (`CS0103`/`CS0246`) during analysis ([PR](https://github.com/dotnet/roslynator/pull/1833)) ## [5.0.0] - 2026-08-21 diff --git a/src/Analyzers/CSharp/Analysis/UnnecessaryNullForgivingOperatorAnalyzer.cs b/src/Analyzers/CSharp/Analysis/UnnecessaryNullForgivingOperatorAnalyzer.cs index f191b8cae1..edfab35e08 100644 --- a/src/Analyzers/CSharp/Analysis/UnnecessaryNullForgivingOperatorAnalyzer.cs +++ b/src/Analyzers/CSharp/Analysis/UnnecessaryNullForgivingOperatorAnalyzer.cs @@ -64,6 +64,7 @@ private static void AnalyzeSuppressNullableWarningExpression(SyntaxNodeAnalysisC } context.ReportDiagnostic(DiagnosticRules.UnnecessaryNullForgivingOperator, suppressExpression.OperatorToken); + return; } } else if (node.IsKind(SyntaxKind.EqualsValueClause)) @@ -80,7 +81,10 @@ private static void AnalyzeSuppressNullableWarningExpression(SyntaxNodeAnalysisC var property = (PropertyDeclarationSyntax)node.Parent; if (IsNullableReferenceType(context, property.Type)) + { context.ReportDiagnostic(DiagnosticRules.UnnecessaryNullForgivingOperator, node); + return; + } } else if (parent.IsKind(SyntaxKind.VariableDeclarator)) { @@ -99,10 +103,12 @@ private static void AnalyzeSuppressNullableWarningExpression(SyntaxNodeAnalysisC if (parent.Parent.IsParentKind(SyntaxKind.FieldDeclaration)) { context.ReportDiagnostic(DiagnosticRules.UnnecessaryNullForgivingOperator, node); + return; } else { context.ReportDiagnostic(DiagnosticRules.UnnecessaryNullForgivingOperator, suppressExpression.OperatorToken); + return; } } } @@ -110,6 +116,19 @@ private static void AnalyzeSuppressNullableWarningExpression(SyntaxNodeAnalysisC } } + if (IsNotNullWithoutSuppression(context, suppressExpression)) + context.ReportDiagnostic(DiagnosticRules.UnnecessaryNullForgivingOperator, suppressExpression.OperatorToken); + + static bool IsNotNullWithoutSuppression(SyntaxNodeAnalysisContext context, PostfixUnaryExpressionSyntax suppressExpression) + { + TypeInfo typeInfo = context.SemanticModel.GetSpeculativeTypeInfo( + suppressExpression.SpanStart, + suppressExpression.Operand.WithoutTrivia(), + SpeculativeBindingOption.BindAsExpression); + + return typeInfo.Nullability.FlowState == NullableFlowState.NotNull; + } + static bool IsNullableReferenceType(SyntaxNodeAnalysisContext context, TypeSyntax type) { if (!type.IsKind(SyntaxKind.NullableType)) diff --git a/src/Tests/Analyzers.Tests/RCS1249UnnecessaryNullForgivingOperatorTests.cs b/src/Tests/Analyzers.Tests/RCS1249UnnecessaryNullForgivingOperatorTests.cs index f6786a5967..3c27c88ec1 100644 --- a/src/Tests/Analyzers.Tests/RCS1249UnnecessaryNullForgivingOperatorTests.cs +++ b/src/Tests/Analyzers.Tests/RCS1249UnnecessaryNullForgivingOperatorTests.cs @@ -12,6 +12,96 @@ public class RCS1249UnnecessaryNullForgivingOperatorTests : AbstractCSharpDiagno { public override DiagnosticDescriptor Descriptor { get; } = DiagnosticRules.UnnecessaryNullForgivingOperator; + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryNullForgivingOperator)] + public async Task Test_NonNullExpressions() + { + await VerifyDiagnosticAndFixAsync(""" +#nullable enable + +class C +{ + private readonly object _value = new object(); + + private object P => new object(); + + private object GetValue() => new object(); + + object M() + { + object value = "value"[|!|]; + value = new()[|!|]; + value = _value[|!|]; + value = P[|!|]; + value = GetValue()[|!|]; + return value; + } +} +""", """ +#nullable enable + +class C +{ + private readonly object _value = new object(); + + private object P => new object(); + + private object GetValue() => new object(); + + object M() + { + object value = "value"; + value = new(); + value = _value; + value = P; + value = GetValue(); + return value; + } +} +"""); + } + + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryNullForgivingOperator)] + public async Task Test_FlowStateNotNull() + { + await VerifyDiagnosticAndFixAsync(@" +#nullable enable + +using System.Collections.Generic; + +class C +{ + object M(Stack stack) + { + object? value; + + while (!stack.TryPop(out value)) + { + } + + return value[|!|]; + } +} +", @" +#nullable enable + +using System.Collections.Generic; + +class C +{ + object M(Stack stack) + { + object? value; + + while (!stack.TryPop(out value)) + { + } + + return value; + } +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryNullForgivingOperator)] public async Task Test_Property() { @@ -166,6 +256,27 @@ void M(string p) "); } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryNullForgivingOperator)] + public async Task TestNoDiagnostic_MaybeNullExpression() + { + await VerifyNoDiagnosticAsync(@" +#nullable enable + +class C +{ + object M(object? value) + { + return value!; + } + + object M2() + { + return default(object)!; + } +} +"); + } + [Fact, Trait(Traits.Analyzer, DiagnosticIdentifiers.UnnecessaryNullForgivingOperator)] public async Task TestNoDiagnostic_MaybeNullWhenAttribute() { From 1d2b0412a9c105c92ad00051f5b903b936530083 Mon Sep 17 00:00:00 2001 From: el kampu <11272320+elkampu@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:04:56 +0200 Subject: [PATCH 2/2] docs: link RCS1249 changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0b1ba0999..49655c52cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Fix analyzer [RCS1249](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1249) to report null-forgiving operators on expressions that are already known to be non-null +- Fix analyzer [RCS1249](https://josefpihrt.github.io/docs/roslynator/analyzers/RCS1249) to report null-forgiving operators on expressions that are already known to be non-null ([PR](https://github.com/dotnet/roslynator/pull/1834)) - [CLI] Reference the compiled output of referenced projects that cannot be loaded into the workspace (for example F# projects), so their types are no longer reported as missing (`CS0103`/`CS0246`) during analysis ([PR](https://github.com/dotnet/roslynator/pull/1833)) ## [5.0.0] - 2026-08-21