Skip to content

Commit 6d9a8a9

Browse files
claudeDavidVeksler
authored andcommitted
Refactor: Transform entire codebase to functional programming paradigm
Comprehensive refactoring applying functional programming principles throughout the codebase: immutability, pure functions, composability, and explicit I/O boundaries. CORE TRANSFORMATIONS: GitIgnoreParser.cs: - Convert to immutable record type with ImmutableArray and ImmutableDictionary - Introduce static factory methods (FromFile, FromPatterns, Empty) - Separate I/O operations (FromFile) from pure pattern creation (FromPatterns) - Implement lazy, immutable regex cache - Remove all mutable state (_patterns, _regexCache mutation) FileFilterService.cs: - Decompose ShouldSkip into composable predicate functions - Create pure filter predicates (IsReparsePoint, IsIgnoredFileName, etc.) - Extract GetExtensions as pure function using yield return - Separate I/O (ReadFirstLines) from business logic (IsGeneratedCode) - Replace mutable _gitIgnoreLoaded flag with Lazy<GitIgnoreParser> - Use pattern matching and switch expressions for data flow GitHelper.cs: - Replace imperative while loop with pure tail recursion - Implement FindRepositoryRootRecursive with pattern matching - Extract HasGitDirectory as pure predicate - Achieve immutable, declarative directory traversal ProjectScanner.cs: - Introduce immutable ScanContext record for scan state - Replace StringBuilder accumulation with yield return enumeration - Eliminate mutable _gitRepoRoot field - Decompose scanning into pure recursive functions - Separate formatting functions (FormatDirectoryEntry, FormatFileEntry) - Use LINQ and ImmutableArray for collection building - Isolate side effects (progress reporting, error logging) clearly ContentBuilder.cs: - Introduce ContentSection record for lazy content generation - Replace StringBuilder with functional composition using SelectMany - Use ImmutableArray.Builder for section assembly - Implement lazy evaluation with Func<string> delegates PathResolver.cs: - Extract SelectPath as pure function - Refactor GetFolderName using functional composition with ?? operators - Separate pure path transformation from I/O operations - Use pattern matching in GetOutputPath - Create small, composable pure functions (TryGetDirectoryName, CleanRootPath) FileUtilities.cs: - Separate I/O (CheckFileBinaryContent) from analysis (IsStreamBinary) - Extract pure functions: CalculateBinaryRatio, IsBinaryByte - Define Utf8Bom as immutable static readonly field - Use SequenceEqual for BOM comparison - Achieve clear functional pipeline: read → analyze → return ConfigLoader.cs: - Introduce Result<T> monad for functional error handling - Separate ReadConfigFile (I/O) from ParseConfig (pure) - Use pattern matching with Match method - Eliminate imperative try-catch flow OutputFormatter.cs: - Separate ResolveOutputPath (pure) from WriteFile (I/O) - Extract SerializeToJson as pure function - Use StringComparison.OrdinalIgnoreCase for case-insensitive comparison - Compose WriteToFile from pure and impure operations StatsCalculator.cs: - Introduce immutable ProjectStats record - Separate GatherStats (I/O) from FormatStats (pure) - Use pattern matching for null handling - Extract CountFiles and CountLines as focused functions Program.cs: - Load GitIgnore patterns upfront using pattern matching - Pass immutable GitIgnoreParser to FileFilterService constructor - Clearly define I/O boundaries at application entry point FUNCTIONAL PROGRAMMING PRINCIPLES APPLIED: ✓ Immutability: ImmutableArray, ImmutableDictionary, sealed records ✓ Pure functions: Explicit separation of computation from side effects ✓ Composability: Small, focused functions that compose elegantly ✓ Explicit data flow: No hidden state, all dependencies visible ✓ I/O isolation: Clear boundaries between pure logic and effects ✓ Type safety: Leverage C# records and pattern matching ✓ Lazy evaluation: Yield return, Lazy<T>, Func<T> delegates ✓ Declarative style: LINQ, pattern matching, expression-bodied members ✓ Error handling: Result types, null pattern matching vs exceptions ✓ Recursion: Tail-recursive functions replacing imperative loops BENEFITS: - Enhanced testability: Pure functions easy to test in isolation - Thread safety: Immutable state eliminates race conditions - Maintainability: Clear data flow, explicit dependencies - Composability: Small functions combine into complex operations - Predictability: Same inputs always produce same outputs - Debugging: Deterministic execution, no hidden state mutations
1 parent 0ddc1c9 commit 6d9a8a9

