File: Section.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.IO;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.EditorConfig.Parsing
{
    internal sealed class Section : IEquatable<Section>
    {
        public string? FilePath { get; init; }
        public bool IsGlobal { get; init; }
        public TextSpan Span { get; init; }
        public string Text { get; init; }
        public string FullText { get; init; }
 
        private bool IsSplatHeader => Text.Equals("*", StringComparison.Ordinal);
        private readonly SectionMatcher? _matcher;
        private readonly string? _containingDirectory;
 
        public Section(string? filePath, bool isGlobal, TextSpan span, string text, string fullText)
        {
            FilePath = filePath;
            IsGlobal = isGlobal;
            Span = span;
            Text = text;
            FullText = fullText;
 
            if (SectionMatcher.TryParseSection(Text, out var matcher))
            {
                _matcher = matcher;
            }
 
            if (FilePath is not null)
            {
                var containingDirectory = Path.GetDirectoryName(FilePath);
                RoslynDebug.AssertNotNull(containingDirectory);
                _containingDirectory = containingDirectory.ToLowerInvariant();
            }
        }
 
        /// <summary>
        /// Returns the default section header text for the given language combination
        /// </summary>
        /// <param name="language">The language combination to find the default header text for.</param>
        /// <returns>the default header text.</returns>
        public static string? GetHeaderTextForLanguage(Language language)
        {
            if (language.HasFlag(Language.CSharp) && language.HasFlag(Language.VisualBasic))
            {
                return "[*.{cs,vb}]\r\n";
            }
            else if (language.HasFlag(Language.CSharp))
            {
                return "[*.cs]\r\n";
            }
            else if (language.HasFlag(Language.VisualBasic))
            {
                return "[*.vb]\r\n";
            }
 
            return null;
        }
 
        /// <summary>
        /// Checks where this header supports the given language for the given match criteria
        /// </summary>
        /// <param name="language">The language to check support for.</param>
        /// <param name="matchKind">The criteria for which we consider a language a mache the default is <see cref="SectionMatch.ExactLanguageMatch"/>.</param>
        /// <returns>If this section is a match for the given language, meaning options can be added here.</returns>
        public bool SupportsLanguage(Language language, SectionMatch matchKind = default)
            => GetMatchKind(language).IsBetterOrEqualMatchThan(matchKind);
 
        /// <summary>
        /// Checks where this header supports the given file path for the given match criteria
        /// </summary>
        /// <param name="codeFilePath">full path to a file</param>
        /// <param name="matchKind">The criteria for which we consider a language a mache the default is <see cref="SectionMatch.ExactLanguageMatch"/>.</param>
        /// <remarks>
        /// If the section header cannot be parsed because it it invalid this method will always return no match.
        /// If no file path was given in the operation that produces this section and a relative path comparison is required to check for support this method will return no match.
        /// </remarks>
        /// <returns>If this section is a match for the given file, meaning options can be added here.</returns>
        public bool SupportsFilePath(string codeFilePath, SectionMatch matchKind = default)
            => GetMatchKind(codeFilePath).IsBetterOrEqualMatchThan(matchKind);
 
        public SectionMatch GetMatchKind(Language language)
        {
            if (IsGlobal)
            {
                return SectionMatch.GlobalSectionMatch;
            }
 
            if (IsSplatHeader)
            {
                return SectionMatch.SplatMatch;
            }
 
            if (_matcher is not { } matcher)
            {
                // the header could not be parsed
                return SectionMatch.NoMatch;
            }
 
            return matcher.GetLanguageMatchKind(language);
        }
 
        public SectionMatch GetMatchKind(string codeFilePath)
        {
            if (IsGlobal)
            {
                return SectionMatch.GlobalSectionMatch;
            }
 
            if (IsSplatHeader)
            {
                return SectionMatch.SplatMatch;
            }
 
            if (_matcher is not { } matcher)
            {
                // the header could not be parsed
                return SectionMatch.NoMatch;
            }
 
            if (!codeFilePath.TryGetLanguageFromFilePath(out var language))
            {
                // the file is from an unknown language
                return SectionMatch.NoMatch;
            }
 
            var languageMatchKind = matcher.GetLanguageMatchKind(language);
            if (_containingDirectory is null)
            {
                if (languageMatchKind.IsWorseMatchThan(SectionMatch.AnyLanguageMatch))
                {
                    // no editorconfig path was given and we need to
                    // evaluate paths relative to each other to give an answer
                    throw new InvalidOperationException("No path given for editorconfig");
                }
 
                return languageMatchKind;
            }
 
            var relativePath = GetPathRelativeToEditorconfig(_containingDirectory, codeFilePath);
            if (relativePath.IsEmpty())
            {
                // we _are_ the editorconfig file so we match
                return SectionMatch.SupersetFilePatternMatch;
            }
 
            return matcher.GetPathMatchKind(relativePath);
 
            static string GetPathRelativeToEditorconfig(string directoryContainingEditorconfig, string codeFilePath)
            {
                var relativePath = PathUtilities.GetRelativePath(directoryContainingEditorconfig, codeFilePath);
                relativePath = relativePath.Replace("\\", "/");
                if (!relativePath.StartsWith("/"))
                {
                    relativePath = "/" + relativePath;
                }
 
                return relativePath;
            }
        }
 
        public static bool operator !=(Section left, Section right)
            => !(left == right);
 
        public static bool operator ==(Section left, Section right)
            => ReferenceEquals(left, right) ||
               (left is not null && left.Equals(right));
 
        public override bool Equals(object? obj)
            => Equals(obj as Section);
 
        public bool Equals(Section? other)
            => ReferenceEquals(this, other) ||
                          (other is not null &&
                           StringComparer.OrdinalIgnoreCase.Equals(FilePath, other.FilePath) &&
                           Span == other.Span &&
                           Text == other.Text);
 
        public override int GetHashCode()
            => Hash.Combine(
                StringComparer.OrdinalIgnoreCase.GetHashCode(FilePath ?? string.Empty),
                Hash.Combine(
                    Span.GetHashCode(),
                    Text.GetHashCode()));
    }
}