File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\EditorConfig\Parsing\Sections\Section.cs
Web Access
Project: src\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 record class 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 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()));
}