diff --git a/Documentation/Diagnostics/PH2163.md b/Documentation/Diagnostics/PH2163.md new file mode 100644 index 000000000..4b32cfcdf --- /dev/null +++ b/Documentation/Diagnostics/PH2163.md @@ -0,0 +1,49 @@ +# PH2163: Avoid NoWarn for analyzer suppression + +| Property | Value | +|--|--| +| Package | [Philips.CodeAnalysis.MaintainabilityAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MaintainabilityAnalyzers) | +| Diagnostic ID | PH2163 | +| Category | [Maintainability](../Maintainability.md) | +| Analyzer | [AvoidNoWarnAnalyzerSuppressionAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzer.cs) +| CodeFix | No | +| Severity | Error | +| Enabled By Default | Yes | + +## Introduction + +The NoWarn project setting should be avoided for suppressing diagnostic warnings. Instead, use .editorconfig files which provide better maintainability, team consistency, and granular control over diagnostic severity. + +This analyzer detects when `` elements are used in project files (.csproj, .vbproj) and suggests using .editorconfig configuration instead. + +## Example + +### Bad +```xml + + + $(NoWarn);CS8002;PH2071 + + +``` + +### Good +```ini +# .editorconfig +[*.cs] +dotnet_diagnostic.CS8002.severity = none +dotnet_diagnostic.PH2071.severity = none +``` + +## Configuration + +This analyzer does not offer any special configuration. The general ways of [suppressing](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings) diagnostics apply. +Solution-level analyzers are enabled by default. To configure, consider using a [.globalconfig](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-files#global-analyzerconfig) file. + +## Benefits of .editorconfig over NoWarn + +1. **Granular control**: Configure different severities (none, suggestion, warning, error) rather than just on/off +2. **File-specific configuration**: Apply rules to specific file patterns +3. **Team consistency**: Shared configuration that works across different IDEs and tools +4. **Version control friendly**: Easily track and review changes to analyzer configuration +5. **IDE integration**: Better support in Visual Studio and other editors \ No newline at end of file diff --git a/Philips.CodeAnalysis.Common/DiagnosticId.cs b/Philips.CodeAnalysis.Common/DiagnosticId.cs index 60aa34ec8..da7310312 100644 --- a/Philips.CodeAnalysis.Common/DiagnosticId.cs +++ b/Philips.CodeAnalysis.Common/DiagnosticId.cs @@ -145,5 +145,6 @@ public enum DiagnosticId AvoidUnlicensedPackages = 2155, AvoidPkcsPaddingWithRsaEncryption = 2158, AvoidUnnecessaryAttributeParentheses = 2159, + AvoidNoWarnAnalyzerSuppression = 2163, } } diff --git a/Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzer.cs b/Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzer.cs new file mode 100644 index 000000000..0b6c199b8 --- /dev/null +++ b/Philips.CodeAnalysis.MaintainabilityAnalyzers/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzer.cs @@ -0,0 +1,109 @@ +// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Philips.CodeAnalysis.Common; + +namespace Philips.CodeAnalysis.MaintainabilityAnalyzers.Maintainability +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class AvoidNoWarnAnalyzerSuppressionAnalyzer : SolutionAnalyzer + { + private const string Title = @"Avoid NoWarn for analyzer suppression"; + private const string MessageFormat = @"Use .editorconfig instead of NoWarn project setting to suppress diagnostics. Configure with 'dotnet_diagnostic.{id}.severity = none'."; + private const string Description = @"NoWarn project settings should be avoided for diagnostic suppression. Use .editorconfig files for better maintainability and team consistency."; + + public AvoidNoWarnAnalyzerSuppressionAnalyzer() + : base(DiagnosticId.AvoidNoWarnAnalyzerSuppression, Title, MessageFormat, Description, Categories.Maintainability, isEnabled: true) + { + } + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationAction(AnalyzeCompilation); + } + + private void AnalyzeCompilation(CompilationAnalysisContext context) + { + var projectFilePath = TryFindProjectFileFromSourcePaths(context); + if (string.IsNullOrEmpty(projectFilePath) || !File.Exists(projectFilePath)) + { + return; + } + + AnalyzeProjectFile(context, projectFilePath); + } + + private void AnalyzeProjectFile(CompilationAnalysisContext context, string projectFilePath) + { + var content = File.ReadAllText(projectFilePath); + + // Simple check for NoWarn elements - avoid XDocument overhead + var lowerContent = content.ToLowerInvariant(); + if (lowerContent.Contains("") || lowerContent.Contains(" sourceDirectories = GetSourceDirectories(context); + + foreach (var sourceDir in sourceDirectories) + { + var foundPath = SearchForProjectFile(sourceDir); + if (foundPath != null) + { + return foundPath; + } + } + + return null; + } + + private static List GetSourceDirectories(CompilationAnalysisContext context) + { + return context.Compilation.SyntaxTrees + .Where(tree => !string.IsNullOrEmpty(tree.FilePath)) + .Select(tree => Path.GetDirectoryName(tree.FilePath)) + .Where(dir => !string.IsNullOrEmpty(dir)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private static string SearchForProjectFile(string startDirectory) + { + if (string.IsNullOrEmpty(startDirectory) || !Directory.Exists(startDirectory)) + { + return null; + } + + var currentDir = new DirectoryInfo(startDirectory); + + // Search up the directory tree for project files + while (currentDir != null) + { + IEnumerable projectFiles = currentDir.GetFiles("*.csproj").Concat(currentDir.GetFiles("*.vbproj")); + FileInfo projectFile = projectFiles.FirstOrDefault(); + + if (projectFile != null) + { + return projectFile.FullName; + } + + currentDir = currentDir.Parent; + } + + return null; + } + } +} \ No newline at end of file diff --git a/Philips.CodeAnalysis.Test/Maintainability/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzerTest.cs b/Philips.CodeAnalysis.Test/Maintainability/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzerTest.cs new file mode 100644 index 000000000..71873a3e8 --- /dev/null +++ b/Philips.CodeAnalysis.Test/Maintainability/Maintainability/AvoidNoWarnAnalyzerSuppressionAnalyzerTest.cs @@ -0,0 +1,60 @@ +// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Philips.CodeAnalysis.MaintainabilityAnalyzers.Maintainability; +using Philips.CodeAnalysis.Test.Helpers; +using Philips.CodeAnalysis.Test.Verifiers; + +namespace Philips.CodeAnalysis.Test.Maintainability.Maintainability +{ + [TestClass] + public class AvoidNoWarnAnalyzerSuppressionAnalyzerTest : DiagnosticVerifier + { + protected override DiagnosticAnalyzer GetDiagnosticAnalyzer() + { + return new AvoidNoWarnAnalyzerSuppressionAnalyzer(); + } + + private string GetTestCode() + { + return @" +class Foo +{ + public void DoSomething() + { + var x = 1; + } +} +"; + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public async Task AvoidNoWarnAnalyzerSuppressionAnalyzerSuccessfulCompilationNoErrorAsync() + { + await VerifySuccessfulCompilation(GetTestCode()).ConfigureAwait(false); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public void AvoidNoWarnAnalyzerSuppressionAnalyzerHasCorrectDiagnosticId() + { + var analyzer = new AvoidNoWarnAnalyzerSuppressionAnalyzer(); + System.Collections.Immutable.ImmutableArray descriptors = analyzer.SupportedDiagnostics; + Assert.HasCount(1, descriptors); + Assert.IsTrue(descriptors.Any(d => d.Id == "PH2163")); + } + + [TestMethod] + [TestCategory(TestDefinitions.UnitTests)] + public void ExtractAnalyzerCodesTest() + { + // Test the static method indirectly by ensuring analyzer doesn't crash + var analyzer = new AvoidNoWarnAnalyzerSuppressionAnalyzer(); + Assert.IsNotNull(analyzer); + } + } +} \ No newline at end of file