From 26bb6f8f5efb5fb2dee113c3aac20bc31acfe047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Matou=C5=A1ek?= Date: Thu, 12 May 2022 13:11:48 -0700 Subject: [PATCH] Revert "Revert "Improve editorconfig options caching on ProjectState (#61131)" (#61216)" This reverts commit 49b0071ce13fc2dd97e3f4e63ae85a06222614b3. --- .../DictionaryAnalyzerConfigOptionsTests.cs | 29 ++++ .../AnalyzerConfigOptions.cs | 8 + .../DictionaryAnalyzerConfigOptions.cs | 6 + .../Core/Portable/PublicAPI.Unshipped.txt | 1 + .../EditorConfigNamingStyleParserTests.cs | 111 +++++++++++- .../DataProvider/SettingsProviderBase.cs | 36 +++- ...nfigOptionSet+AnalyzerConfigOptionsImpl.cs | 6 + ...IncrementalAnalyzer_IncrementalAnalyzer.cs | 4 +- .../AnalyzersCommandHandler.cs | 2 +- .../BaseDiagnosticAndGeneratorItemSource.cs | 8 +- ...torConfigDocumentOptionsProviderFactory.cs | 19 ++- .../OptionSet+AnalyzerConfigOptionsImpl.cs | 7 + .../Workspace/Solution/AnalyzerConfigData.cs | 28 +++ .../Portable/Workspace/Solution/Document.cs | 13 +- .../Portable/Workspace/Solution/Project.cs | 2 +- .../Workspace/Solution/ProjectState.cs | 33 ++-- .../EditorConfigCodeStyleParserTests.cs | 9 +- ...ferenceEditorConfigStorageLocationTests.cs | 161 ++---------------- .../Core/CompilerExtensions.projitems | 4 +- .../StructuredAnalyzerConfigOptions.cs | 97 +++++++++++ .../Parsing/EditorConfigParser.cs | 12 +- .../Parsing/IEditorConfigOptionAccumulator.cs | 3 +- .../NamingStyleOptionAccumulator.cs | 8 +- .../AnalyzerConfigOptionsExtensions.cs | 3 +- .../EditorConfigNamingStyleParser.cs | 48 +----- ...ditorConfigNamingStyleParser_NamingRule.cs | 8 +- ...itorConfigNamingStyleParser_NamingStyle.cs | 6 +- ...ditorConfigNamingStyleParser_SymbolSpec.cs | 6 +- .../Serialization/NamingStylePreferences.cs | 4 + .../Serialization/SymbolSpecification.cs | 4 + .../EditorConfigStorageLocationExtensions.cs | 34 ---- .../EditorConfigStorageLocation`1.cs | 7 +- .../IEditorConfigStorageLocation.cs | 6 +- ...lePreferenceEditorConfigStorageLocation.cs | 26 +-- 34 files changed, 439 insertions(+), 320 deletions(-) create mode 100644 src/Compilers/Core/CodeAnalysisTest/Analyzers/DictionaryAnalyzerConfigOptionsTests.cs create mode 100644 src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs create mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs delete mode 100644 src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs diff --git a/src/Compilers/Core/CodeAnalysisTest/Analyzers/DictionaryAnalyzerConfigOptionsTests.cs b/src/Compilers/Core/CodeAnalysisTest/Analyzers/DictionaryAnalyzerConfigOptionsTests.cs new file mode 100644 index 0000000000000..6e80809c66e6e --- /dev/null +++ b/src/Compilers/Core/CodeAnalysisTest/Analyzers/DictionaryAnalyzerConfigOptionsTests.cs @@ -0,0 +1,29 @@ +// 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.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Xunit; + +namespace Microsoft.CodeAnalysis.UnitTests +{ + public class DictionaryAnalyzerConfigOptionsTests + { + [Fact] + public void BackwardCompatibility() + { + // Note: Older versions of analyzers access this field via reflection. + // https://github.com/dotnet/roslyn/blob/8e3d62a30b833631baaa4e84c5892298f16a8c9e/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs#L21 + Assert.Equal( + typeof(ImmutableDictionary), + typeof(DictionaryAnalyzerConfigOptions).GetField("Options", BindingFlags.Instance | BindingFlags.NonPublic)?.FieldType); + } + } +} diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerConfigOptions.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerConfigOptions.cs index 1e98c9cccb6ee..ea5f6b0944e4c 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerConfigOptions.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/AnalyzerConfigOptions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace Microsoft.CodeAnalysis.Diagnostics @@ -19,5 +20,12 @@ public abstract class AnalyzerConfigOptions /// Get an analyzer config value for the given key, using the . /// public abstract bool TryGetValue(string key, [NotNullWhen(true)] out string? value); + + /// + /// Enumerates unique keys of all available options in no specific order. + /// + /// Not implemented by the derived type. + public virtual IEnumerable Keys + => throw new NotImplementedException(); } } diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DictionaryAnalyzerConfigOptions.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DictionaryAnalyzerConfigOptions.cs index 368b6c4176976..48ac671a064ee 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DictionaryAnalyzerConfigOptions.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DictionaryAnalyzerConfigOptions.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; @@ -13,6 +14,8 @@ internal sealed class DictionaryAnalyzerConfigOptions : AnalyzerConfigOptions public static DictionaryAnalyzerConfigOptions Empty { get; } = new DictionaryAnalyzerConfigOptions(EmptyDictionary); + // Note: Do not rename. Older versions of analyzers access this field via reflection. + // https://github.com/dotnet/roslyn/blob/8e3d62a30b833631baaa4e84c5892298f16a8c9e/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs#L21 internal readonly ImmutableDictionary Options; public DictionaryAnalyzerConfigOptions(ImmutableDictionary options) @@ -20,5 +23,8 @@ public DictionaryAnalyzerConfigOptions(ImmutableDictionary optio public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value) => Options.TryGetValue(key, out value); + + public override IEnumerable Keys + => Options.Keys; } } diff --git a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt index 54e2766c89e34..c2331cd21ec09 100644 --- a/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt +++ b/src/Compilers/Core/Portable/PublicAPI.Unshipped.txt @@ -66,6 +66,7 @@ const Microsoft.CodeAnalysis.WellKnownMemberNames.CheckedExplicitConversionName Microsoft.CodeAnalysis.OperationKind.UTF8String = 124 -> Microsoft.CodeAnalysis.OperationKind Microsoft.CodeAnalysis.Operations.IUTF8StringOperation Microsoft.CodeAnalysis.Operations.IUTF8StringOperation.Value.get -> string! +virtual Microsoft.CodeAnalysis.Diagnostics.AnalyzerConfigOptions.Keys.get -> System.Collections.Generic.IEnumerable! virtual Microsoft.CodeAnalysis.Operations.OperationVisitor.VisitUTF8String(Microsoft.CodeAnalysis.Operations.IUTF8StringOperation! operation) -> void virtual Microsoft.CodeAnalysis.Operations.OperationVisitor.VisitUTF8String(Microsoft.CodeAnalysis.Operations.IUTF8StringOperation! operation, TArgument argument) -> TResult? virtual Microsoft.CodeAnalysis.SymbolVisitor.DefaultVisit(Microsoft.CodeAnalysis.ISymbol! symbol, TArgument argument) -> TResult diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/NamingStyles/EditorConfigNamingStyleParserTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/NamingStyles/EditorConfigNamingStyleParserTests.cs index fff2e11b97eea..8fb3042f2c572 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/NamingStyles/EditorConfigNamingStyleParserTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/NamingStyles/EditorConfigNamingStyleParserTests.cs @@ -7,17 +7,20 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.Editor.UnitTests.Diagnostics.NamingStyles; using Roslyn.Test.Utilities; using Xunit; -using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.EditorConfigNamingStyleParser; using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.SymbolSpecification; namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics.NamingStyles { public class EditorConfigNamingStyleParserTests { + private static NamingStylePreferences ParseDictionary(Dictionary options) + => EditorConfigNamingStyleParser.ParseDictionary(new DictionaryAnalyzerConfigOptions(options.ToImmutableDictionary())); + [Fact] public static void TestPascalCaseRule() { @@ -469,5 +472,111 @@ public static void TestEditorConfigParseForApplicableSymbolKinds() Assert.True(!string.IsNullOrEmpty(editorConfigString)); } } + + [Theory] + [InlineData("a", "b", "a", "public", "public, private")] + [InlineData("b", "a", "a", "public, private", "public")] + [InlineData("b", "a", "b", "public", "public, private")] + [InlineData("a", "b", "b", "public, private", "public")] + [InlineData("a", "b", "a", "*", "*")] + [InlineData("b", "a", "a", "*", "*")] + [InlineData("A", "b", "A", "*", "*")] + [InlineData("b", "A", "A", "*", "*")] + [InlineData("a", "B", "a", "*", "*")] + [InlineData("B", "a", "a", "*", "*")] + [InlineData("A", "B", "A", "*", "*")] + [InlineData("B", "A", "A", "*", "*")] + public static void TestOrderedByAccessibilityBeforeName(string firstName, string secondName, string firstNameAfterOrdering, string firstAccessibility, string secondAccessibility) + { + var namingStylePreferences = ParseDictionary(new Dictionary() + { + [$"dotnet_naming_rule.{firstName}.severity"] = "error", + [$"dotnet_naming_rule.{firstName}.symbols"] = "first_symbols", + [$"dotnet_naming_rule.{firstName}.style"] = $"{firstName}_style", + ["dotnet_naming_symbols.first_symbols.applicable_kinds"] = "method,property", + ["dotnet_naming_symbols.first_symbols.applicable_accessibilities"] = firstAccessibility, + [$"dotnet_naming_style.{firstName}_style.capitalization"] = "pascal_case", + [$"dotnet_naming_style.{secondName}_style.capitalization"] = "camel_case", + [$"dotnet_naming_rule.{secondName}.severity"] = "error", + [$"dotnet_naming_rule.{secondName}.symbols"] = "second_symbols", + [$"dotnet_naming_rule.{secondName}.style"] = $"{secondName}_style", + ["dotnet_naming_symbols.second_symbols.applicable_kinds"] = "method,property", + ["dotnet_naming_symbols.second_symbols.applicable_accessibilities"] = secondAccessibility, + }); + + var secondNameAfterOrdering = firstNameAfterOrdering == firstName ? secondName : firstName; + Assert.Equal($"{firstNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[0].NamingStyle.Name); + Assert.Equal($"{secondNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[1].NamingStyle.Name); + } + + [Theory] + [InlineData("a", "b", "a", "static, readonly", "static")] + [InlineData("b", "a", "a", "static", "static, readonly")] + [InlineData("b", "a", "b", "static, readonly", "static")] + [InlineData("a", "b", "b", "static", "static, readonly")] + [InlineData("a", "b", "a", "", "")] + [InlineData("b", "a", "a", "", "")] + [InlineData("A", "b", "A", "", "")] + [InlineData("b", "A", "A", "", "")] + [InlineData("a", "B", "a", "", "")] + [InlineData("B", "a", "a", "", "")] + [InlineData("A", "B", "A", "", "")] + [InlineData("B", "A", "A", "", "")] + public static void TestOrderedByModifiersBeforeName(string firstName, string secondName, string firstNameAfterOrdering, string firstModifiers, string secondModifiers) + { + var namingStylePreferences = ParseDictionary(new Dictionary() + { + [$"dotnet_naming_rule.{firstName}.severity"] = "error", + [$"dotnet_naming_rule.{firstName}.symbols"] = "first_symbols", + [$"dotnet_naming_rule.{firstName}.style"] = $"{firstName}_style", + ["dotnet_naming_symbols.first_symbols.applicable_kinds"] = "method,property", + ["dotnet_naming_symbols.first_symbols.required_modifiers"] = firstModifiers, + [$"dotnet_naming_style.{firstName}_style.capitalization"] = "pascal_case", + [$"dotnet_naming_style.{secondName}_style.capitalization"] = "camel_case", + [$"dotnet_naming_rule.{secondName}.severity"] = "error", + [$"dotnet_naming_rule.{secondName}.symbols"] = "second_symbols", + [$"dotnet_naming_rule.{secondName}.style"] = $"{secondName}_style", + ["dotnet_naming_symbols.second_symbols.applicable_kinds"] = "method,property", + ["dotnet_naming_symbols.second_symbols.required_modifiers"] = secondModifiers, + }); + + var secondNameAfterOrdering = firstNameAfterOrdering == firstName ? secondName : firstName; + Assert.Equal($"{firstNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[0].NamingStyle.Name); + Assert.Equal($"{secondNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[1].NamingStyle.Name); + } + + [Theory] + [InlineData("a", "b", "a", "method", "method, property")] + [InlineData("b", "a", "a", "method, property", "method")] + [InlineData("b", "a", "b", "method", "method, property")] + [InlineData("a", "b", "b", "method, property", "method")] + [InlineData("a", "b", "a", "*", "*")] + [InlineData("b", "a", "a", "*", "*")] + [InlineData("A", "b", "A", "*", "*")] + [InlineData("b", "A", "A", "*", "*")] + [InlineData("a", "B", "a", "*", "*")] + [InlineData("B", "a", "a", "*", "*")] + [InlineData("A", "B", "A", "*", "*")] + [InlineData("B", "A", "A", "*", "*")] + public static void TestOrderedBySymbolsBeforeName(string firstName, string secondName, string firstNameAfterOrdering, string firstSymbols, string secondSymbols) + { + var namingStylePreferences = ParseDictionary(new Dictionary() + { + [$"dotnet_naming_rule.{firstName}.severity"] = "error", + [$"dotnet_naming_rule.{firstName}.symbols"] = "first_symbols", + [$"dotnet_naming_rule.{firstName}.style"] = $"{firstName}_style", + ["dotnet_naming_symbols.first_symbols.applicable_kinds"] = firstSymbols, + [$"dotnet_naming_style.{firstName}_style.capitalization"] = "pascal_case", + [$"dotnet_naming_style.{secondName}_style.capitalization"] = "camel_case", + [$"dotnet_naming_rule.{secondName}.severity"] = "error", + [$"dotnet_naming_rule.{secondName}.symbols"] = "second_symbols", + [$"dotnet_naming_rule.{secondName}.style"] = $"{secondName}_style", + ["dotnet_naming_symbols.second_symbols.applicable_kinds"] = secondSymbols, + }); + + var secondNameAfterOrdering = firstNameAfterOrdering == firstName ? secondName : firstName; + Assert.Equal($"{firstNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[0].NamingStyle.Name); + Assert.Equal($"{secondNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[1].NamingStyle.Name); + } } } diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs index 1f137a0d8c170..64ec7898fe01e 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs @@ -16,7 +16,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Utilities; -using static Microsoft.CodeAnalysis.ProjectState; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Editor.EditorConfigSettings.DataProvider { @@ -51,7 +51,7 @@ protected void Update() return; } - var configOptionsProvider = new ProjectAnalyzerConfigOptionsProvider(project.State); + var configOptionsProvider = new ProjectState.ProjectAnalyzerConfigOptionsProvider(project.State); var workspaceOptions = configOptionsProvider.GetOptionsForSourcePath(givenFolder.FullName); var result = project.GetAnalyzerConfigOptions(); var options = new CombinedAnalyzerConfigOptions(workspaceOptions, result); @@ -93,9 +93,9 @@ public void RegisterViewModel(ISettingsEditorViewModel viewModel) private sealed class CombinedAnalyzerConfigOptions : AnalyzerConfigOptions { private readonly AnalyzerConfigOptions _workspaceOptions; - private readonly AnalyzerConfigOptionsResult? _result; + private readonly AnalyzerConfigData? _result; - public CombinedAnalyzerConfigOptions(AnalyzerConfigOptions workspaceOptions, AnalyzerConfigOptionsResult? result) + public CombinedAnalyzerConfigOptions(AnalyzerConfigOptions workspaceOptions, AnalyzerConfigData? result) { _workspaceOptions = workspaceOptions; _result = result; @@ -131,6 +131,34 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val value = null; return false; } + + public override IEnumerable Keys + { + get + { + foreach (var key in _workspaceOptions.Keys) + yield return key; + + if (!_result.HasValue) + yield break; + + foreach (var key in _result.Value.AnalyzerOptions.Keys) + { + if (!_workspaceOptions.TryGetValue(key, out _)) + yield return key; + } + + foreach (var (key, severity) in _result.Value.TreeOptions) + { + var diagnosticKey = "dotnet_diagnostic." + key + ".severity"; + if (!_workspaceOptions.TryGetValue(diagnosticKey, out _) && + !_result.Value.AnalyzerOptions.TryGetKey(diagnosticKey, out _)) + { + yield return diagnosticKey; + } + } + } + } } } } diff --git a/src/Features/Core/Portable/Diagnostics/AnalyzerConfigOptionSet+AnalyzerConfigOptionsImpl.cs b/src/Features/Core/Portable/Diagnostics/AnalyzerConfigOptionSet+AnalyzerConfigOptionsImpl.cs index 3fec7795a0148..19a21b395b96f 100644 --- a/src/Features/Core/Portable/Diagnostics/AnalyzerConfigOptionSet+AnalyzerConfigOptionsImpl.cs +++ b/src/Features/Core/Portable/Diagnostics/AnalyzerConfigOptionSet+AnalyzerConfigOptionsImpl.cs @@ -2,7 +2,10 @@ // 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.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Microsoft.CodeAnalysis.PooledObjects; namespace Microsoft.CodeAnalysis.Diagnostics { @@ -28,6 +31,9 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val return _fallbackOptions.TryGetValue(key, out value); } + + public override IEnumerable Keys + => _options.Keys.Concat(_fallbackOptions.Keys.Where(key => !_options.TryGetValue(key, out _))); } } } 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 bf2546750d42c..034940b2462f6 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -381,7 +381,7 @@ private IReadOnlyList GetStateSetsForFullSolutionAnalysis(IEnumerable< return stateSets.Where(s => IsCandidateForFullSolutionAnalysis(s.Analyzer, project, analyzerConfigOptions)).ToList(); } - private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Project project, AnalyzerConfigOptionsResult? analyzerConfigOptions) + private bool IsCandidateForFullSolutionAnalysis(DiagnosticAnalyzer analyzer, Project project, AnalyzerConfigData? analyzerConfigOptions) { // PERF: Don't query descriptors for compiler analyzer or workspace load analyzer, always execute them. if (analyzer == FileContentLoadAnalyzer.Instance || @@ -413,7 +413,7 @@ 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) != ReportDiagnostic.Hidden); + return descriptors.Any(d => d.GetEffectiveSeverity(project.CompilationOptions!, analyzerConfigOptions?.Result) != ReportDiagnostic.Hidden); } private void RaiseProjectDiagnosticsIfNeeded( diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/AnalyzersCommandHandler.cs index a2f36a1e8df05..89101c51cfdba 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); + var severity = diagnosticItem.Descriptor.GetEffectiveSeverity(project.CompilationOptions, analyzerConfigOptions?.Result); selectedItemSeverities.Add(severity); } } diff --git a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs index 621c689c63e31..114541cfd6522 100644 --- a/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs +++ b/src/VisualStudio/Core/Impl/SolutionExplorer/DiagnosticItem/BaseDiagnosticAndGeneratorItemSource.cs @@ -26,7 +26,7 @@ internal abstract partial class BaseDiagnosticAndGeneratorItemSource : IAttached private BulkObservableCollection? _items; private ReportDiagnostic _generalDiagnosticOption; private ImmutableDictionary? _specificDiagnosticOptions; - private AnalyzerConfigOptionsResult? _analyzerConfigOptions; + private AnalyzerConfigData? _analyzerConfigOptions; public BaseDiagnosticAndGeneratorItemSource(Workspace workspace, ProjectId projectId, IAnalyzersCommandHandler commandHandler, IDiagnosticAnalyzerService diagnosticAnalyzerService) { @@ -95,7 +95,7 @@ public IEnumerable Items } } - private BulkObservableCollection CreateDiagnosticAndGeneratorItems(ProjectId projectId, string language, CompilationOptions options, AnalyzerConfigOptionsResult? analyzerConfigOptions) + private BulkObservableCollection CreateDiagnosticAndGeneratorItems(ProjectId projectId, string language, CompilationOptions options, AnalyzerConfigData? analyzerConfigOptions) { // Within an analyzer assembly, an individual analyzer may report multiple different diagnostics // with the same ID. Or, multiple analyzers may report diagnostics with the same ID. Or a @@ -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); + var effectiveSeverity = selectedDiagnostic.GetEffectiveSeverity(options, analyzerConfigOptions?.Result); 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); + var effectiveSeverity = item.Descriptor.GetEffectiveSeverity(project.CompilationOptions, newAnalyzerConfigOptions?.Result); item.UpdateEffectiveSeverity(effectiveSeverity); } } diff --git a/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs b/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs index 0148466ebcc7e..bacb11927cb04 100644 --- a/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs +++ b/src/Workspaces/Core/Portable/Options/EditorConfig/EditorConfigDocumentOptionsProviderFactory.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Roslyn.Utilities; using Microsoft.CodeAnalysis.ErrorReporting; +using Microsoft.CodeAnalysis.Diagnostics; namespace Microsoft.CodeAnalysis.Options.EditorConfig { @@ -21,19 +22,25 @@ private sealed class EditorConfigDocumentOptionsProvider : IDocumentOptionsProvi { public async Task GetOptionsForDocumentAsync(Document document, CancellationToken cancellationToken) { - var options = await document.GetAnalyzerOptionsAsync(cancellationToken).ConfigureAwait(false); - - return new DocumentOptions(options); + var data = await document.GetAnalyzerOptionsAsync(cancellationToken).ConfigureAwait(false); + return new DocumentOptions(data?.AnalyzerConfigOptions); } private sealed class DocumentOptions : IDocumentOptions { - private readonly ImmutableDictionary _options; - public DocumentOptions(ImmutableDictionary options) + private readonly StructuredAnalyzerConfigOptions? _options; + + public DocumentOptions(StructuredAnalyzerConfigOptions? options) => _options = options; public bool TryGetDocumentOption(OptionKey option, out object? value) { + if (_options == null) + { + value = null; + return false; + } + var editorConfigPersistence = (IEditorConfigStorageLocation?)option.Option.StorageLocations.SingleOrDefault(static location => location is IEditorConfigStorageLocation); if (editorConfigPersistence == null) { @@ -43,7 +50,7 @@ public bool TryGetDocumentOption(OptionKey option, out object? value) try { - return editorConfigPersistence.TryGetOption(_options.AsNullable(), option.Option.Type, out value); + return editorConfigPersistence.TryGetOption(_options, option.Option.Type, out value); } catch (Exception e) when (FatalError.ReportAndCatch(e)) { diff --git a/src/Workspaces/Core/Portable/Options/OptionSet+AnalyzerConfigOptionsImpl.cs b/src/Workspaces/Core/Portable/Options/OptionSet+AnalyzerConfigOptionsImpl.cs index 4e0d139840cb3..6e52e3a5db884 100644 --- a/src/Workspaces/Core/Portable/Options/OptionSet+AnalyzerConfigOptionsImpl.cs +++ b/src/Workspaces/Core/Portable/Options/OptionSet+AnalyzerConfigOptionsImpl.cs @@ -2,8 +2,11 @@ // 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.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; namespace Microsoft.CodeAnalysis.Options @@ -39,6 +42,10 @@ public override bool TryGetValue(string key, [NotNullWhen(true)] out string? val value = storageLocation.GetEditorConfigStringValue(typedValue, _optionSet); return true; } + + // no way to enumerate OptionSet + public override IEnumerable Keys + => throw new NotImplementedException(); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs new file mode 100644 index 0000000000000..c417192872bb6 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/AnalyzerConfigData.cs @@ -0,0 +1,28 @@ +// 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.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Microsoft.CodeAnalysis; + +/// +/// Aggregate analyzer config options for a specific path. +/// +internal readonly struct AnalyzerConfigData +{ + private readonly AnalyzerConfigOptionsResult _result; + private readonly StructuredAnalyzerConfigOptions _configOptions; + + public AnalyzerConfigData(AnalyzerConfigOptionsResult result) + { + _result = result; + _configOptions = new StructuredAnalyzerConfigOptions(result.AnalyzerOptions); + } + + 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 c64163a5a1277..7f4f520e8d25a 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -513,7 +513,7 @@ private void InitializeCachedOptions(OptionSet solutionOptions) Interlocked.CompareExchange(ref _cachedOptions, newAsyncLazy, comparand: null); } - internal Task> GetAnalyzerOptionsAsync(CancellationToken cancellationToken) + 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 @@ -535,15 +535,12 @@ internal Task> GetAnalyzerOptionsAsync(Cance } } - if (effectiveFilePath != null) + if (effectiveFilePath == null) { - return Project.State.GetAnalyzerOptionsForPathAsync(effectiveFilePath, cancellationToken); - } - else - { - // Really no idea where this is going, so bail - return Task.FromResult(DictionaryAnalyzerConfigOptions.EmptyDictionary); + return null; } + + return await Project.State.GetAnalyzerOptionsForPathAsync(effectiveFilePath, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs index fa20f2786d59a..5ec37f9535f0e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Project.cs @@ -774,7 +774,7 @@ private void CheckIdsContainedInProject(ImmutableArray documentIds) } } - internal AnalyzerConfigOptionsResult? GetAnalyzerConfigOptions() + internal AnalyzerConfigData? GetAnalyzerConfigOptions() => _projectState.GetAnalyzerConfigOptions(); private string GetDebuggerDisplay() diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs index d36dd5f83160d..9370c87f3b2c6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectState.cs @@ -248,15 +248,13 @@ public AnalyzerOptions AnalyzerOptions additionalFiles: AdditionalDocumentStates.SelectAsArray(static documentState => documentState.AdditionalText), optionsProvider: new ProjectAnalyzerConfigOptionsProvider(this)); - public async Task> GetAnalyzerOptionsForPathAsync( - string path, - CancellationToken cancellationToken) + public async Task GetAnalyzerOptionsForPathAsync(string path, CancellationToken cancellationToken) { - var configSet = await _lazyAnalyzerConfigOptions.GetValueAsync(cancellationToken).ConfigureAwait(false); - return configSet.GetOptionsForSourcePath(path).AnalyzerOptions; + var cache = await _lazyAnalyzerConfigOptions.GetValueAsync(cancellationToken).ConfigureAwait(false); + return cache.GetOptionsForSourcePath(path); } - public AnalyzerConfigOptionsResult? GetAnalyzerConfigOptions() + 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. @@ -296,8 +294,11 @@ internal sealed class ProjectAnalyzerConfigOptionsProvider : AnalyzerConfigOptio public ProjectAnalyzerConfigOptionsProvider(ProjectState projectState) => _projectState = projectState; + private AnalyzerConfigOptionsCache GetCache() + => _projectState._lazyAnalyzerConfigOptions.GetValue(CancellationToken.None); + public override AnalyzerConfigOptions GlobalOptions - => GetOptionsForSourcePath(string.Empty); + => GetCache().GlobalConfigOptions.AnalyzerConfigOptions; public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GetOptionsForSourcePath(tree.FilePath); @@ -309,7 +310,7 @@ public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) } public AnalyzerConfigOptions GetOptionsForSourcePath(string path) - => new DictionaryAnalyzerConfigOptions(_projectState._lazyAnalyzerConfigOptions.GetValue(CancellationToken.None).GetOptionsForSourcePath(path).AnalyzerOptions); + => GetCache().GetOptionsForSourcePath(path).AnalyzerConfigOptions; } private sealed class ProjectSyntaxTreeOptionsProvider : SyntaxTreeOptionsProvider @@ -371,20 +372,20 @@ private static ValueSource ComputeAnalyzerConfigOpti private readonly struct AnalyzerConfigOptionsCache { - private readonly ConcurrentDictionary _sourcePathToResult = new(); - private readonly Func _computeFunction; - private readonly AnalyzerConfigSet _configSet; + private readonly ConcurrentDictionary _sourcePathToResult = new(); + private readonly Func _computeFunction; + private readonly Lazy _global; public AnalyzerConfigOptionsCache(AnalyzerConfigSet configSet) { - _configSet = configSet; - _computeFunction = _configSet.GetOptionsForSourcePath; + _global = new Lazy(() => new AnalyzerConfigData(configSet.GlobalConfigOptions)); + _computeFunction = path => new AnalyzerConfigData(configSet.GetOptionsForSourcePath(path)); } - public AnalyzerConfigOptionsResult GlobalConfigOptions - => _configSet.GlobalConfigOptions; + public AnalyzerConfigData GlobalConfigOptions + => _global.Value; - public AnalyzerConfigOptionsResult GetOptionsForSourcePath(string sourcePath) + public AnalyzerConfigData GetOptionsForSourcePath(string sourcePath) => _sourcePathToResult.GetOrAdd(sourcePath, _computeFunction); } diff --git a/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs b/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs index 6985c5d8ccbcb..fa91961a9e6ac 100644 --- a/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs +++ b/src/Workspaces/CoreTest/CodeStyle/EditorConfigCodeStyleParserTests.cs @@ -3,11 +3,16 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Options; using Roslyn.Test.Utilities; +using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests.CodeStyle @@ -62,7 +67,7 @@ public void TestParseEditorConfigAccessibilityModifiers(string args, int value, var storageLocation = CodeStyleOptions2.RequireAccessibilityModifiers.StorageLocations .OfType>>() .Single(); - var allRawConventions = new Dictionary { { storageLocation.KeyName, args } }; + var allRawConventions = new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.EmptyDictionary.Add(storageLocation.KeyName, args)); Assert.True(storageLocation.TryGetOption(allRawConventions, typeof(CodeStyleOption2), out var parsedCodeStyleOption)); var codeStyleOption = (CodeStyleOption2)parsedCodeStyleOption!; @@ -84,7 +89,7 @@ public void TestParseEditorConfigEndOfLine(string configurationString, string ne var storageLocation = FormattingOptions.NewLine.StorageLocations .OfType>() .Single(); - var allRawConventions = new Dictionary { { storageLocation.KeyName, configurationString } }; + var allRawConventions = new StructuredAnalyzerConfigOptions(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 7e74d51ac4cb4..2fd9b59b756f7 100644 --- a/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs +++ b/src/Workspaces/CoreTest/EditorConfigStorageLocation/NamingStylePreferenceEditorConfigStorageLocationTests.cs @@ -6,8 +6,12 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Microsoft.CodeAnalysis.Options; +using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests.EditorConfig.StorageLocation @@ -18,19 +22,7 @@ public class NamingStylePreferenceEditorConfigStorageLocationTests public static void TestEmptyDictionaryReturnNoNamingStylePreferencesObjectReturnsFalse() { var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var result = editorConfigStorageLocation.TryGetOption(new Dictionary(), typeof(NamingStylePreferences), out _); - Assert.False(result, "Expected TryParseReadonlyDictionary to return 'false' for empty dictionary"); - } - - [Fact] - public static void TestEmptyDictionaryDefaultNamingStylePreferencesObjectReturnsFalse() - { - var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var result = editorConfigStorageLocation.TryGetOption( - new Dictionary(), - typeof(NamingStylePreferences), - out _); - + var result = editorConfigStorageLocation.TryGetOption(new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.Empty), typeof(NamingStylePreferences), out _); Assert.False(result, "Expected TryParseReadonlyDictionary to return 'false' for empty dictionary"); } @@ -38,7 +30,7 @@ public static void TestEmptyDictionaryDefaultNamingStylePreferencesObjectReturns public static void TestNonEmptyDictionaryReturnsTrue() { var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var newDictionary = new Dictionary() + var options = new StructuredAnalyzerConfigOptions(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", @@ -46,155 +38,22 @@ public static void TestNonEmptyDictionaryReturnsTrue() ["dotnet_naming_symbols.method_and_property_symbols.applicable_kinds"] = "method,property", ["dotnet_naming_symbols.method_and_property_symbols.applicable_accessibilities"] = "*", ["dotnet_naming_style.pascal_case_style.capitalization"] = "pascal_case" - }; + }.ToImmutableDictionary(AnalyzerConfigOptions.KeyComparer)); - var result = editorConfigStorageLocation.TryGetOption( - newDictionary, - typeof(NamingStylePreferences), - out var combinedNamingStyles); + var result = editorConfigStorageLocation.TryGetOption(options, typeof(NamingStylePreferences), out var value); Assert.True(result, "Expected non-empty dictionary to return true"); - var namingStylePreferences = Assert.IsAssignableFrom(combinedNamingStyles); + var namingStylePreferences = Assert.IsAssignableFrom(value); Assert.Equal(ReportDiagnostic.Error, namingStylePreferences.Rules.NamingRules[0].EnforcementLevel); } - [Theory] - [InlineData("a", "b", "a", "public", "public, private")] - [InlineData("b", "a", "a", "public, private", "public")] - [InlineData("b", "a", "b", "public", "public, private")] - [InlineData("a", "b", "b", "public, private", "public")] - [InlineData("a", "b", "a", "*", "*")] - [InlineData("b", "a", "a", "*", "*")] - [InlineData("A", "b", "A", "*", "*")] - [InlineData("b", "A", "A", "*", "*")] - [InlineData("a", "B", "a", "*", "*")] - [InlineData("B", "a", "a", "*", "*")] - [InlineData("A", "B", "A", "*", "*")] - [InlineData("B", "A", "A", "*", "*")] - public static void TestOrderedByAccessibilityBeforeName(string firstName, string secondName, string firstNameAfterOrdering, string firstAccessibility, string secondAccessibility) - { - var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var newDictionary = new Dictionary() - { - [$"dotnet_naming_rule.{firstName}.severity"] = "error", - [$"dotnet_naming_rule.{firstName}.symbols"] = "first_symbols", - [$"dotnet_naming_rule.{firstName}.style"] = $"{firstName}_style", - ["dotnet_naming_symbols.first_symbols.applicable_kinds"] = "method,property", - ["dotnet_naming_symbols.first_symbols.applicable_accessibilities"] = firstAccessibility, - [$"dotnet_naming_style.{firstName}_style.capitalization"] = "pascal_case", - [$"dotnet_naming_style.{secondName}_style.capitalization"] = "camel_case", - [$"dotnet_naming_rule.{secondName}.severity"] = "error", - [$"dotnet_naming_rule.{secondName}.symbols"] = "second_symbols", - [$"dotnet_naming_rule.{secondName}.style"] = $"{secondName}_style", - ["dotnet_naming_symbols.second_symbols.applicable_kinds"] = "method,property", - ["dotnet_naming_symbols.second_symbols.applicable_accessibilities"] = secondAccessibility, - }; - - var result = editorConfigStorageLocation.TryGetOption( - newDictionary, - typeof(NamingStylePreferences), - out var combinedNamingStyles); - - var secondNameAfterOrdering = firstNameAfterOrdering == firstName ? secondName : firstName; - Assert.True(result, "Expected non-empty dictionary to return true"); - var namingStylePreferences = Assert.IsAssignableFrom(combinedNamingStyles); - Assert.Equal($"{firstNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[0].NamingStyle.Name); - Assert.Equal($"{secondNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[1].NamingStyle.Name); - } - - [Theory] - [InlineData("a", "b", "a", "static, readonly", "static")] - [InlineData("b", "a", "a", "static", "static, readonly")] - [InlineData("b", "a", "b", "static, readonly", "static")] - [InlineData("a", "b", "b", "static", "static, readonly")] - [InlineData("a", "b", "a", "", "")] - [InlineData("b", "a", "a", "", "")] - [InlineData("A", "b", "A", "", "")] - [InlineData("b", "A", "A", "", "")] - [InlineData("a", "B", "a", "", "")] - [InlineData("B", "a", "a", "", "")] - [InlineData("A", "B", "A", "", "")] - [InlineData("B", "A", "A", "", "")] - public static void TestOrderedByModifiersBeforeName(string firstName, string secondName, string firstNameAfterOrdering, string firstModifiers, string secondModifiers) - { - var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var newDictionary = new Dictionary() - { - [$"dotnet_naming_rule.{firstName}.severity"] = "error", - [$"dotnet_naming_rule.{firstName}.symbols"] = "first_symbols", - [$"dotnet_naming_rule.{firstName}.style"] = $"{firstName}_style", - ["dotnet_naming_symbols.first_symbols.applicable_kinds"] = "method,property", - ["dotnet_naming_symbols.first_symbols.required_modifiers"] = firstModifiers, - [$"dotnet_naming_style.{firstName}_style.capitalization"] = "pascal_case", - [$"dotnet_naming_style.{secondName}_style.capitalization"] = "camel_case", - [$"dotnet_naming_rule.{secondName}.severity"] = "error", - [$"dotnet_naming_rule.{secondName}.symbols"] = "second_symbols", - [$"dotnet_naming_rule.{secondName}.style"] = $"{secondName}_style", - ["dotnet_naming_symbols.second_symbols.applicable_kinds"] = "method,property", - ["dotnet_naming_symbols.second_symbols.required_modifiers"] = secondModifiers, - }; - - var result = editorConfigStorageLocation.TryGetOption( - newDictionary, - typeof(NamingStylePreferences), - out var combinedNamingStyles); - - var secondNameAfterOrdering = firstNameAfterOrdering == firstName ? secondName : firstName; - Assert.True(result, "Expected non-empty dictionary to return true"); - var namingStylePreferences = Assert.IsAssignableFrom(combinedNamingStyles); - Assert.Equal($"{firstNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[0].NamingStyle.Name); - Assert.Equal($"{secondNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[1].NamingStyle.Name); - } - - [Theory] - [InlineData("a", "b", "a", "method", "method, property")] - [InlineData("b", "a", "a", "method, property", "method")] - [InlineData("b", "a", "b", "method", "method, property")] - [InlineData("a", "b", "b", "method, property", "method")] - [InlineData("a", "b", "a", "*", "*")] - [InlineData("b", "a", "a", "*", "*")] - [InlineData("A", "b", "A", "*", "*")] - [InlineData("b", "A", "A", "*", "*")] - [InlineData("a", "B", "a", "*", "*")] - [InlineData("B", "a", "a", "*", "*")] - [InlineData("A", "B", "A", "*", "*")] - [InlineData("B", "A", "A", "*", "*")] - public static void TestOrderedBySymbolsBeforeName(string firstName, string secondName, string firstNameAfterOrdering, string firstSymbols, string secondSymbols) - { - var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); - var newDictionary = new Dictionary() - { - [$"dotnet_naming_rule.{firstName}.severity"] = "error", - [$"dotnet_naming_rule.{firstName}.symbols"] = "first_symbols", - [$"dotnet_naming_rule.{firstName}.style"] = $"{firstName}_style", - ["dotnet_naming_symbols.first_symbols.applicable_kinds"] = firstSymbols, - [$"dotnet_naming_style.{firstName}_style.capitalization"] = "pascal_case", - [$"dotnet_naming_style.{secondName}_style.capitalization"] = "camel_case", - [$"dotnet_naming_rule.{secondName}.severity"] = "error", - [$"dotnet_naming_rule.{secondName}.symbols"] = "second_symbols", - [$"dotnet_naming_rule.{secondName}.style"] = $"{secondName}_style", - ["dotnet_naming_symbols.second_symbols.applicable_kinds"] = secondSymbols, - }; - - var result = editorConfigStorageLocation.TryGetOption( - newDictionary, - typeof(NamingStylePreferences), - out var combinedNamingStyles); - - var secondNameAfterOrdering = firstNameAfterOrdering == firstName ? secondName : firstName; - Assert.True(result, "Expected non-empty dictionary to return true"); - var namingStylePreferences = Assert.IsAssignableFrom(combinedNamingStyles); - Assert.Equal($"{firstNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[0].NamingStyle.Name); - Assert.Equal($"{secondNameAfterOrdering}_style", namingStylePreferences.Rules.NamingRules[1].NamingStyle.Name); - } - [Fact] public static void TestObjectTypeThrowsInvalidOperationException() { var editorConfigStorageLocation = new NamingStylePreferenceEditorConfigStorageLocation(); Assert.Throws(() => { - editorConfigStorageLocation.TryGetOption(new Dictionary(), typeof(object), out var @object); + editorConfigStorageLocation.TryGetOption(new StructuredAnalyzerConfigOptions(DictionaryAnalyzerConfigOptions.Empty), typeof(object), out var @object); }); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems index b3c2638f021c2..c17730357f3ff 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/CompilerExtensions.projitems @@ -193,6 +193,7 @@ + @@ -371,7 +372,6 @@ - @@ -575,7 +575,7 @@ - Diagnostics\AnalyzerConfigOptionsDictionary.cs + Diagnostics\DictionaryAnalyzerConfigOptions.cs diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs new file mode 100644 index 0000000000000..6fb0cb022c226 --- /dev/null +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Diagnostics/StructuredAnalyzerConfigOptions.cs @@ -0,0 +1,97 @@ +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; +using Roslyn.Utilities; + +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 +{ + private readonly AnalyzerConfigOptions _options; + private readonly Lazy _lazyNamingStylePreferences; + + public StructuredAnalyzerConfigOptions(AnalyzerConfigOptions options) + { + _options = options; + _lazyNamingStylePreferences = new Lazy(() => EditorConfigNamingStyleParser.ParseDictionary(_options)); + } + + public StructuredAnalyzerConfigOptions(ImmutableDictionary options) + : this(new DictionaryAnalyzerConfigOptions(options)) + { + Contract.ThrowIfFalse(options.KeyComparer == KeyComparer); + } + + 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) + { + options = structuredOptions; + return true; + } + +#if CODE_STYLE + if (TryGetCorrespondingCodeStyleInstance(configOptions, out options)) + { + return true; + } +#endif + + options = null; + return false; + } + +#if CODE_STYLE + // StructuredAnalyzerConfigOptions is defined in both Worksapce and Code Style layers. It is not public and thus can't be shared between these two. + // However, Code Style layer is compiled against the shared Workspace APIs. The ProjectState creates and holds onto an instance + // of Workspace layer's version of StructuredAnalyzerConfigOptions. This version of the type is not directly usable by Code Style code. + // We create a clone of this instance typed to the Code Style's version of StructuredAnalyzerConfigOptions. + // The conditional weak table maintains 1:1 correspondence between these instances. + // + // In addition, we also map Compiler created DictionaryAnalyzerConfigOptions to StructuredAnalyzerConfigOptions for analyzers that are invoked + // from command line build. + + private static readonly ConditionalWeakTable s_codeStyleStructuredOptions = new(); + private static readonly object s_codeStyleStructuredOptionsLock = new(); + + private static bool TryGetCorrespondingCodeStyleInstance(AnalyzerConfigOptions configOptions, [NotNullWhen(true)] out StructuredAnalyzerConfigOptions? options) + { + if (s_codeStyleStructuredOptions.TryGetValue(configOptions, out options)) + { + return true; + } + + lock (s_codeStyleStructuredOptionsLock) + { + if (!s_codeStyleStructuredOptions.TryGetValue(configOptions, out options)) + { + options = new StructuredAnalyzerConfigOptions(configOptions); + s_codeStyleStructuredOptions.Add(configOptions, options); + } + } + + return true; + } +#endif +} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/EditorConfigParser.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/EditorConfigParser.cs index 51386f9495f64..b48bb5a04538c 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/EditorConfigParser.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/EditorConfigParser.cs @@ -5,6 +5,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.EditorConfig.Parsing @@ -15,8 +16,9 @@ internal static class EditorConfigParser private static readonly Regex s_sectionMatcher = new(@"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$", RegexOptions.Compiled); // Matches EditorConfig property such as "indent_style = space", see https://editorconfig.org for details private static readonly Regex s_propertyMatcher = new(@"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$", RegexOptions.Compiled); + private static ImmutableHashSet ReservedKeys { get; } - = ImmutableHashSet.CreateRange(CaseInsensitiveComparison.Comparer, new[] { + = ImmutableHashSet.CreateRange(AnalyzerConfigOptions.KeyComparer, new[] { "root", "indent_style", "indent_size", @@ -43,8 +45,7 @@ public static TEditorConfigFile Parse where TEditorConfigOption : EditorConfigOption { - var activeSectionProperties = ImmutableDictionary.CreateBuilder( - CaseInsensitiveComparison.Comparer); + var activeSectionProperties = ImmutableDictionary.CreateBuilder(AnalyzerConfigOptions.KeyComparer); var activeSectionName = ""; var activeSectionStart = 0; var activeSectionEnd = 0; @@ -74,8 +75,7 @@ public static TEditorConfigFile Parse( - CaseInsensitiveComparison.Comparer); + activeSectionProperties.Clear(); continue; } @@ -121,7 +121,7 @@ void ProcessActiveSection() var fullText = activeLine.ToString(); var sectionSpan = new TextSpan(activeSectionStart, activeSectionEnd); var previousSection = new Section(pathToFile, isGlobal, sectionSpan, activeSectionName, fullText); - accumulator.ProcessSection(previousSection, activeSectionProperties.ToImmutable()); + accumulator.ProcessSection(previousSection, activeSectionProperties); } static bool IsComment(string line) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/IEditorConfigOptionAccumulator.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/IEditorConfigOptionAccumulator.cs index c750a40f155fc..1b103e4b8a3e5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/IEditorConfigOptionAccumulator.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/IEditorConfigOptionAccumulator.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using System.Collections.Immutable; using Microsoft.CodeAnalysis.Text; @@ -11,7 +12,7 @@ internal interface IEditorConfigOptionAccumulator where TResults : EditorConfigFile where TResult : EditorConfigOption { - void ProcessSection(Section section, ImmutableDictionary properties); + void ProcessSection(Section section, IReadOnlyDictionary properties); TResults Complete(string? filePath); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/NamingStyles/NamingStyleOptionAccumulator.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/NamingStyles/NamingStyleOptionAccumulator.cs index 51bae3ea22c83..d873387893390 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/NamingStyles/NamingStyleOptionAccumulator.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/EditorConfig/Parsing/NamingStyles/NamingStyleOptionAccumulator.cs @@ -2,7 +2,9 @@ // 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.Collections.Generic; using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; using static Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles.EditorConfigNamingStyleParser; @@ -20,11 +22,9 @@ public EditorConfigNamingStyles Complete(string? fileName) return editorConfigNamingStyles; } - public void ProcessSection( - Section section, - ImmutableDictionary properties) + public void ProcessSection(Section section, IReadOnlyDictionary properties) { - foreach (var ruleTitle in GetRuleTitles(TrimDictionary(properties))) + foreach (var ruleTitle in GetRuleTitles(properties)) { if (TryGetSymbolSpec(section, ruleTitle, properties, out var applicableSymbolInfo) && TryGetNamingStyleData(section, ruleTitle, properties, out var namingScheme) && diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/AnalyzerConfigOptionsExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/AnalyzerConfigOptionsExtensions.cs index 31d34a2ff238f..9006335cb9933 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/AnalyzerConfigOptionsExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/AnalyzerConfigOptionsExtensions.cs @@ -84,7 +84,8 @@ private static bool TryGetEditorConfigOption(this AnalyzerConfigOptions analy // This option has .editorconfig storage defined, even if the current configuration does not provide a // value for it. hasEditorConfigStorage = true; - if (configStorageLocation.TryGetOption(analyzerConfigOptions, option.Type, out var objectValue)) + if (StructuredAnalyzerConfigOptions.TryGetStructuredOptions(analyzerConfigOptions, out var structuredOptions) && + configStorageLocation.TryGetOption(structuredOptions, option.Type, out var objectValue)) { value = (T?)objectValue; return true; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser.cs index 567882578d52f..f63d5d5684323 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis.NamingStyles; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; @@ -14,43 +15,15 @@ namespace Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles { internal static partial class EditorConfigNamingStyleParser { - /// - /// The dictionary we get from the VS editorconfig API uses the same dictionary object if there are no changes, so we can cache based on dictionary - /// - // TODO: revisit this cache. The assumption that the dictionary doesn't change in the exact instance is terribly fragile, - // and with the new .editorconfig support won't hold as well as we'd like: a single tree will have a stable instance but - // that won't necessarily be the same across files and projects. - private static readonly ConditionalWeakTable, NamingStylePreferences> _cache = new(); - private static readonly object _cacheLock = new(); - - public static NamingStylePreferences GetNamingStylesFromDictionary(IReadOnlyDictionary rawOptions) + public static NamingStylePreferences ParseDictionary(AnalyzerConfigOptions allRawConventions) { - if (_cache.TryGetValue(rawOptions, out var value)) - { - return value; - } - - lock (_cacheLock) - { - if (!_cache.TryGetValue(rawOptions, out value)) - { - value = ParseDictionary(rawOptions); - _cache.Add(rawOptions, value); - } - - return value; - } - } + var trimmedDictionary = TrimDictionary(allRawConventions); - public static NamingStylePreferences ParseDictionary(IReadOnlyDictionary allRawConventions) - { var symbolSpecifications = ArrayBuilder.GetInstance(); var namingStyles = ArrayBuilder.GetInstance(); var namingRules = ArrayBuilder.GetInstance(); var ruleNames = new Dictionary<(Guid symbolSpecificationID, Guid namingStyleID, ReportDiagnostic enforcementLevel), string>(); - var trimmedDictionary = TrimDictionary(allRawConventions); - foreach (var namingRuleTitle in GetRuleTitles(trimmedDictionary)) { if (TryGetSymbolSpec(namingRuleTitle, trimmedDictionary, out var symbolSpec) && @@ -127,19 +100,12 @@ public static NamingStylePreferences ParseDictionary(IReadOnlyDictionary TrimDictionary(IReadOnlyDictionary allRawConventions) + internal static Dictionary TrimDictionary(AnalyzerConfigOptions allRawConventions) { - // Keys have been lowercased, but values have not. Because values here reference key - // names we need any comparisons to ignore case. - // For example, to make a naming style called "Pascal_Case_style" match up correctly - // with the key "dotnet_naming_style.pascal_case_style.capitalization", we have to - // ignore casing for that lookup. - var trimmedDictionary = new Dictionary(allRawConventions.Count, AnalyzerConfigOptions.KeyComparer); - foreach (var item in allRawConventions) + var trimmedDictionary = new Dictionary(AnalyzerConfigOptions.KeyComparer); + foreach (var key in allRawConventions.Keys) { - var key = item.Key.Trim(); - var value = item.Value; - trimmedDictionary[key] = value; + trimmedDictionary[key.Trim()] = allRawConventions.TryGetValue(key, out var value) ? value : throw new InvalidOperationException(); } return trimmedDictionary; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingRule.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingRule.cs index 894beddc04a6e..4b0d78792aae8 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingRule.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingRule.cs @@ -16,7 +16,7 @@ private static bool TryGetSerializableNamingRule( string namingRuleTitle, SymbolSpecification symbolSpec, NamingStyle namingStyle, - IReadOnlyDictionary conventionsDictionary, + IReadOnlyDictionary conventionsDictionary, [NotNullWhen(true)] out SerializableNamingRule? serializableNamingRule) { if (!TryGetRuleSeverity(namingRuleTitle, conventionsDictionary, out var severity)) @@ -43,13 +43,13 @@ internal static bool TryGetRuleSeverity( private static bool TryGetRuleSeverity( string namingRuleName, - IReadOnlyDictionary conventionsDictionary, + IReadOnlyDictionary conventionsDictionary, out ReportDiagnostic severity) { - var result = TryGetRuleSeverity( + var result = TryGetRuleSeverity( namingRuleName, conventionsDictionary, - x => x!, + x => x, x => null, // we don't have a tuple out var tuple); severity = tuple.severity; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingStyle.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingStyle.cs index 80099a64b1c3a..50e5e709b87c4 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingStyle.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_NamingStyle.cs @@ -47,13 +47,13 @@ internal static bool TryGetNamingStyleData( private static bool TryGetNamingStyleData( string namingRuleName, - IReadOnlyDictionary rawOptions, + IReadOnlyDictionary rawOptions, out NamingStyle namingStyle) { - return TryGetNamingStyleData( + return TryGetNamingStyleData( namingRuleName, rawOptions, - s => s ?? string.Empty, + s => s, x => null, s => (s ?? string.Empty, null), (nameTuple, prefixTuple, suffixTuple, wordSeparatorTuple, capitalizationTuple) => diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_SymbolSpec.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_SymbolSpec.cs index 67e3176c0cd5c..a299f9e7fe7a6 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_SymbolSpec.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/EditorConfig/EditorConfigNamingStyleParser_SymbolSpec.cs @@ -46,13 +46,13 @@ internal static bool TryGetSymbolSpec( private static bool TryGetSymbolSpec( string namingRuleTitle, - IReadOnlyDictionary conventionsDictionary, + IReadOnlyDictionary conventionsDictionary, [NotNullWhen(true)] out SymbolSpecification? symbolSpec) { - return TryGetSymbolSpec( + return TryGetSymbolSpec( namingRuleTitle, conventionsDictionary, - s => (s ?? string.Empty, null), + s => (s, null), () => null, (t0, t1, t2, t3) => new SymbolSpecification( Guid.NewGuid(), diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/NamingStylePreferences.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/NamingStylePreferences.cs index bf1d0e1124e80..980a1801b3be1 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/NamingStylePreferences.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/NamingStylePreferences.cs @@ -56,9 +56,13 @@ public NamingStylePreferences( } public static NamingStylePreferences Default => FromXElement(XElement.Parse(DefaultNamingPreferencesString)); + public static NamingStylePreferences Empty => new(ImmutableArray.Empty, ImmutableArray.Empty, ImmutableArray.Empty); public static string DefaultNamingPreferencesString => _defaultNamingPreferencesString; + public bool IsEmpty + => SymbolSpecifications.IsEmpty && NamingStyles.IsEmpty && NamingRules.IsEmpty; + internal NamingStyle GetNamingStyle(Guid namingStyleID) => NamingStyles.Single(s => s.ID == namingStyleID); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/SymbolSpecification.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/SymbolSpecification.cs index c2cb04b627680..0463567afcbc3 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/SymbolSpecification.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/NamingStyles/Serialization/SymbolSpecification.cs @@ -24,6 +24,7 @@ namespace Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles { [DataContract] + [DebuggerDisplay("{GetDebuggerDisplay(),nq}")] internal sealed class SymbolSpecification : IEquatable, IObjectWritable { private static readonly SymbolSpecification DefaultSymbolSpecificationTemplate = CreateDefaultSymbolSpecification(); @@ -57,6 +58,9 @@ public SymbolSpecification( RequiredModifierList = modifiers.IsDefault ? DefaultSymbolSpecificationTemplate.RequiredModifierList : modifiers; } + private string GetDebuggerDisplay() + => Name; + public static SymbolSpecification CreateDefaultSymbolSpecification() { // This is used to create new, empty symbol specifications for users to then customize. diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs deleted file mode 100644 index a850af2038898..0000000000000 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocationExtensions.cs +++ /dev/null @@ -1,34 +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.Reflection; -using System.Collections.Generic; -using System.Collections.Immutable; -using Microsoft.CodeAnalysis.Diagnostics; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Options -{ - internal static class EditorConfigStorageLocationExtensions - { - public static bool TryGetOption(this IEditorConfigStorageLocation editorConfigStorageLocation, AnalyzerConfigOptions analyzerConfigOptions, Type type, out object? value) - { - // This is a workaround until we have an API for enumeratings AnalyzerConfigOptions. See https://github.com/dotnet/roslyn/issues/41840 - if (analyzerConfigOptions.GetType().FullName == typeof(DictionaryAnalyzerConfigOptions).FullName) - { - var optionsField = analyzerConfigOptions.GetType().GetField(nameof(DictionaryAnalyzerConfigOptions.Options), BindingFlags.NonPublic | BindingFlags.Instance); - Contract.ThrowIfNull(optionsField); - - var options = optionsField.GetValue(analyzerConfigOptions); - Contract.ThrowIfNull(options); - - return editorConfigStorageLocation.TryGetOption((ImmutableDictionary)options, type, out value); - } - - value = null; - return false; - } - } -} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocation`1.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocation`1.cs index fe92fbb888eff..0e349c86bd476 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocation`1.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/EditorConfigStorageLocation`1.cs @@ -8,6 +8,8 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Roslyn.Utilities; #if CODE_STYLE @@ -59,10 +61,9 @@ public EditorConfigStorageLocation(string keyName, Func> par _getEditorConfigStringForValue = getEditorConfigStringForValue ?? throw new ArgumentNullException(nameof(getEditorConfigStringForValue)); } - public bool TryGetOption(IReadOnlyDictionary rawOptions, Type type, out object? result) + public bool TryGetOption(StructuredAnalyzerConfigOptions options, Type type, out object? result) { - if (rawOptions.TryGetValue(KeyName, out var value) - && value is object) + if (options.TryGetValue(KeyName, out var value)) { var ret = TryGetOption(value, type, out var typedResult); result = typedResult; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/IEditorConfigStorageLocation.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/IEditorConfigStorageLocation.cs index fdaca1ff999d2..9d6d0521f2b27 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/IEditorConfigStorageLocation.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/IEditorConfigStorageLocation.cs @@ -4,11 +4,15 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; namespace Microsoft.CodeAnalysis.Options { internal interface IEditorConfigStorageLocation { - bool TryGetOption(IReadOnlyDictionary rawOptions, Type type, out object? value); + bool TryGetOption(StructuredAnalyzerConfigOptions options, Type type, out object? value); } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/NamingStylePreferenceEditorConfigStorageLocation.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/NamingStylePreferenceEditorConfigStorageLocation.cs index e3dd6c3e96ae9..650356dd5b95d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/NamingStylePreferenceEditorConfigStorageLocation.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Options/EditorConfig/NamingStylePreferenceEditorConfigStorageLocation.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles; using Roslyn.Utilities; @@ -12,30 +13,13 @@ namespace Microsoft.CodeAnalysis.Options { internal sealed class NamingStylePreferenceEditorConfigStorageLocation : OptionStorageLocation2, IEditorConfigStorageLocation { - public bool TryGetOption(IReadOnlyDictionary rawOptions, Type type, out object result) - { - var tuple = ParseDictionary(rawOptions, type); - result = tuple.result; - return tuple.succeeded; - } - - private static (object result, bool succeeded) ParseDictionary( - IReadOnlyDictionary allRawConventions, Type type) + public bool TryGetOption(StructuredAnalyzerConfigOptions options, Type type, out object result) { if (type == typeof(NamingStylePreferences)) { - var editorconfigNamingStylePreferences = EditorConfigNamingStyleParser.GetNamingStylesFromDictionary(allRawConventions); - - if (!editorconfigNamingStylePreferences.NamingRules.Any() && - !editorconfigNamingStylePreferences.NamingStyles.Any() && - !editorconfigNamingStylePreferences.SymbolSpecifications.Any()) - { - // We were not able to parse any rules from editorconfig, tell the caller that the parse failed - return (result: editorconfigNamingStylePreferences, succeeded: false); - } - - // no existing naming styles were passed so just return the set of styles that were parsed from editorconfig - return (result: editorconfigNamingStylePreferences, succeeded: true); + var preferences = options.GetNamingStylePreferences(); + result = preferences; + return !preferences.IsEmpty; } throw ExceptionUtilities.UnexpectedValue(type);