File: EditorConfigParser.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.Collections.Immutable;
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.EditorConfig.Parsing
{
    internal static class EditorConfigParser
    {
        // Matches EditorConfig section header such as "[*.{js,py}]", see https://editorconfig.org for details
        private static readonly Regex s_sectionMatcher = new(@"^\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(@"^\s*([\w\.\-_]+)\s*[=:]\s*(.*?)\s*([#;].*)?$", RegexOptions.Compiled);
 
        private static ImmutableHashSet<string> ReservedKeys { get; }
            = ImmutableHashSet.CreateRange(AnalyzerConfigOptions.KeyComparer, new[] {
                "root",
                "indent_style",
                "indent_size",
                "tab_width",
                "end_of_line",
                "charset",
                "trim_trailing_whitespace",
                "insert_final_newline",
            });
 
        private static ImmutableHashSet<string> ReservedValues { get; }
            = ImmutableHashSet.CreateRange(CaseInsensitiveComparison.Comparer, new[] { "unset" });
 
        public static TEditorConfigFile Parse<TEditorConfigFile, TResult, TAccumulator>(string text, string? pathToFile, TAccumulator accumulator)
            where TAccumulator : IEditorConfigOptionAccumulator<TEditorConfigFile, TResult>
            where TEditorConfigFile : EditorConfigFile<TResult>
            where TResult : EditorConfigOption
        {
            return Parse<TEditorConfigFile, TResult, TAccumulator>(SourceText.From(text), pathToFile, accumulator);
        }
 
        public static TEditorConfigFile Parse<TEditorConfigFile, TEditorConfigOption, TAccumulator>(SourceText text, string? pathToFile, TAccumulator accumulator)
            where TAccumulator : IEditorConfigOptionAccumulator<TEditorConfigFile, TEditorConfigOption>
            where TEditorConfigFile : EditorConfigFile<TEditorConfigOption>
            where TEditorConfigOption : EditorConfigOption
        {
            var activeSectionProperties = ImmutableDictionary.CreateBuilder<string, (string value, TextLine? line)>(AnalyzerConfigOptions.KeyComparer);
            var activeSectionName = "";
            var activeSectionStart = 0;
            var activeSectionEnd = 0;
            var activeLine = default(TextLine);
            foreach (var textLine in text.Lines)
            {
                activeLine = textLine;
                var line = textLine.ToString();
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }
 
                if (IsComment(line))
                {
                    continue;
                }
 
                // Section matching
                var sectionMatches = s_sectionMatcher.Matches(line);
                if (sectionMatches is [{ Groups.Count: > 0 }, ..])
                {
                    ProcessActiveSection();
                    var sectionName = sectionMatches[0].Groups[1].Value;
                    Debug.Assert(!string.IsNullOrEmpty(sectionName));
 
                    activeSectionStart = textLine.Start;
                    activeSectionName = sectionName;
                    activeSectionEnd = textLine.End;
                    activeSectionProperties.Clear();
                    continue;
                }
 
                // property matching
                var propMatches = s_propertyMatcher.Matches(line);
                if (propMatches is [{ 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);
                    }
 
                    if (value is null)
                    {
                        activeSectionProperties[key] = (string.Empty, null);
                    }
                    else
                    {
                        activeSectionProperties[key] = (value, textLine);
                        activeSectionEnd = textLine.End;
                    }
 
                    continue;
                }
            }
 
            // add remaining property to the final section
            ProcessActiveSection();
 
            return accumulator.Complete(pathToFile);
 
            void ProcessActiveSection()
            {
                var isGlobal = activeSectionName == "";
                var fullText = activeLine.ToString();
                var sectionSpan = new TextSpan(activeSectionStart, activeSectionEnd);
                var previousSection = new Section(pathToFile, isGlobal, sectionSpan, activeSectionName, fullText);
                accumulator.ProcessSection(previousSection, activeSectionProperties);
            }
 
            static bool IsComment(string line)
            {
                foreach (var c in line)
                {
                    if (!char.IsWhiteSpace(c))
                    {
                        return c == '#' || c == ';';
                    }
                }
 
                return false;
            }
        }
    }
}