From e81c905cffb76774810759797fecdc5b8ff65cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Wed, 3 Sep 2025 13:57:49 -0400 Subject: [PATCH 1/4] Suggest using LazyInitializer instead of CompareExchange --- .../Internals/OperationExtensions.cs | 1 + src/Meziantou.Analyzer/RuleIdentifiers.cs | 1 + ...LazyInitializerEnsureInitializeAnalyzer.cs | 50 +++++++++++++ ...nitializerEnsureInitializeAnalyzerTests.cs | 75 +++++++++++++++++++ 4 files changed, 127 insertions(+) create mode 100644 src/Meziantou.Analyzer/Rules/UseLazyInitializerEnsureInitializeAnalyzer.cs create mode 100644 tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs 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..89ac19bd --- /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 From 0d398fbd5ee49ff47d53f39e7a26d3c66c568bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Wed, 3 Sep 2025 13:59:05 -0400 Subject: [PATCH 2/4] Add doc --- README.md | 1 + docs/README.md | 7 +++++++ docs/Rules/MA0173.md | 6 ++++++ .../configuration/default.editorconfig | 3 +++ .../configuration/none.editorconfig | 3 +++ 5 files changed, 20 insertions(+) create mode 100644 docs/Rules/MA0173.md 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..ceea6abf --- /dev/null +++ b/docs/Rules/MA0173.md @@ -0,0 +1,6 @@ +# 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 From f7631063ce02fe2b585070bbdd98065788294c24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Wed, 3 Sep 2025 14:06:23 -0400 Subject: [PATCH 3/4] Add documentation --- docs/Rules/MA0173.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Rules/MA0173.md b/docs/Rules/MA0173.md index ceea6abf..e305ac10 100644 --- a/docs/Rules/MA0173.md +++ b/docs/Rules/MA0173.md @@ -4,3 +4,4 @@ Interlocked.CompareExchange(ref _field, new Sample(), null); // non-compliant LazyInitializer.EnsureInitialized(ref _field, () => new Sample()); // compliant ``` + From 45e4c39c20269de9a6d4896adcb9f0a43d557f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Wed, 3 Sep 2025 14:07:07 -0400 Subject: [PATCH 4/4] wip --- .../UseLazyInitializerEnsureInitializeAnalyzerTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs index 89ac19bd..6756785c 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseLazyInitializerEnsureInitializeAnalyzerTests.cs @@ -33,7 +33,7 @@ await CreateProjectBuilder() .WithSourceCode(""" Sample a = default; [|System.Threading.Interlocked.CompareExchange(ref a, new Sample(), null)|]; - class Sample; + class Sample { }; """) .ValidateAsync(); } @@ -45,7 +45,7 @@ await CreateProjectBuilder() .WithSourceCode(""" Sample a = default; [|System.Threading.Interlocked.CompareExchange(ref a, new Sample(), default)|]; - class Sample; + class Sample { }; """) .ValidateAsync(); } @@ -57,7 +57,7 @@ await CreateProjectBuilder() .WithSourceCode(""" Sample a = default; System.Threading.Interlocked.CompareExchange(ref a, new Sample(), default); - struct Sample; + struct Sample { }; """) .ValidateAsync(); }