File: EditorConfigNamingStyleParser.cs
Web Access
Project: ..\..\..\src\CodeStyle\Core\Analyzers\Microsoft.CodeAnalysis.CodeStyle.csproj (Microsoft.CodeAnalysis.CodeStyle)
// 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.Linq;
using System.Runtime.CompilerServices;
using Microsoft.CodeAnalysis.NamingStyles;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Diagnostics.Analyzers.NamingStyles
{
    internal static partial class EditorConfigNamingStyleParser
    {
        public static NamingStylePreferences ParseDictionary(AnalyzerConfigOptions allRawConventions)
        {
            var trimmedDictionary = TrimDictionary(allRawConventions);
 
            var symbolSpecifications = ArrayBuilder<SymbolSpecification>.GetInstance();
            var namingStyles = ArrayBuilder<NamingStyle>.GetInstance();
            var namingRules = ArrayBuilder<SerializableNamingRule>.GetInstance();
            var ruleNames = new Dictionary<(Guid symbolSpecificationID, Guid namingStyleID, ReportDiagnostic enforcementLevel), string>();
 
            foreach (var namingRuleTitle in GetRuleTitles(trimmedDictionary))
            {
                if (TryGetSymbolSpec(namingRuleTitle, trimmedDictionary, out var symbolSpec) &&
                    TryGetNamingStyleData(namingRuleTitle, trimmedDictionary, out var namingStyle) &&
                    TryGetSerializableNamingRule(namingRuleTitle, symbolSpec, namingStyle, trimmedDictionary, out var serializableNamingRule))
                {
                    symbolSpecifications.Add(symbolSpec);
                    namingStyles.Add(namingStyle);
                    namingRules.Add(serializableNamingRule);
 
                    var ruleKey = (serializableNamingRule.SymbolSpecificationID, serializableNamingRule.NamingStyleID, serializableNamingRule.EnforcementLevel);
                    if (ruleNames.TryGetValue(ruleKey, out var existingName))
                    {
                        // For duplicated rules, only preserve the one with a name that would sort first
                        var ordinalIgnoreCaseOrdering = StringComparer.OrdinalIgnoreCase.Compare(namingRuleTitle, existingName);
                        if (ordinalIgnoreCaseOrdering > 0)
                        {
                            continue;
                        }
                        else if (ordinalIgnoreCaseOrdering == 0)
                        {
                            var ordinalOrdering = StringComparer.Ordinal.Compare(namingRuleTitle, existingName);
                            if (ordinalOrdering > 0)
                            {
                                continue;
                            }
                        }
                    }
 
                    ruleNames[ruleKey] = namingRuleTitle;
                }
            }
 
            var preferences = new NamingStylePreferences(
                symbolSpecifications.ToImmutableAndFree(),
                namingStyles.ToImmutableAndFree(),
                namingRules.ToImmutableAndFree());
 
            // Deterministically order the naming style rules according to the symbols matched by the rule. The rules
            // are applied in order; later rules are only relevant if earlier rules fail to specify an order.
            //
            // 1. If the modifiers required by rule 'x' are a strict superset of the modifiers required by rule 'y',
            //    then rule 'x' is evaluated before rule 'y'.
            // 2. If the accessibilities allowed by rule 'x' are a strict subset of the accessibilities allowed by rule
            //    'y', then rule 'x' is evaluated before rule 'y'.
            // 3. If the set of symbols matched by rule 'x' are a strict subset of the symbols matched by rule 'y', then
            //    rule 'x' is evaluated before rule 'y'.
            //
            // If none of the above produces an order between two rules 'x' and 'y', then the rules are ordered
            // according to their name, first by OrdinalIgnoreCase and finally by Ordinal.
            //
            // Historical note: rules used to be ordered by their position in the .editorconfig file. However, this
            // relied on an implementation detail of the .editorconfig parser which is not preserved by all
            // implementations. In a review of .editorconfig files in the wild, the rules applied in this section were
            // the closest deterministic match for the files without having any reliance on order. For any pair of rules
            // which a user has trouble ordering, the intersection of the two rules can be broken out into a new rule
            // will always match earlier than the broader rules it was derived from.
            var orderedRules = preferences.Rules.NamingRules
                .OrderBy(rule => rule, NamingRuleModifierListComparer.Instance)
                .ThenBy(rule => rule, NamingRuleAccessibilityListComparer.Instance)
                .ThenBy(rule => rule, NamingRuleSymbolListComparer.Instance)
                .ThenBy(rule => ruleNames[(rule.SymbolSpecification.ID, rule.NamingStyle.ID, rule.EnforcementLevel)], StringComparer.OrdinalIgnoreCase)
                .ThenBy(rule => ruleNames[(rule.SymbolSpecification.ID, rule.NamingStyle.ID, rule.EnforcementLevel)], StringComparer.Ordinal);
 
            return new NamingStylePreferences(
                preferences.SymbolSpecifications,
                preferences.NamingStyles,
                orderedRules.SelectAsArray(
                    rule => new SerializableNamingRule
                    {
                        SymbolSpecificationID = rule.SymbolSpecification.ID,
                        NamingStyleID = rule.NamingStyle.ID,
                        EnforcementLevel = rule.EnforcementLevel,
                    }));
        }
 
        internal static Dictionary<string, string> TrimDictionary(AnalyzerConfigOptions allRawConventions)
        {
            var trimmedDictionary = new Dictionary<string, string>(AnalyzerConfigOptions.KeyComparer);
            foreach (var key in allRawConventions.Keys)
            {
                trimmedDictionary[key.Trim()] = allRawConventions.TryGetValue(key, out var value) ? value : throw new InvalidOperationException();
            }
 
            return trimmedDictionary;
        }
 
        public static IEnumerable<string> GetRuleTitles<T>(IReadOnlyDictionary<string, T> allRawConventions)
            => (from kvp in allRawConventions
                where kvp.Key.Trim().StartsWith("dotnet_naming_rule.", StringComparison.Ordinal)
                let nameSplit = kvp.Key.Split('.')
                where nameSplit.Length == 3
                select nameSplit[1])
                .Distinct();
 
        private abstract class NamingRuleSubsetComparer : IComparer<NamingRule>
        {
            protected NamingRuleSubsetComparer()
            {
            }
 
            public int Compare(NamingRule x, NamingRule y)
            {
                var firstIsSubset = FirstIsSubset(in x, in y);
                var secondIsSubset = FirstIsSubset(in y, in x);
                if (firstIsSubset)
                {
                    return secondIsSubset ? 0 : -1;
                }
                else
                {
                    return secondIsSubset ? 1 : 0;
                }
            }
 
            /// <summary>
            /// Determines if <paramref name="x"/> matches a subset of the symbols matched by <paramref name="y"/>. The
            /// implementation determines which properties of <see cref="NamingRule"/> are considered for this
            /// evaluation. The subset relation does not necessarily indicate a proper subset.
            /// </summary>
            /// <param name="x">The first naming rule.</param>
            /// <param name="y">The second naming rule.</param>
            /// <returns><see langword="true"/> if <paramref name="x"/> matches a subset of the symbols matched by
            /// <paramref name="y"/> on some implementation-defined properties; otherwise, <see langword="false"/>.</returns>
            protected abstract bool FirstIsSubset(in NamingRule x, in NamingRule y);
        }
 
        private sealed class NamingRuleAccessibilityListComparer : NamingRuleSubsetComparer
        {
            internal static readonly NamingRuleAccessibilityListComparer Instance = new();
 
            private NamingRuleAccessibilityListComparer()
            {
            }
 
            protected override bool FirstIsSubset(in NamingRule x, in NamingRule y)
            {
                foreach (var accessibility in x.SymbolSpecification.ApplicableAccessibilityList)
                {
                    if (!y.SymbolSpecification.ApplicableAccessibilityList.Contains(accessibility))
                    {
                        return false;
                    }
                }
 
                return true;
            }
        }
 
        private sealed class NamingRuleModifierListComparer : NamingRuleSubsetComparer
        {
            internal static readonly NamingRuleModifierListComparer Instance = new();
 
            private NamingRuleModifierListComparer()
            {
            }
 
            protected override bool FirstIsSubset(in NamingRule x, in NamingRule y)
            {
                // Since modifiers are "match all", a subset of symbols is matched by a superset of modifiers
                foreach (var modifier in y.SymbolSpecification.RequiredModifierList)
                {
                    if (modifier.ModifierKindWrapper is SymbolSpecification.ModifierKindEnum.IsStatic
                        or SymbolSpecification.ModifierKindEnum.IsReadOnly)
                    {
                        if (x.SymbolSpecification.RequiredModifierList.Any(static x => x.ModifierKindWrapper == SymbolSpecification.ModifierKindEnum.IsConst))
                        {
                            // 'const' implies both 'readonly' and 'static'
                            continue;
                        }
                    }
 
                    if (!x.SymbolSpecification.RequiredModifierList.Contains(modifier))
                    {
                        return false;
                    }
                }
 
                return true;
            }
        }
 
        private sealed class NamingRuleSymbolListComparer : NamingRuleSubsetComparer
        {
            internal static readonly NamingRuleSymbolListComparer Instance = new();
 
            private NamingRuleSymbolListComparer()
            {
            }
 
            protected override bool FirstIsSubset(in NamingRule x, in NamingRule y)
            {
                foreach (var symbolKind in x.SymbolSpecification.ApplicableSymbolKindList)
                {
                    if (!y.SymbolSpecification.ApplicableSymbolKindList.Contains(symbolKind))
                    {
                        return false;
                    }
                }
 
                return true;
            }
        }
    }
}