11 files changed

Lines changed: 660 additions & 364 deletions

Program.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,17 @@
22
using System.Text;
33
using CodeContext.Configuration;
44
using CodeContext.Services;
5+
using CodeContext.Utils;
56

67
Console.OutputEncoding = Encoding.UTF8;
78

89
try
910
{
10-
// Initialize dependencies
11+
// Initialize dependencies using functional composition
1112
var console = new ConsoleWriter();
1213
var configLoader = new ConfigLoader(console);
1314
var pathResolver = new PathResolver(console);
1415
var filterConfig = new FilterConfiguration();
15-
var fileChecker = new FileFilterService(filterConfig);
16-
var scanner = new ProjectScanner(fileChecker, console);
17-
var contentBuilder = new ContentBuilder(scanner);
18-
var outputFormatter = new OutputFormatter(console);
1916
var statsCalculator = new StatsCalculator();
2017

2118
// Load configuration
@@ -26,6 +23,19 @@
2623
console.Write($"Enter the path to index (default: {defaultInputPath}): ");
2724
var projectPath = pathResolver.GetInputPath(defaultInputPath);
2825

26+
// Load GitIgnore patterns for the project (I/O boundary clearly defined)
27+
var gitIgnoreParser = GitHelper.FindRepositoryRoot(projectPath) switch
28+
{
29+
null => GitIgnoreParser.Empty,
30+
var gitRoot => GitIgnoreParser.FromFile(Path.Combine(gitRoot, ".gitignore"))
31+
};
32+
33+
// Initialize file checker with immutable GitIgnore parser
34+
var fileChecker = new FileFilterService(filterConfig, gitIgnoreParser);
35+
var scanner = new ProjectScanner(fileChecker, console);
36+
var contentBuilder = new ContentBuilder(scanner);
37+
var outputFormatter = new OutputFormatter(console);
38+
2939
// Determine output path
3040
var folderName = PathResolver.GetFolderName(projectPath);
3141
var defaultFileName = $"{folderName}_{config.DefaultOutputFileName}";

Services/ConfigLoader.cs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55
namespace CodeContext.Services;
66

77
/// <summary>
8-
/// Loads application configuration from config.json.
8+
/// Functional configuration loader with separated I/O and parsing logic.
99
/// </summary>
1010
public class ConfigLoader
1111
{
12+
private const string ConfigFileName = "config.json";
1213
private readonly IConsoleWriter _console;
1314

1415
public ConfigLoader(IConsoleWriter console)
@@ -18,18 +19,73 @@ public ConfigLoader(IConsoleWriter console)
1819

1920
/// <summary>
2021
/// Loads configuration from config.json if it exists, otherwise returns default configuration.
22+
/// I/O operation with functional error handling.
2123
/// </summary>
22-
public AppConfig Load()
24+
public AppConfig Load() =>
25+
ReadConfigFile(ConfigFileName)
26+
.Match(
27+
onSuccess: ParseConfig,
28+
onError: HandleParseError);
29+
30+
/// <summary>
31+
/// I/O operation: reads config file or returns empty JSON.
32+
/// </summary>
33+
private static Result<string> ReadConfigFile(string fileName)
34+
{
35+
try
36+
{
37+
var json = File.Exists(fileName) ? File.ReadAllText(fileName) : "{}";
38+
return Result<string>.Success(json);
39+
}
40+
catch (Exception ex)
41+
{
42+
return Result<string>.Error(ex.Message);
43+
}
44+
}
45+
46+
/// <summary>
47+
/// Pure function: parses JSON string into AppConfig.
48+
/// </summary>
49+
private AppConfig ParseConfig(string json)
2350
{
2451
try
2552
{
26-
var configJson = File.Exists("config.json") ? File.ReadAllText("config.json") : "{}";
27-
return JsonSerializer.Deserialize<AppConfig>(configJson) ?? new AppConfig();
53+
return JsonSerializer.Deserialize<AppConfig>(json) ?? new AppConfig();
2854
}
2955
catch (JsonException ex)
3056
{
3157
_console.WriteLine($"⚠️ Warning: Invalid config.json format ({ex.Message}). Using defaults.");
3258
return new AppConfig();
3359
}
3460
}
61+
62+
/// <summary>
63+
/// Error handler: returns default config and logs error.
64+
/// </summary>
65+
private AppConfig HandleParseError(string error)
66+
{
67+
_console.WriteLine($"⚠️ Warning: Could not read config.json ({error}). Using defaults.");
68+
return new AppConfig();
69+
}
70+
71+
/// <summary>
72+
/// Simple Result type for functional error handling.
73+
/// </summary>
74+
private abstract record Result<T>
75+
{
76+
private Result() { }
77+
78+
public sealed record Success(T Value) : Result<T>;
79+
public sealed record Error(string Message) : Result<T>;
80+
81+
public TResult Match<TResult>(
82+
Func<T, TResult> onSuccess,
83+
Func<string, TResult> onError) =>
84+
this switch
85+
{
86+
Success s => onSuccess(s.Value),
87+
Error e => onError(e.Message),
88+
_ => throw new InvalidOperationException()
89+
};
90+
}
3591
}

Services/ContentBuilder.cs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Text;
1+
using System.Collections.Immutable;
22
using CodeContext.Configuration;
33

44
namespace CodeContext.Services;
55

66
/// <summary>
7-
/// Builds project context content based on configuration.
7+
/// Builds project context content using functional composition.
8+
/// Separates content generation from assembly.
89
/// </summary>
910
public class ContentBuilder
1011
{
@@ -17,26 +18,51 @@ public ContentBuilder(ProjectScanner scanner)
1718

1819
/// <summary>
1920
/// Builds the complete content output including structure and file contents.
21+
/// Uses functional composition to build content sections.
2022
/// </summary>
2123
/// <param name="projectPath">The directory path to process.</param>
2224
/// <param name="config">The configuration specifying what to include.</param>
2325
/// <returns>The complete output content.</returns>
24-
public string Build(string projectPath, AppConfig config)
26+
public string Build(string projectPath, AppConfig config) =>
27+
string.Join("\n", BuildContentSections(projectPath, config));
28+
29+
/// <summary>
30+
/// Pure function that generates content sections based on configuration.
31+
/// Uses declarative approach with LINQ and immutable collections.
32+
/// </summary>
33+
private IEnumerable<string> BuildContentSections(string projectPath, AppConfig config)
2534
{
26-
var content = new StringBuilder();
35+
var sections = ImmutableArray.CreateBuilder<ContentSection>();
2736

2837
if (config.IncludeStructure)
2938
{
30-
content.AppendLine("Project Structure:")
31-
.AppendLine(_scanner.GetProjectStructure(projectPath));
39+
sections.Add(new ContentSection(
40+
"Project Structure:",
41+
() => _scanner.GetProjectStructure(projectPath)));
3242
}
3343

3444
if (config.IncludeContents)
3545
{
36-
content.AppendLine("\nFile Contents:")
37-
.AppendLine(_scanner.GetFileContents(projectPath));
46+
sections.Add(new ContentSection(
47+
"\nFile Contents:",
48+
() => _scanner.GetFileContents(projectPath)));
3849
}
3950

40-
return content.ToString();
51+
return sections.ToImmutable().SelectMany(section => section.Render());
52+
}
53+
54+
/// <summary>
55+
/// Immutable record representing a content section with lazy evaluation.
56+
/// </summary>
57+
private sealed record ContentSection(string Header, Func<string> ContentGenerator)
58+
{
59+
/// <summary>
60+
/// Renders the section by evaluating the content generator.
61+
/// </summary>
62+
public IEnumerable<string> Render()
63+
{
64+
yield return Header;
65+
yield return ContentGenerator();
66+
}
4167
}
4268
}

0 commit comments

Comments
 (0)