From d2be721f97b33fb3ce3bb07463a17792739197b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9rald=20Barr=C3=A9?= Date: Thu, 17 Oct 2024 20:34:55 -0400 Subject: [PATCH] Add suppressor for IDE0058 on StringBuilder and Directory (#762) --- README.md | 1 + docs/README.md | 1 + src/DocumentationGenerator/Program.cs | 8 +++ .../Rules/DoNotUseAsyncVoidAnalyzer.cs | 3 +- .../Rules/ProcessStartAnalyzer.cs | 2 +- ...temThreadingLockInsteadOfObjectAnalyzer.cs | 16 +----- .../Suppressors/IDE0058Suppressor.cs | 54 ++++++++++++++++++ .../Helpers/ProjectBuilder.cs | 4 +- .../CA1822DecoratedMethodSuppressorTests.cs | 4 +- .../Suppressors/IDE0058SuppressorTests.cs | 56 +++++++++++++++++++ 10 files changed, 130 insertions(+), 19 deletions(-) create mode 100644 src/Meziantou.Analyzer/Suppressors/IDE0058Suppressor.cs create mode 100644 tests/Meziantou.Analyzer.Test/Suppressors/IDE0058SuppressorTests.cs diff --git a/README.md b/README.md index 87c115ad1..f340dd819 100755 --- a/README.md +++ b/README.md @@ -191,5 +191,6 @@ If you are already using other analyzers, you can check [which rules are duplica |--|---------------|-------------| |`MAS0001`|[CA1822](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822?WT.mc_id=DT-MVP-5003978)|Suppress CA1822 on methods decorated with BenchmarkDotNet attributes.| |`MAS0002`|[CA1822](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822?WT.mc_id=DT-MVP-5003978)|Suppress CA1822 on methods decorated with a System.Text.Json attribute such as \[JsonPropertyName\] or \[JsonInclude\].| +|`MAS0003`|[IDE0058](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0058?WT.mc_id=DT-MVP-5003978)|Suppress IDE0058 on well-known types| \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 42376e9ab..07e0e3672 100755 --- a/docs/README.md +++ b/docs/README.md @@ -169,6 +169,7 @@ |--|---------------|-------------| |`MAS0001`|[CA1822](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822?WT.mc_id=DT-MVP-5003978)|Suppress CA1822 on methods decorated with BenchmarkDotNet attributes.| |`MAS0002`|[CA1822](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822?WT.mc_id=DT-MVP-5003978)|Suppress CA1822 on methods decorated with a System.Text.Json attribute such as \[JsonPropertyName\] or \[JsonInclude\].| +|`MAS0003`|[IDE0058](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0058?WT.mc_id=DT-MVP-5003978)|Suppress IDE0058 on well-known types| # .editorconfig - default values diff --git a/src/DocumentationGenerator/Program.cs b/src/DocumentationGenerator/Program.cs index 47ba68145..6f619121b 100644 --- a/src/DocumentationGenerator/Program.cs +++ b/src/DocumentationGenerator/Program.cs @@ -156,6 +156,14 @@ static string GenerateSuppressorsTable(List diagnosticSupp .Append($"https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/").Append(suppression.SuppressedDiagnosticId.ToLowerInvariant()).Append("?WT.mc_id=DT-MVP-5003978") .Append(')'); } + else if (suppression.SuppressedDiagnosticId.StartsWith("IDE", StringComparison.OrdinalIgnoreCase)) + { + sb.Append('[') + .Append(suppression.SuppressedDiagnosticId) + .Append("](") + .Append($"https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/").Append(suppression.SuppressedDiagnosticId.ToLowerInvariant()).Append("?WT.mc_id=DT-MVP-5003978") + .Append(')'); + } else { sb.Append('`').Append(suppression.SuppressedDiagnosticId).Append('`'); diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseAsyncVoidAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseAsyncVoidAnalyzer.cs index 6efb0a509..39acea088 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseAsyncVoidAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseAsyncVoidAnalyzer.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Immutable; +using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Operations; diff --git a/src/Meziantou.Analyzer/Rules/ProcessStartAnalyzer.cs b/src/Meziantou.Analyzer/Rules/ProcessStartAnalyzer.cs index ad3b85182..11e6ae25b 100644 --- a/src/Meziantou.Analyzer/Rules/ProcessStartAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/ProcessStartAnalyzer.cs @@ -86,7 +86,7 @@ public void AnalyzeObjectCreation(OperationAnalysisContext context) var operation = (IObjectCreationOperation)context.Operation; if (IsProcessStartInfoCreation(operation)) { - if (operation is { Initializer: {} initializer } ) + if (operation is { Initializer: { } initializer }) { var useShellExecuteInitializer = initializer.Initializers.OfType() .FirstOrDefault(x => x.Target.Syntax is IdentifierNameSyntax { Identifier.Text: "UseShellExecute" }); diff --git a/src/Meziantou.Analyzer/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzer.cs b/src/Meziantou.Analyzer/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzer.cs index 3d061f6ac..746959b1d 100644 --- a/src/Meziantou.Analyzer/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/UseSystemThreadingLockInsteadOfObjectAnalyzer.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Collections.Immutable; -using System.Linq; -using Meziantou.Analyzer.Internals; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -112,14 +108,8 @@ private void HandleOperation(ISymbol symbol, IOperation operation) } } - public void ExcludeSymbol(ISymbol symbol) - { - _ = _symbols.AddOrUpdate(symbol, addValue: false, (_, _) => false); - } + public void ExcludeSymbol(ISymbol symbol) => _symbols.AddOrUpdate(symbol, addValue: false, (_, _) => false); - public void AddPotentialSymbol(ISymbol symbol) - { - _ = _symbols.TryAdd(symbol, value: true); - } + public void AddPotentialSymbol(ISymbol symbol) => _symbols.TryAdd(symbol, value: true); } } diff --git a/src/Meziantou.Analyzer/Suppressors/IDE0058Suppressor.cs b/src/Meziantou.Analyzer/Suppressors/IDE0058Suppressor.cs new file mode 100644 index 000000000..7a84efdfc --- /dev/null +++ b/src/Meziantou.Analyzer/Suppressors/IDE0058Suppressor.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Meziantou.Analyzer.Suppressors; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class IDE0058Suppressor : DiagnosticSuppressor +{ + private static readonly SuppressionDescriptor Descriptor = new( + id: "MAS0003", + suppressedDiagnosticId: "IDE0058", + justification: "Suppress IDE0058 on well-known types" + ); + + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(Descriptor); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { +#pragma warning disable IDE1006 // Naming Styles + var System_Text_StringBuilder = context.Compilation.GetBestTypeByMetadataName("System.Text.StringBuilder"); + var System_IO_Directory = context.Compilation.GetBestTypeByMetadataName("System.IO.Directory"); +#pragma warning restore IDE1006 + + foreach (var diagnostic in context.ReportedDiagnostics) + { + var tree = diagnostic.Location.SourceTree; + if (tree is null) + continue; + + var semanticModel = context.GetSemanticModel(tree); + if (semanticModel is null) + continue; + + var node = tree.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + var operation = semanticModel.GetOperation(node, context.CancellationToken); + if (operation is IInvocationOperation invocation) + { + // StringBuilder + if (invocation.TargetMethod.Name is "Append" or "AppendLine" or "AppendJoin" or "AppendFormat" or "Clear" or "Remove" or "Insert" or "Replace" && invocation.TargetMethod.ContainingType.IsEqualTo(System_Text_StringBuilder)) + { + context.ReportSuppression(Suppression.Create(Descriptor, diagnostic)); + } + + // Directory.CreateDirectory + if (invocation.TargetMethod.Name is "CreateDirectory" && invocation.TargetMethod.ContainingType.IsEqualTo(System_IO_Directory)) + { + context.ReportSuppression(Suppression.Create(Descriptor, diagnostic)); + } + } + } + } +} diff --git a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs index a2946d970..599672fa1 100755 --- a/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs +++ b/tests/Meziantou.Analyzer.Test/Helpers/ProjectBuilder.cs @@ -134,14 +134,14 @@ public ProjectBuilder WithAnalyzerFromNuGet(string packageName, string version, public ProjectBuilder WithMicrosoftCodeAnalysisNetAnalyzers(params string[] ruleIds) => WithAnalyzerFromNuGet( "Microsoft.CodeAnalysis.NetAnalyzers", - "7.0.1", + "9.0.0-preview.24454.1", paths: ["analyzers/dotnet/cs/Microsoft.CodeAnalysis"], ruleIds); public ProjectBuilder WithMicrosoftCodeAnalysisCSharpCodeStyleAnalyzers(params string[] ruleIds) => WithAnalyzerFromNuGet( "Microsoft.CodeAnalysis.CSharp.CodeStyle", - "4.10.0-2.final", + "4.12.0-2.final", paths: ["analyzers/dotnet/cs/"], ruleIds); diff --git a/tests/Meziantou.Analyzer.Test/Suppressors/CA1822DecoratedMethodSuppressorTests.cs b/tests/Meziantou.Analyzer.Test/Suppressors/CA1822DecoratedMethodSuppressorTests.cs index 86e8691b1..41a163052 100644 --- a/tests/Meziantou.Analyzer.Test/Suppressors/CA1822DecoratedMethodSuppressorTests.cs +++ b/tests/Meziantou.Analyzer.Test/Suppressors/CA1822DecoratedMethodSuppressorTests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +#if ROSLYN_4_10_OR_GREATER +using System.Threading.Tasks; using Meziantou.Analyzer.Suppressors; using TestHelper; using Xunit; @@ -65,3 +66,4 @@ internal sealed class Sample .ValidateAsync(); } } +#endif \ No newline at end of file diff --git a/tests/Meziantou.Analyzer.Test/Suppressors/IDE0058SuppressorTests.cs b/tests/Meziantou.Analyzer.Test/Suppressors/IDE0058SuppressorTests.cs new file mode 100644 index 000000000..aebe8ffe8 --- /dev/null +++ b/tests/Meziantou.Analyzer.Test/Suppressors/IDE0058SuppressorTests.cs @@ -0,0 +1,56 @@ +#if ROSLYN_4_10_OR_GREATER +using System.Threading.Tasks; +using Meziantou.Analyzer.Suppressors; +using Microsoft.CodeAnalysis; +using TestHelper; +using Xunit; + +namespace Meziantou.Analyzer.Test.Suppressors; +public sealed class IDE0058SuppressorTests +{ + private static ProjectBuilder CreateProjectBuilder() + => new ProjectBuilder() + .WithMicrosoftCodeAnalysisCSharpCodeStyleAnalyzers("IDE0058") + .WithAnalyzer() + .WithOutputKind(OutputKind.ConsoleApplication); + + // Ensure the diagnostic is reported without the suppressor + [Fact] + public async Task IDE0058IsReported() + => await new ProjectBuilder() + .WithMicrosoftCodeAnalysisCSharpCodeStyleAnalyzers("IDE0058") + .WithOutputKind(OutputKind.ConsoleApplication) + .WithSourceCode(""" + static void A() + { + [|new System.Text.StringBuilder().Append("Hello")|]; + [|System.IO.Directory.CreateDirectory("dir")|]; + } + """) + .ValidateAsync(); + + [Fact] + public async Task StringBuilder_Append() + => await CreateProjectBuilder() + .WithSourceCode(""" + static void A() + { + var sb = new System.Text.StringBuilder(); + sb.Append("Hello"); + System.Console.WriteLine(sb.ToString()); + } + """) + .ValidateAsync(); + + [Fact] + public async Task Directory_CreateDirectory() + => await CreateProjectBuilder() + .WithSourceCode(""" + static void A() + { + System.IO.Directory.CreateDirectory("dir"); + } + """) + .ValidateAsync(); +} +#endif \ No newline at end of file