From 58b16ba375745af69efd463b757810c642f86b72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:50:53 +0000 Subject: [PATCH 1/3] Initial plan From c5da815b8602731f66c9a63cd663875c84f59f86 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:53:59 +0000 Subject: [PATCH 2/3] refactor: optimize file enumeration to skip build directories during traversal Co-authored-by: HandyS11 <62420910+HandyS11@users.noreply.github.com> --- .../EfAnalysis/EntityFileDiscovery.cs | 118 ++++++++++++++---- 1 file changed, 95 insertions(+), 23 deletions(-) diff --git a/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs b/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs index ea3c68e..2e5a405 100644 --- a/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs +++ b/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs @@ -173,22 +173,49 @@ public static HashSet ExtractEntityTypeNames(ClassDeclarationSyntax cont /// For each file, it calls to process the file and add matching /// entity type names and their file paths to the dictionary. /// Any access errors encountered during directory traversal are ignored. + /// Directories like bin, obj, .git, and node_modules are skipped during traversal for performance. /// private static async Task SearchDirectoryForEntitiesAsync( string searchDir, HashSet entityTypeNames, string normalizedContextPath, Dictionary entityFiles) + { + await SearchDirectoryRecursiveAsync(searchDir, entityTypeNames, normalizedContextPath, entityFiles); + } + + /// + /// Recursively searches a directory for C# files, skipping common build and version control directories. + /// + /// The current directory to search. + /// A set of entity type names to search for in the C# files. + /// The normalized file path of the context file to exclude from the search. + /// + /// A dictionary where the keys are entity type names and the values are the corresponding file paths. + /// + /// A task that represents the asynchronous operation. + /// + /// This method implements manual recursion to avoid descending into directories that typically + /// contain build artifacts or dependencies (bin, obj, .git, node_modules), improving performance + /// for large projects. + /// + private static async Task SearchDirectoryRecursiveAsync( + string currentDir, + HashSet entityTypeNames, + string normalizedContextPath, + Dictionary entityFiles) { try { + // Process files in the current directory var options = new EnumerationOptions { - RecurseSubdirectories = true, IgnoreInaccessible = true, AttributesToSkip = FileAttributes.System + RecurseSubdirectories = false, + IgnoreInaccessible = true, + AttributesToSkip = FileAttributes.System }; - foreach (var csFile in Directory.EnumerateFiles(searchDir, EfAnalysisConstants.FilePatterns.CSharpFiles, - options)) + foreach (var csFile in Directory.EnumerateFiles(currentDir, EfAnalysisConstants.FilePatterns.CSharpFiles, options)) { var fullPath = Path.GetFullPath(csFile); if (fullPath.Equals(normalizedContextPath, StringComparison.OrdinalIgnoreCase)) @@ -196,14 +223,19 @@ private static async Task SearchDirectoryForEntitiesAsync( continue; } - // Skip common non-source directories that can be large - var pathSegments = fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - if (pathSegments.Any(s => s is "bin" or "obj" or ".git" or "node_modules")) + await ProcessSourceFileAsync(fullPath, entityTypeNames, entityFiles); + } + + // Recursively process subdirectories, skipping excluded directories + foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", options)) + { + var dirName = Path.GetFileName(subDir); + if (dirName is "bin" or "obj" or ".git" or "node_modules") { continue; } - await ProcessSourceFileAsync(fullPath, entityTypeNames, entityFiles); + await SearchDirectoryRecursiveAsync(subDir, entityTypeNames, normalizedContextPath, entityFiles); } } catch @@ -386,43 +418,85 @@ private static DirectoryInfo FindSolutionRoot(string startDirectory, int maxLeve /// if the files are found; otherwise, the dictionary will be empty. /// /// - /// This method iterates through the provided base class names and attempts to locate their corresponding - /// file paths by calling the method. If a file is found, it is added - /// to the resulting dictionary. If no file is found for a base class name, it is skipped. + /// This method recursively searches for base class files while skipping common build and version control + /// directories (bin, obj, .git, node_modules) to improve performance for large projects. /// public static Dictionary SearchForBaseClassFiles( HashSet baseClassNames, DirectoryInfo solutionRoot) { var baseClassFiles = new Dictionary(); + SearchForBaseClassFilesRecursive(solutionRoot.FullName, baseClassNames, baseClassFiles, 0, 10); + return baseClassFiles; + } + + /// + /// Recursively searches for base class files, skipping common build and version control directories. + /// + /// The current directory to search. + /// A set of base class names to search for. + /// + /// A dictionary to store the found base class files where the keys are base class names + /// and the values are the corresponding file paths. + /// + /// The current recursion depth. + /// The maximum recursion depth to prevent infinite recursion. + /// + /// This method implements manual recursion to avoid descending into directories that typically + /// contain build artifacts or dependencies (bin, obj, .git, node_modules), improving performance + /// for large projects. + /// + private static void SearchForBaseClassFilesRecursive( + string currentDir, + HashSet baseClassNames, + Dictionary baseClassFiles, + int currentDepth, + int maxDepth) + { + if (currentDepth >= maxDepth || baseClassFiles.Count == baseClassNames.Count) + { + return; + } try { var options = new EnumerationOptions { - RecurseSubdirectories = true, IgnoreInaccessible = true, MaxRecursionDepth = 10 + RecurseSubdirectories = false, + IgnoreInaccessible = true, + AttributesToSkip = FileAttributes.System }; - foreach (var file in Directory.EnumerateFiles(solutionRoot.FullName, "*.cs", options)) + // Process files in the current directory + foreach (var file in Directory.EnumerateFiles(currentDir, "*.cs", options)) { - var fullPath = Path.GetFullPath(file); - // Skip common non-source directories that can be large - var pathSegments = fullPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - if (pathSegments.Any(s => s is "bin" or "obj" or ".git" or "node_modules")) + var fileName = Path.GetFileNameWithoutExtension(file); + if (baseClassNames.Contains(fileName)) { - continue; + var fullPath = Path.GetFullPath(file); + baseClassFiles.TryAdd(fileName, fullPath); + + if (baseClassFiles.Count == baseClassNames.Count) + { + return; + } } + } - var fileName = Path.GetFileNameWithoutExtension(file); - if (!baseClassNames.Contains(fileName)) + // Recursively process subdirectories, skipping excluded directories + foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", options)) + { + var dirName = Path.GetFileName(subDir); + if (dirName is "bin" or "obj" or ".git" or "node_modules") { continue; } - baseClassFiles.TryAdd(fileName, fullPath); + SearchForBaseClassFilesRecursive(subDir, baseClassNames, baseClassFiles, currentDepth + 1, maxDepth); + if (baseClassFiles.Count == baseClassNames.Count) { - break; + return; } } } @@ -430,7 +504,5 @@ public static Dictionary SearchForBaseClassFiles( { // Ignore access errors } - - return baseClassFiles; } } \ No newline at end of file From 13a9f73ef319e984413f3db5d5bfc067b1aa59f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:55:14 +0000 Subject: [PATCH 3/3] refactor: extract max search depth as named constant Co-authored-by: HandyS11 <62420910+HandyS11@users.noreply.github.com> --- .../Services/EfAnalysis/EntityFileDiscovery.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs b/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs index 2e5a405..9f9a090 100644 --- a/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs +++ b/src/ProjGraph.Lib/Services/EfAnalysis/EntityFileDiscovery.cs @@ -16,6 +16,11 @@ namespace ProjGraph.Lib.Services.EfAnalysis; /// public static class EntityFileDiscovery { + /// + /// Maximum recursion depth when searching for base class files to prevent infinite recursion + /// and limit search scope to reasonable project structures. + /// + private const int MaxSearchDepth = 10; /// /// Discovers the file paths of entity files within the specified search directories. /// @@ -426,7 +431,7 @@ public static Dictionary SearchForBaseClassFiles( DirectoryInfo solutionRoot) { var baseClassFiles = new Dictionary(); - SearchForBaseClassFilesRecursive(solutionRoot.FullName, baseClassNames, baseClassFiles, 0, 10); + SearchForBaseClassFilesRecursive(solutionRoot.FullName, baseClassNames, baseClassFiles, 0, MaxSearchDepth); return baseClassFiles; }