From 385e8d9afff6874b19c1b842e391b6febe4e6760 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Thu, 13 Feb 2025 14:34:30 -0800 Subject: [PATCH 1/6] TDD --- .../Run/RunWorkerTests.cs | 175 ++++++++++-------- 1 file changed, 98 insertions(+), 77 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs index e13bc0a6fd9..a88c45f8ff5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs @@ -18,8 +18,11 @@ namespace NuGetUpdater.Core.Test.Run; public class RunWorkerTests { - [Fact] - public async Task UpdateSinglePackageProducedExpectedAPIMessages() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task UpdateSinglePackageProducedExpectedAPIMessages(string EOL) { await RunAsync( packages: [], @@ -43,7 +46,7 @@ await RunAsync( - """) + """.Replace("\n", EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -94,7 +97,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -114,7 +117,7 @@ await File.WriteAllTextAsync(projectPath, """ - """)) + """.Replace("\n", EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -199,7 +202,7 @@ await File.WriteAllTextAsync(projectPath, """ - """, + """.Replace("\n", EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -212,11 +215,14 @@ await File.WriteAllTextAsync(projectPath, """ ); } - [Fact] - public async Task UpdateHandlesSemicolonsInPackageReference() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task UpdateHandlesSemicolonsInPackageReference(string EOL) { - var repoMetadata = XElement.Parse(""""""); - var repoMetadata2 = XElement.Parse(""""""); + var repoMetadata = XElement.Parse("""""".Replace("\n", EOL)); + var repoMetadata2 = XElement.Parse("""""".Replace("\n", EOL)); await RunAsync( packages: [ @@ -246,7 +252,7 @@ await RunAsync( - """) + """.Replace("\n", EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -299,7 +305,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -319,7 +325,7 @@ await File.WriteAllTextAsync(projectPath, """ - """)) + """.Replace("\n", EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -447,7 +453,7 @@ await File.WriteAllTextAsync(projectPath, """ - """, + """.Replace("\n", EOL), } ], @@ -461,8 +467,11 @@ await File.WriteAllTextAsync(projectPath, """ ); } - [Fact] - public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler(string EOL) { await RunAsync( packages: @@ -486,7 +495,7 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("project.csproj", """ @@ -496,7 +505,7 @@ await RunAsync( - """) + """.Replace("\n", EOL)) ], discoveryWorker: new TestDiscoveryWorker((_input) => { @@ -517,11 +526,14 @@ await RunAsync( ); } - [Fact] - public async Task UpdateHandlesPackagesConfigFiles() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task UpdateHandlesPackagesConfigFiles(string EOL) { - var repoMetadata = XElement.Parse(""""""); - var repoMetadata2 = XElement.Parse(""""""); + var repoMetadata = XElement.Parse("""""".Replace("\n", EOL)); + var repoMetadata2 = XElement.Parse("""""".Replace("\n", EOL)); await RunAsync( packages: [ @@ -551,13 +563,13 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("some-dir/packages.config", """ - """), + """.Replace("\n", EOL)), ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -630,7 +642,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); break; case "Some.Package2": await File.WriteAllTextAsync(projectPath, """ @@ -648,14 +660,14 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); var packagesConfigPath = Path.Join(Path.GetDirectoryName(projectPath)!, "packages.config"); await File.WriteAllTextAsync(packagesConfigPath, """ - """); + """.Replace("\n", EOL)); break; default: throw new NotSupportedException(); @@ -676,7 +688,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -691,7 +703,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.Replace("\n", EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -815,7 +827,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.Replace("\n", EOL), }, new DependencyFile() { @@ -836,7 +848,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.Replace("\n", EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -849,11 +861,14 @@ await File.WriteAllTextAsync(packagesConfigPath, """ ); } - [Fact] - public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles(string EOL) { - var repoMetadata = XElement.Parse(""""""); - var repoMetadata2 = XElement.Parse(""""""); + var repoMetadata = XElement.Parse("""""".Replace("\n", EOL)); + var repoMetadata2 = XElement.Parse("""""".Replace("\n", EOL)); await RunAsync( packages: [ @@ -886,13 +901,13 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("some-dir/ProjectA/packages.config", """ - """), + """.Replace("\n", EOL)), ("some-dir/ProjectB/ProjectB.csproj", """ @@ -902,13 +917,13 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("some-dir/ProjectB/packages.config", """ - """), + """.Replace("\n", EOL)), ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -999,7 +1014,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); break; case ("ProjectA.csproj", "Some.Package2"): await File.WriteAllTextAsync(projectPath, """ @@ -1020,13 +1035,13 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); await File.WriteAllTextAsync(packagesConfigPath, """ - """); + """.Replace("\n", EOL)); break; case ("ProjectB.csproj", "Some.Package"): await File.WriteAllTextAsync(projectPath, """ @@ -1038,7 +1053,7 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); break; case ("ProjectB.csproj", "Some.Package2"): await File.WriteAllTextAsync(projectPath, """ @@ -1056,13 +1071,13 @@ await File.WriteAllTextAsync(projectPath, """ - """); + """.Replace("\n", EOL)); await File.WriteAllTextAsync(packagesConfigPath, """ - """); + """.Replace("\n", EOL)); break; default: throw new NotSupportedException(); @@ -1083,7 +1098,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1101,7 +1116,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1112,7 +1127,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1127,7 +1142,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """)) + """.Replace("\n", EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1337,7 +1352,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.Replace("\n", EOL), }, new DependencyFile() { @@ -1361,7 +1376,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.Replace("\n", EOL), }, new DependencyFile() { @@ -1372,7 +1387,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.Replace("\n", EOL), }, new DependencyFile() { @@ -1393,7 +1408,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """, + """.Replace("\n", EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1406,8 +1421,11 @@ await File.WriteAllTextAsync(packagesConfigPath, """ ); } - [Fact] - public async Task UpdatedFilesAreOnlyReportedOnce() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task UpdatedFilesAreOnlyReportedOnce(string EOL) { await RunAsync( job: new() @@ -1434,14 +1452,14 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("Directory.Build.props", """ 1.0.0 - """), + """.Replace("\n", EOL)), ("project1/project1.csproj", """ @@ -1451,7 +1469,7 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("project2/project2.csproj", """ @@ -1461,7 +1479,7 @@ await RunAsync( - """) + """.Replace("\n", EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -1526,7 +1544,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.1.0 - """); + """.Replace("\n", EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -1543,7 +1561,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.0.0 - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1558,7 +1576,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1573,7 +1591,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ - """)) + """.Replace("\n", EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1698,7 +1716,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.1.0 - """, + """.Replace("\n", EOL), } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1711,8 +1729,11 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ ); } - [Fact] - public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories() + [Theory] + [InlineData(["\r"])] + [InlineData(["\n"])] + [InlineData(["\r\n"])] + public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories(string EOL) { // this test passes `null` for discovery, analyze, and update workers to fully test the desired behavior @@ -1754,7 +1775,7 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("Directory.Build.props", ""), ("Directory.Build.targets", ""), ("Directory.Packages.props", """ @@ -1763,7 +1784,7 @@ await RunAsync( false - """), + """.Replace("\n", EOL)), ("library1/library1.csproj", """ @@ -1773,7 +1794,7 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("library2/library2.csproj", """ @@ -1783,7 +1804,7 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ("library3/library3.csproj", """ @@ -1793,7 +1814,7 @@ await RunAsync( - """), + """.Replace("\n", EOL)), ], discoveryWorker: null, analyzeWorker: null, @@ -1824,7 +1845,7 @@ await RunAsync( false - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1839,7 +1860,7 @@ await RunAsync( - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1854,7 +1875,7 @@ await RunAsync( - """)) + """.Replace("\n", EOL))) }, new DependencyFile() { @@ -1869,7 +1890,7 @@ await RunAsync( - """)) + """.Replace("\n", EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -2020,7 +2041,7 @@ await RunAsync( - """ + """.Replace("\n", EOL) }, new() { @@ -2036,7 +2057,7 @@ await RunAsync( - """ + """.Replace("\n", EOL) } ], BaseCommitSha = "TEST-COMMIT-SHA", From 755bc76a9a1a86b1ea6b0fa0f96b482a1c928537 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 18 Feb 2025 11:05:58 -0800 Subject: [PATCH 2/6] implement functionality --- .../NuGetUpdater.Core/Run/RunWorker.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index 3b020d68edc..70a14e73144 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.RegularExpressions; using Microsoft.Extensions.FileSystemGlobbing; @@ -24,6 +25,28 @@ public class RunWorker private readonly IAnalyzeWorker _analyzeWorker; private readonly IUpdaterWorker _updaterWorker; + /// + /// Used to save (and then restore) which line endings are predominant in a file. + /// + private enum EOLType + { + /// + /// Line feed - \n + /// Typical on most systems. + /// + LF, + /// + /// Carrage return - \r + /// Typical on older MacOS, unlikely (but possible) to come up here + /// + CR, + /// + /// Carrage return and line feed - \r\n. + /// Typical on Windows + /// + CRLF + }; + internal static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, @@ -122,6 +145,7 @@ private async Task RunForDirectory(Job job, DirectoryInfo repoContent // TODO: pull out relevant dependencies, then check each for updates and track the changes var originalDependencyFileContents = new Dictionary(); + var originalDependencyFileEOFs = new Dictionary(); var actualUpdatedDependencies = new List(); // track original contents for later handling @@ -131,6 +155,7 @@ async Task TrackOriginalContentsAsync(string directory, string fileName) var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath); var content = await File.ReadAllTextAsync(localFullPath); originalDependencyFileContents[repoFullPath] = content; + originalDependencyFileEOFs[repoFullPath] = GetPredominantEOL(content); } foreach (var project in discoveryResult.Projects) @@ -205,6 +230,9 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) var updatedContent = await File.ReadAllTextAsync(localFullPath); if (updatedContent != originalContent) { + updatedContent = FixEOL(updatedContent, originalDependencyFileEOFs[repoFullPath]); + await File.WriteAllTextAsync(localFullPath, updatedContent); + updatedDependencyFiles[localFullPath] = new DependencyFile() { Name = Path.GetFileName(repoFullPath), @@ -266,6 +294,56 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) return result; } + /// + /// Analyze the input string and find the most common line ending type. + /// + /// The string to analyze + /// The most common type of line ending in the input string. + private static EOLType GetPredominantEOL(string content) + { + // Get stats on EOL characters/character sequences, if one predominates choose that for writing later. + var lfcount = content.Count(c => c == '\n'); + var crcount = content.Count(c => c == '\r'); + var crlfcount = Regex.Matches(content, "\r\n").Count(); + + // Since CRLF contains both a CR and a LF, subtract it from those counts + lfcount -= crlfcount; + crcount -= crlfcount; + if (crcount > lfcount && crcount > crlfcount) + { + return EOLType.CR; + } + else if (crlfcount > lfcount) + { + return EOLType.CRLF; + } + else + { + return EOLType.LF; + } + } + + /// + /// Given a line ending, modify the input string to uniformly use that line ending. + /// + /// The input string, which may have any combination of line endings. + /// The line ending type to use across the result. + /// The string with any line endings swapped to the desired type. + /// If EOLType is an unexpected value. + private static string FixEOL(string content, EOLType desiredEOL) + { + switch (desiredEOL) + { + case EOLType.LF: + return Regex.Replace(content, "(\r\n|\r)", "\n"); + case EOLType.CR: + return Regex.Replace(content, "(\r\n|\n)", "\r"); + case EOLType.CRLF: + return Regex.Replace(content, "(\r\n|\r|\n)", "\r\n"); + } + throw new ArgumentOutOfRangeException(nameof(desiredEOL)); + } + internal static IEnumerable<(string ProjectPath, Dependency Dependency)> GetUpdateOperations(WorkspaceDiscoveryResult discovery) { // discovery is grouped by project then dependency, but we want to pivot and return a list of update operations sorted by dependency name then project path From 2cf9b129be89f11ec68ede08d373fa544107ae43 Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 18 Feb 2025 14:53:28 -0800 Subject: [PATCH 3/6] spelling --- .../lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index 70a14e73144..59aa672c043 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -36,12 +36,12 @@ private enum EOLType /// LF, /// - /// Carrage return - \r + /// Carriage return - \r /// Typical on older MacOS, unlikely (but possible) to come up here /// CR, /// - /// Carrage return and line feed - \r\n. + /// Carriage return and line feed - \r\n. /// Typical on Windows /// CRLF From 9a3b83574d4c8301b03d0cd5809c25f4a5fb032d Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Tue, 18 Feb 2025 15:10:46 -0800 Subject: [PATCH 4/6] Move EOL fix based on feedback --- .../lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index 59aa672c043..a307fe4ae54 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -228,10 +228,12 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) var localFullPath = Path.GetFullPath(Path.Join(repoContentsPath.FullName, repoFullPath)); var originalContent = originalDependencyFileContents[repoFullPath]; var updatedContent = await File.ReadAllTextAsync(localFullPath); + + updatedContent = FixEOL(updatedContent, originalDependencyFileEOFs[repoFullPath]); + await File.WriteAllTextAsync(localFullPath, updatedContent); + if (updatedContent != originalContent) { - updatedContent = FixEOL(updatedContent, originalDependencyFileEOFs[repoFullPath]); - await File.WriteAllTextAsync(localFullPath, updatedContent); updatedDependencyFiles[localFullPath] = new DependencyFile() { From 78b259c8465fdcd39e519c33e5c37cc30cf6cdfb Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Wed, 19 Feb 2025 10:01:12 -0800 Subject: [PATCH 5/6] Handle more EOLs in test file to avoid transient breaks after local saves --- .../Run/RunWorkerTests.cs | 126 +++++++++--------- .../Run/StringExtensions.cs | 6 + 2 files changed, 69 insertions(+), 63 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs index a88c45f8ff5..baa206419e7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs @@ -46,7 +46,7 @@ await RunAsync( - """.Replace("\n", EOL)) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -97,7 +97,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -117,7 +117,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -202,7 +202,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -221,8 +221,8 @@ await File.WriteAllTextAsync(projectPath, """ [InlineData(["\r\n"])] public async Task UpdateHandlesSemicolonsInPackageReference(string EOL) { - var repoMetadata = XElement.Parse("""""".Replace("\n", EOL)); - var repoMetadata2 = XElement.Parse("""""".Replace("\n", EOL)); + var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); + var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); await RunAsync( packages: [ @@ -252,7 +252,7 @@ await RunAsync( - """.Replace("\n", EOL)) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -305,7 +305,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -325,7 +325,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -453,7 +453,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), } ], @@ -495,7 +495,7 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("project.csproj", """ @@ -505,7 +505,7 @@ await RunAsync( - """.Replace("\n", EOL)) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker((_input) => { @@ -532,8 +532,8 @@ await RunAsync( [InlineData(["\r\n"])] public async Task UpdateHandlesPackagesConfigFiles(string EOL) { - var repoMetadata = XElement.Parse("""""".Replace("\n", EOL)); - var repoMetadata2 = XElement.Parse("""""".Replace("\n", EOL)); + var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); + var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); await RunAsync( packages: [ @@ -563,13 +563,13 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("some-dir/packages.config", """ - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -642,7 +642,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); break; case "Some.Package2": await File.WriteAllTextAsync(projectPath, """ @@ -660,14 +660,14 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); var packagesConfigPath = Path.Join(Path.GetDirectoryName(projectPath)!, "packages.config"); await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); break; default: throw new NotSupportedException(); @@ -688,7 +688,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -703,7 +703,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -827,7 +827,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, new DependencyFile() { @@ -848,7 +848,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -867,8 +867,8 @@ await File.WriteAllTextAsync(packagesConfigPath, """ [InlineData(["\r\n"])] public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles(string EOL) { - var repoMetadata = XElement.Parse("""""".Replace("\n", EOL)); - var repoMetadata2 = XElement.Parse("""""".Replace("\n", EOL)); + var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); + var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); await RunAsync( packages: [ @@ -901,13 +901,13 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("some-dir/ProjectA/packages.config", """ - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("some-dir/ProjectB/ProjectB.csproj", """ @@ -917,13 +917,13 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("some-dir/ProjectB/packages.config", """ - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -1014,7 +1014,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); break; case ("ProjectA.csproj", "Some.Package2"): await File.WriteAllTextAsync(projectPath, """ @@ -1035,13 +1035,13 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); break; case ("ProjectB.csproj", "Some.Package"): await File.WriteAllTextAsync(projectPath, """ @@ -1053,7 +1053,7 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); break; case ("ProjectB.csproj", "Some.Package2"): await File.WriteAllTextAsync(projectPath, """ @@ -1071,13 +1071,13 @@ await File.WriteAllTextAsync(projectPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL)); + """.SetEOL(EOL)); break; default: throw new NotSupportedException(); @@ -1098,7 +1098,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1116,7 +1116,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1127,7 +1127,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1142,7 +1142,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1352,7 +1352,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, new DependencyFile() { @@ -1376,7 +1376,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, new DependencyFile() { @@ -1387,7 +1387,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, new DependencyFile() { @@ -1408,7 +1408,7 @@ await File.WriteAllTextAsync(packagesConfigPath, """ - """.Replace("\n", EOL), + """.SetEOL(EOL), }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1452,14 +1452,14 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("Directory.Build.props", """ 1.0.0 - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("project1/project1.csproj", """ @@ -1469,7 +1469,7 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("project2/project2.csproj", """ @@ -1479,7 +1479,7 @@ await RunAsync( - """.Replace("\n", EOL)) + """.SetEOL(EOL)) ], discoveryWorker: new TestDiscoveryWorker(_input => { @@ -1544,7 +1544,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.1.0 - """.Replace("\n", EOL)); + """.SetEOL(EOL)); return new UpdateOperationResult(); }), expectedResult: new RunResult() @@ -1561,7 +1561,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.0.0 - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1576,7 +1576,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1591,7 +1591,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1716,7 +1716,7 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ 1.1.0 - """.Replace("\n", EOL), + """.SetEOL(EOL), } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -1775,7 +1775,7 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("Directory.Build.props", ""), ("Directory.Build.targets", ""), ("Directory.Packages.props", """ @@ -1784,7 +1784,7 @@ await RunAsync( false - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("library1/library1.csproj", """ @@ -1794,7 +1794,7 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("library2/library2.csproj", """ @@ -1804,7 +1804,7 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ("library3/library3.csproj", """ @@ -1814,7 +1814,7 @@ await RunAsync( - """.Replace("\n", EOL)), + """.SetEOL(EOL)), ], discoveryWorker: null, analyzeWorker: null, @@ -1845,7 +1845,7 @@ await RunAsync( false - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1860,7 +1860,7 @@ await RunAsync( - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1875,7 +1875,7 @@ await RunAsync( - """.Replace("\n", EOL))) + """.SetEOL(EOL))) }, new DependencyFile() { @@ -1890,7 +1890,7 @@ await RunAsync( - """.Replace("\n", EOL))) + """.SetEOL(EOL))) } ], BaseCommitSha = "TEST-COMMIT-SHA", @@ -2041,7 +2041,7 @@ await RunAsync( - """.Replace("\n", EOL) + """.SetEOL(EOL) }, new() { @@ -2057,7 +2057,7 @@ await RunAsync( - """.Replace("\n", EOL) + """.SetEOL(EOL) } ], BaseCommitSha = "TEST-COMMIT-SHA", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs new file mode 100644 index 00000000000..e0e2ac60a43 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs @@ -0,0 +1,6 @@ +namespace NuGetUpdater.Core.Test.Run; + +public static class StringExtensions +{ + public static string SetEOL(this string input, string EOL) => input.Replace("\r\n", EOL).Replace("\r", EOL).Replace("\n", EOL); +} From 80b92cd2a0cebc289433249067707114898feb6f Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Wed, 19 Feb 2025 11:30:20 -0800 Subject: [PATCH 6/6] move EOL management to its own utility class, add test for just that --- .../Run/RunWorkerTests.cs | 58 +++++++------- .../Run/StringExtensions.cs | 6 -- .../Utilities/EOLHandlingTests.cs | 23 ++++++ .../NuGetUpdater.Core/Run/RunWorker.cs | 78 +------------------ .../Utilities/EOLHandling.cs | 78 +++++++++++++++++++ 5 files changed, 135 insertions(+), 108 deletions(-) delete mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs index baa206419e7..a38b2746682 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/RunWorkerTests.cs @@ -14,15 +14,17 @@ namespace NuGetUpdater.Core.Test.Run; +using static NuGetUpdater.Core.Utilities.EOLHandling; + using TestFile = (string Path, string Content); public class RunWorkerTests { [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task UpdateSinglePackageProducedExpectedAPIMessages(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateSinglePackageProducedExpectedAPIMessages(EOLType EOL) { await RunAsync( packages: [], @@ -216,10 +218,10 @@ await File.WriteAllTextAsync(projectPath, """ } [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task UpdateHandlesSemicolonsInPackageReference(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateHandlesSemicolonsInPackageReference(EOLType EOL) { var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); @@ -468,10 +470,10 @@ await File.WriteAllTextAsync(projectPath, """ } [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task PrivateSourceAuthenticationFailureIsForwaredToApiHandler(EOLType EOL) { await RunAsync( packages: @@ -527,10 +529,10 @@ await RunAsync( } [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task UpdateHandlesPackagesConfigFiles(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateHandlesPackagesConfigFiles(EOLType EOL) { var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); @@ -862,10 +864,10 @@ await File.WriteAllTextAsync(packagesConfigPath, """ } [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdateHandlesPackagesConfigFromReferencedCsprojFiles(EOLType EOL) { var repoMetadata = XElement.Parse("""""".SetEOL(EOL)); var repoMetadata2 = XElement.Parse("""""".SetEOL(EOL)); @@ -1422,10 +1424,10 @@ await File.WriteAllTextAsync(packagesConfigPath, """ } [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task UpdatedFilesAreOnlyReportedOnce(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdatedFilesAreOnlyReportedOnce(EOLType EOL) { await RunAsync( job: new() @@ -1730,10 +1732,10 @@ await File.WriteAllTextAsync(directoryBuildPropsPath, """ } [Theory] - [InlineData(["\r"])] - [InlineData(["\n"])] - [InlineData(["\r\n"])] - public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories(string EOL) + [InlineData(EOLType.CR)] + [InlineData(EOLType.LF)] + [InlineData(EOLType.CRLF)] + public async Task UpdatePackageWithDifferentVersionsInDifferentDirectories(EOLType EOL) { // this test passes `null` for discovery, analyze, and update workers to fully test the desired behavior diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs deleted file mode 100644 index e0e2ac60a43..00000000000 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Run/StringExtensions.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace NuGetUpdater.Core.Test.Run; - -public static class StringExtensions -{ - public static string SetEOL(this string input, string EOL) => input.Replace("\r\n", EOL).Replace("\r", EOL).Replace("\n", EOL); -} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs new file mode 100644 index 00000000000..ad3c9c194c4 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/EOLHandlingTests.cs @@ -0,0 +1,23 @@ +using System.Text.RegularExpressions; + +using Xunit; + +using static NuGetUpdater.Core.Utilities.EOLHandling; + +namespace NuGetUpdater.Core.Test.Utilities +{ + public class EOLHandlingTests + { + [Theory] + [InlineData(EOLType.LF, "\n")] + [InlineData(EOLType.CR, "\r")] + [InlineData(EOLType.CRLF, "\r\n")] + public void ValidateEOLNormalizesFromLF(EOLType eolType, string literal) + { + var teststring = "this\ris\na\r\nstring\rwith\nmixed\r\nline\rendings\n."; + var changed = teststring.SetEOL(eolType); + var lineEndings = Regex.Split(changed, "\\S+"); + Assert.All(lineEndings, lineEnding => lineEnding.Equals(literal)); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs index a307fe4ae54..2a5f3773b7a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Run/RunWorker.cs @@ -14,6 +14,8 @@ using NuGetUpdater.Core.Run.ApiModel; using NuGetUpdater.Core.Utilities; +using static NuGetUpdater.Core.Utilities.EOLHandling; + namespace NuGetUpdater.Core.Run; public class RunWorker @@ -25,28 +27,6 @@ public class RunWorker private readonly IAnalyzeWorker _analyzeWorker; private readonly IUpdaterWorker _updaterWorker; - /// - /// Used to save (and then restore) which line endings are predominant in a file. - /// - private enum EOLType - { - /// - /// Line feed - \n - /// Typical on most systems. - /// - LF, - /// - /// Carriage return - \r - /// Typical on older MacOS, unlikely (but possible) to come up here - /// - CR, - /// - /// Carriage return and line feed - \r\n. - /// Typical on Windows - /// - CRLF - }; - internal static readonly JsonSerializerOptions SerializerOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower, @@ -155,7 +135,7 @@ async Task TrackOriginalContentsAsync(string directory, string fileName) var localFullPath = Path.Join(repoContentsPath.FullName, repoFullPath); var content = await File.ReadAllTextAsync(localFullPath); originalDependencyFileContents[repoFullPath] = content; - originalDependencyFileEOFs[repoFullPath] = GetPredominantEOL(content); + originalDependencyFileEOFs[repoFullPath] = content.GetPredominantEOL(); } foreach (var project in discoveryResult.Projects) @@ -229,7 +209,7 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) var originalContent = originalDependencyFileContents[repoFullPath]; var updatedContent = await File.ReadAllTextAsync(localFullPath); - updatedContent = FixEOL(updatedContent, originalDependencyFileEOFs[repoFullPath]); + updatedContent = updatedContent.SetEOL(originalDependencyFileEOFs[repoFullPath]); await File.WriteAllTextAsync(localFullPath, updatedContent); if (updatedContent != originalContent) @@ -296,56 +276,6 @@ async Task AddUpdatedFileIfDifferentAsync(string directory, string fileName) return result; } - /// - /// Analyze the input string and find the most common line ending type. - /// - /// The string to analyze - /// The most common type of line ending in the input string. - private static EOLType GetPredominantEOL(string content) - { - // Get stats on EOL characters/character sequences, if one predominates choose that for writing later. - var lfcount = content.Count(c => c == '\n'); - var crcount = content.Count(c => c == '\r'); - var crlfcount = Regex.Matches(content, "\r\n").Count(); - - // Since CRLF contains both a CR and a LF, subtract it from those counts - lfcount -= crlfcount; - crcount -= crlfcount; - if (crcount > lfcount && crcount > crlfcount) - { - return EOLType.CR; - } - else if (crlfcount > lfcount) - { - return EOLType.CRLF; - } - else - { - return EOLType.LF; - } - } - - /// - /// Given a line ending, modify the input string to uniformly use that line ending. - /// - /// The input string, which may have any combination of line endings. - /// The line ending type to use across the result. - /// The string with any line endings swapped to the desired type. - /// If EOLType is an unexpected value. - private static string FixEOL(string content, EOLType desiredEOL) - { - switch (desiredEOL) - { - case EOLType.LF: - return Regex.Replace(content, "(\r\n|\r)", "\n"); - case EOLType.CR: - return Regex.Replace(content, "(\r\n|\n)", "\r"); - case EOLType.CRLF: - return Regex.Replace(content, "(\r\n|\r|\n)", "\r\n"); - } - throw new ArgumentOutOfRangeException(nameof(desiredEOL)); - } - internal static IEnumerable<(string ProjectPath, Dependency Dependency)> GetUpdateOperations(WorkspaceDiscoveryResult discovery) { // discovery is grouped by project then dependency, but we want to pivot and return a list of update operations sorted by dependency name then project path diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs new file mode 100644 index 00000000000..33339c9c8da --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/EOLHandling.cs @@ -0,0 +1,78 @@ +using System.Text.RegularExpressions; + +namespace NuGetUpdater.Core.Utilities; + +public static class EOLHandling +{ + /// + /// Used to save (and then restore) which line endings are predominant in a file. + /// + public enum EOLType + { + /// + /// Line feed - \n + /// Typical on most systems. + /// + LF, + /// + /// Carriage return - \r + /// Typical on older MacOS, unlikely (but possible) to come up here + /// + CR, + /// + /// Carriage return and line feed - \r\n. + /// Typical on Windows + /// + CRLF + }; + + /// + /// Analyze the input string and find the most common line ending type. + /// + /// The string to analyze + /// The most common type of line ending in the input string. + public static EOLType GetPredominantEOL(this string content) + { + // Get stats on EOL characters/character sequences, if one predominates choose that for writing later. + var lfcount = content.Count(c => c == '\n'); + var crcount = content.Count(c => c == '\r'); + var crlfcount = Regex.Matches(content, "\r\n").Count(); + + // Since CRLF contains both a CR and a LF, subtract it from those counts + lfcount -= crlfcount; + crcount -= crlfcount; + if (crcount > lfcount && crcount > crlfcount) + { + return EOLType.CR; + } + else if (crlfcount > lfcount) + { + return EOLType.CRLF; + } + else + { + return EOLType.LF; + } + } + + /// + /// Given a line ending, modify the input string to uniformly use that line ending. + /// + /// The input string, which may have any combination of line endings. + /// The line ending type to use across the result. + /// The string with any line endings swapped to the desired type. + /// If EOLType is an unexpected value. + public static string SetEOL(this string content, EOLType desiredEOL) + { + switch (desiredEOL) + { + case EOLType.LF: + return Regex.Replace(content, "(\r\n|\r)", "\n"); + case EOLType.CR: + return Regex.Replace(content, "(\r\n|\n)", "\r"); + case EOLType.CRLF: + return Regex.Replace(content, "(\r\n|\r|\n)", "\r\n"); + } + throw new ArgumentOutOfRangeException(nameof(desiredEOL)); + } +}