File: Options\GlobalOptionsTests.cs
Web Access
Project: src\src\EditorFeatures\Test\Microsoft.CodeAnalysis.EditorFeatures.UnitTests.csproj (Microsoft.CodeAnalysis.EditorFeatures.UnitTests)
// 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.Composition;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Microsoft.CodeAnalysis.AddImport;
using Microsoft.CodeAnalysis.BraceMatching;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.DocumentHighlighting;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.UnitTests;
using Microsoft.CodeAnalysis.FindUsages;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.ImplementType;
using Microsoft.CodeAnalysis.InlineHints;
using Microsoft.CodeAnalysis.MetadataAsSource;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Structure;
using Microsoft.CodeAnalysis.SymbolSearch;
using Microsoft.CodeAnalysis.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.UnitTests;
 
[UseExportProvider]
public sealed class GlobalOptionsTests
{
    [Export(typeof(IGlobalOptionService)), Shared, PartNotDiscoverable]
    internal sealed class TestGlobalOptions : IGlobalOptionService
    {
        public readonly List<OptionKey2> AccessedOptionKeys = [];
 
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public TestGlobalOptions()
        {
        }
 
        private void OnOptionAccessed(OptionKey2 key)
        {
            AccessedOptionKeys.Add(key);
        }
 
        bool IOptionsReader.TryGetOption<T>(OptionKey2 optionKey, out T value)
        {
            value = GetOption<T>(optionKey);
            return true;
        }
 
        public T GetOption<T>(Option2<T> option)
            => GetOption<T>(new OptionKey2(option));
 
        public T GetOption<T>(PerLanguageOption2<T> option, string languageName)
            => GetOption<T>(new OptionKey2(option, languageName));
 
        public T GetOption<T>(OptionKey2 optionKey)
        {
            OnOptionAccessed(optionKey);
            return (T)OptionsTestHelpers.GetDifferentValue(typeof(T), optionKey.Option.DefaultValue)!;
        }
 
        #region Unused
 
        public ImmutableArray<object?> GetOptions(ImmutableArray<OptionKey2> optionKeys)
            => throw new NotImplementedException();
 
        public bool RefreshOption(OptionKey2 optionKey, object? newValue)
            => throw new NotImplementedException();
 
        public void SetGlobalOption<T>(Option2<T> option, T value)
            => throw new NotImplementedException();
 
        public void SetGlobalOption<T>(PerLanguageOption2<T> option, string language, T value)
            => throw new NotImplementedException();
 
        public void SetGlobalOption(OptionKey2 optionKey, object? value)
            => throw new NotImplementedException();
 
        public bool SetGlobalOptions(ImmutableArray<KeyValuePair<OptionKey2, object?>> options)
            => throw new NotImplementedException();
 
        public void AddOptionChangedHandler(object target, WeakEventHandler<OptionChangedEventArgs> handler)
            => throw new NotImplementedException();
 
        public void RemoveOptionChangedHandler(object target, WeakEventHandler<OptionChangedEventArgs> handler)
            => throw new NotImplementedException();
 
        public StructuredAnalyzerConfigOptions GetAnalyzerConfigOptions(string language)
            => throw new NotImplementedException();
 
        #endregion
    }
 
    private static void VerifyDataMembersHaveNonDefaultValues(object options, object defaultOptions, string? language = null)
    {
        Assert.Equal(options.GetType(), defaultOptions.GetType());
        Recurse(options.GetType(), options, defaultOptions, language);
 
        static void Recurse(Type type, object options, object defaultOptions, string? language)
        {
            foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
            {
                if (property.GetCustomAttributes<DataMemberAttribute>().Any())
                {
                    // value initialized from global options:
                    var value = property.GetValue(options);
 
                    // default value for the option -- may be different then default(T):
                    var defaultValue = property.GetValue(defaultOptions);
 
                    if (OptionDefinition.IsSupportedOptionType(property.PropertyType))
                    {
                        // Skip validation of ReloadChangedAnalyzerReferences.  The test options store returns 'true'
                        // for 'null' (which the option uses to mean 'try the feature flag').  Which is also equivalent
                        // to the default for this option.
                        if (IsStoredInGlobalOptions(property, language) &&
                            property.Name != nameof(WorkspaceConfigurationOptions.ReloadChangedAnalyzerReferences))
                        {
                            Assert.False(Equals(value, defaultValue), $"{type.FullName}.{property.Name} not initialized from global options");
                        }
                    }
                    else
                    {
                        var propertyType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
 
                        if (propertyType != property.PropertyType)
                        {
                            var getValueOrDefault = property.PropertyType.GetMethod("GetValueOrDefault", []);
                            value = getValueOrDefault.Invoke(value, []);
                            defaultValue = getValueOrDefault.Invoke(defaultValue, []);
                        }
 
                        Recurse(propertyType, value, defaultValue, language);
                    }
                }
            }
        }
    }
 
