diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs index 99371f49965bb..75836c406e1c4 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis; @@ -18,8 +19,27 @@ internal readonly struct AnalyzerConfigData public AnalyzerConfigData(AnalyzerConfigOptionsResult result) { - ConfigOptions = StructuredAnalyzerConfigOptions.Create(result.AnalyzerOptions); + ConfigOptions = StructuredAnalyzerConfigOptions.Create(GetAggregatedOptions(result)); AnalyzerOptions = result.AnalyzerOptions; TreeOptions = result.TreeOptions; } + + private static ImmutableDictionary GetAggregatedOptions(AnalyzerConfigOptionsResult result) + { + if (result.TreeOptions.IsEmpty) + return result.AnalyzerOptions; + + var builder = ImmutableDictionary.CreateBuilder(result.AnalyzerOptions.KeyComparer, result.AnalyzerOptions.ValueComparer); + builder.AddRange(result.AnalyzerOptions); + + foreach (var (id, severity) in result.TreeOptions) + { + var key = $"dotnet_diagnostic.{id}.severity"; + var value = severity.ToEditorConfigString(); + builder.Add(key, value); + } + + return builder.ToImmutable(); + + } } diff --git a/src/Workspaces/CoreTest/Options/DiagnosticSeverityOptionsTests.cs b/src/Workspaces/CoreTest/Options/DiagnosticSeverityOptionsTests.cs new file mode 100644 index 0000000000000..f9912f04608d6 --- /dev/null +++ b/src/Workspaces/CoreTest/Options/DiagnosticSeverityOptionsTests.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Test.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Test.Utilities; +using Xunit; +using static Microsoft.CodeAnalysis.UnitTests.SolutionTestHelpers; + +namespace Microsoft.CodeAnalysis.UnitTests.Options +{ + [UseExportProvider] + [Trait(Traits.Feature, Traits.Features.Workspace)] + public class DiagnosticSeverityOptionsTests : TestBase + { + [Theory, CombinatorialData] + public async Task TestGetEffectiveSeverityFromProjectOptions(bool testGlobalConfig, bool testBulkConfiguration) + { + using var workspace = CreateWorkspace(); + + var projectId = ProjectId.CreateNewId(); + var project = workspace.CurrentSolution + .AddProject(projectId, "proj1", "proj1.dll", LanguageNames.CSharp) + .AddDocument(DocumentId.CreateNewId(projectId), "goo.cs", SourceText.From("public class Goo { }", Encoding.UTF8, SourceHashAlgorithms.Default), filePath: @"z:\\goo.cs") + .Projects.Single(); + + var descriptor = new DiagnosticDescriptor("ID1000", "Title", "Message", "Category", DiagnosticSeverity.Warning, isEnabledByDefault: true); + var editorConfigHeader = testGlobalConfig ? "is_global = true" : "[*.cs]"; + var editorConfigEntry = testBulkConfiguration + ? "dotnet_analyzer_diagnostic.severity = error" + : "dotnet_diagnostic.ID1000.severity = error"; + var analyzerConfigText = $@" +{editorConfigHeader} +{editorConfigEntry} +"; + project = project.AddAnalyzerConfigDocument(".editorconfig", SourceText.From(analyzerConfigText), filePath: @"z:\\.editorconfig").Project; + + workspace.TryApplyChanges(project.Solution); + + var document = project.Documents.Single(); + var tree = await document.GetRequiredSyntaxTreeAsync(CancellationToken.None).ConfigureAwait(false); + var optionsProvider = project.AnalyzerOptions.AnalyzerConfigOptionsProvider; + var analyzerConfigOptions = testGlobalConfig ? optionsProvider.GlobalOptions : optionsProvider.GetOptions(tree); + Assert.Equal(ReportDiagnostic.Error, descriptor.GetEffectiveSeverity(analyzerConfigOptions)); + + var compilation = await project.GetRequiredCompilationAsync(CancellationToken.None).ConfigureAwait(false); + Assert.Equal(ReportDiagnostic.Error, descriptor.GetEffectiveSeverity(compilation.Options, tree, project.AnalyzerOptions)); + } + } +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs index eada350447e61..292fab1b059ef 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs @@ -55,6 +55,41 @@ public static ReportDiagnostic GetEffectiveSeverity( return effectiveSeverity; } + /// + /// Gets document-level effective severity of the given accounting for severity configurations from both the following sources: + /// 1. Compilation options from ruleset file, if any, and command line options such as /nowarn, /warnaserror, etc. + /// 2. Analyzer config documents at the document root directory or in ancestor directories. + /// + public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor descriptor, CompilationOptions compilationOptions, SyntaxTree tree, AnalyzerOptions analyzerOptions) + { + var effectiveSeverity = descriptor.GetEffectiveSeverity(compilationOptions); + + // Apply analyzer config options, unless configured with a non-default value in compilation options. + // Note that compilation options (/nowarn, /warnaserror) override analyzer config options. + if (!compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var reportDiagnostic) || + reportDiagnostic == ReportDiagnostic.Default) + { + // First check for tree-level analyzer config options. + var analyzerConfigOptions = analyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(tree); + var severityInEditorConfig = descriptor.GetEffectiveSeverity(analyzerConfigOptions); + if (severityInEditorConfig != ReportDiagnostic.Default) + { + effectiveSeverity = severityInEditorConfig; + } + else + { + // If not found, check for global analyzer config options. + var severityInGlobalConfig = descriptor.GetEffectiveSeverity(analyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions); + if (severityInGlobalConfig != ReportDiagnostic.Default) + { + effectiveSeverity = severityInGlobalConfig; + } + } + } + + return effectiveSeverity; + } + public static bool IsDefinedInEditorConfig(this DiagnosticDescriptor descriptor, AnalyzerConfigOptions analyzerConfigOptions) { // Check if the option is defined explicitly in the editorconfig