// 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 Microsoft.CodeAnalysis.NamingStyles; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Text; 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 _ = ArrayBuilder<(NamingRule rule, int priority, string title)>.GetInstance(out var namingRules); foreach (var namingRuleTitle in GetRuleTitles(trimmedDictionary)) { if (TryGetSymbolSpecification(namingRuleTitle, trimmedDictionary, out var symbolSpec) && TryGetNamingStyle(namingRuleTitle, trimmedDictionary, out var namingStyle) && TryGetRule(namingRuleTitle, symbolSpec, namingStyle, trimmedDictionary, out var rule, out var priority)) { namingRules.Add((rule.Value, priority, namingRuleTitle)); } } // Deterministically order the naming style rules. // // Rules of the same priority are ordered 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 = namingRules .OrderBy(item => item.priority) .ThenBy(item => item.rule, NamingRuleModifierListComparer.Instance) .ThenBy(item => item.rule, NamingRuleAccessibilityListComparer.Instance) .ThenBy(item => item.rule, NamingRuleSymbolListComparer.Instance) .ThenBy(item => item.title, StringComparer.OrdinalIgnoreCase) .ThenBy(item => item.title, StringComparer.Ordinal); using var _1 = ArrayBuilder<SymbolSpecification>.GetInstance(out var symbolSpecifications); using var _2 = ArrayBuilder<NamingStyle>.GetInstance(out var namingStyles); using var _3 = ArrayBuilder<SerializableNamingRule>.GetInstance(out var serializableRules); foreach (var (rule, _, _) in orderedRules) { symbolSpecifications.Add(rule.SymbolSpecification); namingStyles.Add(rule.NamingStyle); serializableRules.Add(new SerializableNamingRule { SymbolSpecificationID = rule.SymbolSpecification.ID, NamingStyleID = rule.NamingStyle.ID, EnforcementLevel = rule.EnforcementLevel, }); } return new NamingStylePreferences( symbolSpecifications.ToImmutable(), namingStyles.ToImmutable(), serializableRules.ToImmutable()); } 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(IReadOnlyDictionary<string, string> 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 static Property<TValue> GetProperty<TValue>( IReadOnlyDictionary<string, string> entries, string group, string ruleName, string componentIdentifier, Func<string, TValue> parser, TValue defaultValue) { var key = $"{group}.{ruleName}.{componentIdentifier}"; var value = entries.TryGetValue(key, out var str) ? parser(str) : defaultValue; return new(key, value); } private readonly struct Property<TValue>(string key, TValue value) { public string Key { get; } = key; public TValue Value { get; } = value; public TextSpan? GetSpan(IReadOnlyDictionary<string, TextLine> lines) => lines.TryGetValue(Key, out var line) ? line.Span : null; } 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; } } } |