diff --git a/CoDepend/.codepend/snapshot b/CoDepend/.codepend/snapshot index aa638e3..ee7a9eb 100644 Binary files a/CoDepend/.codepend/snapshot and b/CoDepend/.codepend/snapshot differ diff --git a/CoDepend/Application/ChangeDetector.cs b/CoDepend/Application/ChangeDetector.cs index 5276ea3..7ceae58 100644 --- a/CoDepend/Application/ChangeDetector.cs +++ b/CoDepend/Application/ChangeDetector.cs @@ -23,12 +23,6 @@ private sealed record ProjectFileStructure( Dictionary> ChildrenByDir ); - private sealed record ExclusionRule( - string[] DirPrefixes, - string[] Segments, - string[] FileSuffixes - ); - public static async Task GetProjectChangesAsync( ParserOptions parserOptions, ProjectDependencyGraph? lastSavedGraph, @@ -38,7 +32,7 @@ public static async Task GetProjectChangesAsync( ? Path.GetFullPath(parserOptions.BaseOptions.ProjectRoot) : parserOptions.BaseOptions.FullRootPath; - var rules = CompileExclusions(parserOptions.Exclusions); + var rules = ExclusionManager.CompileExclusions(parserOptions.Exclusions); var current = await Task.Run( () => ScanCurrentProjectFileStructure(projectRoot, parserOptions.FileExtensions, rules, ct), @@ -210,62 +204,10 @@ private static void AddDirAndAncestors( return RelativePath.Directory(projectRoot, parentSlice); } - private static ExclusionRule CompileExclusions(IReadOnlyList exclusions) - { - List dirPrefixes = []; - List segments = []; - List suffixes = []; - - foreach (var entry in exclusions) - { - var norm = NormaliseExclusionEntry(entry); - if (norm is null) continue; - - if (ToDirPrefix(norm) is { } prefix) { dirPrefixes.Add(prefix); continue; } - if (norm.StartsWith("*.", StringComparison.Ordinal)) { suffixes.Add(norm[1..]); continue; } - segments.Add(norm); - } - - return new ExclusionRule( - DirPrefixes: [.. dirPrefixes], - Segments: [.. segments], - FileSuffixes: [.. suffixes] - ); - } - - // Returns null for blank/empty entries; otherwise strips **/ prefix, normalises slashes, and trims trailing dot. - private static string? NormaliseExclusionEntry(string? entry) - { - var exclusion = (entry ?? string.Empty).Trim(); - if (exclusion.Length == 0) return null; - - if (exclusion.StartsWith("**/", StringComparison.Ordinal)) exclusion = exclusion[3..]; - - var norm = exclusion.Replace('\\', '/'); - return norm.EndsWith('.') ? norm[..^1] : norm; - } - - // Returns the canonical dir-prefix form when norm represents a directory pattern, otherwise null. - private static string? ToDirPrefix(string norm) - { - // relative path with trailing '/' -> dir - if (norm.EndsWith('/')) - { - var p = norm.StartsWith("./", StringComparison.Ordinal) ? norm[2..] : norm; - return p.EndsWith('/') ? p : p + "/"; - } - - // relative path containing '/' but no trailing slash -> dir - if (norm.Contains('/')) - return norm + "/"; - - return null; - } - private static ProjectFileStructure ScanCurrentProjectFileStructure( string projectRoot, IReadOnlyList extensions, - ExclusionRule rules, + ExclusionManager.ExclusionRule rules, CancellationToken ct) { var extensionSet = new HashSet(extensions, StringComparer.OrdinalIgnoreCase); @@ -297,7 +239,7 @@ void ScanDirectory(string dirAbs) foreach (var fileAbs in fileAbsList) { ct.ThrowIfCancellationRequested(); - if (IsExcluded(projectRoot, fileAbs, rules)) continue; + if (ExclusionManager.IsExcluded(projectRoot, fileAbs, rules)) continue; if (!extensionSet.Contains(Path.GetExtension(fileAbs))) continue; var fileRel = RelativePath.File(projectRoot, fileAbs); @@ -306,7 +248,7 @@ void ScanDirectory(string dirAbs) } var includedSubdirs = subdirs - .Where(s => !IsExcluded(projectRoot, s, rules)) + .Where(s => !ExclusionManager.IsExcluded(projectRoot, s, rules)) .ToArray(); foreach (var subAbs in includedSubdirs) @@ -338,7 +280,7 @@ private static (List deletedFilesRel, List deletedDi IReadOnlySet currentDirs, #pragma warning restore CA1859 // Use concrete types when possible for improved performance - ExclusionRule rules, + ExclusionManager.ExclusionRule rules, CancellationToken ct) { List deletedFiles = []; @@ -355,7 +297,7 @@ private static (List deletedFilesRel, List deletedDi continue; var absPath = PathNormaliser.GetAbsolutePath(projectRoot, item.Path.Value); - if (IsExcluded(projectRoot, absPath, rules)) + if (ExclusionManager.IsExcluded(projectRoot, absPath, rules)) continue; var isFile = item.Type == ProjectItemType.File; @@ -401,61 +343,10 @@ private static IReadOnlyDictionary> Fr ); } - private static bool IsExcluded(string projectRoot, string content, ExclusionRule rules) - { - var path = GetRelative(projectRoot, content); - var pathSeparater = '/'; - var pathWithSlash = path + pathSeparater; - var pathWithBothSlashes = pathSeparater + path + pathSeparater; - - // Do not change to linq - this is called on every file in a project and linq would allocate too much space on large systems - foreach (var rule in rules.DirPrefixes) - { - if (pathWithSlash.StartsWith(rule, StringComparison.OrdinalIgnoreCase) - || pathWithBothSlashes.Contains(pathSeparater + rule, StringComparison.OrdinalIgnoreCase)) - return true; - } - - var segments = path.Split(pathSeparater, StringSplitOptions.RemoveEmptyEntries); - // Do not change to linq - this is called on every file in a project and linq would allocate too much space on large systems - foreach (var segment in segments) - { - foreach (var ban in rules.Segments) - { - if (MatchesSuffixPattern(segment, ban)) - return true; - } - } - - var fileName = Path.GetFileName(path); - // Do not change to linq - this is called on every file in a project and linq would allocate too much space on large systems - foreach (var suf in rules.FileSuffixes) - { - if (fileName.EndsWith(suf, StringComparison.OrdinalIgnoreCase)) - return true; - } - return false; - } - - public static bool MatchesSuffixPattern(string value, string pattern) - { - if (!pattern.Contains('*')) - return string.Equals(value, pattern, StringComparison.OrdinalIgnoreCase); - - var suffix = pattern.TrimStart('*'); - return value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); - } - private static bool IsProjectRoot(RelativePath path) => string.Equals(path.Value, "./", StringComparison.Ordinal) || string.Equals(path.Value, ".", StringComparison.Ordinal); private static DateTime TrimMilliseconds(DateTime dt) => new(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, 0, dt.Kind); - - private static string GetRelative(string root, string path) - { - var rel = Path.GetRelativePath(root, path); - return rel.Replace('\\', '/'); - } } \ No newline at end of file diff --git a/CoDepend/Application/ExclusionManager.cs b/CoDepend/Application/ExclusionManager.cs new file mode 100644 index 0000000..ef2f0f4 --- /dev/null +++ b/CoDepend/Application/ExclusionManager.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using CoDepend.Domain.Models; + +namespace CoDepend.Application; + +public static class ExclusionManager +{ + public sealed record ExclusionRule( + string[] DirPrefixes, + string[] Segments, + string[] FileSuffixes + ); + + public static ExclusionRule CompileExclusions(IReadOnlyList exclusions) + { + List dirPrefixes = []; + List segments = []; + List suffixes = []; + + foreach (var entry in exclusions) + { + var norm = NormalizeExclusionEntry(entry); + if (norm is null) continue; + + if (ToDirPrefix(norm) is { } prefix) { dirPrefixes.Add(prefix); continue; } + if (norm.StartsWith("*.", StringComparison.Ordinal)) { suffixes.Add(norm[1..]); continue; } + segments.Add(norm); + } + + return new ExclusionRule( + DirPrefixes: [.. dirPrefixes], + Segments: [.. segments], + FileSuffixes: [.. suffixes] + ); + } + + public static string? NormalizeExclusionEntry(string? entry) + { + var exclusion = (entry ?? string.Empty).Trim(); + if (exclusion.Length == 0) return null; + + if (exclusion.StartsWith("**/", StringComparison.Ordinal)) exclusion = exclusion[3..]; + + var norm = exclusion.Replace('\\', '/'); + return norm.EndsWith('.') ? norm[..^1] : norm; + } + + public static bool IsExcluded(string projectRoot, string content, ExclusionRule rules) + { + var path = GetRelative(projectRoot, content); + var pathSeparater = '/'; + var pathWithSlash = path + pathSeparater; + var pathWithBothSlashes = pathSeparater + path + pathSeparater; + + // Do not change to linq - this is called on every file in a project and linq would allocate too much space on large systems + foreach (var rule in rules.DirPrefixes) + { + if (pathWithSlash.StartsWith(rule, StringComparison.OrdinalIgnoreCase) + || pathWithBothSlashes.Contains(pathSeparater + rule, StringComparison.OrdinalIgnoreCase)) + return true; + } + + var segments = path.Split(pathSeparater, StringSplitOptions.RemoveEmptyEntries); + // Do not change to linq - this is called on every file in a project and linq would allocate too much space on large systems + foreach (var segment in segments) + { + foreach (var ban in rules.Segments) + { + if (MatchesSuffixPattern(segment, ban)) + return true; + } + } + + var fileName = Path.GetFileName(path); + // Do not change to linq - this is called on every file in a project and linq would allocate too much space on large systems + foreach (var suf in rules.FileSuffixes) + { + if (fileName.EndsWith(suf, StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } + + public static bool MatchesSuffixPattern(string value, string pattern) + { + if (!pattern.Contains('*')) + return string.Equals(value, pattern, StringComparison.OrdinalIgnoreCase); + + var suffix = pattern.TrimStart('*'); + return value.EndsWith(suffix, StringComparison.OrdinalIgnoreCase); + } + + private static string? ToDirPrefix(string norm) + { + // relative path with trailing '/' -> dir + if (norm.EndsWith('/')) + { + var p = norm.StartsWith("./", StringComparison.Ordinal) ? norm[2..] : norm; + return p.EndsWith('/') ? p : p + "/"; + } + + // relative path containing '/' but no trailing slash -> dir + if (norm.Contains('/')) + return norm + "/"; + + return null; + } + + private static string GetRelative(string root, string path) + { + var rel = Path.GetRelativePath(root, path); + return rel.Replace('\\', '/'); + } +} \ No newline at end of file