Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|⚠️|❌|❌|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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|<span title='Info'>ℹ️</span>|✔️|❌|
|[MA0189](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0189.md)|Design|Use InlineArray instead of fixed-size buffers|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0190](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0190.md)|Design|Use partial property instead of partial method for GeneratedRegex|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0191](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0191.md)|Design|Do not use the null-forgiving operator|<span title='Warning'>⚠️</span>|❌|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```
64 changes: 64 additions & 0 deletions docs/Rules/MA0191.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# MA0191 - Do not use the null-forgiving operator
<!-- sources -->
Source: [DoNotUseNullForgivenessAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotUseNullForgivenessAnalyzer.cs)
<!-- sources -->

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
````
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
43 changes: 43 additions & 0 deletions src/Meziantou.Analyzer/Rules/DoNotUseNullForgivenessAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -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<DiagnosticDescriptor> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<DoNotUseNullForgivenessAnalyzer>();
}

[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();
}
}
Loading