File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Options\EditorConfigValueSerializer.cs
Web Access
Project: src\src\Workspaces\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj (Microsoft.CodeAnalysis.Workspaces)
// 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 Microsoft.CodeAnalysis.CodeStyle;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Options;
 
internal static class EditorConfigValueSerializer
{
    private static string EscapeLineBreaks(string str)
        => str.Replace("\\r", "\r").Replace("\\n", "\n");
 
    private static string UnescapeLineBreaks(string str)
        => str.Replace("\r", "\\r").Replace("\n", "\\n");
 
    private static readonly EditorConfigValueSerializer<bool> s_bool = new(
        parseValue: ParseBoolean,
        serializeValue: SerializeBoolean);
 
    private static readonly EditorConfigValueSerializer<int> s_int32 = new(
        parseValue: str => int.TryParse(str, out var result) ? result : new Optional<int>(),
        serializeValue: StringExtensions.GetNumeral);
 
    private static readonly EditorConfigValueSerializer<string> s_string = new(
        parseValue: str => EscapeLineBreaks(str),
        serializeValue: UnescapeLineBreaks);
 
    private static readonly EditorConfigValueSerializer<bool?> s_nullableBoolean = new(
        parseValue: ParseNullableBoolean,
        serializeValue: value => value == null ? "null" : SerializeBoolean(value.Value));
 
    private static Optional<bool> ParseBoolean(string str)
        => bool.TryParse(str, out var result) ? result : new Optional<bool>();
 
    private static string SerializeBoolean(bool value)
        => value ? "true" : "false";
 
    private static Optional<bool?> ParseNullableBoolean(string str)
    {
        if (str.Equals("null", StringComparison.InvariantCultureIgnoreCase))
        {
            return new Optional<bool?>(null);
        }
 
        var optionalBool = ParseBoolean(str);
        return optionalBool.HasValue ? new Optional<bool?>(optionalBool.Value) : new Optional<bool?>();
    }
 
    public static EditorConfigValueSerializer<T> GetDefault<T>(bool isEditorConfigOption)
    {
        if (typeof(T) == typeof(bool))
            return (EditorConfigValueSerializer<T>)(object)s_bool;
 
        if (typeof(T) == typeof(int))
            return (EditorConfigValueSerializer<T>)(object)s_int32;
 
        if (typeof(T) == typeof(string))
            return (EditorConfigValueSerializer<T>)(object)s_string;
 
        if (typeof(T) == typeof(bool?))
            return (EditorConfigValueSerializer<T>)(object)s_nullableBoolean;
 
        // editorconfig options must have a serializer:
        if (isEditorConfigOption)
            throw ExceptionUtilities.UnexpectedValue(typeof(T));
 
        return EditorConfigValueSerializer<T>.Unsupported;
    }
 
    public static EditorConfigValueSerializer<string> String(string emptyStringRepresentation)
        => new(parseValue: str => str.Equals(emptyStringRepresentation, StringComparison.Ordinal) ? default(Optional<string>) : EscapeLineBreaks(str),
               serializeValue: value => string.IsNullOrEmpty(value) ? emptyStringRepresentation : UnescapeLineBreaks(value));
 
    public static EditorConfigValueSerializer<CodeStyleOption2<T>> CodeStyle<T>(CodeStyleOption2<T> defaultValue)
    {
        if (typeof(T) == typeof(bool))
            return (EditorConfigValueSerializer<CodeStyleOption2<T>>)(object)CodeStyle((CodeStyleOption2<bool>)(object)defaultValue);
 
        if (typeof(T) == typeof(string))
            return (EditorConfigValueSerializer<CodeStyleOption2<T>>)(object)CodeStyle((CodeStyleOption2<string>)(object)defaultValue);
 
        throw ExceptionUtilities.UnexpectedValue(typeof(T));
    }
 
    public static EditorConfigValueSerializer<CodeStyleOption2<bool>> CodeStyle(CodeStyleOption2<bool> defaultValue)
        => new(parseValue: str => CodeStyleHelpers.TryParseBoolEditorConfigCodeStyleOption(str, defaultValue, out var result) ? result : new Optional<CodeStyleOption2<bool>>(),
               serializeValue: value => (value.Value ? "true" : "false") + CodeStyleHelpers.GetEditorConfigStringNotificationPart(value, defaultValue));
 
