Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified CoDepend/.codepend/snapshot
Binary file not shown.
111 changes: 5 additions & 106 deletions CoDepend/Application/ChangeDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using CoDepend.Domain.Models;
using CoDepend.Domain.Models.Records;
using CoDepend.Domain.Utils;
using static CoDepend.Application.ExclusionManager;
using ProjectChanges = CoDepend.Domain.Models.Records.ProjectChanges;
using ProjectDependencyGraph = CoDepend.Domain.Models.ProjectDependencyGraph;

Expand All @@ -23,12 +24,6 @@ private sealed record ProjectFileStructure(
Dictionary<RelativePath, HashSet<RelativePath>> ChildrenByDir
);

private sealed record ExclusionRule(
string[] DirPrefixes,
string[] Segments,
string[] FileSuffixes
);

public static async Task<ProjectChanges> GetProjectChangesAsync(
ParserOptions parserOptions,
ProjectDependencyGraph? lastSavedGraph,
Expand All @@ -38,7 +33,7 @@ public static async Task<ProjectChanges> 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),
Expand Down Expand Up @@ -210,57 +205,6 @@ private static void AddDirAndAncestors(
return RelativePath.Directory(projectRoot, parentSlice);
}

private static ExclusionRule CompileExclusions(IReadOnlyList<string> exclusions)
{
List<string> dirPrefixes = [];
List<string> segments = [];
List<string> 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,
Expand Down Expand Up @@ -297,7 +241,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);
Expand All @@ -306,7 +250,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)
Expand Down Expand Up @@ -355,7 +299,7 @@ private static (List<RelativePath> deletedFilesRel, List<RelativePath> 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;
Expand Down Expand Up @@ -401,51 +345,6 @@ private static IReadOnlyDictionary<RelativePath, IReadOnlyList<RelativePath>> 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);
Expand Down
125 changes: 125 additions & 0 deletions CoDepend/Application/ExclusionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.IO;

namespace CoDepend.Application;

public static class ExclusionManager
{
public sealed record ExclusionRule(
string[] DirPrefixes,
string[] Segments,
string[] FileSuffixes
);

public static ExclusionRule CompileExclusions(IReadOnlyList<string> exclusions)
{
List<string> dirPrefixes = [];
List<string> segments = [];
List<string> 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 sep = '/';
var pathWithSlash = path + sep;
var pathWithBothSlashes = sep + path + sep;

foreach (var rule in rules.DirPrefixes)
{
if (pathWithSlash.StartsWith(rule, StringComparison.OrdinalIgnoreCase) ||
pathWithBothSlashes.Contains(sep + rule, StringComparison.OrdinalIgnoreCase))
return true;
}

var segments = path.Split(sep, StringSplitOptions.RemoveEmptyEntries);

foreach (var segment in segments)
{
foreach (var ban in rules.Segments)
{
if (MatchesSuffixPattern(segment, ban))
return true;
}
}

var fileName = Path.GetFileName(path);

foreach (var suf in rules.FileSuffixes)
{
if (fileName.EndsWith(suf, StringComparison.OrdinalIgnoreCase))
return true;
}

return false;
}

private static string? ToDirPrefix(string norm)
{
if (norm.EndsWith('/'))
{
var p = norm.StartsWith("./", StringComparison.Ordinal) ? norm[2..] : norm;
return p.EndsWith('/') ? p : p + "/";
}

if (norm.Contains('/'))
return norm + "/";

return null;
}

private 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 GetRelative(string root, string path)
{
var rel = Path.GetRelativePath(root, path);
return rel.Replace('\\', '/');
}
}