From 83c4bc48e786292d6c7839b0bfa9813b227c54d9 Mon Sep 17 00:00:00 2001 From: nissemand243 <47781194+nissemand243@users.noreply.github.com> Date: Tue, 14 Apr 2026 11:09:28 +0200 Subject: [PATCH] All task 2 --- CoDepend/.codepend/snapshot | Bin 2541 -> 2354 bytes CoDepend/Application/ChangeDetector.cs | 121 ++--------------------- CoDepend/Application/ExclusionManager.cs | 117 ++++++++++++++++++++++ 3 files changed, 123 insertions(+), 115 deletions(-) create mode 100644 CoDepend/Application/ExclusionManager.cs diff --git a/CoDepend/.codepend/snapshot b/CoDepend/.codepend/snapshot index aa638e386a9c420fe83fb2ee05c97d4985dbf67a..ee7a9ebc7453847e60cb6e881be0222195cef5da 100644 GIT binary patch literal 2354 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 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