Skip to content

Commit d5e50c2

Browse files
committed
Refactor services and improve file filtering logic
Refactored multiple service classes to use primary constructors and field initializers for dependencies, simplifying code and improving readability. Enhanced file filtering logic in FileFilterService with pattern matching. Replaced custom file collection logic in CodeContextTools with a call to ProjectScanner's new GetAllFiles method. Updated FileUtilities to use Span for buffer operations and improved binary content detection. Various minor improvements to immutability and collection initialization throughout the codebase.
1 parent ea9ff1b commit d5e50c2

11 files changed

Lines changed: 128 additions & 162 deletions

Configuration/FilterConfiguration.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Frozen;
2+
13
namespace CodeContext.Configuration;
24

35
/// <summary>
@@ -13,7 +15,7 @@ public class FilterConfiguration
1315
/// <summary>
1416
/// File extensions to ignore during processing.
1517
/// </summary>
16-
public HashSet<string> IgnoredExtensions { get; init; } = new(StringComparer.OrdinalIgnoreCase)
18+
public FrozenSet<string> IgnoredExtensions { get; init; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
1719
{
1820
// Executable and library files
1921
".exe", ".dll", ".pdb", ".bin", ".obj", ".lib", ".so", ".dylib", ".a", ".o",
@@ -107,12 +109,12 @@ public class FilterConfiguration
107109
".bat", ".sh", ".cmd", ".ps1",
108110

109111
".sql"
110-
};
112+
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
111113

112114
/// <summary>
113115
/// Directory names to ignore during processing.
114116
/// </summary>
115-
public HashSet<string> IgnoredDirectories { get; init; } = new(StringComparer.OrdinalIgnoreCase)
117+
public FrozenSet<string> IgnoredDirectories { get; init; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
116118
{
117119
".sonarqube",
118120

@@ -243,18 +245,18 @@ public class FilterConfiguration
243245

244246
// Pipenv
245247
".venv"
246-
};
248+
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
247249

248250
/// <summary>
249251
/// File names to ignore during processing.
250252
/// </summary>
251-
public HashSet<string> IgnoredFiles { get; init; } = new(StringComparer.OrdinalIgnoreCase)
253+
public FrozenSet<string> IgnoredFiles { get; init; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
252254
{
253255
".bzrignore", ".coveragerc", ".editorconfig", ".env", ".env.development",
254256
".env.production", ".env.local", ".env.test", ".eslintrc", ".gitattributes",
255257
"thumbs.db", "desktop.ini", ".DS_Store", "npm-debug.log", "yarn-error.log",
256258
"package-lock.json", "yarn.lock", "composer.lock", ".gitignore"
257-
};
259+
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
258260

259261
/// <summary>
260262
/// Number of lines to check for generated code markers.

Mcp/CodeContextTools.cs

Lines changed: 3 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,9 @@ namespace CodeContext.Mcp;
1313
/// Provides intelligent code context generation with token budget optimization.
1414
/// </summary>
1515
[McpServerToolType]
16-
public class CodeContextTools
16+
public class CodeContextTools(IConsoleWriter console)
1717
{
18-
private readonly IConsoleWriter _console;
19-
20-
public CodeContextTools(IConsoleWriter console)
21-
{
22-
_console = console;
23-
}
18+
private readonly IConsoleWriter _console = console;
2419

2520
/// <summary>
2621
/// Gets optimized code context for a specific task within a token budget.
@@ -281,64 +276,5 @@ public string GetFileContent(
281276
/// </summary>
282277
private static List<(string path, string content)> GetAllProjectFiles(
283278
ProjectScanner scanner,
284-
string projectPath)
285-
{
286-
var files = new List<(string path, string content)>();
287-
var context = GitHelper.FindRepositoryRoot(projectPath) ?? projectPath;
288-
289-
CollectFiles(scanner, projectPath, context, files);
290-
291-
return files;
292-
}
293-
294-
private static void CollectFiles(
295-
ProjectScanner scanner,
296-
string currentPath,
297-
string rootPath,
298-
List<(string path, string content)> files)
299-
{
300-
try
301-
{
302-
var fileCheckerField = scanner.GetType()
303-
.GetField("_fileChecker", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
304-
var fileChecker = fileCheckerField?.GetValue(scanner) as IFileChecker;
305-
306-
var entries = Directory.EnumerateFileSystemEntries(currentPath)
307-
.Where(e => fileChecker == null || !fileChecker.ShouldSkip(new FileInfo(e), rootPath))
308-
.ToList();
309-
310-
foreach (var entry in entries)
311-
{
312-
if (Directory.Exists(entry))
313-
{
314-
CollectFiles(scanner, entry, rootPath, files);
315-
}
316-
else if (File.Exists(entry))
317-
{
318-
try
319-
{
320-
var content = File.ReadAllText(entry);
321-
var relativePath = Path.GetRelativePath(rootPath, entry);
322-
files.Add((relativePath, content));
323-
}
324-
catch (UnauthorizedAccessException)
325-
{
326-
// Skip files with permission issues
327-
}
328-
catch (IOException)
329-
{
330-
// Skip files that are locked or in use
331-
}
332-
}
333-
}
334-
}
335-
catch (UnauthorizedAccessException)
336-
{
337-
// Skip directories with permission issues
338-
}
339-
catch (DirectoryNotFoundException)
340-
{
341-
// Skip if directory was deleted during scan
342-
}
343-
}
279+
string projectPath) => scanner.GetAllFiles(projectPath);
344280
}

Services/ConfigLoader.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,10 @@ namespace CodeContext.Services;
77
/// <summary>
88
/// Functional configuration loader with separated I/O and parsing logic.
99
/// </summary>
10-
public class ConfigLoader
10+
public class ConfigLoader(IConsoleWriter console)
1111
{
1212
private const string ConfigFileName = "config.json";
13-
private readonly IConsoleWriter _console;
14-
15-
public ConfigLoader(IConsoleWriter console)
16-
{
17-
_console = console;
18-
}
13+
private readonly IConsoleWriter _console = console;
1914

2015
/// <summary>
2116
/// Loads configuration from config.json if it exists, otherwise returns default configuration.

Services/ContentBuilder.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,9 @@ namespace CodeContext.Services;
77
/// Builds project context content using functional composition.
88
/// Separates content generation from assembly.
99
/// </summary>
10-
public class ContentBuilder
10+
public class ContentBuilder(ProjectScanner scanner)
1111
{
12-
private readonly ProjectScanner _scanner;
13-
14-
public ContentBuilder(ProjectScanner scanner)
15-
{
16-
_scanner = scanner;
17-
}
12+
private readonly ProjectScanner _scanner = scanner;
1813

1914
/// <summary>
2015
/// Builds the complete content output including structure and file contents.

Services/FileFilterService.cs

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,38 +33,30 @@ public bool ShouldSkip(FileSystemInfo info, string rootPath)
3333
Guard.NotNull(info, nameof(info));
3434
Guard.NotNullOrEmpty(rootPath, nameof(rootPath));
3535

36-
// Compose filter predicates - each is a pure function or has well-defined side effects
37-
return IsReparsePoint(info)
38-
|| ContainsIgnoredDirectory(info, rootPath)
39-
|| (IsFile(info) && ShouldSkipFile(info, rootPath));
36+
// Use pattern matching for more expressive filtering
37+
return info switch
38+
{
39+
{ Attributes: var attr } when attr.HasFlag(FileAttributes.ReparsePoint) => true,
40+
_ when ContainsIgnoredDirectory(info, rootPath) => true,
41+
FileInfo file => ShouldSkipFile(file, rootPath),
42+
_ => false
43+
};
4044
}
4145

42-
private static bool IsReparsePoint(FileSystemInfo info) =>
43-
info.Attributes.HasFlag(FileAttributes.ReparsePoint);
44-
45-
private static bool IsFile(FileSystemInfo info) =>
46-
!info.Attributes.HasFlag(FileAttributes.Directory);
47-
4846
private bool ContainsIgnoredDirectory(FileSystemInfo info, string rootPath)
4947
{
5048
var relativePath = Path.GetRelativePath(rootPath, info.FullName);
5149
var pathParts = relativePath.Split(Path.DirectorySeparatorChar);
5250
return pathParts.Any(_config.IgnoredDirectories.Contains);
5351
}
5452

55-
private bool ShouldSkipFile(FileSystemInfo info, string rootPath) =>
56-
IsIgnoredFileName(info.Name)
57-
|| ShouldSkipByExtension(info.Name)
58-
|| IsFileTooLarge(info)
59-
|| ShouldSkipByGitIgnore(info.FullName, rootPath)
60-
|| IsBinaryFile(info.FullName)
61-
|| IsGeneratedCode(info.FullName);
62-
63-
private bool IsIgnoredFileName(string fileName) =>
64-
_config.IgnoredFiles.Contains(fileName);
65-
66-
private bool IsFileTooLarge(FileSystemInfo info) =>
67-
info is FileInfo fileInfo && fileInfo.Length > _config.MaxFileSizeBytes;
53+
private bool ShouldSkipFile(FileInfo file, string rootPath) =>
54+
_config.IgnoredFiles.Contains(file.Name)
55+
|| ShouldSkipByExtension(file.Name)
56+
|| file.Length > _config.MaxFileSizeBytes
57+
|| ShouldSkipByGitIgnore(file.FullName, rootPath)
58+
|| IsBinaryFile(file.FullName)
59+
|| IsGeneratedCode(file.FullName);
6860

6961
private bool IsBinaryFile(string filePath) =>
7062
FileUtilities.IsBinaryFile(filePath, _config.BinaryCheckChunkSize, _config.BinaryThreshold);

Services/FileRelevanceScorer.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace CodeContext.Services;
77
/// Scores files based on relevance to a query or task description.
88
/// Uses keyword matching, path analysis, and file characteristics.
99
/// </summary>
10-
public class FileRelevanceScorer
10+
public class FileRelevanceScorer(string projectPath)
1111
{
1212
// Scoring weights for different factors
1313
private const double FileNameWeight = 0.30;
@@ -28,12 +28,7 @@ public class FileRelevanceScorer
2828
private const double IndexFileBoost = 0.15;
2929
private const double TestFileBoost = 0.1;
3030

31-
private readonly string _projectPath;
32-
33-
public FileRelevanceScorer(string projectPath)
34-
{
35-
_projectPath = Guard.NotNullOrEmpty(projectPath, nameof(projectPath));
36-
}
31+
private readonly string _projectPath = Guard.NotNullOrEmpty(projectPath, nameof(projectPath));
3732

3833
/// <summary>
3934
/// Represents a scored file with relevance information.
@@ -55,7 +50,7 @@ Dictionary<string, double> ScoreBreakdown
5550
/// <returns>Scored file with relevance score between 0 and 1.</returns>
5651
public ScoredFile ScoreFile(string filePath, string content, string query)
5752
{
58-
var breakdown = new Dictionary<string, double>();
53+
Dictionary<string, double> breakdown = [];
5954
var keywords = ExtractKeywords(query);
6055

6156
// 1. File name relevance (30% weight)
@@ -92,10 +87,10 @@ public ScoredFile ScoreFile(string filePath, string content, string query)
9287
private static List<string> ExtractKeywords(string query)
9388
{
9489
if (string.IsNullOrWhiteSpace(query))
95-
return new List<string>();
90+
return [];
9691

9792
// Remove common words and split into keywords
98-
var commonWords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
93+
HashSet<string> commonWords = new(StringComparer.OrdinalIgnoreCase)
9994
{
10095
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
10196
"of", "with", "by", "from", "as", "is", "was", "are", "were", "be",

Services/OutputFormatter.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@ namespace CodeContext.Services;
66
/// <summary>
77
/// Functional output formatter with separated I/O and formatting logic.
88
/// </summary>
9-
public class OutputFormatter
9+
public class OutputFormatter(IConsoleWriter console)
1010
{
11-
private readonly IConsoleWriter _console;
12-
13-
public OutputFormatter(IConsoleWriter console)
14-
{
15-
_console = console;
16-
}
11+
private readonly IConsoleWriter _console = console;
1712

1813
/// <summary>
1914
/// Writes content to the specified output location (I/O operation).

Services/PathResolver.cs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,9 @@ namespace CodeContext.Services;
77
/// Resolves and validates input and output paths with user interaction.
88
/// Separates pure path operations from I/O side effects.
99
/// </summary>
10-
public class PathResolver
10+
public class PathResolver(IConsoleWriter console)
1111
{
12-
private readonly IConsoleWriter _console;
13-
14-
public PathResolver(IConsoleWriter console)
15-
{
16-
_console = console;
17-
}
12+
private readonly IConsoleWriter _console = console;
1813

1914
/// <summary>
2015
/// Gets and validates the input directory path (I/O operation).

Services/ProjectScanner.cs

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,10 @@ namespace CodeContext.Services;
88
/// Functional service for scanning and analyzing project directories.
99
/// Uses immutable data structures and separates I/O from pure logic.
1010
/// </summary>
11-
public class ProjectScanner
11+
public class ProjectScanner(IFileChecker fileChecker, IConsoleWriter console)
1212
{
13-
private readonly IFileChecker _fileChecker;
14-
private readonly IConsoleWriter _console;
15-
16-
public ProjectScanner(IFileChecker fileChecker, IConsoleWriter console)
17-
{
18-
_fileChecker = Guard.NotNull(fileChecker, nameof(fileChecker));
19-
_console = Guard.NotNull(console, nameof(console));
20-
}
13+
private readonly IFileChecker _fileChecker = Guard.NotNull(fileChecker, nameof(fileChecker));
14+
private readonly IConsoleWriter _console = Guard.NotNull(console, nameof(console));
2115

2216
/// <summary>
2317
/// Gets user input with a prompt (pure I/O operation).
@@ -292,4 +286,67 @@ private void WriteProgress(int current, int total)
292286
var percent = (int)((current / (double)total) * 100);
293287
_console.Write($"\r⏳ Progress: {percent}% ({current}/{total})");
294288
}
289+
290+
/// <summary>
291+
/// Gets all files in the project directory tree with their contents.
292+
/// </summary>
293+
/// <param name="projectPath">The directory path to scan.</param>
294+
/// <returns>List of tuples containing (relative path, content) for each file.</returns>
295+
public List<(string path, string content)> GetAllFiles(string projectPath)
296+
{
297+
Guard.DirectoryExists(projectPath, nameof(projectPath));
298+
299+
var context = ScanContext.Create(projectPath);
300+
var files = new List<(string path, string content)>();
301+
302+
CollectAllFiles(projectPath, context.GitRepoRoot, files);
303+
304+
return files;
305+
}
306+
307+
/// <summary>
308+
/// Recursively collects all files with their contents.
309+
/// </summary>
310+
private void CollectAllFiles(string currentPath, string rootPath, List<(string path, string content)> files)
311+
{
312+
try
313+
{
314+
var entries = Directory.EnumerateFileSystemEntries(currentPath)
315+
.Where(e => ShouldIncludeEntry(e, rootPath))
316+
.ToList();
317+
318+
foreach (var entry in entries)
319+
{
320+
if (Directory.Exists(entry))
321+
{
322+
CollectAllFiles(entry, rootPath, files);
323+
}
324+
else if (File.Exists(entry))
325+
{
326+
try
327+
{
328+
var content = File.ReadAllText(entry);
329+
var relativePath = Path.GetRelativePath(rootPath, entry);
330+
files.Add((relativePath, content));
331+
}
332+
catch (UnauthorizedAccessException)
333+
{
334+
// Skip files with permission issues
335+
}
336+
catch (IOException)
337+
{
338+
// Skip files that are locked or in use
339+
}
340+
}
341+
}
342+
}
343+
catch (UnauthorizedAccessException)
344+
{
345+
// Skip directories with permission issues
346+
}
347+
catch (DirectoryNotFoundException)
348+
{
349+
// Skip if directory was deleted during scan
350+
}
351+
}
295352
}

0 commit comments

Comments
 (0)