// 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; using System.IO; using System.Text.RegularExpressions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis { /// /// Represents a single EditorConfig file, see https://editorconfig.org for details about the format. /// public sealed partial class AnalyzerConfig { // Matches EditorConfig section header such as "[*.{js,py}]", see https://editorconfig.org for details private static readonly Regex s_sectionMatcher = new Regex(@"^\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 Regex(@"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$", RegexOptions.Compiled); /// /// Key that indicates if this config is a global config /// internal const string GlobalKey = "is_global"; /// /// Key that indicates the precedence of this config when is true /// internal const string GlobalLevelKey = "global_level"; /// /// Filename that indicates this file is a user provided global config /// internal const string UserGlobalConfigName = ".globalconfig"; /// /// A set of keys that are reserved for special interpretation for the editorconfig specification. /// All values corresponding to reserved keys in a (key,value) property pair are always lowercased /// during parsing. /// /// /// This list was retrieved from https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties /// at 2018-04-21 19:37:05Z. New keys may be added to this list in newer versions, but old ones will /// not be removed. /// internal static ImmutableHashSet ReservedKeys { get; } = ImmutableHashSet.CreateRange(Section.PropertiesKeyComparer, new[] { "root", "indent_style", "indent_size", "tab_width", "end_of_line", "charset", "trim_trailing_whitespace", "insert_final_newline", }); /// /// A set of values that are reserved for special use for the editorconfig specification /// and will always be lower-cased by the parser. /// internal static ImmutableHashSet ReservedValues { get; } = ImmutableHashSet.CreateRange(CaseInsensitiveComparison.Comparer, new[] { "unset" }); internal Section GlobalSection { get; } /// /// The directory the editorconfig was contained in, with all directory separators /// replaced with '/'. /// internal string NormalizedDirectory { get; } /// /// The path passed to during construction. /// internal string PathToFile { get; } /// /// Comparer for sorting files by path length. /// internal static Comparer DirectoryLengthComparer { get; } = Comparer.Create( (e1, e2) => e1.NormalizedDirectory.Length.CompareTo(e2.NormalizedDirectory.Length)); internal ImmutableArray
NamedSections { get; } /// /// Gets whether this editorconfig is a topmost editorconfig. /// internal bool IsRoot => GlobalSection.Properties.TryGetValue("root", out string? val) && val == "true"; /// /// Gets whether this editorconfig is a global editorconfig. /// internal bool IsGlobal => _hasGlobalFileName || GlobalSection.Properties.ContainsKey(GlobalKey); /// /// Get the global level of this config, used to resolve conflicting keys /// /// /// A user can explicitly set the global level via the . /// When no global level is explicitly set, we use a heuristic: /// /// /// Any file matching the is determined to be a user supplied global config and gets a level of 100 /// /// /// Any other file gets a default level of 0 /// /// /// /// This value is unused when is false. /// internal int GlobalLevel { get { if (GlobalSection.Properties.TryGetValue(GlobalLevelKey, out string? val) && int.TryParse(val, out int level)) { return level; } else if (_hasGlobalFileName) { return 100; } else { return 0; } } } private readonly bool _hasGlobalFileName; private AnalyzerConfig( Section globalSection, ImmutableArray
namedSections, string pathToFile) { GlobalSection = globalSection; NamedSections = namedSections; PathToFile = pathToFile; _hasGlobalFileName = Path.GetFileName(pathToFile).Equals(UserGlobalConfigName, StringComparison.OrdinalIgnoreCase); // Find the containing directory and normalize the path separators string directory = Path.GetDirectoryName(pathToFile) ?? pathToFile; NormalizedDirectory = PathUtilities.NormalizeWithForwardSlash(directory); } /// /// Parses an editor config file text located at the given path. No parsing /// errors are reported. If any line contains a parse error, it is dropped. /// public static AnalyzerConfig Parse(string text, string? pathToFile) { return Parse(SourceText.From(text), pathToFile); } /// /// Parses an editor config file text located at the given path. No parsing /// errors are reported. If any line contains a parse error, it is dropped. /// public static AnalyzerConfig Parse(SourceText text, string? pathToFile) { if (pathToFile is null || !Path.IsPathRooted(pathToFile) || string.IsNullOrEmpty(Path.GetFileName(pathToFile))) { throw new ArgumentException("Must be an absolute path to an editorconfig file", nameof(pathToFile)); } Section? globalSection = null; var namedSectionBuilder = ImmutableArray.CreateBuilder
(); // N.B. The editorconfig documentation is quite loose on property interpretation. // Specifically, it says: // Currently all properties and values are case-insensitive. // They are lowercased when parsed. // To accommodate this, we use a lower case Unicode mapping when adding to the // dictionary, but we also use a case-insensitive key comparer when doing lookups var activeSectionProperties = ImmutableDictionary.CreateBuilder( Section.PropertiesKeyComparer); string activeSectionName = ""; foreach (var textLine in text.Lines) { string line = textLine.ToString(); if (string.IsNullOrWhiteSpace(line)) { continue; } if (IsComment(line)) { continue; } var sectionMatches = s_sectionMatcher.Matches(line); if (sectionMatches.Count > 0 && sectionMatches[0].Groups.Count > 0) { addNewSection(); var sectionName = sectionMatches[0].Groups[1].Value; Debug.Assert(!string.IsNullOrEmpty(sectionName)); activeSectionName = sectionName; activeSectionProperties = ImmutableDictionary.CreateBuilder( Section.PropertiesKeyComparer); continue; } var propMatches = s_propertyMatcher.Matches(line); if (propMatches.Count > 0 && propMatches[0].Groups.Count > 1) { var key = propMatches[0].Groups[1].Value; var value = propMatches[0].Groups[2].Value; Debug.Assert(!string.IsNullOrEmpty(key)); Debug.Assert(key == key.Trim()); Debug.Assert(value == value?.Trim()); key = CaseInsensitiveComparison.ToLower(key); if (ReservedKeys.Contains(key) || ReservedValues.Contains(value)) { value = CaseInsensitiveComparison.ToLower(value); } activeSectionProperties[key] = value ?? ""; continue; } } // Add the last section addNewSection(); return new AnalyzerConfig(globalSection!, namedSectionBuilder.ToImmutable(), pathToFile); void addNewSection() { // Close out the previous section var previousSection = new Section(activeSectionName, activeSectionProperties.ToImmutable()); if (activeSectionName == "") { // This is the global section globalSection = previousSection; } else { namedSectionBuilder.Add(previousSection); } } } private static bool IsComment(string line) { foreach (char c in line) { if (!char.IsWhiteSpace(c)) { return c == '#' || c == ';'; } } return false; } /// /// Represents a named section of the editorconfig file, which consists of a name followed by a set /// of key-value pairs. /// internal sealed class Section { /// /// Used to compare s of sections. Specified by editorconfig to /// be a case-sensitive comparison. /// public static StringComparison NameComparer { get; } = StringComparison.Ordinal; /// /// Used to compare s of sections. Specified by editorconfig to /// be a case-sensitive comparison. /// public static IEqualityComparer NameEqualityComparer { get; } = StringComparer.Ordinal; /// /// Used to compare keys in . The editorconfig spec defines property /// keys as being compared case-insensitively according to Unicode lower-case rules. /// public static StringComparer PropertiesKeyComparer { get; } = CaseInsensitiveComparison.Comparer; public Section(string name, ImmutableDictionary properties) { Name = name; Properties = properties; } /// /// The name as present directly in the section specification of the editorconfig file. /// public string Name { get; } /// /// Keys and values for this section. All keys are lower-cased according to the /// EditorConfig specification and keys are compared case-insensitively. Values are /// lower-cased if the value appears in /// or if the corresponding key is in . Otherwise, /// the values are the literal values present in the source. /// public ImmutableDictionary Properties { get; } } } }