diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs index 64ec7898fe01e..af8711db437a8 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Editor.EditorConfigSettings.Extensions; @@ -51,10 +52,9 @@ protected void Update() return; } - var configOptionsProvider = new ProjectState.ProjectAnalyzerConfigOptionsProvider(project.State); - var workspaceOptions = configOptionsProvider.GetOptionsForSourcePath(givenFolder.FullName); + var configData = project.State.GetAnalyzerOptionsForPath(givenFolder.FullName, CancellationToken.None); var result = project.GetAnalyzerConfigOptions(); - var options = new CombinedAnalyzerConfigOptions(workspaceOptions, result); + var options = new CombinedAnalyzerConfigOptions(configData.ConfigOptions, result); UpdateOptions(options, Workspace.Options); } diff --git a/src/EditorFeatures/Core/Formatting/InferredIndentationDocumentOptionsProviderFactory.cs b/src/EditorFeatures/Core/Formatting/InferredIndentationDocumentOptionsProviderFactory.cs deleted file mode 100644 index dd88c7cea9c41..0000000000000 --- a/src/EditorFeatures/Core/Formatting/InferredIndentationDocumentOptionsProviderFactory.cs +++ /dev/null @@ -1,109 +0,0 @@ -// 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; -using System.ComponentModel.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Text; -using Microsoft.VisualStudio.Text; -using Microsoft.VisualStudio.Text.Editor; -using Microsoft.VisualStudio.Utilities; - -namespace Microsoft.CodeAnalysis.Formatting -{ - [Export(typeof(IDocumentOptionsProviderFactory))] - [Order(After = PredefinedDocumentOptionsProviderNames.EditorConfig)] - internal sealed class InferredIndentationDocumentOptionsProviderFactory : IDocumentOptionsProviderFactory - { - private readonly IIndentationManagerService _indentationManagerService; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public InferredIndentationDocumentOptionsProviderFactory(IIndentationManagerService indentationManagerService) - => _indentationManagerService = indentationManagerService; - - public IDocumentOptionsProvider? TryCreate(Workspace workspace) - => new DocumentOptionsProvider(_indentationManagerService); - - private class DocumentOptionsProvider : IDocumentOptionsProvider - { - private readonly IIndentationManagerService _indentationManagerService; - - public DocumentOptionsProvider(IIndentationManagerService indentationManagerService) - => _indentationManagerService = indentationManagerService; - - public Task GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken) - => Task.FromResult(new DocumentOptions(document.Project.Solution.Workspace, document.Id, _indentationManagerService)); - - private sealed class DocumentOptions : IDocumentOptions - { - private readonly Workspace _workspace; - private readonly DocumentId _documentId; - private readonly IIndentationManagerService _indentationManagerService; - - public DocumentOptions(Workspace workspace, DocumentId id, IIndentationManagerService indentationManagerService) - { - _workspace = workspace; - _documentId = id; - _indentationManagerService = indentationManagerService; - } - - public bool TryGetDocumentOption(OptionKey option, out object? value) - { - // We have to go back to the original workspace to see if this document is open, and if so, grab the text container. The API - // from the editor is defined on a text buffer, and once a Document is forked it's definitely not holding onto a buffer anymore. - if (_workspace.IsDocumentOpen(_documentId)) - { - var currentDocument = _workspace.CurrentSolution.GetDocument(_documentId); - if (currentDocument != null && currentDocument.TryGetText(out var text)) - { - var textBuffer = text.Container.TryGetTextBuffer(); - - if (textBuffer != null) - { - return TryGetOptionForBuffer(textBuffer, option, out value); - } - else - { - FatalError.ReportAndCatch(new System.Exception("We had an open document but it wasn't associated with a buffer. That meant we coudln't apply formatting settings.")); - } - } - } - - value = null; - return false; - } - - private bool TryGetOptionForBuffer(ITextBuffer textBuffer, OptionKey option, out object? value) - { - if (option.Option == FormattingOptions.UseTabs) - { - value = !_indentationManagerService.UseSpacesForWhitespace(textBuffer, explicitFormat: false); - return true; - } - else if (option.Option == FormattingOptions.TabSize) - { - value = _indentationManagerService.GetTabSize(textBuffer, explicitFormat: false); - return true; - } - else if (option.Option == FormattingOptions.IndentationSize) - { - value = _indentationManagerService.GetIndentSize(textBuffer, explicitFormat: false); - return true; - } - else - { - value = null; - return false; - } - } - } - } - } -} diff --git a/src/EditorFeatures/Core/Formatting/LegacyIndentationManagerWorkspaceService.cs b/src/EditorFeatures/Core/Formatting/LegacyIndentationManagerWorkspaceService.cs new file mode 100644 index 0000000000000..75aa893b288c4 --- /dev/null +++ b/src/EditorFeatures/Core/Formatting/LegacyIndentationManagerWorkspaceService.cs @@ -0,0 +1,40 @@ +// 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; +using System.Composition; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; +using Microsoft.VisualStudio.Text; +using Microsoft.VisualStudio.Text.Editor; + +namespace Microsoft.CodeAnalysis.Formatting; + +[ExportWorkspaceService(typeof(ILegacyIndentationManagerWorkspaceService)), Shared] +internal sealed class LegacyIndentationManagerWorkspaceService : ILegacyIndentationManagerWorkspaceService +{ + private readonly IIndentationManagerService _indentationManagerService; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public LegacyIndentationManagerWorkspaceService(IIndentationManagerService indentationManagerService) + { + _indentationManagerService = indentationManagerService; + } + + private static ITextBuffer GetRequiredTextBuffer(SourceText text) + => text.Container.TryGetTextBuffer() ?? throw new InvalidOperationException( + "We had an open document but it wasn't associated with a buffer. That meant we couldn't apply formatting settings."); + + public bool UseSpacesForWhitespace(SourceText text) + => _indentationManagerService.UseSpacesForWhitespace(GetRequiredTextBuffer(text), explicitFormat: false); + + public int GetTabSize(SourceText text) + => _indentationManagerService.GetTabSize(GetRequiredTextBuffer(text), explicitFormat: false); + + public int GetIndentSize(SourceText text) + => _indentationManagerService.GetIndentSize(GetRequiredTextBuffer(text), explicitFormat: false); +} diff --git a/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs b/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs index c51e3a6f6fc16..2b63d91443dbb 100644 --- a/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs +++ b/src/EditorFeatures/Core/Options/LegacyGlobalOptionsWorkspaceService.cs @@ -4,6 +4,7 @@ using System; using System.Composition; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; namespace Microsoft.CodeAnalysis.Options @@ -22,5 +23,11 @@ public LegacyGlobalOptionsWorkspaceService(IGlobalOptionService globalOptions) { GlobalOptions = globalOptions; } + + public bool RazorUseTabs + => GlobalOptions.GetOption(RazorLineFormattingOptionsStorage.UseTabs); + + public int RazorTabSize + => GlobalOptions.GetOption(RazorLineFormattingOptionsStorage.TabSize); } } diff --git a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs index 3afeea0bb9836..756534e28cf01 100644 --- a/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs +++ b/src/EditorFeatures/TestUtilities/LanguageServer/AbstractLanguageServerProtocolTests.cs @@ -50,8 +50,8 @@ public abstract partial class AbstractLanguageServerProtocolTests private class TestSpanMapperProvider : IDocumentServiceProvider { - TService IDocumentServiceProvider.GetService() - => (TService)(object)new TestSpanMapper(); + TService? IDocumentServiceProvider.GetService() where TService : class + => typeof(TService) == typeof(ISpanMappingService) ? (TService)(object)new TestSpanMapper() : null; } internal class TestSpanMapper : ISpanMappingService diff --git a/src/Features/Core/Portable/ExternalAccess/Razor/Api/RazorDocumentOptionsProviderFactory.cs b/src/Features/Core/Portable/ExternalAccess/Razor/Api/RazorDocumentOptionsProviderFactory.cs deleted file mode 100644 index 87a01af6e36db..0000000000000 --- a/src/Features/Core/Portable/ExternalAccess/Razor/Api/RazorDocumentOptionsProviderFactory.cs +++ /dev/null @@ -1,75 +0,0 @@ -// 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; -using System.Composition; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Options; - -namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Api -{ - [Shared] - [Export(typeof(IDocumentOptionsProviderFactory))] - [ExtensionOrder(Before = PredefinedDocumentOptionsProviderNames.EditorConfig)] - internal sealed class RazorDocumentOptionsProviderFactory : IDocumentOptionsProviderFactory - { - private readonly Lazy _innerRazorDocumentOptionsService; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public RazorDocumentOptionsProviderFactory( - Lazy innerRazorDocumentOptionsService) - { - if (innerRazorDocumentOptionsService is null) - { - throw new ArgumentNullException(nameof(innerRazorDocumentOptionsService)); - } - - _innerRazorDocumentOptionsService = innerRazorDocumentOptionsService; - } - - public IDocumentOptionsProvider? TryCreate(Workspace workspace) - => new RazorDocumentOptionsProvider(_innerRazorDocumentOptionsService); - - private sealed class RazorDocumentOptionsProvider : IDocumentOptionsProvider - { - public readonly Lazy RazorDocumentOptionsService; - - public RazorDocumentOptionsProvider(Lazy razorDocumentOptionsService) - { - RazorDocumentOptionsService = razorDocumentOptionsService; - } - - public async Task GetOptionsForDocumentAsync( - Document document, - CancellationToken cancellationToken) - { - if (!document.IsRazorDocument()) - { - return null; - } - - var options = await RazorDocumentOptionsService.Value.GetOptionsForDocumentAsync( - document, cancellationToken).ConfigureAwait(false); - return new RazorDocumentOptions(options); - } - } - - // Used to convert IRazorDocumentOptions -> IDocumentOptions - private sealed class RazorDocumentOptions : IDocumentOptions - { - private readonly IRazorDocumentOptions _razorOptions; - - public RazorDocumentOptions(IRazorDocumentOptions razorOptions) - { - _razorOptions = razorOptions; - } - - public bool TryGetDocumentOption(OptionKey option, out object? value) - => _razorOptions.TryGetDocumentOption(option, out value); - } - } -} diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index 034940b2462f6..33b6e4642e081 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -372,16 +372,12 @@ private IReadOnlyList GetStateSetsForFullSolutionAnalysis(IEnumerable< stateSets = stateSets.Where(s => !s.FromBuild(project.Id)); } - // Compute analyzer config options for computing effective severity. - // Note that these options are not cached onto the project, so we compute it once upfront. - var analyzerConfigOptions = project.GetAnalyzerConfigOptions(); - // Include only analyzers we want to run for full solution analysis. // Analyzers not included here will never be saved because result is unknown. - return stateSets.Where(s => IsCandidateForFullSolutionAnalysis(s.Analyzer, project, analyzerConfigOptions)).ToList(); + return stateSets.Where(s => IsCandidateForFullSolutionAnalysis(s.Analyzer, project)).ToList(); } - private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Project project, AnalyzerConfigData? analyzerConfigOptions) + private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Project project) { // PERF: Don't query descriptors for compiler analyzer or workspace load analyzer, always execute them. if (analyzer == FileContentLoadAnalyzer.Instance || @@ -413,7 +409,9 @@ private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Pro // For most of analyzers, the number of diagnostic descriptors is small, so this should be cheap. var descriptors = DiagnosticAnalyzerInfoCache.GetDiagnosticDescriptors(analyzer); - return descriptors.Any(d => d.GetEffectiveSeverity(project.CompilationOptions!, analyzerConfigOptions?.Result) != ReportDiagnostic.Hidden); + var analyzerConfigOptions = project.GetAnalyzerConfigOptions(); + + return descriptors.Any(d => d.GetEffectiveSeverity(project.CompilationOptions!, analyzerConfigOptions?.AnalyzerOptions, analyzerConfigOptions?.TreeOptions) != ReportDiagnostic.Hidden); } private void RaiseProjectDiagnosticsIfNeeded( diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs index 89101c51cfdba..2be007a5001e7 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs @@ -296,7 +296,7 @@ private void UpdateSeverityMenuItemsChecked() foreach (var diagnosticItem in group) { - var severity = diagnosticItem.Descriptor.GetEffectiveSeverity(project.CompilationOptions, analyzerConfigOptions?.Result); + var severity = diagnosticItem.Descriptor.GetEffectiveSeverity(project.CompilationOptions, analyzerConfigOptions?.AnalyzerOptions, analyzerConfigOptions?.TreeOptions); selectedItemSeverities.Add(severity); } } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs index 114541cfd6522..61f7fdfedb9d2 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs @@ -117,7 +117,7 @@ private BulkObservableCollection CreateDiagnosticAndGeneratorItems(Pro .Select(g => { var selectedDiagnostic = g.OrderBy(d => d, s_comparer).First(); - var effectiveSeverity = selectedDiagnostic.GetEffectiveSeverity(options, analyzerConfigOptions?.Result); + var effectiveSeverity = selectedDiagnostic.GetEffectiveSeverity(options, analyzerConfigOptions?.AnalyzerOptions, analyzerConfigOptions?.TreeOptions); return new DiagnosticItem(projectId, AnalyzerReference, selectedDiagnostic, effectiveSeverity, CommandHandler); })); @@ -183,7 +183,7 @@ void OnProjectConfigurationChanged() foreach (var item in _items.OfType()) { - var effectiveSeverity = item.Descriptor.GetEffectiveSeverity(project.CompilationOptions, newAnalyzerConfigOptions?.Result); + var effectiveSeverity = item.Descriptor.GetEffectiveSeverity(project.CompilationOptions, newAnalyzerConfigOptions?.AnalyzerOptions, newAnalyzerConfigOptions?.TreeOptions); item.UpdateEffectiveSeverity(effectiveSeverity); } } diff --git a/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs b/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs index bacb11927cb04..84af82e9c2268 100644 --- a/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs +++ b/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs @@ -10,6 +10,7 @@ using Roslyn.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Diagnostics; +using static Microsoft.CodeAnalysis.ProjectState; namespace Microsoft.CodeAnalysis.Options.EditorConfig { @@ -22,8 +23,9 @@ private sealed class EditorConfigDocumentOptionsProvider : IDocumentOptionsProvi { public async Task GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken) { - var data = await document.GetAnalyzerOptionsAsync(cancellationToken).ConfigureAwait(false); - return new DocumentOptions(data?.AnalyzerConfigOptions); + var provider = (ProjectAnalyzerConfigOptionsProvider)document.Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider; + var options = await provider.GetOptionsAsync(document.DocumentState, cancellationToken).ConfigureAwait(false); + return new DocumentOptions(options); } private sealed class DocumentOptions : IDocumentOptions diff --git a/src/Workspaces/Core/Portable/Options/ILegacyGlobalOptionsWorkspaceService.cs b/src/Workspaces/Core/Portable/Options/ILegacyGlobalOptionsWorkspaceService.cs index 28ef99b1777dc..09e25ff7610f1 100644 --- a/src/Workspaces/Core/Portable/Options/ILegacyGlobalOptionsWorkspaceService.cs +++ b/src/Workspaces/Core/Portable/Options/ILegacyGlobalOptionsWorkspaceService.cs @@ -13,5 +13,8 @@ namespace Microsoft.CodeAnalysis.Options internal interface ILegacyGlobalOptionsWorkspaceService : IWorkspaceService { public IGlobalOptionService GlobalOptions { get; } + + public bool RazorUseTabs { get; } + public int RazorTabSize { get; } } } diff --git a/src/Workspaces/Core/Portable/Options/ILegacyIndentationManagerWorkspaceService.cs b/src/Workspaces/Core/Portable/Options/ILegacyIndentationManagerWorkspaceService.cs new file mode 100644 index 0000000000000..2511f3c7af9a4 --- /dev/null +++ b/src/Workspaces/Core/Portable/Options/ILegacyIndentationManagerWorkspaceService.cs @@ -0,0 +1,22 @@ +// 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; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Formatting; + +/// +/// Enables legacy APIs to access indentation inference editor APIs from workspace. +/// https://github.com/dotnet/roslyn/issues/61109 +/// +internal interface ILegacyIndentationManagerWorkspaceService : IWorkspaceService +{ + bool UseSpacesForWhitespace(SourceText text); + int GetTabSize(SourceText text); + int GetIndentSize(SourceText text); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs index f28f82c91a99c..5098275406113 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/DocumentService/Extensions.cs @@ -8,7 +8,7 @@ namespace Microsoft.CodeAnalysis.Host { internal static class Extensions { - private const string RazorCSharp = "RazorCSharp"; + private const string RazorCSharpLspClientName = "RazorCSharp"; public static bool CanApplyChange([NotNullWhen(returnValue: true)] this TextDocument? document) => document?.State.CanApplyChange() ?? false; @@ -23,6 +23,9 @@ public static bool SupportsDiagnostics([NotNullWhen(returnValue: true)] this Tex => document?.Services.GetService()?.SupportDiagnostics ?? false; public static bool IsRazorDocument(this TextDocument document) - => document.Services.GetService()?.DiagnosticsLspClientName == RazorCSharp; + => IsRazorDocument(document.State); + + public static bool IsRazorDocument(this TextDocumentState documentState) + => documentState.Services.GetService()?.DiagnosticsLspClientName == RazorCSharpLspClientName; } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs index c417192872bb6..99371f49965bb 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs @@ -12,17 +12,14 @@ namespace Microsoft.CodeAnalysis; /// internal readonly struct AnalyzerConfigData { - private readonly AnalyzerConfigOptionsResult _result; - private readonly StructuredAnalyzerConfigOptions _configOptions; + public readonly StructuredAnalyzerConfigOptions ConfigOptions; + public readonly ImmutableDictionary AnalyzerOptions; + public readonly ImmutableDictionary TreeOptions; public AnalyzerConfigData(AnalyzerConfigOptionsResult result) { - _result = result; - _configOptions = new StructuredAnalyzerConfigOptions(result.AnalyzerOptions); + ConfigOptions = StructuredAnalyzerConfigOptions.Create(result.AnalyzerOptions); + AnalyzerOptions = result.AnalyzerOptions; + TreeOptions = result.TreeOptions; } - - public AnalyzerConfigOptionsResult Result => _result; - public StructuredAnalyzerConfigOptions AnalyzerConfigOptions => _configOptions; - public ImmutableDictionary AnalyzerOptions => _result.AnalyzerOptions; - public ImmutableDictionary TreeOptions => _result.TreeOptions; } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index 7f4f520e8d25a..27f7c17b2f5fc 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -512,35 +512,5 @@ private void InitializeCachedOptions(OptionSet solutionOptions) Interlocked.CompareExchange(ref _cachedOptions, newAsyncLazy, comparand: null); } - - internal async Task GetAnalyzerOptionsAsync(CancellationToken cancellationToken) - { - var projectFilePath = Project.FilePath; - // We need to work out path to this document. Documents may not have a "real" file path if they're something created - // as a part of a code action, but haven't been written to disk yet. - string? effectiveFilePath = null; - - if (FilePath != null) - { - effectiveFilePath = FilePath; - } - else if (Name != null && projectFilePath != null) - { - var projectPath = PathUtilities.GetDirectoryName(projectFilePath); - - if (!RoslynString.IsNullOrEmpty(projectPath) && - PathUtilities.GetDirectoryName(projectFilePath) is string directory) - { - effectiveFilePath = PathUtilities.CombinePathsUnchecked(directory, Name); - } - } - - if (effectiveFilePath == null) - { - return null; - } - - return await Project.State.GetAnalyzerOptionsForPathAsync(effectiveFilePath, cancellationToken).ConfigureAwait(false); - } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index 9370c87f3b2c6..7e892afb42c48 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -13,7 +13,11 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; +using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Serialization; using Roslyn.Utilities; @@ -254,42 +258,46 @@ public async Task GetAnalyzerOptionsForPathAsync(string path return cache.GetOptionsForSourcePath(path); } + public AnalyzerConfigData GetAnalyzerOptionsForPath(string path, CancellationToken cancellationToken) + => _lazyAnalyzerConfigOptions.GetValue(cancellationToken).GetOptionsForSourcePath(path); + public AnalyzerConfigData? GetAnalyzerConfigOptions() { - // We need to find the analyzer config options at the root of the project. - // Currently, there is no compiler API to query analyzer config options for a directory in a language agnostic fashion. - // So, we use a dummy language-specific file name appended to the project directory to query analyzer config options. + var extension = _projectInfo.Language switch + { + LanguageNames.CSharp => ".cs", + LanguageNames.VisualBasic => ".vb", + _ => null + }; - var projectDirectory = PathUtilities.GetDirectoryName(_projectInfo.FilePath); - if (!PathUtilities.IsAbsolute(projectDirectory)) + if (extension == null) { return null; } - var fileName = Guid.NewGuid().ToString(); - string sourceFilePath; - switch (_projectInfo.Language) + if (!PathUtilities.IsAbsolute(_projectInfo.FilePath)) { - case LanguageNames.CSharp: - // Suppression should be removed or addressed https://github.com/dotnet/roslyn/issues/41636 - sourceFilePath = PathUtilities.CombineAbsoluteAndRelativePaths(projectDirectory, $"{fileName}.cs")!; - break; + return null; + } - case LanguageNames.VisualBasic: - // Suppression should be removed or addressed https://github.com/dotnet/roslyn/issues/41636 - sourceFilePath = PathUtilities.CombineAbsoluteAndRelativePaths(projectDirectory, $"{fileName}.vb")!; - break; + // We need to find the analyzer config options at the root of the project. + // Currently, there is no compiler API to query analyzer config options for a directory in a language agnostic fashion. + // So, we use a dummy language-specific file name appended to the project directory to query analyzer config options. + // NIL character is invalid in paths so it will never match any pattern in editorconfig, but editorconfig parsing allows it. + // TODO: https://github.com/dotnet/roslyn/issues/61217 - default: - return null; - } + var projectDirectory = PathUtilities.GetDirectoryName(_projectInfo.FilePath); + Contract.ThrowIfNull(projectDirectory); + + var sourceFilePath = PathUtilities.CombinePathsUnchecked(projectDirectory, "\0" + extension); - return _lazyAnalyzerConfigOptions.GetValue(CancellationToken.None).GetOptionsForSourcePath(sourceFilePath); + return GetAnalyzerOptionsForPath(sourceFilePath, CancellationToken.None); } internal sealed class ProjectAnalyzerConfigOptionsProvider : AnalyzerConfigOptionsProvider { private readonly ProjectState _projectState; + private RazorDesignTimeAnalyzerConfigOptions? _lazyRazorDesignTimeOptions = null; public ProjectAnalyzerConfigOptionsProvider(ProjectState projectState) => _projectState = projectState; @@ -298,19 +306,200 @@ private AnalyzerConfigOptionsCache GetCache() => _projectState._lazyAnalyzerConfigOptions.GetValue(CancellationToken.None); public override AnalyzerConfigOptions GlobalOptions - => GetCache().GlobalConfigOptions.AnalyzerConfigOptions; + => GetCache().GlobalConfigOptions.ConfigOptions; public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) - => GetOptionsForSourcePath(tree.FilePath); + { + var documentId = DocumentState.GetDocumentIdForTree(tree); + var cache = GetCache(); + if (documentId != null && _projectState.DocumentStates.TryGetState(documentId, out var documentState)) + { + var result = GetOptions(cache, documentState); + if (result != null) + { + return result; + } + } + + return GetOptionsForSourcePath(cache, tree.FilePath); + } + + internal async ValueTask GetOptionsAsync(DocumentState documentState, CancellationToken cancellationToken) + { + var cache = await _projectState._lazyAnalyzerConfigOptions.GetValueAsync(cancellationToken).ConfigureAwait(false); + return GetOptions(cache, documentState); + } + + private StructuredAnalyzerConfigOptions? GetOptions(in AnalyzerConfigOptionsCache cache, DocumentState documentState) + { + if (documentState.IsRazorDocument()) + { + return _lazyRazorDesignTimeOptions ??= new RazorDesignTimeAnalyzerConfigOptions(_projectState.LanguageServices.WorkspaceServices); + } + + var filePath = GetEffectiveFilePath(documentState); + if (filePath == null) + { + return null; + } + + var options = GetOptionsForSourcePath(cache, filePath); + var workspace = _projectState._solutionServices.Workspace; + + var legacyIndentationService = workspace.Services.GetService(); + if (legacyIndentationService == null) + { + return options; + } + + return new AnalyzerConfigWithInferredIndentationOptions(options, workspace, legacyIndentationService, documentState.Id); + } public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) { // TODO: correctly find the file path, since it looks like we give this the document's .Name under the covers if we don't have one - return GetOptionsForSourcePath(textFile.Path); + return GetOptionsForSourcePath(GetCache(), textFile.Path); } - public AnalyzerConfigOptions GetOptionsForSourcePath(string path) - => GetCache().GetOptionsForSourcePath(path).AnalyzerConfigOptions; + private static StructuredAnalyzerConfigOptions GetOptionsForSourcePath(in AnalyzerConfigOptionsCache cache, string path) + => cache.GetOptionsForSourcePath(path).ConfigOptions; + + private string? GetEffectiveFilePath(DocumentState documentState) + { + if (!string.IsNullOrEmpty(documentState.FilePath)) + { + return documentState.FilePath; + } + + // We need to work out path to this document. Documents may not have a "real" file path if they're something created + // as a part of a code action, but haven't been written to disk yet. + + var projectFilePath = _projectState.FilePath; + + if (documentState.Name != null && projectFilePath != null) + { + var projectPath = PathUtilities.GetDirectoryName(projectFilePath); + + if (!RoslynString.IsNullOrEmpty(projectPath) && + PathUtilities.GetDirectoryName(projectFilePath) is string directory) + { + return PathUtilities.CombinePathsUnchecked(directory, documentState.Name); + } + } + + return null; + } + } + + /// + /// Provides editorconfig options for Razor design-time documents. + /// Razor does not support editorconfig options but has custom settings for a few formatting options whose values + /// are only available in-proc and the same for all Razor design-time documents. + /// This type emulates these options as analyzer config options. + /// + private sealed class RazorDesignTimeAnalyzerConfigOptions : StructuredAnalyzerConfigOptions + { + private readonly ILegacyGlobalOptionsWorkspaceService? _globalOptions; + + public RazorDesignTimeAnalyzerConfigOptions(HostWorkspaceServices services) + { + // not available OOP: + _globalOptions = services.GetService(); + } + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + if (_globalOptions != null) + { + if (key == "indent_style") + { + value = _globalOptions.RazorUseTabs ? "tab" : "space"; + return true; + } + + if (key == "tab_width" || key == "indent_size") + { + value = _globalOptions.RazorTabSize.ToString(); + return true; + } + } + + value = null; + return false; + } + + public override IEnumerable Keys + { + get + { + if (_globalOptions != null) + { + yield return "indent_style"; + yield return "tab_width"; + yield return "indent_size"; + } + } + } + + public override NamingStylePreferences GetNamingStylePreferences() + => NamingStylePreferences.Empty; + } + + /// + /// Provides analyzer config options with indentation options overridden by editor indentation inference for open documents. + /// TODO: Remove once https://github.com/dotnet/roslyn/issues/61109 is addressed. + /// + private sealed class AnalyzerConfigWithInferredIndentationOptions : StructuredAnalyzerConfigOptions + { + private readonly Workspace _workspace; + private readonly ILegacyIndentationManagerWorkspaceService _service; + private readonly DocumentId _documentId; + private readonly StructuredAnalyzerConfigOptions _options; + + public AnalyzerConfigWithInferredIndentationOptions(StructuredAnalyzerConfigOptions options, Workspace workspace, ILegacyIndentationManagerWorkspaceService service, DocumentId documentId) + { + _workspace = workspace; + _service = service; + _documentId = documentId; + _options = options; + } + + public override NamingStylePreferences GetNamingStylePreferences() + => _options.GetNamingStylePreferences(); + + public override IEnumerable Keys + => _options.Keys; + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + { + // For open documents override indentation option values with values inferred by the editor: + if (key is "indent_style" or "tab_width" or "indent_size" && + _workspace.IsDocumentOpen(_documentId)) + { + var currentDocument = _workspace.CurrentSolution.GetDocument(_documentId); + if (currentDocument != null && currentDocument.TryGetText(out var text)) + { + try + { + value = key switch + { + "indent_style" => _service.UseSpacesForWhitespace(text) ? "space" : "tab", + "tab_width" => _service.GetTabSize(text).ToString(), + "indent_size" => _service.GetIndentSize(text).ToString(), + _ => throw ExceptionUtilities.UnexpectedValue(key) + }; + + return true; + } + catch (Exception e) when (FatalError.ReportAndCatch(e)) + { + // fall through + } + } + } + + return _options.TryGetValue(key, out value); + } } private sealed class ProjectSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider diff --git a/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs b/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs index fa91961a9e6ac..851afd8ccbe48 100644 --- a/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs +++ b/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs @@ -67,7 +67,7 @@ public void TestParseEditorConfigAccessibilityModifiers(string args, int value, var storageLocation = CodeStyleOptions2.RequireAccessibilityModifiers.StorageLocations .OfType>>() .Single(); - var allRawConventions = new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.EmptyDictionary.Add(storageLocation.KeyName, args)); + var allRawConventions = StructuredAnalyzerConfigOptions.Create(DictionaryAnalyzerConfigOptions.EmptyDictionary.Add(storageLocation.KeyName, args)); Assert.True(storageLocation.TryGetOption(allRawConventions, typeof(CodeStyleOption2), out var parsedCodeStyleOption)); var codeStyleOption = (CodeStyleOption2)parsedCodeStyleOption!; @@ -89,7 +89,7 @@ public void TestParseEditorConfigEndOfLine(string configurationString, string ne var storageLocation = FormattingOptions.NewLine.StorageLocations .OfType>() .Single(); - var allRawConventions = new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.EmptyDictionary.Add(storageLocation.KeyName, configurationString)); + var allRawConventions = StructuredAnalyzerConfigOptions.Create(DictionaryAnalyzerConfigOptions.EmptyDictionary.Add(storageLocation.KeyName, configurationString)); Assert.True(storageLocation.TryGetOption(allRawConventions, typeof(string), out var parsedNewLine)); Assert.Equal(newLine, (string?)parsedNewLine); diff --git a/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs b/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs index 2fd9b59b756f7..28d71b7effce5 100644 --- a/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs +++ b/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs @@ -22,7 +22,7 @@ public class NamingStylePreferenceEditorConfigStorageLocationTests public static void TestEmptyDictionaryReturnNoNamingStylePreferencesObjectReturnsFalse() { var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var result = editorConfigStorageLocation.TryGetOption(new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.Empty), typeof(NamingStylePreferences), out _); + var result = editorConfigStorageLocation.TryGetOption(StructuredAnalyzerConfigOptions.Create(DictionaryAnalyzerConfigOptions.EmptyDictionary), typeof(NamingStylePreferences), out _); Assert.False(result, "Expected TryParseReadonlyDictionary to return 'false' for empty dictionary"); } @@ -30,7 +30,7 @@ public static void TestEmptyDictionaryReturnNoNamingStylePreferencesObjectReturn public static void TestNonEmptyDictionaryReturnsTrue() { var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var options = new StructuredAnalyzerConfigOptions(new Dictionary() + var options = StructuredAnalyzerConfigOptions.Create(new Dictionary() { ["dotnet_naming_rule.methods_and_properties_must_be_pascal_case.severity"] = "error", ["dotnet_naming_rule.methods_and_properties_must_be_pascal_case.symbols"] = "method_and_property_symbols", @@ -53,7 +53,7 @@ public static void TestObjectTypeThrowsInvalidOperationException() var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); Assert.Throws(() => { - editorConfigStorageLocation.TryGetOption(new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.Empty), typeof(object), out var @object); + editorConfigStorageLocation.TryGetOption(StructuredAnalyzerConfigOptions.Create(DictionaryAnalyzerConfigOptions.EmptyDictionary), typeof(object), out var @object); }); } } diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index 3f8445a9501dc..8b24b531764e7 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -3385,5 +3385,52 @@ public async Task ReplacingTextMultipleTimesDoesNotRootIntermediateCopiesIfCompi GC.KeepAlive(finalSolution); } + + [Theory] + [InlineData("a/proj.csproj", "a/.editorconfig", "a/b/test.cs", "*.cs", true, true)] + [InlineData("a/proj.csproj", "a/b/.editorconfig", "a/b/test.cs", "*.cs", false, true)] + [InlineData("a/proj.csproj", "a/.editorconfig", null, "*.cs", true, true)] + [InlineData("a/proj.csproj", "a/b/.editorconfig", null, "*.cs", false, false)] + [InlineData("a/proj.csproj", "a/.editorconfig", "", "*.cs", true, true)] + [InlineData("a/proj.csproj", "a/b/.editorconfig", "", "*.cs", false, false)] + [InlineData(null, "a/.editorconfig", "a/b/test.cs", "*.cs", false, true)] + [InlineData(null, "a/.editorconfig", null, "*.cs", false, false)] + [InlineData("a/proj.csproj", "a/.editorconfig", null, "*test.cs", false, true)] + public async Task EditorConfigOptions(string projectPath, string configPath, string sourcePath, string pattern, bool appliedToEntireProject, bool appliedToDocument) + { + projectPath = string.IsNullOrEmpty(projectPath) ? projectPath : Path.Combine(TempRoot.Root, projectPath); + configPath = Path.Combine(TempRoot.Root, configPath); + sourcePath = string.IsNullOrEmpty(sourcePath) ? sourcePath : Path.Combine(TempRoot.Root, sourcePath); + + using var workspace = CreateWorkspace(); + var projectId = ProjectId.CreateNewId(); + + var projectInfo = ProjectInfo.Create( + projectId, + VersionStamp.Default, + name: "proj1", + assemblyName: "proj1.dll", + language: LanguageNames.CSharp, + filePath: projectPath); + + var documentId = DocumentId.CreateNewId(projectId); + + var solution = workspace.CurrentSolution + .AddProject(projectInfo) + .AddDocument(documentId, "test.cs", SourceText.From("public class C { }"), filePath: sourcePath) + .AddAnalyzerConfigDocument(DocumentId.CreateNewId(projectId), ".editorconfig", SourceText.From($"[{pattern}]\nindent_style = tab"), filePath: configPath); + + var document = solution.GetRequiredDocument(documentId); + + var documentOptions = await document.GetOptionsAsync(CancellationToken.None); + Assert.Equal(appliedToDocument, documentOptions.GetOption(FormattingOptions2.UseTabs)); + + var syntaxTree = await document.GetSyntaxTreeAsync(); + var documentOptionsViaSyntaxTree = document.Project.State.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(syntaxTree); + Assert.Equal(appliedToDocument, documentOptionsViaSyntaxTree.TryGetValue("indent_style", out var value) == true && value == "tab"); + + var projectOptions = document.Project.GetAnalyzerConfigOptions(); + Assert.Equal(appliedToEntireProject, projectOptions?.AnalyzerOptions.TryGetValue("indent_style", out value) == true && value == "tab"); + } } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs index 6fb0cb022c226..041573f48731d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs @@ -17,32 +17,37 @@ namespace Microsoft.CodeAnalysis.Diagnostics; /// that memoize structured (parsed) form of certain complex options to avoid parsing them multiple times. /// Storages of these complex options may directly call the specialized getters to reuse the cached values. /// -internal sealed class StructuredAnalyzerConfigOptions : AnalyzerConfigOptions +internal abstract class StructuredAnalyzerConfigOptions : AnalyzerConfigOptions { - private readonly AnalyzerConfigOptions _options; - private readonly Lazy _lazyNamingStylePreferences; - - public StructuredAnalyzerConfigOptions(AnalyzerConfigOptions options) + internal sealed class Implementation : StructuredAnalyzerConfigOptions { - _options = options; - _lazyNamingStylePreferences = new Lazy(() => EditorConfigNamingStyleParser.ParseDictionary(_options)); + private readonly AnalyzerConfigOptions _options; + private readonly Lazy _lazyNamingStylePreferences; + + public Implementation(AnalyzerConfigOptions options) + { + _options = options; + _lazyNamingStylePreferences = new Lazy(() => EditorConfigNamingStyleParser.ParseDictionary(_options)); + } + + public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) + => _options.TryGetValue(key, out value); + + public override IEnumerable Keys + => _options.Keys; + + public override NamingStylePreferences GetNamingStylePreferences() + => _lazyNamingStylePreferences.Value; } - public StructuredAnalyzerConfigOptions(ImmutableDictionary options) - : this(new DictionaryAnalyzerConfigOptions(options)) + public abstract NamingStylePreferences GetNamingStylePreferences(); + + public static StructuredAnalyzerConfigOptions Create(ImmutableDictionary options) { Contract.ThrowIfFalse(options.KeyComparer == KeyComparer); + return new Implementation(new DictionaryAnalyzerConfigOptions(options)); } - public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) - => _options.TryGetValue(key, out value); - - public override IEnumerable Keys - => _options.Keys; - - public NamingStylePreferences GetNamingStylePreferences() - => _lazyNamingStylePreferences.Value; - public static bool TryGetStructuredOptions(AnalyzerConfigOptions configOptions, [NotNullWhen(true)] out StructuredAnalyzerConfigOptions? options) { if (configOptions is StructuredAnalyzerConfigOptions structuredOptions) @@ -86,7 +91,7 @@ private static bool TryGetCorrespondingCodeStyleInstance(AnalyzerConfigOptions c { if (!s_codeStyleStructuredOptions.TryGetValue(configOptions, out options)) { - options = new StructuredAnalyzerConfigOptions(configOptions); + options = new Implementation(configOptions); s_codeStyleStructuredOptions.Add(configOptions, options); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs index 566c0afd3d3ea..831700cd46652 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/DiagnosticDescriptorExtensions.cs @@ -30,18 +30,22 @@ public static ImmutableArray ImmutableCustomTags(this DiagnosticDescript /// 1. Compilation options from ruleset file, if any, and command line options such as /nowarn, /warnaserror, etc. /// 2. Analyzer config documents at the project root directory or in ancestor directories. /// - public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor descriptor, CompilationOptions compilationOptions, AnalyzerConfigOptionsResult? analyzerConfigOptions) + public static ReportDiagnostic GetEffectiveSeverity( + this DiagnosticDescriptor descriptor, + CompilationOptions compilationOptions, + ImmutableDictionary? analyzerOptions, + ImmutableDictionary? treeOptions) { 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 (analyzerConfigOptions.HasValue && + if (treeOptions != null && analyzerOptions != null && (!compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var reportDiagnostic) || reportDiagnostic == ReportDiagnostic.Default)) { - if (analyzerConfigOptions.Value.TreeOptions.TryGetValue(descriptor.Id, out reportDiagnostic) && reportDiagnostic != ReportDiagnostic.Default || - TryGetSeverityFromBulkConfiguration(descriptor, analyzerConfigOptions.Value, out reportDiagnostic)) + if (treeOptions.TryGetValue(descriptor.Id, out reportDiagnostic) && reportDiagnostic != ReportDiagnostic.Default || + TryGetSeverityFromBulkConfiguration(descriptor, analyzerOptions, out reportDiagnostic)) { Debug.Assert(reportDiagnostic != ReportDiagnostic.Default); effectiveSeverity = reportDiagnostic; @@ -145,11 +149,9 @@ public static ReportDiagnostic GetEffectiveSeverity(this DiagnosticDescriptor de /// private static bool TryGetSeverityFromBulkConfiguration( DiagnosticDescriptor descriptor, - AnalyzerConfigOptionsResult analyzerConfigOptions, + ImmutableDictionary analyzerOptions, out ReportDiagnostic severity) { - Debug.Assert(!analyzerConfigOptions.TreeOptions.ContainsKey(descriptor.Id)); - // Analyzer bulk configuration does not apply to: // 1. Disabled by default diagnostics // 2. Compiler diagnostics @@ -164,7 +166,7 @@ private static bool TryGetSeverityFromBulkConfiguration( // If user has explicitly configured default severity for the diagnostic category, that should be respected. // For example, 'dotnet_analyzer_diagnostic.category-security.severity = error' var categoryBasedKey = $"{DotnetAnalyzerDiagnosticPrefix}.{CategoryPrefix}-{descriptor.Category}.{SeveritySuffix}"; - if (analyzerConfigOptions.AnalyzerOptions.TryGetValue(categoryBasedKey, out var value) && + if (analyzerOptions.TryGetValue(categoryBasedKey, out var value) && EditorConfigSeverityStrings.TryParse(value, out severity)) { return true; @@ -172,7 +174,7 @@ private static bool TryGetSeverityFromBulkConfiguration( // Otherwise, if user has explicitly configured default severity for all analyzer diagnostics, that should be respected. // For example, 'dotnet_analyzer_diagnostic.severity = error' - if (analyzerConfigOptions.AnalyzerOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) && + if (analyzerOptions.TryGetValue(DotnetAnalyzerDiagnosticSeverityKey, out value) && EditorConfigSeverityStrings.TryParse(value, out severity)) { return true;