diff --git a/README.md b/README.md
index 926b0169..3b151917 100755
--- a/README.md
+++ b/README.md
@@ -188,6 +188,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0170](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0170.md)|Design|Type cannot be used as an attribute argument|⚠️|❌|❌|
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️|
|[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌|
+|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|❌|
diff --git a/docs/README.md b/docs/README.md
index 2edf082c..df614d90 100755
--- a/docs/README.md
+++ b/docs/README.md
@@ -172,6 +172,7 @@
|[MA0170](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0170.md)|Design|Type cannot be used as an attribute argument|⚠️|❌|❌|
|[MA0171](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0171.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️|
|[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌|
+|[MA0173](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0173.md)|Design|Use LazyInitializer.EnsureInitialize|ℹ️|✔️|❌|
|Id|Suppressed rule|Justification|
|--|---------------|-------------|
@@ -696,6 +697,9 @@ dotnet_diagnostic.MA0171.severity = none
# MA0172: Both sides of the logical operation are identical
dotnet_diagnostic.MA0172.severity = none
+
+# MA0173: Use LazyInitializer.EnsureInitialize
+dotnet_diagnostic.MA0173.severity = suggestion
```
# .editorconfig - all rules disabled
@@ -1213,4 +1217,7 @@ dotnet_diagnostic.MA0171.severity = none
# MA0172: Both sides of the logical operation are identical
dotnet_diagnostic.MA0172.severity = none
+
+# MA0173: Use LazyInitializer.EnsureInitialize
+dotnet_diagnostic.MA0173.severity = none
```
diff --git a/docs/Rules/MA0173.md b/docs/Rules/MA0173.md
new file mode 100644
index 00000000..e305ac10
--- /dev/null
+++ b/docs/Rules/MA0173.md
@@ -0,0 +1,7 @@
+# MA0173 - Use LazyInitializer.EnsureInitialize
+
+```c#
+Interlocked.CompareExchange(ref _field, new Sample(), null); // non-compliant
+LazyInitializer.EnsureInitialized(ref _field, () => new Sample()); // compliant
+```
+
diff --git a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
index bafccfa2..5cad7507 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/default.editorconfig
@@ -514,3 +514,6 @@ dotnet_diagnostic.MA0171.severity = none
# MA0172: Both sides of the logical operation are identical
dotnet_diagnostic.MA0172.severity = none
+
+# MA0173: Use LazyInitializer.EnsureInitialize
+dotnet_diagnostic.MA0173.severity = suggestion
diff --git a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
index 6dedb33a..1770f021 100644
--- a/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
+++ b/src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
@@ -514,3 +514,6 @@ dotnet_diagnostic.MA0171.severity = none
# MA0172: Both sides of the logical operation are identical
dotnet_diagnostic.MA0172.severity = none
+
+# MA0173: Use LazyInitializer.EnsureInitialize
+dotnet_diagnostic.MA0173.severity = none
diff --git a/src/Meziantou.Analyzer/Internals/OperationExtensions.cs b/src/Meziantou.Analyzer/Internals/OperationExtensions.cs
index f03bfcc0..b877367c 100644
--- a/src/Meziantou.Analyzer/Internals/OperationExtensions.cs
+++ b/src/Meziantou.Analyzer/Internals/OperationExtensions.cs
@@ -283,4 +283,5 @@ static bool IsValid(Location location, int operationLocation, int? staticContext
}
public static bool IsConstantZero(this IOperation operation) => operation is { ConstantValue: { HasValue: true, Value: 0 or 0L or 0u or 0uL or 0f or 0d or 0m } };
+ public static bool IsNull(this IOperation operation) => operation is { ConstantValue: { HasValue: true, Value: null } };
}
diff --git a/src/Meziantou.Analyzer/RuleIdentifiers.cs b/src/Meziantou.Analyzer/RuleIdentifiers.cs
index 509e8a70..56b55de8 100755
--- a/src/Meziantou.Analyzer/RuleIdentifiers.cs
+++ b/src/Meziantou.Analyzer/RuleIdentifiers.cs
@@ -175,6 +175,7 @@ internal static class RuleIdentifiers
public const string TypeCannotBeUsedInAnAttributeParameter = "MA0170";
public const string UsePatternMatchingInsteadOfHasvalue = "MA0171";
public const string BothSideOfTheConditionAreIdentical = "MA0172";
+ public const string UseLazyInitializerEnsureInitialize = "MA0173";
public static string GetHelpUri(string identifier)
{
diff --git a/src/Meziantou.Analyzer/Rules/UseLazyInitializerEnsureInitializeAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseLazyInitializerEnsureInitializeAnalyzer.cs
new file mode 100644
index 00000000..9533c078
--- /dev/null
+++ b/src/Meziantou.Analyzer/Rules/UseLazyInitializerEnsureInitializeAnalyzer.cs
@@ -0,0 +1,50 @@
+using System.Collections.Immutable;
+using Meziantou.Analyzer.Internals;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+namespace Meziantou.Analyzer.Rules;
+
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public class UseLazyInitializerEnsureInitializeAnalyzer : DiagnosticAnalyzer
+{
+ private static readonly DiagnosticDescriptor Rule = new(
+ RuleIdentifiers.UseLazyInitializerEnsureInitialize,
+ title: "Use LazyInitializer.EnsureInitialize",
+ messageFormat: "Use LazyInitializer.EnsureInitialize",
+ RuleCategories.Design,
+ DiagnosticSeverity.Info,
+ isEnabledByDefault: true,
+ description: "",
+ helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.UseLazyInitializerEnsureInitialize));
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+
+ context.RegisterCompilationStartAction(compilationContext =>
+ {
+ var interlockedType = compilationContext.Compilation.GetBestTypeByMetadataName("System.Threading.Interlocked");
+
+ compilationContext.RegisterOperationAction(context =>
+ {
+ var operation = (IInvocationOperation)context.Operation;
+ var targetMethod = operation.TargetMethod;
+
+ // Interlocked.CompareExchange(ref _instance, new Sample(), null)
+ if (operation.Arguments.Length is 3 && targetMethod.Name is "CompareExchange" && targetMethod.ContainingType.IsEqualTo(interlockedType))
+ {
+ if (operation.Arguments[2].Value.IsNull() && operation.Arguments[1].Value is IObjectCreationOperation)
+ {
+ context.ReportDiagnostic(Rule, operation);
+ }
+ }
+ }, OperationKind.Invocation);
+ });
+ }
+
+}
diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs
new file mode 100644
index 00000000..6756785c
--- /dev/null
+++ b/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs
@@ -0,0 +1,75 @@
+using Meziantou.Analyzer.Rules;
+using Meziantou.Analyzer.Test.Helpers;
+using TestHelper;
+using Xunit;
+
+namespace Meziantou.Analyzer.Test.Rules;
+
+public sealed class UseLazyInitializerEnsureInitializeAnalyzerTests
+{
+ private static ProjectBuilder CreateProjectBuilder()
+ {
+ return new ProjectBuilder()
+ .WithAnalyzer()
+ .WithTargetFramework(TargetFramework.NetLatest)
+ .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication);
+ }
+
+ [Fact]
+ public async Task NewObject_Null()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ object a = default;
+ [|System.Threading.Interlocked.CompareExchange(ref a, new object(), null)|];
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task NewCustomClass_Null()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ Sample a = default;
+ [|System.Threading.Interlocked.CompareExchange(ref a, new Sample(), null)|];
+ class Sample { };
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task NewCustomClass_Default()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ Sample a = default;
+ [|System.Threading.Interlocked.CompareExchange(ref a, new Sample(), default)|];
+ class Sample { };
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task NewCustomStruct()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ Sample a = default;
+ System.Threading.Interlocked.CompareExchange(ref a, new Sample(), default);
+ struct Sample { };
+ """)
+ .ValidateAsync();
+ }
+
+ [Fact]
+ public async Task NewInt32_Zero()
+ {
+ await CreateProjectBuilder()
+ .WithSourceCode("""
+ int a = default;
+ System.Threading.Interlocked.CompareExchange(ref a, 0, 0);
+ """)
+ .ValidateAsync();
+ }
+}
\ No newline at end of file