From 48266916d4415bdf038eda07690efa6126ebe357 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Wed, 12 Nov 2025 13:23:06 -0800 Subject: [PATCH] Add suppressor support for `dotnet format` --- .../Analyzers/AnalyzerFormatter.cs | 7 +++++ .../Analyzers/CodeStyleInformationProvider.cs | 17 ++++++++++-- .../suppressor_project/.editorconfig | 4 +++ .../suppressor_project/Program.cs | 26 +++++++++++++++++++ .../suppressor_project.csproj | 12 +++++++++ .../CodeFormatterTests.cs | 18 +++++++++++++ 6 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/.editorconfig create mode 100644 test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/Program.cs create mode 100644 test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/suppressor_project.csproj diff --git a/src/BuiltInTools/dotnet-format/Analyzers/AnalyzerFormatter.cs b/src/BuiltInTools/dotnet-format/Analyzers/AnalyzerFormatter.cs index 9b78488b0aeb..d711ac3baeb2 100644 --- a/src/BuiltInTools/dotnet-format/Analyzers/AnalyzerFormatter.cs +++ b/src/BuiltInTools/dotnet-format/Analyzers/AnalyzerFormatter.cs @@ -315,6 +315,13 @@ internal static async Task DoesAnalyzerSupportLanguage(analyzer, project.Language)); foreach (var analyzer in filteredAnalyzer) { + // Allow suppressors unconditionally + if (analyzer is DiagnosticSuppressor suppressor) + { + analyzers.Add(suppressor); + continue; + } + // Filter by excluded diagnostics if (!excludeDiagnostics.IsEmpty && analyzer.SupportedDiagnostics.All(descriptor => excludeDiagnostics.Contains(descriptor.Id))) diff --git a/src/BuiltInTools/dotnet-format/Analyzers/CodeStyleInformationProvider.cs b/src/BuiltInTools/dotnet-format/Analyzers/CodeStyleInformationProvider.cs index 701f16410335..ebaa07487e06 100644 --- a/src/BuiltInTools/dotnet-format/Analyzers/CodeStyleInformationProvider.cs +++ b/src/BuiltInTools/dotnet-format/Analyzers/CodeStyleInformationProvider.cs @@ -33,6 +33,11 @@ public ImmutableDictionary GetAnalyzersAndFixers( .Select(path => new AnalyzerFileReference(path, analyzerAssemblyLoader)); var analyzersByLanguage = new Dictionary(); + + // We need AnalyzerReferenceInformationProvider to get all project suppressors + var referenceProvider = new AnalyzerReferenceInformationProvider(); + var perProjectAnalyzersAndFixers = referenceProvider.GetAnalyzersAndFixers(workspace, solution, formatOptions, logger); + return solution.Projects .ToImmutableDictionary( project => project.Id, @@ -40,9 +45,17 @@ public ImmutableDictionary GetAnalyzersAndFixers( { if (!analyzersByLanguage.TryGetValue(project.Language, out var analyzersAndFixers)) { - var analyzers = references.SelectMany(reference => reference.GetAnalyzers(project.Language)).ToImmutableArray(); + var analyzers = ImmutableArray.CreateBuilder(); + analyzers.AddRange(references.SelectMany(reference => reference.GetAnalyzers(project.Language))); var codeFixes = AnalyzerFinderHelpers.LoadFixers(references.Select(reference => reference.GetAssembly()), project.Language); - analyzersAndFixers = new AnalyzersAndFixers(analyzers, codeFixes); + + // Add project suppressors to featured analyzers + if (perProjectAnalyzersAndFixers.TryGetValue(project.Id, out var thisProjectAnalyzersAndFixers)) + { + analyzers.AddRange(thisProjectAnalyzersAndFixers.Analyzers.OfType()); + } + + analyzersAndFixers = new AnalyzersAndFixers(analyzers.ToImmutableArray(), codeFixes); analyzersByLanguage.Add(project.Language, analyzersAndFixers); } diff --git a/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/.editorconfig b/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/.editorconfig new file mode 100644 index 000000000000..d8a687a201e4 --- /dev/null +++ b/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*.cs] +dotnet_diagnostic.IDE0051.severity = warning # Remove unused private member \ No newline at end of file diff --git a/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/Program.cs b/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/Program.cs new file mode 100644 index 000000000000..70c479485b15 --- /dev/null +++ b/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/Program.cs @@ -0,0 +1,26 @@ +using System; +using UnityEngine; + +namespace for_code_formatter +{ + class Program : MonoBehaviour + { + // This method should trigger IDE0051 (remove unused private member) in a regular project. + // But given we simulate a Unity MonoBehavior and we include Microsoft.Unity.Analyzers nuget, + // given Update is a well-known Unity message, this IDE0051 should be suppressed by USP0003. + // see https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/main/doc/USP0003.md + void Update() + { + + } + } +} + +namespace UnityEngine +{ + public class MonoBehaviour + { + // This is a placeholder for the Unity MonoBehaviour class. + // In a real Unity project, this would be part of the Unity engine. + } +} \ No newline at end of file diff --git a/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/suppressor_project.csproj b/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/suppressor_project.csproj new file mode 100644 index 000000000000..16a5d98d12b0 --- /dev/null +++ b/test/TestAssets/dotnet-format/for_code_formatter/suppressor_project/suppressor_project.csproj @@ -0,0 +1,12 @@ + + + + Library + netstandard2.0 + + + + + + + \ No newline at end of file diff --git a/test/dotnet-format.UnitTests/CodeFormatterTests.cs b/test/dotnet-format.UnitTests/CodeFormatterTests.cs index c74bc433ea8c..84b3c02b0481 100644 --- a/test/dotnet-format.UnitTests/CodeFormatterTests.cs +++ b/test/dotnet-format.UnitTests/CodeFormatterTests.cs @@ -39,6 +39,9 @@ public class CodeFormatterTests private static readonly string s_generatorSolutionPath = Path.Combine("for_code_formatter", "generator_solution"); private static readonly string s_generatorSolutionFileName = "generator_solution.sln"; + private static readonly string s_suppressorProjectPath = Path.Combine("for_code_formatter", "suppressor_project"); + private static readonly string s_suppressorProjectFilePath = Path.Combine(s_suppressorProjectPath, "suppressor_project.csproj"); + private static string[] EmptyFilesList => Array.Empty(); private Regex FindFormattingLogLine => new Regex(@"((.*)\(\d+,\d+\): (.*))\r|((.*)\(\d+,\d+\): (.*))"); @@ -627,6 +630,21 @@ await TestFormatWorkspaceAsync( } } + [MSBuildFact] + public async Task SuppressorsHandledInProject() + { + await TestFormatWorkspaceAsync( + s_suppressorProjectFilePath, + include: EmptyFilesList, + exclude: EmptyFilesList, + includeGenerated: false, + expectedExitCode: 0, + expectedFilesFormatted: 0, + expectedFileCount: 3, + codeStyleSeverity: DiagnosticSeverity.Warning, + fixCategory: FixCategory.CodeStyle); + } + internal async Task TestFormatWorkspaceAsync( string workspaceFilePath, string[] include,