    private static TestWorkspace CreateWorkspace(out TestGlobalOptions globalOptions)
    {
        var composition = EditorTestCompositions.LanguageServerProtocolEditorFeatures.
            AddExcludedPartTypes(typeof(GlobalOptionService)).
            AddParts(typeof(TestGlobalOptions));
 
        var workspace = new TestWorkspace(composition: composition);
        globalOptions = Assert.IsType<TestGlobalOptions>(workspace.ExportProvider.GetExportedValue<IGlobalOptionService>());
        return workspace;
    }
 
    /// <summary>
    /// Properties for options not stored in global options.
    /// </summary>
    private static bool IsStoredInGlobalOptions(PropertyInfo property, string? language)
        => !(property.DeclaringType == typeof(AddImportPlacementOptions) && property.Name == nameof(AddImportPlacementOptions.AllowInHiddenRegions) ||
             property.DeclaringType == typeof(AddImportPlacementOptions) && property.Name == nameof(AddImportPlacementOptions.UsingDirectivePlacement) && language == LanguageNames.VisualBasic ||
             property.DeclaringType == typeof(DocumentFormattingOptions) && property.Name == nameof(DocumentFormattingOptions.FileHeaderTemplate) ||
             property.DeclaringType == typeof(DocumentFormattingOptions) && property.Name == nameof(DocumentFormattingOptions.InsertFinalNewLine) ||
             property.DeclaringType == typeof(ClassificationOptions) && property.Name == nameof(ClassificationOptions.FrozenPartialSemantics) ||
             property.DeclaringType == typeof(HighlightingOptions) && property.Name == nameof(HighlightingOptions.FrozenPartialSemantics) ||
             property.DeclaringType == typeof(BlockStructureOptions) && property.Name == nameof(BlockStructureOptions.IsMetadataAsSource));
 
    /// <summary>
    /// Our mock <see cref="IGlobalOptionService"/> implementation returns a non-default value for each option it reads.
    /// Option objects initialized from this service thus should have all their data properties initialized to
    /// non-default values. We then enumerate these properties via reflection and compare each property value with the
    /// default instance of the respective options type.
    /// </summary>
    [Theory]
    [InlineData(LanguageNames.CSharp)]
    [InlineData(LanguageNames.VisualBasic)]
    public void ReadingOptionsFromGlobalOptions(string language)
    {
        using var workspace = CreateWorkspace(out var globalOptions);
        var languageServices = workspace.Services.SolutionServices.GetLanguageServices(language);
 
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetBraceMatchingOptions(language), BraceMatchingOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetFindUsagesOptions(language), FindUsagesOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetInlineHintsOptions(language), InlineHintsOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetAutoFormattingOptions(language), AutoFormattingOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetBlockStructureOptions(language, isMetadataAsSource: false), BlockStructureOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetDocumentationCommentOptions(globalOptions.GetLineFormattingOptions(language), language), DocumentationCommentOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetImplementTypeOptions(language), ImplementTypeOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetMetadataAsSourceOptions(), MetadataAsSourceOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetMemberDisplayOptions(language), MemberDisplayOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetSymbolSearchOptions(language), SymbolSearchOptions.Default, language);
        VerifyDataMembersHaveNonDefaultValues(globalOptions.GetWorkspaceConfigurationOptions(), WorkspaceConfigurationOptions.Default);
    }
}