diff --git a/README.md b/README.md index 8450848e0..077c58fba 100755 --- a/README.md +++ b/README.md @@ -205,6 +205,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0188](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0188.md)|Design|Use System.TimeProvider instead of a custom time abstraction|ℹ️|✔️|❌| |[MA0189](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0189.md)|Design|Use InlineArray instead of fixed-size buffers|ℹ️|✔️|✔️| |[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|ℹ️|✔️|✔️| +|[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|⚠️|❌|❌| diff --git a/docs/README.md b/docs/README.md index 711afa5e9..08d5ae0c4 100755 --- a/docs/README.md +++ b/docs/README.md @@ -189,6 +189,7 @@ |[MA0188](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0188.md)|Design|Use System.TimeProvider instead of a custom time abstraction|ℹ️|✔️|❌| |[MA0189](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0189.md)|Design|Use InlineArray instead of fixed-size buffers|ℹ️|✔️|✔️| |[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|ℹ️|✔️|✔️| +|[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|⚠️|❌|❌| |Id|Suppressed rule|Justification| |--|---------------|-------------| @@ -771,6 +772,9 @@ dotnet_diagnostic.MA0189.severity = suggestion # MA0190: Use partial property instead of partial method for GeneratedRegex dotnet_diagnostic.MA0190.severity = suggestion + +# MA0191: Do not use the null-forgiving operator +dotnet_diagnostic.MA0191.severity = none ``` # .editorconfig - all rules disabled @@ -1339,4 +1343,7 @@ dotnet_diagnostic.MA0189.severity = none # MA0190: Use partial property instead of partial method for GeneratedRegex dotnet_diagnostic.MA0190.severity = none + +# MA0191: Do not use the null-forgiving operator +dotnet_diagnostic.MA0191.severity = none ``` diff --git a/docs/Rules/MA0191.md b/docs/Rules/MA0191.md new file mode 100644 index 000000000..1f11d5728 --- /dev/null +++ b/docs/Rules/MA0191.md @@ -0,0 +1,64 @@ +# MA0191 - Do not use the null-forgiving operator + +Source: [DoNotUseNullForgivenessAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotUseNullForgivenessAnalyzer.cs) + + +The null-forgiving operator (`!`) suppresses nullable warnings from the compiler. Using `null!` or `default!` to assign or initialize a value is a code smell that hides potential null reference issues by telling the compiler to ignore a null assignment. + +This rule reports usages of the null-forgiving operator where the operand is `null`, `default`, or `default(T)`. + +## Non-compliant code + +````csharp +#nullable enable +class Sample +{ + // Field initialized with null! + HttpClient _httpClient = null!; + + // Property initialized with null! + string TempDir { get; set; } = null!; + + // default! + string _value = default!; + + // default(T)! + string _value2 = default(string)!; +} +```` + +## Compliant code + +````csharp +#nullable enable +class Sample +{ + // Initialized with a real value + HttpClient _httpClient = new HttpClient(); + + // Property with a non-null default + string TempDir { get; set; } = string.Empty; + + // Using ! on non-null/default expressions is allowed + string? _nullable = GetNullable(); + string _value = _nullable!; // allowed – not null! or default! +} +```` + +## When it's necessary + +In some cases the null-forgiving operator is unavoidable, such as when integrating with dependency injection frameworks, deserializers, or model binders that initialize properties after construction. In these situations, suppress the warning with `#pragma warning disable` and include a comment explaining why the null-forgiving operator is justified. + +````csharp +#pragma warning disable MA0191 // The DI container will inject this before use +HttpClient _httpClient = default!; +#pragma warning restore MA0191 +```` + +## Configuration + +This rule is disabled by default. To enable it, add the following to your `.editorconfig` file: + +````editorconfig +dotnet_diagnostic.MA0191.severity = warning +```` diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig index d49f3766c..c1f0232be 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig @@ -565,3 +565,6 @@ dotnet_diagnostic.MA0189.severity = suggestion # MA0190: Use partial property instead of partial method for GeneratedRegex dotnet_diagnostic.MA0190.severity = suggestion + +# MA0191: Do not use the null-forgiving operator +dotnet_diagnostic.MA0191.severity = none diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig index d9307299a..3cc8e7a39 100644 --- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig +++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig @@ -565,3 +565,6 @@ dotnet_diagnostic.MA0189.severity = none # MA0190: Use partial property instead of partial method for GeneratedRegex dotnet_diagnostic.MA0190.severity = none + +# MA0191: Do not use the null-forgiving operator +dotnet_diagnostic.MA0191.severity = none diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs index 08a659b40..7d2445750 100755 --- a/src/Meziantou.Analyzer/RuleIdentifiers.cs +++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs @@ -190,6 +190,7 @@ internal static class RuleIdentifiers public const string UseTimeProviderInsteadOfInterface = "MA0188"; public const string UseInlineArrayInsteadOfFixedBuffer = "MA0189"; public const string UsePartialPropertyInsteadOfPartialMethodForGeneratedRegex = "MA0190"; + public const string DoNotUseNullForgiveness = "MA0191"; public static string GetHelpUri(string identifier) { diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseNullForgivenessAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseNullForgivenessAnalyzer.cs new file mode 100644 index 000000000..b1d9a6814 --- /dev/null +++ b/src/Meziantou.Analyzer/Rules/DoNotUseNullForgivenessAnalyzer.cs @@ -0,0 +1,43 @@ +using System.Collections.Immutable; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Meziantou.Analyzer.Rules; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class DoNotUseNullForgivenessAnalyzer : DiagnosticAnalyzer +{ + private static readonly DiagnosticDescriptor Rule = new( + RuleIdentifiers.DoNotUseNullForgiveness, + title: "Do not use the null-forgiving operator", + messageFormat: "Do not use the null-forgiving operator", + RuleCategories.Design, + DiagnosticSeverity.Warning, + isEnabledByDefault: false, + description: "", + helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotUseNullForgiveness)); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxNodeAction(AnalyzeSyntax, SyntaxKind.SuppressNullableWarningExpression); + } + + private static void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var node = (PostfixUnaryExpressionSyntax)context.Node; + if (!node.Operand.IsKind(SyntaxKind.NullLiteralExpression) && + !node.Operand.IsKind(SyntaxKind.DefaultLiteralExpression) && + !node.Operand.IsKind(SyntaxKind.DefaultExpression)) + return; + + context.ReportDiagnostic(Rule, node); + } +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseNullForgivenessAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseNullForgivenessAnalyzerTests.cs new file mode 100644 index 000000000..21690731c --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseNullForgivenessAnalyzerTests.cs @@ -0,0 +1,124 @@ +using Meziantou.Analyzer.Rules; +using TestHelper; +using Xunit; + +namespace Meziantou.Analyzer.Test.Rules; + +public sealed class DoNotUseNullForgivenessAnalyzerTests +{ + private static ProjectBuilder CreateProjectBuilder() + { + return new ProjectBuilder() + .WithTargetFramework(Helpers.TargetFramework.Net9_0) + .WithAnalyzer(); + } + + [Fact] + public async Task NullForgiveness_NullLiteral_ReportsDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Sample + { + string _field = [|null!|]; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NullForgiveness_DefaultLiteral_ReportsDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Sample + { + string _field = [|default!|]; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NullForgiveness_DefaultExpression_ReportsDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Sample + { + string _field = [|default(string)!|]; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NullForgiveness_Property_ReportsDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Sample + { + string Prop { get; set; } = [|null!|]; + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NullForgiveness_VariableAssignment_ReportsDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Sample + { + void M() + { + string s = [|null!|]; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NullForgiveness_MemberAccess_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Model + { + public string? Value { get; set; } + } + class Sample + { + void M(Model model) + { + _ = model.Value!.Length; + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task NoNullForgiveness_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + #nullable enable + class Sample + { + string _field = "value"; + string Prop { get; set; } = "value"; + } + """) + .ValidateAsync(); + } +}