    public static EditorConfigValueSerializer<CodeStyleOption2<string>> CodeStyle(CodeStyleOption2<string> defaultValue)
        => new(parseValue: str => CodeStyleHelpers.TryParseStringEditorConfigCodeStyleOption(str, defaultValue, out var result) ? result : new Optional<CodeStyleOption2<string>>(),
               serializeValue: value => value.Value.ToLowerInvariant() + CodeStyleHelpers.GetEditorConfigStringNotificationPart(value, defaultValue));
 
    /// <summary>
    /// Creates a serializer for an enum value that uses the enum field names.
    /// </summary>
    public static EditorConfigValueSerializer<T> CreateSerializerForEnum<T>() where T : struct, Enum
        => new(
            parseValue: str => TryParseEnum<T>(str, out var result) ? new Optional<T>(result) : new Optional<T>(),
            serializeValue: value => value.ToString());
 
    /// <summary>
    /// Creates a serializer for an enum value given a <paramref name="map"/> between value names and the corresponding enum values.
    /// </summary>
    public static EditorConfigValueSerializer<T> CreateSerializerForEnum<T>(BidirectionalMap<string, T> map) where T : struct, Enum
        => CreateSerializerForEnum(map, ImmutableDictionary<string, T>.Empty);
 
    /// <summary>
    /// Creates a serializer for an enum value given a <paramref name="map"/> between value names and the corresponding enum values.
    /// <paramref name="alternative"/> specifies alternative value representations for backward compatibility.
    /// </summary>
    public static EditorConfigValueSerializer<T> CreateSerializerForEnum<T>(BidirectionalMap<string, T> map, ImmutableDictionary<string, T> alternative) where T : struct, Enum
        => new(parseValue: str => map.TryGetValue(str, out var result) || alternative.TryGetValue(str, out result) ? new Optional<T>(result) : new Optional<T>(),
               serializeValue: value => map.TryGetKey(value, out var key) ? key : throw ExceptionUtilities.UnexpectedValue(value));
 
    /// <summary>
    /// Creates a serializer for an enum value given a <paramref name="entries"/> between value names and the corresponding enum values.
    /// <paramref name="alternativeEntries"/> specifies alternative value representations for backward compatibility.
    /// </summary>
    public static EditorConfigValueSerializer<T> CreateSerializerForEnum<T>(IEnumerable<(string name, T value)> entries, IEnumerable<(string name, T value)> alternativeEntries) where T : struct, Enum
    {
        var map = new BidirectionalMap<string, T>(entries, StringComparer.OrdinalIgnoreCase);
        var alternativeMap = ImmutableDictionary<string, T>.Empty.WithComparers(keyComparer: StringComparer.OrdinalIgnoreCase)
            .AddRange(alternativeEntries.Select(static p => KeyValuePairUtil.Create(p.name, p.value)));
 
        return CreateSerializerForEnum(map, alternativeMap);
    }
 
    public static EditorConfigValueSerializer<T?> CreateSerializerForNullableEnum<T>() where T : struct, Enum
    {
        return new EditorConfigValueSerializer<T?>(
            parseValue: ParseValueForNullableEnum,
            serializeValue: value => value == null ? "null" : value.Value.ToString());
 
        static Optional<T?> ParseValueForNullableEnum(string str)
        {
            if (str.Equals("null", StringComparison.InvariantCultureIgnoreCase))
            {
                return new Optional<T?>(null);
            }
 
            if (TryParseEnum<T>(str, out var parsedValue))
            {
                return new Optional<T?>(parsedValue);
            }
 
            return new Optional<T?>();
        }
    }
 
    private static bool TryParseEnum<T>(string str, out T result) where T : struct, Enum
    {
        result = default;
        // Block any int value.
        if (int.TryParse(str, out _))
        {
            return false;
        }
 
        // Enum.TryParse parses every enum as flags enum, we don't want to multiple values to be specified for enums are not flags.
        if (str.Contains(","))
        {
            return false;
        }
 
        return Enum.TryParse(str, ignoreCase: true, out result);
    }
}