File: Internal\MatcherContext.cs
Web Access
Project: src\src\libraries\Microsoft.Extensions.FileSystemGlobbing\src\Microsoft.Extensions.FileSystemGlobbing.csproj (Microsoft.Extensions.FileSystemGlobbing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments;
using Microsoft.Extensions.FileSystemGlobbing.Util;
 
namespace Microsoft.Extensions.FileSystemGlobbing.Internal
{
    /// <summary>
    /// This API supports infrastructure and is not intended to be used
    /// directly from your code. This API may change or be removed in future releases.
    /// </summary>
    public class MatcherContext
    {
        private readonly DirectoryInfoBase _root;
        private readonly IPatternContext[] _includePatternContexts;
        private readonly IPatternContext[] _excludePatternContexts;
        private readonly List<FilePatternMatch> _files;
 
        private readonly HashSet<string> _declaredLiteralFolderSegmentInString;
        private readonly HashSet<LiteralPathSegment> _declaredLiteralFileSegments = new HashSet<LiteralPathSegment>();
 
        private bool _declaredParentPathSegment;
        private bool _declaredWildcardPathSegment;
 
        private readonly StringComparison _comparisonType;
 
        public MatcherContext(
            IEnumerable<IPattern> includePatterns,
            IEnumerable<IPattern> excludePatterns,
            DirectoryInfoBase directoryInfo,
            StringComparison comparison)
        {
            _root = directoryInfo;
            _files = new List<FilePatternMatch>();
            _comparisonType = comparison;
 
            _includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToArray();
            _excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToArray();
 
            _declaredLiteralFolderSegmentInString = new HashSet<string>(StringComparisonHelper.GetStringComparer(comparison));
        }
 
        public PatternMatchingResult Execute()
        {
            _files.Clear();
 
            Match(_root, parentRelativePath: null);
 
            return new PatternMatchingResult(_files, _files.Count > 0);
        }
 
        private void Match(DirectoryInfoBase directory, string? parentRelativePath)
        {
            // Request all the including and excluding patterns to push current directory onto their status stack.
            PushDirectory(directory);
            Declare();
 
            var entities = new List<FileSystemInfoBase?>();
            if (_declaredWildcardPathSegment || _declaredLiteralFileSegments.Count != 0)
            {
                entities.AddRange(directory.EnumerateFileSystemInfos());
            }
            else
            {
                IEnumerable<FileSystemInfoBase> candidates = directory.EnumerateFileSystemInfos();
                foreach (FileSystemInfoBase candidate in candidates)
                {
                    if (candidate is DirectoryInfoBase &&
                        _declaredLiteralFolderSegmentInString.Contains(candidate.Name))
                    {
                        entities.Add(candidate);
                    }
                }
            }
 
            if (_declaredParentPathSegment)
            {
                entities.Add(directory.GetDirectory(".."));
            }
 
            // collect files and sub directories
            var subDirectories = new List<DirectoryInfoBase>();
            foreach (FileSystemInfoBase? entity in entities)
            {
                if (entity is FileInfoBase fileInfo)
                {
                    PatternTestResult result = MatchPatternContexts(fileInfo, (pattern, file) => pattern.Test(file));
                    if (result.IsSuccessful)
                    {
                        _files.Add(new FilePatternMatch(
                            path: CombinePath(parentRelativePath, fileInfo.Name),
                            stem: result.Stem));
                    }
 
                    continue;
                }
 
                if (entity is DirectoryInfoBase directoryInfo)
                {
                    if (MatchPatternContexts(directoryInfo, (pattern, dir) => pattern.Test(dir)))
                    {
                        subDirectories.Add(directoryInfo);
                    }
 
                    continue;
                }
            }
 
            // Matches the sub directories recursively
            foreach (DirectoryInfoBase subDir in subDirectories)
            {
                string relativePath = CombinePath(parentRelativePath, subDir.Name);
 
                Match(subDir, relativePath);
            }
 
            // Request all the including and excluding patterns to pop their status stack.
            PopDirectory();
        }
 
        private void Declare()
        {
            _declaredLiteralFileSegments.Clear();
            _declaredParentPathSegment = false;
            _declaredWildcardPathSegment = false;
 
            foreach (IPatternContext include in _includePatternContexts)
            {
                include.Declare(DeclareInclude);
            }
        }
 
        private void DeclareInclude(IPathSegment patternSegment, bool isLastSegment)
        {
            if (patternSegment is LiteralPathSegment literalSegment)
            {
                if (isLastSegment)
                {
                    _declaredLiteralFileSegments.Add(literalSegment);
                }
                else
                {
                    _declaredLiteralFolderSegmentInString.Add(literalSegment.Value);
                }
            }
            else if (patternSegment is ParentPathSegment)
            {
                _declaredParentPathSegment = true;
            }
            else if (patternSegment is WildcardPathSegment)
            {
                _declaredWildcardPathSegment = true;
            }
        }
 
        internal static string CombinePath(string? left, string right)
        {
            if (string.IsNullOrEmpty(left))
            {
                return right;
            }
            else
            {
                return $"{left}/{right}";
            }
        }
 
        // Used to adapt Test(DirectoryInfoBase) for the below overload
        private bool MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, bool> test)
        {
            return MatchPatternContexts(
                fileinfo,
                (ctx, file) =>
                {
                    if (test(ctx, file))
                    {
                        return PatternTestResult.Success(stem: string.Empty);
                    }
                    else
                    {
                        return PatternTestResult.Failed;
                    }
                }).IsSuccessful;
        }
 
        private PatternTestResult MatchPatternContexts<TFileInfoBase>(TFileInfoBase fileinfo, Func<IPatternContext, TFileInfoBase, PatternTestResult> test)
        {
            PatternTestResult result = PatternTestResult.Failed;
 
            // If the given file/directory matches any including pattern, continues to next step.
            foreach (IPatternContext context in _includePatternContexts)
            {
                PatternTestResult localResult = test(context, fileinfo);
                if (localResult.IsSuccessful)
                {
                    result = localResult;
                    break;
                }
            }
 
            // If the given file/directory doesn't match any of the including pattern, returns false.
            if (!result.IsSuccessful)
            {
                return PatternTestResult.Failed;
            }
 
            // If the given file/directory matches any excluding pattern, returns false.
            foreach (IPatternContext context in _excludePatternContexts)
            {
                if (test(context, fileinfo).IsSuccessful)
                {
                    return PatternTestResult.Failed;
                }
            }
 
            return result;
        }
 
        private void PopDirectory()
        {
            foreach (IPatternContext context in _excludePatternContexts)
            {
                context.PopDirectory();
            }
 
            foreach (IPatternContext context in _includePatternContexts)
            {
                context.PopDirectory();
            }
        }
 
        private void PushDirectory(DirectoryInfoBase directory)
        {
            foreach (IPatternContext context in _includePatternContexts)
            {
                context.PushDirectory(directory);
            }
 
            foreach (IPatternContext context in _excludePatternContexts)
            {
                context.PushDirectory(directory);
            }
        }
    }
}