From 16d220686943389d27db588453b3dd21c812392a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 20:17:50 -0400 Subject: [PATCH 1/3] Add code fixers for selected MA rules Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 16 +-- docs/README.md | 16 +-- docs/Rules/MA0144.md | 2 +- docs/Rules/MA0146.md | 2 +- docs/Rules/MA0152.md | 2 +- docs/Rules/MA0156.md | 2 +- docs/Rules/MA0157.md | 2 +- docs/Rules/MA0158.md | 2 +- docs/Rules/MA0160.md | 2 +- docs/Rules/MA0169.md | 2 +- ...waitableTypeMustHaveTheAsyncSuffixFixer.cs | 6 +- ...UseContainsKeyInsteadOfTryGetValueFixer.cs | 70 +++++++++ .../UseEqualsMethodInsteadOfOperatorFixer.cs | 86 +++++++++++ ...gSystemInsteadOfRuntimeInformationFixer.cs | 74 ++++++++++ ...SystemThreadingLockInsteadOfObjectFixer.cs | 80 +++++++++++ .../Rules/UseTaskUnwrapFixer.cs | 98 +++++++++++++ ...lidateUnsafeAccessorAttributeUsageFixer.cs | 136 ++++++++++++++++++ ...TypeMustHaveTheAsyncSuffixAnalyzerTests.cs | 38 +++++ ...insKeyInsteadOfTryGetValueAnalyzerTests.cs | 12 +- ...alsMethodInsteadOfOperatorAnalyzerTests.cs | 25 +++- ...nsteadOfRuntimeInformationAnalyzerTests.cs | 17 +++ ...readingLockInsteadOfObjectAnalyzerTests.cs | 27 +++- .../Rules/UseTaskUnwrapAnalyzerTests.cs | 13 ++ ...safeAccessorAttributeUsageAnalyzerTests.cs | 15 +- 24 files changed, 715 insertions(+), 30 deletions(-) create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs create mode 100644 src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs diff --git a/README.md b/README.md index c99d1a52b..ba6ee06c4 100755 --- a/README.md +++ b/README.md @@ -159,23 +159,23 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators for null check|ℹ️|❌|✔️| |[MA0142](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0142.md)|Usage|Use pattern matching instead of equality operators for null check|ℹ️|❌|✔️| |[MA0143](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0143.md)|Design|Primary constructor parameters should be readonly|⚠️|✔️|❌| -|[MA0144](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0144.md)|Performance|Use System.OperatingSystem to check the current OS|⚠️|✔️|❌| +|[MA0144](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0144.md)|Performance|Use System.OperatingSystem to check the current OS|⚠️|✔️|✔️| |[MA0145](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0145.md)|Usage|Signature for \[UnsafeAccessorAttribute\] method is not valid|⚠️|✔️|❌| -|[MA0146](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0146.md)|Usage|Name must be set explicitly on local functions|⚠️|✔️|❌| +|[MA0146](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0146.md)|Usage|Name must be set explicitly on local functions|⚠️|✔️|✔️| |[MA0147](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0147.md)|Usage|Avoid async void method for delegate|⚠️|✔️|❌| |[MA0148](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0148.md)|Usage|Use pattern matching instead of equality operators for discrete value|ℹ️|❌|✔️| |[MA0149](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0149.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️| |[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|✔️|❌| |[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️|❌| -|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️|❌| +|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️|✔️| |[MA0153](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0153.md)|Design|Do not log symbols decorated with DataClassificationAttribute directly|⚠️|✔️|❌| |[MA0154](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0154.md)|Design|Use langword in XML comment|ℹ️|✔️|✔️| |[MA0155](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0155.md)|Design|Do not use async void methods|⚠️|❌|❌| -|[MA0156](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0156.md)|Design|Use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|❌| -|[MA0157](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0157.md)|Design|Do not use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|❌| -|[MA0158](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0158.md)|Performance|Use System.Threading.Lock|⚠️|✔️|❌| +|[MA0156](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0156.md)|Design|Use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|✔️| +|[MA0157](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0157.md)|Design|Do not use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|✔️| +|[MA0158](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0158.md)|Performance|Use System.Threading.Lock|⚠️|✔️|✔️| |[MA0159](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0159.md)|Performance|Use 'Order' instead of 'OrderBy'|ℹ️|✔️|✔️| -|[MA0160](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md)|Performance|Use ContainsKey instead of TryGetValue|ℹ️|✔️|❌| +|[MA0160](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md)|Performance|Use ContainsKey instead of TryGetValue|ℹ️|✔️|✔️| |[MA0161](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0161.md)|Usage|UseShellExecute must be explicitly set|ℹ️|❌|❌| |[MA0162](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0162.md)|Usage|Use Process.Start overload with ProcessStartInfo|ℹ️|❌|❌| |[MA0163](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0163.md)|Usage|UseShellExecute must be false when redirecting standard input or output|⚠️|✔️|❌| @@ -183,7 +183,7 @@ If you are already using other analyzers, you can check [which rules are duplica |[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|ℹ️|✔️|✔️| |[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|ℹ️|❌|❌| |[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|❌|❌| -|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️|❌| +|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️|✔️| |[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 HasValue for Nullable\ check|ℹ️|❌|✔️| |[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌| diff --git a/docs/README.md b/docs/README.md index e84bdac82..bcac686fc 100755 --- a/docs/README.md +++ b/docs/README.md @@ -143,23 +143,23 @@ |[MA0141](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0141.md)|Usage|Use pattern matching instead of inequality operators for null check|ℹ️|❌|✔️| |[MA0142](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0142.md)|Usage|Use pattern matching instead of equality operators for null check|ℹ️|❌|✔️| |[MA0143](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0143.md)|Design|Primary constructor parameters should be readonly|⚠️|✔️|❌| -|[MA0144](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0144.md)|Performance|Use System.OperatingSystem to check the current OS|⚠️|✔️|❌| +|[MA0144](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0144.md)|Performance|Use System.OperatingSystem to check the current OS|⚠️|✔️|✔️| |[MA0145](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0145.md)|Usage|Signature for \[UnsafeAccessorAttribute\] method is not valid|⚠️|✔️|❌| -|[MA0146](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0146.md)|Usage|Name must be set explicitly on local functions|⚠️|✔️|❌| +|[MA0146](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0146.md)|Usage|Name must be set explicitly on local functions|⚠️|✔️|✔️| |[MA0147](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0147.md)|Usage|Avoid async void method for delegate|⚠️|✔️|❌| |[MA0148](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0148.md)|Usage|Use pattern matching instead of equality operators for discrete value|ℹ️|❌|✔️| |[MA0149](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0149.md)|Usage|Use pattern matching instead of inequality operators for discrete value|ℹ️|❌|✔️| |[MA0150](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0150.md)|Design|Do not call the default object.ToString explicitly|⚠️|✔️|❌| |[MA0151](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0151.md)|Usage|DebuggerDisplay must contain valid members|⚠️|✔️|❌| -|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️|❌| +|[MA0152](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0152.md)|Performance|Use Unwrap instead of using await twice|ℹ️|✔️|✔️| |[MA0153](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0153.md)|Design|Do not log symbols decorated with DataClassificationAttribute directly|⚠️|✔️|❌| |[MA0154](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0154.md)|Design|Use langword in XML comment|ℹ️|✔️|✔️| |[MA0155](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0155.md)|Design|Do not use async void methods|⚠️|❌|❌| -|[MA0156](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0156.md)|Design|Use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|❌| -|[MA0157](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0157.md)|Design|Do not use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|❌| -|[MA0158](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0158.md)|Performance|Use System.Threading.Lock|⚠️|✔️|❌| +|[MA0156](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0156.md)|Design|Use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|✔️| +|[MA0157](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0157.md)|Design|Do not use 'Async' suffix when a method returns IAsyncEnumerable\|⚠️|❌|✔️| +|[MA0158](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0158.md)|Performance|Use System.Threading.Lock|⚠️|✔️|✔️| |[MA0159](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0159.md)|Performance|Use 'Order' instead of 'OrderBy'|ℹ️|✔️|✔️| -|[MA0160](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md)|Performance|Use ContainsKey instead of TryGetValue|ℹ️|✔️|❌| +|[MA0160](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md)|Performance|Use ContainsKey instead of TryGetValue|ℹ️|✔️|✔️| |[MA0161](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0161.md)|Usage|UseShellExecute must be explicitly set|ℹ️|❌|❌| |[MA0162](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0162.md)|Usage|Use Process.Start overload with ProcessStartInfo|ℹ️|❌|❌| |[MA0163](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0163.md)|Usage|UseShellExecute must be false when redirecting standard input or output|⚠️|✔️|❌| @@ -167,7 +167,7 @@ |[MA0166](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0166.md)|Usage|Forward the TimeProvider to methods that take one|ℹ️|✔️|✔️| |[MA0167](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0167.md)|Usage|Use an overload with a TimeProvider argument|ℹ️|❌|❌| |[MA0168](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0168.md)|Performance|Use readonly struct for in or ref readonly parameter|ℹ️|❌|❌| -|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️|❌| +|[MA0169](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0169.md)|Design|Use Equals method instead of operator|⚠️|✔️|✔️| |[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 HasValue for Nullable\ check|ℹ️|❌|✔️| |[MA0172](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0172.md)|Usage|Both sides of the logical operation are identical|⚠️|❌|❌| diff --git a/docs/Rules/MA0144.md b/docs/Rules/MA0144.md index 0e206dced..50f37ed1e 100644 --- a/docs/Rules/MA0144.md +++ b/docs/Rules/MA0144.md @@ -1,6 +1,6 @@ # MA0144 - Use System.OperatingSystem to check the current OS -Source: [UseOperatingSystemInsteadOfRuntimeInformationAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseOperatingSystemInsteadOfRuntimeInformationAnalyzer.cs) +Sources: [UseOperatingSystemInsteadOfRuntimeInformationAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseOperatingSystemInsteadOfRuntimeInformationAnalyzer.cs), [UseOperatingSystemInsteadOfRuntimeInformationFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs) Use `System.OperatingSystem` to check the current OS instead of `RuntimeInformation`. diff --git a/docs/Rules/MA0146.md b/docs/Rules/MA0146.md index f4a8f0b75..c344d3e19 100644 --- a/docs/Rules/MA0146.md +++ b/docs/Rules/MA0146.md @@ -1,6 +1,6 @@ # MA0146 - Name must be set explicitly on local functions -Source: [ValidateUnsafeAccessorAttributeUsageAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ValidateUnsafeAccessorAttributeUsageAnalyzer.cs) +Sources: [ValidateUnsafeAccessorAttributeUsageAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/ValidateUnsafeAccessorAttributeUsageAnalyzer.cs), [ValidateUnsafeAccessorAttributeUsageFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs) Local function names are mangle by the compiler, so the `Name` named constructor parameter is required diff --git a/docs/Rules/MA0152.md b/docs/Rules/MA0152.md index 9d0ebd58d..baac1ccfd 100644 --- a/docs/Rules/MA0152.md +++ b/docs/Rules/MA0152.md @@ -1,6 +1,6 @@ # MA0152 - Use Unwrap instead of using await twice -Source: [UseTaskUnwrapAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs) +Sources: [UseTaskUnwrapAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseTaskUnwrapAnalyzer.cs), [UseTaskUnwrapFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs) Prefer using [`Unwrap`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskextensions.unwrap?view=net-8.0&WT.mc_id=DT-MVP-5003978) instead of using `await` twice diff --git a/docs/Rules/MA0156.md b/docs/Rules/MA0156.md index b03c8ded0..df6a64a11 100644 --- a/docs/Rules/MA0156.md +++ b/docs/Rules/MA0156.md @@ -1,6 +1,6 @@ # MA0156 - Use 'Async' suffix when a method returns IAsyncEnumerable\ -Source: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs) +Sources: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs), [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs) Methods that return `IAsyncEnumerable` should have the Async suffix. diff --git a/docs/Rules/MA0157.md b/docs/Rules/MA0157.md index 5c957a1b7..42d8f4c94 100644 --- a/docs/Rules/MA0157.md +++ b/docs/Rules/MA0157.md @@ -1,6 +1,6 @@ # MA0157 - Do not use 'Async' suffix when a method returns IAsyncEnumerable\ -Source: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs) +Sources: [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzer.cs), [MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs) Methods that do not return `IAsyncEnumerable` should not have the Async suffix. diff --git a/docs/Rules/MA0158.md b/docs/Rules/MA0158.md index a0e4f2310..3d59009e7 100644 --- a/docs/Rules/MA0158.md +++ b/docs/Rules/MA0158.md @@ -1,6 +1,6 @@ # MA0158 - Use System.Threading.Lock -Source: [UseSystemThreadingLockInsteadOfObjectAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzer.cs) +Sources: [UseSystemThreadingLockInsteadOfObjectAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzer.cs), [UseSystemThreadingLockInsteadOfObjectFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs) Starting with .NET 9 and C# 13, you can use `System.Threading.Lock`. When a field or a local variable is only used inside `lock`, this rule will suggest using `System.Threading.Lock` instead of `object`. diff --git a/docs/Rules/MA0160.md b/docs/Rules/MA0160.md index 54fe893d9..2a74197b0 100644 --- a/docs/Rules/MA0160.md +++ b/docs/Rules/MA0160.md @@ -1,6 +1,6 @@ # MA0160 - Use ContainsKey instead of TryGetValue -Source: [UseContainsKeyInsteadOfTryGetValueAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzer.cs) +Sources: [UseContainsKeyInsteadOfTryGetValueAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzer.cs), [UseContainsKeyInsteadOfTryGetValueFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs) ````c# diff --git a/docs/Rules/MA0169.md b/docs/Rules/MA0169.md index 33abc7d18..8b47de3df 100644 --- a/docs/Rules/MA0169.md +++ b/docs/Rules/MA0169.md @@ -1,6 +1,6 @@ # MA0169 - Use Equals method instead of operator -Source: [UseEqualsMethodInsteadOfOperatorAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseEqualsMethodInsteadOfOperatorAnalyzer.cs) +Sources: [UseEqualsMethodInsteadOfOperatorAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/UseEqualsMethodInsteadOfOperatorAnalyzer.cs), [UseEqualsMethodInsteadOfOperatorFixer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs) When a type overrides the `Equals` method, but does not define the equality operators, using `==` or `!=` will do a reference comparison. This can lead to unexpected behavior, as the `Equals` method may be overridden to provide a value comparison. This rule is to ensure that the `Equals` method is used. diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs index e5d6e5c49..a3a986585 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer.cs @@ -14,7 +14,9 @@ public sealed class MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixFixer : public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix, - RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix); + RuleIdentifiers.MethodsNotReturningAnAwaitableTypeMustNotHaveTheAsyncSuffix, + RuleIdentifiers.MethodsReturningIAsyncEnumerableMustHaveTheAsyncSuffix, + RuleIdentifiers.MethodsNotReturningIAsyncEnumerableMustNotHaveTheAsyncSuffix); public override FixAllProvider? GetFixAllProvider() => null; @@ -52,7 +54,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) { string newName; string title; - if (diagnostic.Id == RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix) + if (diagnostic.Id is RuleIdentifiers.MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffix or RuleIdentifiers.MethodsReturningIAsyncEnumerableMustHaveTheAsyncSuffix) { newName = methodSymbol.Name + "Async"; title = $"Rename to '{newName}'"; diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs new file mode 100644 index 000000000..38960d2b3 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs @@ -0,0 +1,70 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseContainsKeyInsteadOfTryGetValueFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseContainsKeyInsteadOfTryGetValue); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + const string title = "Use ContainsKey"; + context.RegisterCodeFix( + CodeAction.Create(title, ct => UseContainsKey(context.Document, nodeToFix, ct), equivalenceKey: title), + context.Diagnostics); + } + + private static async Task UseContainsKey(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (FindInvocation(editor.SemanticModel, nodeToFix, cancellationToken) is not { Arguments.Length: 2 } operation) + return document; + + if (operation.TargetMethod.Name != "TryGetValue") + return document; + + if (operation.Arguments[1].Value is not IDiscardOperation) + return document; + + if (operation.Syntax is not InvocationExpressionSyntax invocationSyntax || + invocationSyntax.Expression is not MemberAccessExpressionSyntax memberAccess) + { + return document; + } + + var newInvocation = invocationSyntax + .WithExpression(memberAccess.WithName(IdentifierName("ContainsKey"))) + .WithArgumentList(ArgumentList(SeparatedList(new[] { invocationSyntax.ArgumentList.Arguments[0] }))); + + editor.ReplaceNode(invocationSyntax, newInvocation.WithTriviaFrom(invocationSyntax).WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + private static IInvocationOperation? FindInvocation(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + foreach (var candidate in node.AncestorsAndSelf()) + { + if (semanticModel.GetOperation(candidate, cancellationToken) is IInvocationOperation invocation) + return invocation; + } + + return null; + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs new file mode 100644 index 000000000..347fb17c6 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseEqualsMethodInsteadOfOperatorFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseEqualsMethodInsteadOfOperator); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + const string title = "Use Equals"; + context.RegisterCodeFix( + CodeAction.Create(title, ct => UseEquals(context.Document, nodeToFix, ct), equivalenceKey: title), + context.Diagnostics); + } + + private static async Task UseEquals(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (FindBinaryOperation(editor.SemanticModel, nodeToFix, cancellationToken) is not { } operation) + return document; + + if (operation.Syntax is not BinaryExpressionSyntax binaryExpression) + return document; + + if (operation.LeftOperand.Syntax is not ExpressionSyntax leftOperand || operation.RightOperand.Syntax is not ExpressionSyntax rightOperand) + return document; + + ExpressionSyntax newExpression = InvocationExpression( + MemberAccessExpression( + Microsoft.CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression, + PredefinedType(Token(Microsoft.CodeAnalysis.CSharp.SyntaxKind.ObjectKeyword)), + IdentifierName(nameof(System.Object.Equals))), + ArgumentList(SeparatedList(new[] { Argument(rightOperand.WithoutTrivia()) }))); + newExpression = ((InvocationExpressionSyntax)newExpression).WithArgumentList( + ArgumentList(SeparatedList(new[] { Argument(leftOperand.WithoutTrivia()), Argument(rightOperand.WithoutTrivia()) }))); + + if (operation.OperatorKind is BinaryOperatorKind.NotEquals) + { + newExpression = PrefixUnaryExpression(Microsoft.CodeAnalysis.CSharp.SyntaxKind.LogicalNotExpression, Parenthesize(newExpression)); + } + + editor.ReplaceNode(binaryExpression, newExpression.WithTriviaFrom(binaryExpression).WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + private static IBinaryOperation? FindBinaryOperation(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + foreach (var candidate in node.AncestorsAndSelf()) + { + if (semanticModel.GetOperation(candidate, cancellationToken) is IBinaryOperation binaryOperation) + return binaryOperation; + } + + return null; + } + + private static ExpressionSyntax Parenthesize(ExpressionSyntax expression) + => expression switch + { + IdentifierNameSyntax or + ThisExpressionSyntax or + BaseExpressionSyntax or + InvocationExpressionSyntax or + MemberAccessExpressionSyntax or + ElementAccessExpressionSyntax => expression, + _ => ParenthesizedExpression(expression), + }; +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs new file mode 100644 index 000000000..d3dc9abdc --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs @@ -0,0 +1,74 @@ +using System.Collections.Immutable; +using System.Composition; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseOperatingSystemInsteadOfRuntimeInformationFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseOperatingSystemInsteadOfRuntimeInformation); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + const string title = "Use System.OperatingSystem"; + context.RegisterCodeFix( + CodeAction.Create(title, ct => UseOperatingSystem(context.Document, nodeToFix, ct), equivalenceKey: title), + context.Diagnostics); + } + + private static async Task UseOperatingSystem(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (FindInvocation(editor.SemanticModel, nodeToFix, cancellationToken) is not { Arguments.Length: 1 } operation) + return document; + + if (operation.Arguments[0].Value is not IMemberReferenceOperation { Member.Name: var osPlatformName }) + return document; + + var methodName = osPlatformName switch + { + "Windows" => "IsWindows", + "Linux" => "IsLinux", + "OSX" => "IsMacOS", + "FreeBSD" => "IsFreeBSD", + _ => null, + }; + if (methodName is null) + return document; + + var operatingSystemType = editor.SemanticModel.Compilation.GetBestTypeByMetadataName("System.OperatingSystem"); + if (operatingSystemType is null) + return document; + + var invocationExpression = editor.Generator.InvocationExpression( + editor.Generator.MemberAccessExpression(editor.Generator.TypeExpression(operatingSystemType), methodName)); + + editor.ReplaceNode(operation.Syntax, invocationExpression.WithTriviaFrom(operation.Syntax).WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + private static IInvocationOperation? FindInvocation(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + foreach (var candidate in node.AncestorsAndSelf()) + { + if (semanticModel.GetOperation(candidate, cancellationToken) is IInvocationOperation invocation) + return invocation; + } + + return null; + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs new file mode 100644 index 000000000..3017d4658 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs @@ -0,0 +1,80 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Meziantou.Analyzer.Internals; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseSystemThreadingLockInsteadOfObjectFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseSystemThreadingLockInsteadOfObject); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + const string title = "Use System.Threading.Lock"; + context.RegisterCodeFix( + CodeAction.Create(title, ct => UseLockType(context.Document, nodeToFix, ct), equivalenceKey: title), + context.Diagnostics); + } + + private static async Task UseLockType(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var lockType = editor.SemanticModel.Compilation.GetBestTypeByMetadataName("System.Threading.Lock"); + if (lockType is null) + return document; + + var variableDeclarator = nodeToFix.FirstAncestorOrSelf(); + if (variableDeclarator is null) + return document; + + if (editor.SemanticModel.GetDeclaredSymbol(variableDeclarator, cancellationToken) is not ISymbol symbol) + return document; + + if (variableDeclarator.Parent is not VariableDeclarationSyntax declaration || declaration.Variables.Count != 1) + return document; + + editor.ReplaceNode( + declaration.Type, + ((TypeSyntax)editor.Generator.TypeExpression(lockType)).WithTriviaFrom(declaration.Type).WithAdditionalAnnotations(Formatter.Annotation)); + + if (variableDeclarator.Initializer is not null && IsObjectCreation(editor.SemanticModel, variableDeclarator.Initializer.Value, cancellationToken)) + { + editor.ReplaceNode(variableDeclarator.Initializer.Value, ImplicitObjectCreationExpression().WithTriviaFrom(variableDeclarator.Initializer.Value)); + } + + foreach (var assignment in editor.OriginalRoot.DescendantNodes().OfType()) + { + var leftSymbol = editor.SemanticModel.GetSymbolInfo(assignment.Left, cancellationToken).Symbol; + if (!SymbolEqualityComparer.Default.Equals(leftSymbol, symbol)) + continue; + + if (!IsObjectCreation(editor.SemanticModel, assignment.Right, cancellationToken)) + continue; + + editor.ReplaceNode(assignment.Right, ImplicitObjectCreationExpression().WithTriviaFrom(assignment.Right)); + } + + return editor.GetChangedDocument(); + + static bool IsObjectCreation(SemanticModel semanticModel, ExpressionSyntax expression, CancellationToken cancellationToken) + => semanticModel.GetOperation(expression, cancellationToken) is IObjectCreationOperation { Type.SpecialType: SpecialType.System_Object }; + + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs new file mode 100644 index 000000000..238c8a852 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs @@ -0,0 +1,98 @@ +using System.Collections.Immutable; +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class UseTaskUnwrapFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseTaskUnwrap); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + const string title = "Use Unwrap"; + context.RegisterCodeFix( + CodeAction.Create(title, ct => UseUnwrap(context.Document, nodeToFix, ct), equivalenceKey: title), + context.Diagnostics); + } + + private static async Task UseUnwrap(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + if (FindAwait(editor.SemanticModel, nodeToFix, cancellationToken) is not { } awaitOperation) + return document; + + if (awaitOperation.Syntax is not AwaitExpressionSyntax awaitExpression) + return document; + + if (awaitOperation.Operation is IAwaitOperation innerAwait) + { + var unwrappedExpression = InvocationExpression( + MemberAccessExpression( + Microsoft.CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression, + WrapForMemberAccess((ExpressionSyntax)innerAwait.Operation.Syntax.WithoutTrivia()), + IdentifierName("Unwrap"))); + + var newNode = awaitExpression.WithExpression(unwrappedExpression.WithTriviaFrom(awaitExpression.Expression)); + editor.ReplaceNode(awaitExpression, newNode.WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + if (awaitOperation.Operation is IInvocationOperation { Instance: IAwaitOperation innerAwaitOperation } && + awaitExpression.Expression is InvocationExpressionSyntax invocation && + invocation.Expression is MemberAccessExpressionSyntax memberAccess) + { + var unwrappedExpression = InvocationExpression( + MemberAccessExpression( + Microsoft.CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression, + WrapForMemberAccess((ExpressionSyntax)innerAwaitOperation.Operation.Syntax.WithoutTrivia()), + IdentifierName("Unwrap"))); + + var newExpression = invocation.WithExpression(memberAccess.WithExpression(unwrappedExpression.WithTriviaFrom(memberAccess.Expression))); + editor.ReplaceNode(awaitExpression, awaitExpression.WithExpression(newExpression).WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + + return document; + } + + private static IAwaitOperation? FindAwait(SemanticModel semanticModel, SyntaxNode node, CancellationToken cancellationToken) + { + foreach (var candidate in node.AncestorsAndSelf()) + { + if (semanticModel.GetOperation(candidate, cancellationToken) is IAwaitOperation awaitOperation) + return awaitOperation; + } + + return null; + } + + private static ExpressionSyntax WrapForMemberAccess(ExpressionSyntax expression) + => expression switch + { + IdentifierNameSyntax or + GenericNameSyntax or + ThisExpressionSyntax or + BaseExpressionSyntax or + InvocationExpressionSyntax or + MemberAccessExpressionSyntax or + ElementAccessExpressionSyntax => expression, + _ => ParenthesizedExpression(expression), + }; +} + diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs new file mode 100644 index 000000000..a41cc945b --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs @@ -0,0 +1,136 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Meziantou.Analyzer.Rules; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public sealed class ValidateUnsafeAccessorAttributeUsageFixer : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UnsafeAccessorAttribute_NameMustBeSet); + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); + if (nodeToFix is null) + return; + + const string title = "Set UnsafeAccessor Name"; + context.RegisterCodeFix( + CodeAction.Create(title, ct => SetNameProperty(context.Document, nodeToFix, ct), equivalenceKey: title), + context.Diagnostics); + } + + private static async Task SetNameProperty(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); + var declaration = (SyntaxNode?)nodeToFix.FirstAncestorOrSelf() ?? nodeToFix.FirstAncestorOrSelf(); + if (declaration is null) + declaration = editor.OriginalRoot; + + foreach (var (attributeList, methodName) in EnumerateCandidateAttributes(declaration)) + { + foreach (var attribute in attributeList.Attributes) + { + if (!IsUnsafeAccessorAttribute(attribute)) + continue; + + if (HasNameProperty(attribute)) + return document; + + if (methodName.Length == 0) + return document; + + var argument = AttributeArgument( + NameEquals(IdentifierName("Name")), + nameColon: null, + expression: LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(methodName))); + + var newArgumentList = attribute.ArgumentList is null + ? AttributeArgumentList(SeparatedList(new[] { argument })) + : attribute.ArgumentList.AddArguments(argument); + var newAttribute = attribute.WithArgumentList(newArgumentList); + + editor.ReplaceNode(attribute, newAttribute.WithAdditionalAnnotations(Formatter.Annotation)); + return editor.GetChangedDocument(); + } + } + + return document; + } + + private static IEnumerable<(AttributeListSyntax AttributeList, string MethodName)> EnumerateCandidateAttributes(SyntaxNode declaration) + { + switch (declaration) + { + case LocalFunctionStatementSyntax localFunction: + foreach (var attributeList in localFunction.AttributeLists) + { + yield return (attributeList, localFunction.Identifier.ValueText); + } + + yield break; + + case MethodDeclarationSyntax method: + foreach (var attributeList in method.AttributeLists) + { + yield return (attributeList, method.Identifier.ValueText); + } + + foreach (var localFunctionDeclaration in method.DescendantNodes().OfType()) + { + foreach (var attributeList in localFunctionDeclaration.AttributeLists) + { + yield return (attributeList, localFunctionDeclaration.Identifier.ValueText); + } + } + + yield break; + } + + foreach (var localFunctionDeclaration in declaration.DescendantNodes().OfType()) + { + foreach (var attributeList in localFunctionDeclaration.AttributeLists) + { + yield return (attributeList, localFunctionDeclaration.Identifier.ValueText); + } + } + } + + private static bool HasNameProperty(AttributeSyntax attribute) + { + if (attribute.ArgumentList is null) + return false; + + foreach (var argument in attribute.ArgumentList.Arguments) + { + if (argument.NameEquals?.Name.Identifier.ValueText == "Name") + return true; + } + + return false; + } + + private static bool IsUnsafeAccessorAttribute(AttributeSyntax attribute) + => IsUnsafeAccessorAttributeName(attribute.Name); + + private static bool IsUnsafeAccessorAttributeName(NameSyntax name) + => name switch + { + IdentifierNameSyntax identifier => identifier.Identifier.ValueText is "UnsafeAccessor" or "UnsafeAccessorAttribute", + QualifiedNameSyntax qualified => IsUnsafeAccessorAttributeName(qualified.Right), + AliasQualifiedNameSyntax aliasQualified => IsUnsafeAccessorAttributeName(aliasQualified.Name), + _ => false, + }; +} diff --git a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs index ce2ede6cc..78c75e0a0 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/MethodsReturningAnAwaitableTypeMustHaveTheAsyncSuffixAnalyzerTests.cs @@ -147,6 +147,25 @@ class TypeName .ShouldReportDiagnosticWithMessage("Method returning IAsyncEnumerable must use the 'Async' suffix") .ValidateAsync(); + [Fact] + public Task IAsyncEnumerableWithoutSuffix_CodeFix_AddsAsyncSuffix() + => CreateProjectBuilder() + .WithSourceCode(""" + class TypeName + { + System.Collections.Generic.IAsyncEnumerable {|MA0156:Foo|}() => throw null; + void Caller() { _ = Foo(); } + } + """) + .ShouldFixCodeWith(""" + class TypeName + { + System.Collections.Generic.IAsyncEnumerable FooAsync() => throw null; + void Caller() { _ = FooAsync(); } + } + """) + .ValidateAsync(); + [Fact] public Task IAsyncEnumerableWithSuffix() => CreateProjectBuilder() @@ -159,6 +178,25 @@ class TypeName .ShouldReportDiagnosticWithMessage("Method returning IAsyncEnumerable must not use the 'Async' suffix") .ValidateAsync(); + [Fact] + public Task IAsyncEnumerableWithSuffix_CodeFix_RemovesAsyncSuffix() + => CreateProjectBuilder() + .WithSourceCode(""" + class TypeName + { + System.Collections.Generic.IAsyncEnumerable {|MA0157:FooAsync|}() => throw null; + void Caller() { _ = FooAsync(); } + } + """) + .ShouldFixCodeWith(""" + class TypeName + { + System.Collections.Generic.IAsyncEnumerable Foo() => throw null; + void Caller() { _ = Foo(); } + } + """) + .ValidateAsync(); + [Fact] public Task IgnoreTestMethods() => CreateProjectBuilder() diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs index 944b8dd3a..aa8e0fbae 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseContainsKeyInsteadOfTryGetValueAnalyzerTests.cs @@ -7,7 +7,8 @@ public sealed class UseContainsKeyInsteadOfTryGetValueAnalyzerTests private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Fact] @@ -39,6 +40,15 @@ void Test(System.Collections.Generic.IDictionary dict) } } """) + .ShouldFixCodeWith(""" + class ClassTest + { + void Test(System.Collections.Generic.IDictionary dict) + { + dict.ContainsKey(""); + } + } + """) .ValidateAsync(); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseEqualsMethodInsteadOfOperatorAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseEqualsMethodInsteadOfOperatorAnalyzerTests.cs index 1e89f92e6..a365ea704 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseEqualsMethodInsteadOfOperatorAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseEqualsMethodInsteadOfOperatorAnalyzerTests.cs @@ -9,7 +9,8 @@ private static ProjectBuilder CreateProjectBuilder() return new ProjectBuilder() .WithTargetFramework(Helpers.TargetFramework.Net9_0) .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication) - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Theory] @@ -22,6 +23,28 @@ await CreateProjectBuilder() {{type}} b = null; _ = [|a == b|]; """) + .ShouldFixCodeWith($$""" + {{type}} a = null; + {{type}} b = null; + _ = object.Equals(a, b); + """) + .ValidateAsync(); + } + + [Fact] + public async Task Report_NotEqualsOperator() + { + await CreateProjectBuilder() + .WithSourceCode(""" + System.Net.IPAddress a = null; + System.Net.IPAddress b = null; + _ = [|a != b|]; + """) + .ShouldFixCodeWith(""" + System.Net.IPAddress a = null; + System.Net.IPAddress b = null; + _ = !object.Equals(a, b); + """) .ValidateAsync(); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseOperatingSystemInsteadOfRuntimeInformationAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseOperatingSystemInsteadOfRuntimeInformationAnalyzerTests.cs index f8e95dcc2..e064cfd04 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseOperatingSystemInsteadOfRuntimeInformationAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseOperatingSystemInsteadOfRuntimeInformationAnalyzerTests.cs @@ -9,6 +9,7 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithAnalyzer() + .WithCodeFixProvider() .WithTargetFramework(TargetFramework.Net8_0) .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication); } @@ -20,6 +21,22 @@ await CreateProjectBuilder() .WithSourceCode(""" [|System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows)|]; """) + .ShouldFixCodeWith(""" + System.OperatingSystem.IsWindows(); + """) + .ValidateAsync(); + } + + [Fact] + public async Task ShouldReport_MacOS() + { + await CreateProjectBuilder() + .WithSourceCode(""" + [|System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)|]; + """) + .ShouldFixCodeWith(""" + System.OperatingSystem.IsMacOS(); + """) .ValidateAsync(); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzerTests.cs index 8e4981c0f..345c00a5e 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzerTests.cs @@ -10,7 +10,8 @@ private static ProjectBuilder CreateProjectBuilder() return new ProjectBuilder() .WithTargetFramework(TargetFramework.Net9_0) .WithLanguageVersion(Microsoft.CodeAnalysis.CSharp.LanguageVersion.Preview) - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Fact] @@ -73,6 +74,14 @@ class TypeName void A() { lock(_lock) { } } } """) + .ShouldFixCodeWith(""" + class TypeName + { + System.Threading.Lock _lock = new(); + + void A() { lock(_lock) { } } + } + """) .ValidateAsync(); } @@ -281,6 +290,22 @@ public void Run() } } """) + .ShouldFixCodeWith(""" + public sealed class A + { + private readonly System.Threading.Lock _lock; + + public A() + { + _lock = new(); + } + + public void Run() + { + lock (_lock) { } + } + } + """) .ValidateAsync(); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs index 69c48aeab..2eb7d0409 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/UseTaskUnwrapAnalyzerTests.cs @@ -10,6 +10,7 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithAnalyzer() + .WithCodeFixProvider() .WithTargetFramework(TargetFramework.Net6_0) .WithOutputKind(Microsoft.CodeAnalysis.OutputKind.ConsoleApplication); } @@ -24,6 +25,12 @@ await CreateProjectBuilder() Task a = null; [|await await a|]; """) + .ShouldFixCodeWith(""" + using System.Threading.Tasks; + + Task a = null; + await a.Unwrap(); + """) .ValidateAsync(); } @@ -50,6 +57,12 @@ await CreateProjectBuilder() Task a = null; [|await (await a).ConfigureAwait(false)|]; """) + .ShouldFixCodeWith(""" + using System.Threading.Tasks; + + Task a = null; + await a.Unwrap().ConfigureAwait(false); + """) .ValidateAsync(); } diff --git a/tests/Meziantou.Analyzer.Test/Rules/ValidateUnsafeAccessorAttributeUsageAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/ValidateUnsafeAccessorAttributeUsageAnalyzerTests.cs index 630920640..b03e7d23b 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/ValidateUnsafeAccessorAttributeUsageAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/ValidateUnsafeAccessorAttributeUsageAnalyzerTests.cs @@ -9,7 +9,8 @@ private static ProjectBuilder CreateProjectBuilder() { return new ProjectBuilder() .WithTargetFramework(TargetFramework.Net8_0) - .WithAnalyzer(); + .WithAnalyzer() + .WithCodeFixProvider(); } [Fact] @@ -43,6 +44,18 @@ void A() } } """) + .ShouldFixCodeWith(""" + using System.Runtime.CompilerServices; + class Sample + { + void A() + { + // Local function name are mangle by the compiler, so the Name property is required + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "B")] + extern static ref int B(System.Version a); + } + } + """) .ValidateAsync(); } From 404b0d04c85597ed64845c706bccb8dde39bb4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 20:23:01 -0400 Subject: [PATCH 2/3] Fix IDE1006 naming violations in new code fixers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs | 4 ++-- .../Rules/UseEqualsMethodInsteadOfOperatorFixer.cs | 4 ++-- .../UseOperatingSystemInsteadOfRuntimeInformationFixer.cs | 4 ++-- .../Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs | 4 ++-- src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs | 4 ++-- .../Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs index 38960d2b3..0815f3ff2 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseContainsKeyInsteadOfTryGetValueFixer.cs @@ -25,9 +25,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; - const string title = "Use ContainsKey"; + const string Title = "Use ContainsKey"; context.RegisterCodeFix( - CodeAction.Create(title, ct => UseContainsKey(context.Document, nodeToFix, ct), equivalenceKey: title), + CodeAction.Create(Title, ct => UseContainsKey(context.Document, nodeToFix, ct), equivalenceKey: Title), context.Diagnostics); } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs index 347fb17c6..a36025e50 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs @@ -25,9 +25,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; - const string title = "Use Equals"; + const string Title = "Use Equals"; context.RegisterCodeFix( - CodeAction.Create(title, ct => UseEquals(context.Document, nodeToFix, ct), equivalenceKey: title), + CodeAction.Create(Title, ct => UseEquals(context.Document, nodeToFix, ct), equivalenceKey: Title), context.Diagnostics); } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs index d3dc9abdc..5b5ec6945 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseOperatingSystemInsteadOfRuntimeInformationFixer.cs @@ -24,9 +24,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; - const string title = "Use System.OperatingSystem"; + const string Title = "Use System.OperatingSystem"; context.RegisterCodeFix( - CodeAction.Create(title, ct => UseOperatingSystem(context.Document, nodeToFix, ct), equivalenceKey: title), + CodeAction.Create(Title, ct => UseOperatingSystem(context.Document, nodeToFix, ct), equivalenceKey: Title), context.Diagnostics); } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs index 3017d4658..1692acf6c 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseSystemThreadingLockInsteadOfObjectFixer.cs @@ -27,9 +27,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; - const string title = "Use System.Threading.Lock"; + const string Title = "Use System.Threading.Lock"; context.RegisterCodeFix( - CodeAction.Create(title, ct => UseLockType(context.Document, nodeToFix, ct), equivalenceKey: title), + CodeAction.Create(Title, ct => UseLockType(context.Document, nodeToFix, ct), equivalenceKey: Title), context.Diagnostics); } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs index 238c8a852..8f2fccc7d 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs @@ -25,9 +25,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; - const string title = "Use Unwrap"; + const string Title = "Use Unwrap"; context.RegisterCodeFix( - CodeAction.Create(title, ct => UseUnwrap(context.Document, nodeToFix, ct), equivalenceKey: title), + CodeAction.Create(Title, ct => UseUnwrap(context.Document, nodeToFix, ct), equivalenceKey: Title), context.Diagnostics); } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs index a41cc945b..d6785ef06 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/ValidateUnsafeAccessorAttributeUsageFixer.cs @@ -26,9 +26,9 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) if (nodeToFix is null) return; - const string title = "Set UnsafeAccessor Name"; + const string Title = "Set UnsafeAccessor Name"; context.RegisterCodeFix( - CodeAction.Create(title, ct => SetNameProperty(context.Document, nodeToFix, ct), equivalenceKey: title), + CodeAction.Create(Title, ct => SetNameProperty(context.Document, nodeToFix, ct), equivalenceKey: Title), context.Diagnostics); } From b8522806d7f9c8d6d9410b856a2d26a9b23569b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Mon, 30 Mar 2026 20:35:11 -0400 Subject: [PATCH 3/3] Use Parentheses extension in new code fixers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UseEqualsMethodInsteadOfOperatorFixer.cs | 15 ++------------- .../Rules/UseTaskUnwrapFixer.cs | 18 +++--------------- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs index a36025e50..7287bf6fc 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseEqualsMethodInsteadOfOperatorFixer.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Composition; +using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -54,7 +55,7 @@ private static async Task UseEquals(Document document, SyntaxNode node if (operation.OperatorKind is BinaryOperatorKind.NotEquals) { - newExpression = PrefixUnaryExpression(Microsoft.CodeAnalysis.CSharp.SyntaxKind.LogicalNotExpression, Parenthesize(newExpression)); + newExpression = PrefixUnaryExpression(Microsoft.CodeAnalysis.CSharp.SyntaxKind.LogicalNotExpression, (ExpressionSyntax)newExpression.Parentheses()); } editor.ReplaceNode(binaryExpression, newExpression.WithTriviaFrom(binaryExpression).WithAdditionalAnnotations(Formatter.Annotation)); @@ -71,16 +72,4 @@ private static async Task UseEquals(Document document, SyntaxNode node return null; } - - private static ExpressionSyntax Parenthesize(ExpressionSyntax expression) - => expression switch - { - IdentifierNameSyntax or - ThisExpressionSyntax or - BaseExpressionSyntax or - InvocationExpressionSyntax or - MemberAccessExpressionSyntax or - ElementAccessExpressionSyntax => expression, - _ => ParenthesizedExpression(expression), - }; } diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs index 8f2fccc7d..91f32079a 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/UseTaskUnwrapFixer.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Composition; +using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -45,7 +46,7 @@ private static async Task UseUnwrap(Document document, SyntaxNode node var unwrappedExpression = InvocationExpression( MemberAccessExpression( Microsoft.CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression, - WrapForMemberAccess((ExpressionSyntax)innerAwait.Operation.Syntax.WithoutTrivia()), + (ExpressionSyntax)((ExpressionSyntax)innerAwait.Operation.Syntax.WithoutTrivia()).Parentheses(), IdentifierName("Unwrap"))); var newNode = awaitExpression.WithExpression(unwrappedExpression.WithTriviaFrom(awaitExpression.Expression)); @@ -60,7 +61,7 @@ awaitExpression.Expression is InvocationExpressionSyntax invocation && var unwrappedExpression = InvocationExpression( MemberAccessExpression( Microsoft.CodeAnalysis.CSharp.SyntaxKind.SimpleMemberAccessExpression, - WrapForMemberAccess((ExpressionSyntax)innerAwaitOperation.Operation.Syntax.WithoutTrivia()), + (ExpressionSyntax)((ExpressionSyntax)innerAwaitOperation.Operation.Syntax.WithoutTrivia()).Parentheses(), IdentifierName("Unwrap"))); var newExpression = invocation.WithExpression(memberAccess.WithExpression(unwrappedExpression.WithTriviaFrom(memberAccess.Expression))); @@ -81,18 +82,5 @@ awaitExpression.Expression is InvocationExpressionSyntax invocation && return null; } - - private static ExpressionSyntax WrapForMemberAccess(ExpressionSyntax expression) - => expression switch - { - IdentifierNameSyntax or - GenericNameSyntax or - ThisExpressionSyntax or - BaseExpressionSyntax or - InvocationExpressionSyntax or - MemberAccessExpressionSyntax or - ElementAccessExpressionSyntax => expression, - _ => ParenthesizedExpression(expression), - }; }