diff --git a/CentralConfigGenerator.Core.Tests/Analyzers/PackageAnalyzerTests.cs b/CentralConfigGenerator.Core.Tests/Analyzers/PackageAnalyzerTests.cs index 9355501..aab343a 100644 --- a/CentralConfigGenerator.Core.Tests/Analyzers/PackageAnalyzerTests.cs +++ b/CentralConfigGenerator.Core.Tests/Analyzers/PackageAnalyzerTests.cs @@ -1,4 +1,5 @@ using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Models; namespace CentralConfigGenerator.Core.Tests.Analyzers; @@ -242,4 +243,166 @@ public void ExtractPackageVersions_ShouldHandleComplexVersionStrings() result.ShouldContainKey("Package2"); result["Package2"].ShouldBe("2.0.0-beta+metadata"); } -} \ No newline at end of file + + [Fact] + public void ExtractPackageVersions_ShouldHandleSemanticVersioning_PrereleaseComparison() + { + // Arrange + var projectFiles = new List + { + new() + { + Path = "Project1.csproj", + Content = @" + + + + " + }, + new() + { + Path = "Project2.csproj", + Content = @" + + + + " + } + }; + + // Act + var result = _analyzer.ExtractPackageVersions(projectFiles); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(1); + result.ShouldContainKey("Package1"); + // beta > alpha in SemVer + result["Package1"].ShouldBe("1.0.0-beta.1"); + } + + [Fact] + public void ExtractPackageVersions_ShouldPreferStableOverPrerelease() + { + // Arrange + var projectFiles = new List + { + new() + { + Path = "Project1.csproj", + Content = @" + + + + " + }, + new() + { + Path = "Project2.csproj", + Content = @" + + + + " + } + }; + + // Act + var result = _analyzer.ExtractPackageVersions(projectFiles); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(1); + result.ShouldContainKey("Package1"); + // Stable version > pre-release + result["Package1"].ShouldBe("1.0.0"); + } + + [Fact] + public void ExtractPackageVersions_ShouldHandleVersionRanges() + { + // Arrange + var projectFiles = new List + { + new() + { + Path = "Project1.csproj", + Content = @" + + + + + " + } + }; + + // Act + var result = _analyzer.ExtractPackageVersions(projectFiles); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(2); + result.ShouldContainKey("Package1"); + // Original range preserved + result["Package1"].ShouldBe("[1.0.0,2.0.0)"); + result.ShouldContainKey("Package2"); + result["Package2"].ShouldBe("[2.0.0,)"); + } + + [Fact] + public void ExtractPackageVersions_ShouldHandleFloatingVersions() + { + // Arrange + var projectFiles = new List + { + new() + { + Path = "Project1.csproj", + Content = @" + + + + + " + } + }; + + // Act + var result = _analyzer.ExtractPackageVersions(projectFiles); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(2); + result.ShouldContainKey("Package1"); + result["Package1"].ShouldBe("1.0.*"); + result.ShouldContainKey("Package2"); + result["Package2"].ShouldBe("1.*"); + } + + [Fact] + public void ExtractPackageVersions_ShouldHandleCommitHashVersions() + { + // Arrange + var projectFiles = new List + { + new() + { + Path = "Project1.csproj", + Content = @" + + + + " + } + }; + + // Act + var result = _analyzer.ExtractPackageVersions(projectFiles); + + // Assert + result.ShouldNotBeNull(); + result.Count.ShouldBe(1); + result.ShouldContainKey("Package1"); + result["Package1"].ShouldBe("1.0.0+abcdef123456"); + } +} diff --git a/CentralConfigGenerator.Core.Tests/Analyzers/ProjectAnalyzerTests.cs b/CentralConfigGenerator.Core.Tests/Analyzers/ProjectAnalyzerTests.cs index 01fae76..17fd208 100644 --- a/CentralConfigGenerator.Core.Tests/Analyzers/ProjectAnalyzerTests.cs +++ b/CentralConfigGenerator.Core.Tests/Analyzers/ProjectAnalyzerTests.cs @@ -1,4 +1,5 @@ using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Models; namespace CentralConfigGenerator.Core.Tests.Analyzers; diff --git a/CentralConfigGenerator.Core.Tests/CentralConfigGenerator.Core.Tests.csproj b/CentralConfigGenerator.Core.Tests/CentralConfigGenerator.Core.Tests.csproj index 4c7aa0f..0ebbfec 100644 --- a/CentralConfigGenerator.Core.Tests/CentralConfigGenerator.Core.Tests.csproj +++ b/CentralConfigGenerator.Core.Tests/CentralConfigGenerator.Core.Tests.csproj @@ -10,6 +10,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/CentralConfigGenerator.Core.Tests/Generators/BuildPropsGeneratorTests.cs b/CentralConfigGenerator.Core.Tests/Generators/BuildPropsGeneratorTests.cs index ea1dad2..55e1714 100644 --- a/CentralConfigGenerator.Core.Tests/Generators/BuildPropsGeneratorTests.cs +++ b/CentralConfigGenerator.Core.Tests/Generators/BuildPropsGeneratorTests.cs @@ -1,5 +1,6 @@ using CentralConfigGenerator.Core.Generators; using System.Xml.Linq; +using CentralConfigGenerator.Core.Generators.Abstractions; namespace CentralConfigGenerator.Core.Tests.Generators; diff --git a/CentralConfigGenerator.Core.Tests/Generators/PackagesPropsGeneratorTests.cs b/CentralConfigGenerator.Core.Tests/Generators/PackagesPropsGeneratorTests.cs index eba4800..ba55132 100644 --- a/CentralConfigGenerator.Core.Tests/Generators/PackagesPropsGeneratorTests.cs +++ b/CentralConfigGenerator.Core.Tests/Generators/PackagesPropsGeneratorTests.cs @@ -1,5 +1,6 @@ using CentralConfigGenerator.Core.Generators; using System.Xml.Linq; +using CentralConfigGenerator.Core.Generators.Abstractions; namespace CentralConfigGenerator.Core.Tests.Generators; diff --git a/CentralConfigGenerator.Core/Analyzers/Abstractions/IEnhancedPackageAnalyzer.cs b/CentralConfigGenerator.Core/Analyzers/Abstractions/IEnhancedPackageAnalyzer.cs new file mode 100644 index 0000000..fd1ffc5 --- /dev/null +++ b/CentralConfigGenerator.Core/Analyzers/Abstractions/IEnhancedPackageAnalyzer.cs @@ -0,0 +1,11 @@ +using CentralConfigGenerator.Core.Models; + +namespace CentralConfigGenerator.Core.Analyzers.Abstractions; + +public interface IEnhancedPackageAnalyzer +{ + Task AnalyzePackagesAsync( + IEnumerable projectFiles, + CancellationToken cancellationToken = default + ); +} diff --git a/CentralConfigGenerator.Core/Analyzers/IPackageAnalyzer.cs b/CentralConfigGenerator.Core/Analyzers/Abstractions/IPackageAnalyzer.cs similarity index 74% rename from CentralConfigGenerator.Core/Analyzers/IPackageAnalyzer.cs rename to CentralConfigGenerator.Core/Analyzers/Abstractions/IPackageAnalyzer.cs index 0e90c97..56907c6 100644 --- a/CentralConfigGenerator.Core/Analyzers/IPackageAnalyzer.cs +++ b/CentralConfigGenerator.Core/Analyzers/Abstractions/IPackageAnalyzer.cs @@ -1,6 +1,6 @@ using CentralConfigGenerator.Core.Models; -namespace CentralConfigGenerator.Core.Analyzers; +namespace CentralConfigGenerator.Core.Analyzers.Abstractions; public interface IPackageAnalyzer { diff --git a/CentralConfigGenerator.Core/Analyzers/IProjectAnalyzer.cs b/CentralConfigGenerator.Core/Analyzers/Abstractions/IProjectAnalyzer.cs similarity index 75% rename from CentralConfigGenerator.Core/Analyzers/IProjectAnalyzer.cs rename to CentralConfigGenerator.Core/Analyzers/Abstractions/IProjectAnalyzer.cs index ebb9094..165346f 100644 --- a/CentralConfigGenerator.Core/Analyzers/IProjectAnalyzer.cs +++ b/CentralConfigGenerator.Core/Analyzers/Abstractions/IProjectAnalyzer.cs @@ -1,6 +1,6 @@ using CentralConfigGenerator.Core.Models; -namespace CentralConfigGenerator.Core.Analyzers; +namespace CentralConfigGenerator.Core.Analyzers.Abstractions; public interface IProjectAnalyzer { diff --git a/CentralConfigGenerator.Core/Analyzers/EnhancedPackageAnalyzer.cs b/CentralConfigGenerator.Core/Analyzers/EnhancedPackageAnalyzer.cs new file mode 100644 index 0000000..4acf9fd --- /dev/null +++ b/CentralConfigGenerator.Core/Analyzers/EnhancedPackageAnalyzer.cs @@ -0,0 +1,209 @@ +using System.Xml.Linq; +using CentralConfigGenerator.Core.Analyzers.Abstractions; +using CentralConfigGenerator.Core.Models; +using CentralConfigGenerator.Core.Services; +using CentralConfigGenerator.Core.Services.Abstractions; +using NuGet.Versioning; + +namespace CentralConfigGenerator.Core.Analyzers; + +public class PackageAnalysisResult +{ + public Dictionary ResolvedVersions { get; set; } = new(); + public Dictionary> Conflicts { get; set; } = new(); + public List Warnings { get; set; } = new(); +} + +public class VersionConflict +{ + public string ProjectFile { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public bool IsPreRelease { get; set; } + public bool IsRange { get; set; } +} + +public class VersionWarning +{ + public string PackageName { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; + public WarningLevel Level { get; set; } +} + +public enum WarningLevel +{ + Info, + Warning, + Error, +} + +public class EnhancedPackageAnalyzer( + IVersionConflictResolver conflictResolver, + IVersionCompatibilityChecker compatibilityChecker +) : IEnhancedPackageAnalyzer +{ + public async Task AnalyzePackagesAsync( + IEnumerable projectFiles, + CancellationToken cancellationToken = default + ) + { + var result = new PackageAnalysisResult(); + var packageVersionsByPackage = + new Dictionary>(); + + // Collect all versions + foreach (var projectFile in projectFiles) + { + try + { + var xDoc = XDocument.Parse(projectFile.Content); + var packageReferences = xDoc.Descendants("PackageReference"); + + foreach (var packageRef in packageReferences) + { + var packageName = packageRef.Attribute("Include")?.Value; + var versionAttr = packageRef.Attribute("Version"); + + if (string.IsNullOrWhiteSpace(packageName) || versionAttr == null) + continue; + + var version = versionAttr.Value; + + if (!packageVersionsByPackage.ContainsKey(packageName)) + packageVersionsByPackage[packageName] = new List<(string, string)>(); + + packageVersionsByPackage[packageName].Add((projectFile.Path, version)); + } + } + catch (Exception ex) + { + result.Warnings.Add( + new VersionWarning + { + PackageName = projectFile.Path, + Message = $"Failed to parse project file: {ex.Message}", + Level = WarningLevel.Error, + } + ); + } + } + + // Resolve conflicts and create final versions + foreach (var package in packageVersionsByPackage) + { + var packageName = package.Key; + var versions = package.Value; + var uniqueVersions = versions.Select(v => v.Version).Distinct().ToList(); + + if (uniqueVersions.Count == 1) + { + // No conflict + result.ResolvedVersions[packageName] = uniqueVersions[0]; + } + else + { + // Conflict detected + result.Conflicts[packageName] = versions + .Select(v => CreateVersionConflict(v.ProjectPath, v.Version)) + .ToList(); + + // Try to resolve with the highest version strategy + try + { + var resolvedVersion = conflictResolver.Resolve( + packageName, + uniqueVersions, + VersionResolutionStrategy.Highest + ); + + result.ResolvedVersions[packageName] = resolvedVersion; + + result.Warnings.Add( + new VersionWarning + { + PackageName = packageName, + Message = $"Multiple versions found. Resolved to: {resolvedVersion}", + Level = WarningLevel.Warning, + } + ); + } + catch (Exception ex) + { + result.Warnings.Add( + new VersionWarning + { + PackageName = packageName, + Message = $"Failed to resolve version conflict: {ex.Message}", + Level = WarningLevel.Error, + } + ); + + // Fallback: use most recent version + result.ResolvedVersions[packageName] = uniqueVersions.OrderDescending().First(); + } + } + + // Check for pre-release usage + CheckForPrereleaseUsage( + packageName, + result.ResolvedVersions[packageName], + result.Warnings + ); + + // Check compatibility + var compatibilityResult = await compatibilityChecker.CheckCompatibilityAsync( + packageName, + result.ResolvedVersions[packageName] + ); + + if (compatibilityResult.SuggestedVersion != null) + { + result.Warnings.Add( + new VersionWarning + { + PackageName = packageName, + Message = + $"Consider upgrading to version {compatibilityResult.SuggestedVersion} for better compatibility.", + Level = WarningLevel.Info, + } + ); + } + } + + return result; + } + + private static VersionConflict CreateVersionConflict(string projectPath, string version) + { + var conflict = new VersionConflict { ProjectFile = projectPath, Version = version }; + + if (NuGetVersion.TryParse(version, out var nugetVersion)) + { + conflict.IsPreRelease = nugetVersion.IsPrerelease; + } + else if (VersionRange.TryParse(version, out _)) + { + conflict.IsRange = true; + } + + return conflict; + } + + private static void CheckForPrereleaseUsage( + string packageName, + string version, + List warnings + ) + { + if (NuGetVersion.TryParse(version, out var nugetVersion) && nugetVersion.IsPrerelease) + { + warnings.Add( + new VersionWarning + { + PackageName = packageName, + Message = $"Using pre-release version: {version}", + Level = WarningLevel.Info, + } + ); + } + } +} diff --git a/CentralConfigGenerator.Core/Analyzers/PackageAnalyzer.cs b/CentralConfigGenerator.Core/Analyzers/PackageAnalyzer.cs index f6bfb55..f834cae 100644 --- a/CentralConfigGenerator.Core/Analyzers/PackageAnalyzer.cs +++ b/CentralConfigGenerator.Core/Analyzers/PackageAnalyzer.cs @@ -1,5 +1,7 @@ using System.Xml.Linq; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Models; +using NuGet.Versioning; using Spectre.Console; namespace CentralConfigGenerator.Core.Analyzers; @@ -8,7 +10,7 @@ public class PackageAnalyzer : IPackageAnalyzer { public Dictionary ExtractPackageVersions(IEnumerable projectFiles) { - var packageVersions = new Dictionary(); + var packageVersions = new Dictionary(); var stringVersions = new Dictionary(); foreach (var projectFile in projectFiles) @@ -31,18 +33,39 @@ public Dictionary ExtractPackageVersions(IEnumerable packageVersions[packageName] + ) + { + packageVersions[packageName] = nugetVersion; + stringVersions[packageName] = versionStr; + } + } + else if (NuGetVersion.TryParse(versionStr, out var nugetVersion)) { - if (!packageVersions.ContainsKey(packageName) || version > packageVersions[packageName]) + if ( + !packageVersions.ContainsKey(packageName) + || nugetVersion > packageVersions[packageName] + ) { - packageVersions[packageName] = version; + packageVersions[packageName] = nugetVersion; stringVersions[packageName] = versionStr; } } + else + { + // For non-parseable versions (like variables), just store them + stringVersions.TryAdd(packageName, versionStr); + } } } catch (Exception ex) @@ -55,4 +78,4 @@ public Dictionary ExtractPackageVersions(IEnumerable - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/CentralConfigGenerator.Core/Generators/IBuildPropsGenerator.cs b/CentralConfigGenerator.Core/Generators/Abstractions/IBuildPropsGenerator.cs similarity index 65% rename from CentralConfigGenerator.Core/Generators/IBuildPropsGenerator.cs rename to CentralConfigGenerator.Core/Generators/Abstractions/IBuildPropsGenerator.cs index be040dd..71ae427 100644 --- a/CentralConfigGenerator.Core/Generators/IBuildPropsGenerator.cs +++ b/CentralConfigGenerator.Core/Generators/Abstractions/IBuildPropsGenerator.cs @@ -1,4 +1,4 @@ -namespace CentralConfigGenerator.Core.Generators; +namespace CentralConfigGenerator.Core.Generators.Abstractions; public interface IBuildPropsGenerator { diff --git a/CentralConfigGenerator.Core/Generators/IPackagesPropsGenerator.cs b/CentralConfigGenerator.Core/Generators/Abstractions/IPackagesPropsGenerator.cs similarity index 66% rename from CentralConfigGenerator.Core/Generators/IPackagesPropsGenerator.cs rename to CentralConfigGenerator.Core/Generators/Abstractions/IPackagesPropsGenerator.cs index 0cb32a3..3509ee0 100644 --- a/CentralConfigGenerator.Core/Generators/IPackagesPropsGenerator.cs +++ b/CentralConfigGenerator.Core/Generators/Abstractions/IPackagesPropsGenerator.cs @@ -1,4 +1,4 @@ -namespace CentralConfigGenerator.Core.Generators; +namespace CentralConfigGenerator.Core.Generators.Abstractions; public interface IPackagesPropsGenerator { diff --git a/CentralConfigGenerator.Core/Generators/BuildPropsGenerator.cs b/CentralConfigGenerator.Core/Generators/BuildPropsGenerator.cs index af720b9..48d5ad7 100644 --- a/CentralConfigGenerator.Core/Generators/BuildPropsGenerator.cs +++ b/CentralConfigGenerator.Core/Generators/BuildPropsGenerator.cs @@ -1,4 +1,5 @@ using System.Xml.Linq; +using CentralConfigGenerator.Core.Generators.Abstractions; namespace CentralConfigGenerator.Core.Generators; @@ -6,17 +7,14 @@ public class BuildPropsGenerator : IBuildPropsGenerator { public string GenerateBuildPropsContent(Dictionary commonProperties) { - var xDoc = new XDocument( - new XElement("Project", - new XElement("PropertyGroup") - ) - ); + var xDoc = new XDocument(new XElement("Project", new XElement("PropertyGroup"))); var propertyGroup = xDoc.Root!.Element("PropertyGroup")!; // Define the required properties we want to include + var requiredPropertyNames = new[] { "TargetFramework", "ImplicitUsings", "Nullable" }; - + // Add the properties that match our required list foreach (var propertyName in requiredPropertyNames) { @@ -39,4 +37,4 @@ public string GenerateBuildPropsContent(Dictionary commonPropert return xDoc.ToString(); } -} \ No newline at end of file +} diff --git a/CentralConfigGenerator.Core/Generators/PackagesPropsGenerator.cs b/CentralConfigGenerator.Core/Generators/PackagesPropsGenerator.cs index 1c6fdca..5fdc6bf 100644 --- a/CentralConfigGenerator.Core/Generators/PackagesPropsGenerator.cs +++ b/CentralConfigGenerator.Core/Generators/PackagesPropsGenerator.cs @@ -1,4 +1,5 @@ using System.Xml.Linq; +using CentralConfigGenerator.Core.Generators.Abstractions; namespace CentralConfigGenerator.Core.Generators; @@ -7,8 +8,10 @@ public class PackagesPropsGenerator : IPackagesPropsGenerator public string GeneratePackagesPropsContent(Dictionary packageVersions) { var xDoc = new XDocument( - new XElement("Project", - new XElement("PropertyGroup", + new XElement( + "Project", + new XElement( + "PropertyGroup", new XElement("ManagePackageVersionsCentrally", "true") ), new XElement("ItemGroup") @@ -20,7 +23,8 @@ public string GeneratePackagesPropsContent(Dictionary packageVer foreach (var package in packageVersions.OrderBy(p => p.Key)) { itemGroup.Add( - new XElement("PackageVersion", + new XElement( + "PackageVersion", new XAttribute("Include", package.Key), new XAttribute("Version", package.Value) ) @@ -29,4 +33,4 @@ public string GeneratePackagesPropsContent(Dictionary packageVer return xDoc.ToString(); } -} \ No newline at end of file +} diff --git a/CentralConfigGenerator.Core/Models/CompatibilityCheckResult.cs b/CentralConfigGenerator.Core/Models/CompatibilityCheckResult.cs new file mode 100644 index 0000000..baafc8e --- /dev/null +++ b/CentralConfigGenerator.Core/Models/CompatibilityCheckResult.cs @@ -0,0 +1,8 @@ +namespace CentralConfigGenerator.Core.Models; + +public class CompatibilityCheckResult +{ + public bool IsCompatible { get; set; } + public List Issues { get; set; } = []; + public string? SuggestedVersion { get; set; } +} \ No newline at end of file diff --git a/CentralConfigGenerator.Core/Models/ParsedVersion.cs b/CentralConfigGenerator.Core/Models/ParsedVersion.cs new file mode 100644 index 0000000..3995596 --- /dev/null +++ b/CentralConfigGenerator.Core/Models/ParsedVersion.cs @@ -0,0 +1,10 @@ +using NuGet.Versioning; + +namespace CentralConfigGenerator.Core.Models; + +public record ParsedVersion +{ + public required string Original { get; init; } + public NuGetVersion? Parsed { get; init; } + public VersionRange? Range { get; init; } +} \ No newline at end of file diff --git a/CentralConfigGenerator.Core/Services/Abstractions/IVersionCompatibilityChecker.cs b/CentralConfigGenerator.Core/Services/Abstractions/IVersionCompatibilityChecker.cs new file mode 100644 index 0000000..883a012 --- /dev/null +++ b/CentralConfigGenerator.Core/Services/Abstractions/IVersionCompatibilityChecker.cs @@ -0,0 +1,12 @@ +using CentralConfigGenerator.Core.Models; + +namespace CentralConfigGenerator.Core.Services.Abstractions; + +public interface IVersionCompatibilityChecker +{ + Task CheckCompatibilityAsync( + string packageId, + string version, + CancellationToken cancellationToken = default + ); +} diff --git a/CentralConfigGenerator.Core/Services/Abstractions/IVersionConflictResolver.cs b/CentralConfigGenerator.Core/Services/Abstractions/IVersionConflictResolver.cs new file mode 100644 index 0000000..8131cbd --- /dev/null +++ b/CentralConfigGenerator.Core/Services/Abstractions/IVersionConflictResolver.cs @@ -0,0 +1,6 @@ +namespace CentralConfigGenerator.Core.Services.Abstractions; + +public interface IVersionConflictResolver +{ + string Resolve(string packageName, IEnumerable versions, VersionResolutionStrategy strategy); +} \ No newline at end of file diff --git a/CentralConfigGenerator.Core/Services/Abstractions/IVersionConflictVisualizer.cs b/CentralConfigGenerator.Core/Services/Abstractions/IVersionConflictVisualizer.cs new file mode 100644 index 0000000..d261337 --- /dev/null +++ b/CentralConfigGenerator.Core/Services/Abstractions/IVersionConflictVisualizer.cs @@ -0,0 +1,8 @@ +using CentralConfigGenerator.Core.Analyzers; + +namespace CentralConfigGenerator.Core.Services.Abstractions; + +public interface IVersionConflictVisualizer +{ + void DisplayResults(PackageAnalysisResult result); +} \ No newline at end of file diff --git a/CentralConfigGenerator.Core/Services/VersionCompatibilityChecker.cs b/CentralConfigGenerator.Core/Services/VersionCompatibilityChecker.cs new file mode 100644 index 0000000..ac696df --- /dev/null +++ b/CentralConfigGenerator.Core/Services/VersionCompatibilityChecker.cs @@ -0,0 +1,89 @@ +using CentralConfigGenerator.Core.Models; +using CentralConfigGenerator.Core.Services.Abstractions; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace CentralConfigGenerator.Core.Services; + +public class VersionCompatibilityChecker : IVersionCompatibilityChecker +{ + private readonly SourceRepository _repository; + + public VersionCompatibilityChecker() + { + var providers = new List>(Repository.Provider.GetCoreV3()); + _repository = new SourceRepository( + new PackageSource("https://api.nuget.org/v3/index.json"), + providers + ); + } + + public async Task CheckCompatibilityAsync( + string packageId, + string version, + CancellationToken cancellationToken = default + ) + { + var result = new CompatibilityCheckResult(); + if (!NuGetVersion.TryParse(version, out var nugetVersion)) + { + result.IsCompatible = false; + result.Issues.Add($"Invalid version format: {version}"); + return result; + } + + // NuGet API lookup + try + { + var metadataResource = await _repository.GetResourceAsync( + cancellationToken + ); + var searchMetadata = await metadataResource.GetMetadataAsync( + packageId, + includePrerelease: true, + includeUnlisted: false, + new SourceCacheContext(), + NullLogger.Instance, + CancellationToken.None + ); + + var allVersions = searchMetadata + .Select(m => m.Identity.Version) + .OrderByDescending(v => v) + .ToList(); + + if (allVersions.Count == 0) + { + result.Issues.Add("Package not found in NuGet repository."); + result.IsCompatible = false; + return result; + } + + // Check for pre-release + if (nugetVersion.IsPrerelease) + { + result.Issues.Add( + "Pre-release version detected. Consider using a stable release for production." + ); + } + + // Check outdated major version + var latestStable = allVersions.FirstOrDefault(v => !v.IsPrerelease); + if (latestStable != null && nugetVersion.Major < latestStable.Major - 1) + { + result.Issues.Add( + $"This version is significantly outdated. Latest stable is {latestStable}." + ); + result.SuggestedVersion ??= latestStable.ToNormalizedString(); + } + } + catch (Exception ex) + { + result.Issues.Add($"Failed to fetch package metadata: {ex.Message}"); + } + + return result; + } +} diff --git a/CentralConfigGenerator.Core/Services/VersionConflictResolver.cs b/CentralConfigGenerator.Core/Services/VersionConflictResolver.cs new file mode 100644 index 0000000..9ad5eb8 --- /dev/null +++ b/CentralConfigGenerator.Core/Services/VersionConflictResolver.cs @@ -0,0 +1,122 @@ +using CentralConfigGenerator.Core.Models; +using CentralConfigGenerator.Core.Services.Abstractions; +using NuGet.Versioning; + +namespace CentralConfigGenerator.Core.Services; + +public enum VersionResolutionStrategy +{ + Highest, + Lowest, + MostCommon, + Manual, +} + +public class VersionConflictResolver : IVersionConflictResolver +{ + public string Resolve( + string packageName, + IEnumerable versions, + VersionResolutionStrategy strategy + ) + { + var versionList = versions.ToList(); + + if (versionList.Count == 0) + { + throw new ArgumentException("No versions provided for resolution"); + } + + if (versionList.Count == 1) + { + return versionList[0]; + } + + // Parse all versions into NuGetVersion objects where possible + var parsedVersions = versionList + .Select(v => new ParsedVersion + { + Original = v, + Parsed = NuGetVersion.TryParse(v, out var parsed) ? parsed : null, + Range = VersionRange.TryParse(v, out var range) ? range : null, + }) + .ToList(); + + return strategy switch + { + VersionResolutionStrategy.Highest => ResolveHighest(parsedVersions), + VersionResolutionStrategy.Lowest => ResolveLowest(parsedVersions), + VersionResolutionStrategy.MostCommon => ResolveMostCommon(versionList), + VersionResolutionStrategy.Manual => throw new InvalidOperationException( + $"Manual resolution required for package '{packageName}'. Versions found: {string.Join(", ", versionList)}" + ), + _ => throw new ArgumentOutOfRangeException(nameof(strategy)), + }; + } + + private static string ResolveHighest(List parsedVersions) + { + // First, try to find the highest parsed version + var highestParsed = parsedVersions + .Where(v => v.Parsed != null) + .OrderByDescending(v => v.Parsed) + .FirstOrDefault(); + + if (highestParsed != null) + { + return highestParsed.Original; + } + + // If no parsed versions, try version ranges + var versionRanges = parsedVersions + .Where(v => v.Range is { HasLowerBound: true }) + .OrderByDescending(v => v.Range!.MinVersion) + .FirstOrDefault(); + + if (versionRanges != null) + { + return versionRanges.Original; + } + + // Fallback to string comparison + return parsedVersions.OrderByDescending(v => v.Original).First().Original; + } + + private static string ResolveLowest(List parsedVersions) + { + // Try to find the lowest parsed version + var lowestParsed = parsedVersions + .Where(v => v.Parsed != null) + .OrderBy(v => v.Parsed) + .FirstOrDefault(); + + if (lowestParsed != null) + { + return lowestParsed.Original; + } + + // If no parsed versions, try version ranges + var versionRanges = parsedVersions + .Where(v => v.Range is { HasLowerBound: true }) + .OrderBy(v => v.Range!.MinVersion) + .FirstOrDefault(); + + if (versionRanges != null) + { + return versionRanges.Original; + } + + // Fallback to string comparison + return parsedVersions.OrderBy(v => v.Original).First().Original; + } + + private static string ResolveMostCommon(List versions) + { + return versions + .GroupBy(v => v) + .OrderByDescending(g => g.Count()) + .ThenBy(g => g.Key) + .First() + .Key; + } +} diff --git a/CentralConfigGenerator.Core/Services/VersionConflictVisualizer.cs b/CentralConfigGenerator.Core/Services/VersionConflictVisualizer.cs new file mode 100644 index 0000000..5059d92 --- /dev/null +++ b/CentralConfigGenerator.Core/Services/VersionConflictVisualizer.cs @@ -0,0 +1,121 @@ +using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Services.Abstractions; +using Spectre.Console; + +namespace CentralConfigGenerator.Core.Services; + +public class VersionConflictVisualizer : IVersionConflictVisualizer +{ + public void DisplayResults(PackageAnalysisResult result) + { + // Display summary + var summaryTable = new Table(); + summaryTable.AddColumn("Metric"); + summaryTable.AddColumn(new TableColumn("Value").Centered()); + + summaryTable.AddRow("Total Packages", result.ResolvedVersions.Count.ToString()); + summaryTable.AddRow("Packages with Conflicts", result.Conflicts.Count.ToString()); + summaryTable.AddRow("Warnings", result.Warnings.Count.ToString()); + + AnsiConsole.Write( + new Panel(summaryTable) + .Header("[bold green]Package Analysis Summary[/]") + .Border(BoxBorder.Rounded) + ); + + AnsiConsole.WriteLine(); + + // Display conflicts + if (result.Conflicts.Count != 0) + { + AnsiConsole.MarkupLine("[bold red]Version Conflicts Detected:[/]"); + + var conflictTable = new Table(); + conflictTable.AddColumn("Package"); + conflictTable.AddColumn("Project"); + conflictTable.AddColumn("Version"); + conflictTable.AddColumn("Type"); + + foreach (var conflict in result.Conflicts) + { + foreach (var detail in conflict.Value) + { + string versionType; + if (detail.IsRange) + { + versionType = "Range"; + } + else if (detail.IsPreRelease) + { + versionType = "Pre-release"; + } + else + { + versionType = "Release"; + } + + conflictTable.AddRow( + conflict.Key, + Markup.Escape(Path.GetFileName(detail.ProjectFile)), + detail.Version, + versionType + ); + } + + // Add separator row + if (conflict.Key != result.Conflicts.Keys.Last()) + { + conflictTable.AddEmptyRow(); + } + } + + AnsiConsole.Write(conflictTable); + AnsiConsole.WriteLine(); + } + + // Display warnings + if (result.Warnings.Count != 0) + { + AnsiConsole.MarkupLine("[bold yellow]Warnings:[/]"); + + var warningsTable = new Table(); + warningsTable.AddColumn("Level"); + warningsTable.AddColumn("Package"); + warningsTable.AddColumn("Message"); + + foreach (var warning in result.Warnings.OrderBy(w => w.Level)) + { + var levelMarkup = warning.Level switch + { + WarningLevel.Info => "[blue]Info[/]", + WarningLevel.Warning => "[yellow]Warning[/]", + WarningLevel.Error => "[red]Error[/]", + _ => "[white]Unknown[/]", + }; + + warningsTable.AddRow( + levelMarkup, + warning.PackageName, + Markup.Escape(warning.Message) + ); + } + + AnsiConsole.Write(warningsTable); + AnsiConsole.WriteLine(); + } + + // Display resolved versions + AnsiConsole.MarkupLine("[bold green]Resolved Package Versions:[/]"); + + var versionTable = new Table(); + versionTable.AddColumn("Package"); + versionTable.AddColumn("Version"); + + foreach (var package in result.ResolvedVersions.OrderBy(p => p.Key)) + { + versionTable.AddRow(package.Key, package.Value); + } + + AnsiConsole.Write(versionTable); + } +} diff --git a/CentralConfigGenerator.Tests/Commands/BuildPropsCommandTests.cs b/CentralConfigGenerator.Tests/Commands/BuildPropsCommandTests.cs index 155b70b..44c0bed 100644 --- a/CentralConfigGenerator.Tests/Commands/BuildPropsCommandTests.cs +++ b/CentralConfigGenerator.Tests/Commands/BuildPropsCommandTests.cs @@ -1,6 +1,8 @@ using CentralConfigGenerator.Commands; using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Generators; +using CentralConfigGenerator.Core.Generators.Abstractions; using CentralConfigGenerator.Core.Models; using CentralConfigGenerator.Services.Abstractions; diff --git a/CentralConfigGenerator.Tests/Commands/PackagesPropsCommandTests.cs b/CentralConfigGenerator.Tests/Commands/PackagesPropsCommandTests.cs index 208a513..88b2e8c 100644 --- a/CentralConfigGenerator.Tests/Commands/PackagesPropsCommandTests.cs +++ b/CentralConfigGenerator.Tests/Commands/PackagesPropsCommandTests.cs @@ -1,6 +1,8 @@ using CentralConfigGenerator.Commands; using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Generators; +using CentralConfigGenerator.Core.Generators.Abstractions; using CentralConfigGenerator.Core.Models; using CentralConfigGenerator.Services.Abstractions; diff --git a/CentralConfigGenerator/CentralConfigGenerator.csproj b/CentralConfigGenerator/CentralConfigGenerator.csproj index b3404fc..94137ab 100644 --- a/CentralConfigGenerator/CentralConfigGenerator.csproj +++ b/CentralConfigGenerator/CentralConfigGenerator.csproj @@ -1,44 +1,45 @@ - - Exe - true - true - true - CentralConfigGenerator - true - central-config - ./nupkg - - - - README.md - - A modern .NET tool for automatically generating centralized configuration files for .NET projects. CentralConfig analyzes your solution structure and creates properly configured `Directory.Build.props` and `Directory.Packages.props` files to standardize settings across your projects. + + Exe + true + true + true + CentralConfigGenerator + true + central-config + ./nupkg + + + + README.md + + A modern .NET tool for automatically generating centralized configuration files for .NET projects. + CentralConfig analyzes your solution structure and creates properly configured `Directory.Build.props` and `Directory.Packages.props` files to standardize settings across your projects. - 1.0.1 - Taras Kovalenko - Copyright Taras Kovalenko - dotnet;msbuild;build;props;packages;centralized;configuration;directory-build-props;sdk;cli;tool;nuget - CentralConfigGenerator - MIT - https://github.com/TarasKovalenko/CentralConfigGenerator - https://github.com/TarasKovalenko/CentralConfigGenerator.git - git - false - - - - - - - - - - - - - - - - + 1.1.0 + Taras Kovalenko + Copyright Taras Kovalenko + dotnet;msbuild;build;props;packages;centralized;configuration;directory-build-props;sdk;cli;tool;nuget;CPM;centralised + CentralConfigGenerator + MIT + https://github.com/TarasKovalenko/CentralConfigGenerator + https://github.com/TarasKovalenko/CentralConfigGenerator.git + git + false + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CentralConfigGenerator/Commands/BuildPropsCommand.cs b/CentralConfigGenerator/Commands/BuildPropsCommand.cs index 34cbd2a..115c1bd 100644 --- a/CentralConfigGenerator/Commands/BuildPropsCommand.cs +++ b/CentralConfigGenerator/Commands/BuildPropsCommand.cs @@ -1,6 +1,8 @@ using System.Xml.Linq; using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Generators; +using CentralConfigGenerator.Core.Generators.Abstractions; using CentralConfigGenerator.Extensions; using CentralConfigGenerator.Services.Abstractions; @@ -15,13 +17,18 @@ IFileService fileService { public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite) { - MsgLogger.LogInformation("Generating Directory.Build.props for directory: {0}", directory.FullName); + MsgLogger.LogInformation( + "Generating Directory.Build.props for directory: {0}", + directory.FullName + ); var targetPath = Path.Combine(directory.FullName, "Directory.Build.props"); if (fileService.Exists(targetPath) && !overwrite) { - MsgLogger.LogWarning("File Directory.Build.props already exists. Use --overwrite to replace it."); + MsgLogger.LogWarning( + "File Directory.Build.props already exists. Use --overwrite to replace it." + ); return; } @@ -65,7 +72,7 @@ public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite) { // Find all property elements with the specified name var propertyElements = xDoc.Descendants(property).ToList(); - + foreach (var element in propertyElements) { element.Remove(); @@ -85,4 +92,4 @@ public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite) } } } -} \ No newline at end of file +} diff --git a/CentralConfigGenerator/Commands/EnhancedPackagesPropsCommand.cs b/CentralConfigGenerator/Commands/EnhancedPackagesPropsCommand.cs new file mode 100644 index 0000000..044758e --- /dev/null +++ b/CentralConfigGenerator/Commands/EnhancedPackagesPropsCommand.cs @@ -0,0 +1,154 @@ +using System.Xml.Linq; +using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; +using CentralConfigGenerator.Core.Generators; +using CentralConfigGenerator.Core.Generators.Abstractions; +using CentralConfigGenerator.Core.Services; +using CentralConfigGenerator.Core.Services.Abstractions; +using CentralConfigGenerator.Services.Abstractions; +using Spectre.Console; + +namespace CentralConfigGenerator.Commands; + +public class EnhancedPackagesPropsCommand( + IEnhancedPackageAnalyzer packageAnalyzer, + IProjectFileService projectFileService, + IPackagesPropsGenerator packagesPropsGenerator, + IFileService fileService, + IVersionConflictVisualizer conflictVisualizer +) +{ + public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite, bool verbose = false) + { + AnsiConsole + .Status() + .Start( + "Scanning for project files...", + ctx => + { + ctx.Spinner(Spinner.Known.Star); + ctx.SpinnerStyle(Style.Parse("green")); + } + ); + + var targetPath = Path.Combine(directory.FullName, "Directory.Packages.props"); + + if (fileService.Exists(targetPath) && !overwrite) + { + AnsiConsole.MarkupLine( + "[yellow]Warning:[/] File Directory.Packages.props already exists. Use --overwrite to replace it." + ); + return; + } + + var projectFiles = await projectFileService.ScanDirectoryForProjectsAsync(directory); + + if (projectFiles.Count == 0) + { + AnsiConsole.MarkupLine( + "[yellow]Warning:[/] No .csproj files found in the directory tree." + ); + return; + } + + AnsiConsole.MarkupLine($"[green]Found {projectFiles.Count} project files[/]"); + + // Analyze packages with enhanced analyzer + var analysisResult = await AnsiConsole + .Status() + .StartAsync( + "Analyzing package versions...", + async ctx => + { + ctx.Spinner(Spinner.Known.Star); + ctx.SpinnerStyle(Style.Parse("green")); + return await packageAnalyzer.AnalyzePackagesAsync(projectFiles); + } + ); + + // Display analysis results + conflictVisualizer.DisplayResults(analysisResult); + + // Ask for confirmation if conflicts exist + if (analysisResult.Conflicts.Any()) + { + if ( + !AnsiConsole.Confirm( + "Version conflicts were detected. Continue with resolved versions?" + ) + ) + { + AnsiConsole.MarkupLine("[red]Operation cancelled by user.[/]"); + return; + } + } + + // Generate Directory.Packages.props + var packagesPropsContent = packagesPropsGenerator.GeneratePackagesPropsContent( + analysisResult.ResolvedVersions + ); + await fileService.WriteAllTextAsync(targetPath, packagesPropsContent); + + AnsiConsole.MarkupLine($"[green]Created Directory.Packages.props at {targetPath}[/]"); + + // Update project files + var updateConfirmed = AnsiConsole.Confirm("Remove version attributes from project files?"); + if (!updateConfirmed) + { + AnsiConsole.MarkupLine( + "[yellow]Skipping project file updates. You'll need to manually remove Version attributes.[/]" + ); + return; + } + + await AnsiConsole + .Progress() + .StartAsync(async ctx => + { + var task = ctx.AddTask( + "[green]Updating project files[/]", + maxValue: projectFiles.Count + ); + + foreach (var projectFile in projectFiles) + { + try + { + var xDoc = XDocument.Parse(projectFile.Content); + var changed = false; + + var packageReferences = xDoc.Descendants("PackageReference").ToList(); + + foreach (var packageRef in packageReferences) + { + var versionAttr = packageRef.Attribute("Version"); + if (versionAttr != null) + { + versionAttr.Remove(); + changed = true; + } + } + + if (changed) + { + await fileService.WriteAllTextAsync(projectFile.Path, xDoc.ToString()); + if (verbose) + { + AnsiConsole.MarkupLine($"[dim]Updated: {projectFile.Path}[/]"); + } + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine( + $"[red]Error updating {projectFile.Path}: {ex.Message}[/]" + ); + } + + task.Increment(1); + } + }); + + AnsiConsole.MarkupLine("[green]Successfully updated all project files![/]"); + } +} diff --git a/CentralConfigGenerator/Commands/PackagesPropsCommand.cs b/CentralConfigGenerator/Commands/PackagesPropsCommand.cs index 8a17313..65d7970 100644 --- a/CentralConfigGenerator/Commands/PackagesPropsCommand.cs +++ b/CentralConfigGenerator/Commands/PackagesPropsCommand.cs @@ -1,6 +1,8 @@ using System.Xml.Linq; using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Generators; +using CentralConfigGenerator.Core.Generators.Abstractions; using CentralConfigGenerator.Extensions; using CentralConfigGenerator.Services.Abstractions; @@ -15,14 +17,18 @@ IFileService fileService { public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite) { - MsgLogger.LogInformation("Generating Directory.Packages.props for directory: {0}", - directory.FullName); + MsgLogger.LogInformation( + "Generating Directory.Packages.props for directory: {0}", + directory.FullName + ); var targetPath = Path.Combine(directory.FullName, "Directory.Packages.props"); if (fileService.Exists(targetPath) && !overwrite) { - MsgLogger.LogWarning("File Directory.Packages.props already exists. Use --overwrite to replace it."); + MsgLogger.LogWarning( + "File Directory.Packages.props already exists. Use --overwrite to replace it." + ); return; } @@ -44,7 +50,9 @@ public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite) MsgLogger.LogDebug("Package: {0} = {1}", package.Key, package.Value); } - var packagesPropsContent = packagesPropsGenerator.GeneratePackagesPropsContent(packageVersions); + var packagesPropsContent = packagesPropsGenerator.GeneratePackagesPropsContent( + packageVersions + ); await fileService.WriteAllTextAsync(targetPath, packagesPropsContent); @@ -74,15 +82,20 @@ public async Task ExecuteAsync(DirectoryInfo directory, bool overwrite) if (changed) { await fileService.WriteAllTextAsync(projectFile.Path, xDoc.ToString()); - MsgLogger.LogInformation("Updated package references in project file: {0}", - projectFile.Path); + MsgLogger.LogInformation( + "Updated package references in project file: {0}", + projectFile.Path + ); } } catch (Exception ex) { - MsgLogger.LogError(ex, "Error updating package references in project file: {0}", - projectFile.Path); + MsgLogger.LogError( + ex, + "Error updating package references in project file: {0}", + projectFile.Path + ); } } } -} \ No newline at end of file +} diff --git a/CentralConfigGenerator/Extensions/MsgLogger.cs b/CentralConfigGenerator/Extensions/MsgLogger.cs index 283aa32..c334a5b 100644 --- a/CentralConfigGenerator/Extensions/MsgLogger.cs +++ b/CentralConfigGenerator/Extensions/MsgLogger.cs @@ -25,4 +25,4 @@ public static void LogError(Exception exception, string message, params object[] LogError(message, args); AnsiConsole.WriteException(exception, ExceptionFormats.ShortenEverything); } -} \ No newline at end of file +} diff --git a/CentralConfigGenerator/Program.cs b/CentralConfigGenerator/Program.cs index c224504..1e7e9b4 100644 --- a/CentralConfigGenerator/Program.cs +++ b/CentralConfigGenerator/Program.cs @@ -1,7 +1,11 @@ -using System.CommandLine; +using System.CommandLine; using CentralConfigGenerator.Commands; using CentralConfigGenerator.Core.Analyzers; +using CentralConfigGenerator.Core.Analyzers.Abstractions; using CentralConfigGenerator.Core.Generators; +using CentralConfigGenerator.Core.Generators.Abstractions; +using CentralConfigGenerator.Core.Services; +using CentralConfigGenerator.Core.Services.Abstractions; using CentralConfigGenerator.Services; using CentralConfigGenerator.Services.Abstractions; using Microsoft.Extensions.DependencyInjection; @@ -16,12 +20,19 @@ static async Task Main(string[] args) var rootCommand = new RootCommand { - Description = "A tool to generate centralized configuration files for .NET projects" + Description = "A tool to generate centralized configuration files for .NET projects", }; var buildCommand = new Command("build", "Generate Directory.Build.props file"); var packagesCommand = new Command("packages", "Generate Directory.Packages.props file"); - var allCommand = new Command("all", "Generate both Directory.Build.props and Directory.Packages.props files"); + var packagesEnhancedCommand = new Command( + "packages-enhanced", + "Generate Directory.Packages.props file with enhanced version analysis" + ); + var allCommand = new Command( + "all", + "Generate both Directory.Build.props and Directory.Packages.props files" + ); var directoryOption = new Option( ["--directory", "-d"], @@ -49,40 +60,73 @@ static async Task Main(string[] args) packagesCommand.AddOption(overwriteOption); packagesCommand.AddOption(verboseOption); + packagesEnhancedCommand.AddOption(directoryOption); + packagesEnhancedCommand.AddOption(overwriteOption); + packagesEnhancedCommand.AddOption(verboseOption); + allCommand.AddOption(directoryOption); allCommand.AddOption(overwriteOption); allCommand.AddOption(verboseOption); - buildCommand.SetHandler(async (directory, overwrite, _) => - { - var command = services.GetRequiredService(); - ArgumentNullException.ThrowIfNull(command); - - await command.ExecuteAsync(directory, overwrite); - }, directoryOption, overwriteOption, verboseOption); - - packagesCommand.SetHandler(async (directory, overwrite, _) => - { - var command = services.GetRequiredService(); - ArgumentNullException.ThrowIfNull(command); - - await command.ExecuteAsync(directory, overwrite); - }, directoryOption, overwriteOption, verboseOption); + buildCommand.SetHandler( + async (directory, overwrite, _) => + { + var command = services.GetRequiredService(); + ArgumentNullException.ThrowIfNull(command); + + await command.ExecuteAsync(directory, overwrite); + }, + directoryOption, + overwriteOption, + verboseOption + ); - allCommand.SetHandler(async (directory, overwrite, _) => - { - var buildPropsCommand = services.GetRequiredService(); - ArgumentNullException.ThrowIfNull(buildPropsCommand); + packagesCommand.SetHandler( + async (directory, overwrite, _) => + { + var command = services.GetRequiredService(); + ArgumentNullException.ThrowIfNull(command); + + await command.ExecuteAsync(directory, overwrite); + }, + directoryOption, + overwriteOption, + verboseOption + ); - var packagesPropsCommand = services.GetRequiredService(); - ArgumentNullException.ThrowIfNull(packagesPropsCommand); + packagesEnhancedCommand.SetHandler( + async (directory, overwrite, verbose) => + { + var command = services.GetRequiredService(); + ArgumentNullException.ThrowIfNull(command); + + await command.ExecuteAsync(directory, overwrite, verbose); + }, + directoryOption, + overwriteOption, + verboseOption + ); - await buildPropsCommand.ExecuteAsync(directory, overwrite); - await packagesPropsCommand.ExecuteAsync(directory, overwrite); - }, directoryOption, overwriteOption, verboseOption); + allCommand.SetHandler( + async (directory, overwrite, _) => + { + var buildPropsCommand = services.GetRequiredService(); + ArgumentNullException.ThrowIfNull(buildPropsCommand); + + var packagesPropsCommand = services.GetRequiredService(); + ArgumentNullException.ThrowIfNull(packagesPropsCommand); + + await buildPropsCommand.ExecuteAsync(directory, overwrite); + await packagesPropsCommand.ExecuteAsync(directory, overwrite); + }, + directoryOption, + overwriteOption, + verboseOption + ); rootCommand.AddCommand(buildCommand); rootCommand.AddCommand(packagesCommand); + rootCommand.AddCommand(packagesEnhancedCommand); rootCommand.AddCommand(allCommand); return await rootCommand.InvokeAsync(args); @@ -92,17 +136,27 @@ public static ServiceProvider ConfigureServices() { var services = new ServiceCollection(); + // Original services services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + // Enhanced services + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // Commands services.AddTransient(); services.AddTransient(); + services.AddTransient(); + // Common services services.AddSingleton(); services.AddSingleton(); return services.BuildServiceProvider(); } -} \ No newline at end of file +} diff --git a/CentralConfigGenerator/Services/Abstractions/IFileService.cs b/CentralConfigGenerator/Services/Abstractions/IFileService.cs index 5c72e29..8163441 100644 --- a/CentralConfigGenerator/Services/Abstractions/IFileService.cs +++ b/CentralConfigGenerator/Services/Abstractions/IFileService.cs @@ -3,6 +3,8 @@ public interface IFileService { bool Exists(string path); + Task ReadAllTextAsync(string path); + Task WriteAllTextAsync(string path, string contents); } \ No newline at end of file diff --git a/CentralConfigGenerator/docs/README.md b/CentralConfigGenerator/docs/README.md deleted file mode 100644 index 4d8256f..0000000 --- a/CentralConfigGenerator/docs/README.md +++ /dev/null @@ -1,190 +0,0 @@ -# CentralConfigGenerator - -[![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://taraskovalenko.github.io/) -[![build](https://github.com/TarasKovalenko/CentralConfigGenerator/actions/workflows/dotnet.yml/badge.svg)](https://github.com/TarasKovalenko/CentralConfigGenerator/actions) -[![CentralConfigGenerator NuGet current](https://img.shields.io/nuget/v/CentralConfigGenerator?label=CentralConfigGenerator)](https://www.nuget.org/packages/CentralConfigGenerator/) - -## Goals -A modern .NET tool for automatically generating centralized configuration files for .NET projects. CentralConfig analyzes your solution structure and creates properly configured `Directory.Build.props` and `Directory.Packages.props` files to standardize settings across your projects. - -## Terms of use - -By using this project or its source code, for any purpose and in any shape or form, you grant your **implicit agreement** to all of the following statements: - -- You unequivocally condemn Russia and its military aggression against Ukraine -- You recognize that Russia is an occupant that unlawfully invaded a sovereign state -- You agree that [Russia is a terrorist state](https://www.europarl.europa.eu/doceo/document/RC-9-2022-0482_EN.html) -- You fully support Ukraine's territorial integrity, including its claims over [temporarily occupied territories](https://en.wikipedia.org/wiki/Russian-occupied_territories_of_Ukraine) -- You reject false narratives perpetuated by Russian state propaganda - -To learn more about the war and how you can help, [click here](https://war.ukraine.ua/). Glory to Ukraine! 🇺🇦 - -## Overview - -CentralConfigGenerator helps you maintain consistent configuration across multiple .NET projects by: - -1. Automatically generating `Directory.Build.props` files with common project properties -2. Automatically generating `Directory.Packages.props` files with centralized package versions -3. Updating your project files to use these centralized configurations - -## Installation - -```bash -dotnet tool install --global CentralConfigGenerator -``` - -## Usage - -### Basic Commands - -CentralConfigGenerator provides three main commands: - -```bash -# Generate Directory.Build.props file with common project properties -central-config build [options] - -# Generate Directory.Packages.props file for centralized package versions -central-config packages [options] - -# Generate both files in one command -central-config all [options] -``` - -### Command Options - -All commands support the following options: - -- `-d, --directory `: Specify the directory to scan (defaults to current directory) -- `-o, --overwrite`: Overwrite existing files (off by default) -- `-v, --verbose`: Enable verbose logging - -### Examples - -#### Generate Directory.Build.props - -```bash -# Generate Directory.Build.props in the current directory -central-config build - -# Generate in a specific directory and overwrite if exists -central-config build -d C:\Projects\MySolution -o -``` - -#### Generate Directory.Packages.props - -```bash -# Generate Directory.Packages.props in the current directory -central-config packages - -# Generate in a specific directory with verbose logging -central-config packages -d C:\Projects\MySolution -v -``` - -#### Generate Both Files - -```bash -# Generate both files in one command -central-config all - -# Generate both files with all options -central-config all -d C:\Projects\MySolution -o -v -``` - -## How It Works - -### Directory.Build.props Generation - -The `build` command: - -1. Scans all `.csproj` files in the specified directory and subdirectories -2. Identifies common properties that appear in multiple projects -3. Extracts these properties into a `Directory.Build.props` file -4. Removes the extracted properties from individual project files - -By default, CentralConfigGenerator will focus on the following key properties: -- `TargetFramework` -- `ImplicitUsings` -- `Nullable` - -### Directory.Packages.props Generation - -The `packages` command: - -1. Scans all `.csproj` files in the specified directory and subdirectories -2. Extracts all package references and their versions -3. For each package, uses the highest version found across all projects -4. Generates a `Directory.Packages.props` file with these package versions -5. Removes version attributes from `PackageReference` elements in project files - -### Understanding the Generated Files - -#### Directory.Build.props - -```xml - - - net9.0 - enable - enable - - -``` - -#### Directory.Packages.props - -```xml - - - true - - - - - - - - -``` - -## Benefits - -- **Consistent Configuration**: Ensure all projects use the same framework versions, language features, and code quality settings -- **Simplified Updates**: Update package versions or project settings in a single location -- **Reduced Duplication**: Remove redundant configuration from individual project files -- **Improved Maintainability**: Make your solution more maintainable by centralizing common settings - -## Common Scenarios - -### Migrating Existing Solutions - -For existing solutions with many projects, use CentralConfigGenerator to centralize configuration: - -```bash -# Navigate to the solution root -cd MySolution - -# Generate both configuration files with verbose output -central-config all -v -``` - -## Limitations - -- Projects with highly customized or conflicting settings may require manual adjustment after running CentralConfigGenerator -- For properties to be included in Directory.Build.props, they must appear with identical values in most projects -- Version conflicts in packages will be resolved by selecting the highest version found - -## Local installation - -To install the tool locally for development or testing, clone the repository and run: - -```bash -dotnet tool install --global --add-source .\CentralConfigGenerator\nupkg\ CentralConfigGenerator -``` - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/Directory.Packages.props b/Directory.Packages.props index 8a1c14e..0a689d9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,12 +7,17 @@ + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + \ No newline at end of file diff --git a/QUICK_START_ENHANCED.md b/QUICK_START_ENHANCED.md new file mode 100644 index 0000000..a5a1764 --- /dev/null +++ b/QUICK_START_ENHANCED.md @@ -0,0 +1,170 @@ +# Quick Start Guide: Enhanced Package Analysis + +This guide will help you get started with the enhanced package analysis features of CentralConfigGenerator. + +## Prerequisites + +- .NET SDK 9.0 or later +- CentralConfigGenerator installed globally + +## Basic Usage + +### 1. Analyze Your Solution + +Navigate to your solution directory and run: + +```bash +central-config packages-enhanced -v +``` + +This will: +- Scan all project files +- Detect version conflicts +- Show a visual analysis report +- Ask for confirmation before making changes + +### 2. Understanding the Output + +You'll see a comprehensive report like this: + +``` +Package Analysis Summary +┌─────────────────────────┬─────────┐ +│ Metric │ Value │ +├─────────────────────────┼─────────┤ +│ Total Packages │ 15 │ +│ Packages with Conflicts │ 3 │ +│ Warnings │ 4 │ +└─────────────────────────┴─────────┘ + +Version Conflicts Detected: +┌─────────────────────┬──────────────────┬─────────────┬─────────────┐ +│ Package │ Project │ Version │ Type │ +├─────────────────────┼──────────────────┼─────────────┼─────────────┤ +│ Newtonsoft.Json │ Web.csproj │ 11.0.2 │ Release │ +│ Newtonsoft.Json │ Core.csproj │ 13.0.3 │ Release │ +│ │ │ │ │ +│ Serilog │ Web.csproj │ 2.10.0 │ Release │ +│ Serilog │ Tests.csproj │ 3.0.1 │ Release │ +└─────────────────────┴──────────────────┴─────────────┴─────────────┘ +``` + +### 3. Conflict Resolution + +The tool will automatically resolve conflicts using the highest version strategy. You'll be asked to confirm: + +``` +Version conflicts were detected. Continue with resolved versions? [y/n] (y): +``` + +### 4. Review Changes + +After confirmation, the tool will: +1. Create a `Directory.Packages.props` file +2. Remove version attributes from project files +3. Show a summary of changes + +## Advanced Features + +### Check Specific Directory + +```bash +central-config packages-enhanced -d ./src/MyProjects -v +``` + +### Force Overwrite + +```bash +central-config packages-enhanced --overwrite +``` + +### Dry Run (Coming Soon) + +```bash +central-config packages-enhanced --dry-run +``` + +## Common Scenarios + +### Scenario 1: Mixed Pre-release and Stable Versions + +If you have both pre-release and stable versions: + +```xml + + + + + +``` + +Result: The stable version `1.0.0` will be selected. + +### Scenario 2: Version Ranges + +When projects use version ranges: + +```xml + + + + + +``` + +Result: The range `[1.0.0,2.0.0)` will be preserved as it encompasses the specific version. + +### Scenario 3: Outdated Packages + +The tool will warn about significantly outdated packages: + +``` +Warnings: +┌─────────┬─────────────────────┬─────────────────────────────────────────────┐ +│ Level │ Package │ Message │ +├─────────┼─────────────────────┼─────────────────────────────────────────────┤ +│ Warning │ EntityFramework │ This version is significantly outdated │ +└─────────┴─────────────────────┴─────────────────────────────────────────────┘ +``` + +## Troubleshooting + +### Issue: Variable Versions + +If you have versions like `$(MyVersion)`: + +```xml + +``` + +These will be preserved as-is in the centralized configuration. + +### Issue: Conflicting Pre-release Versions + +When multiple pre-release versions conflict: + +``` +MyPackage 1.0.0-alpha.1 +MyPackage 1.0.0-beta.1 +``` + +The tool correctly identifies that `beta.1` is newer than `alpha.1`. + +## Best Practices + +1. **Review Before Confirming**: Always review the conflict resolution before accepting +2. **Test After Migration**: Run your tests after centralizing package versions +3. **Use Stable Versions**: Prefer stable versions over pre-release in production +4. **Regular Updates**: Run the analysis periodically to keep versions consistent + +## Next Steps + +- Learn about [version management details](VERSION_MANAGEMENT.md) +- Read the [full documentation](README.md) + +## Getting Help + +If you encounter issues: + +1. Run with verbose logging: `central-config packages-enhanced -v` +2. Open an issue on [GitHub](https://github.com/TarasKovalenko/CentralConfigGenerator/issues) diff --git a/README.md b/README.md index 4d8256f..d749389 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ CentralConfigGenerator helps you maintain consistent configuration across multip 1. Automatically generating `Directory.Build.props` files with common project properties 2. Automatically generating `Directory.Packages.props` files with centralized package versions -3. Updating your project files to use these centralized configurations +3. Providing advanced version conflict resolution using NuGet.Versioning +4. Offering compatibility checking and visual analysis of version conflicts +5. Updating your project files to use these centralized configurations ## Installation @@ -37,7 +39,7 @@ dotnet tool install --global CentralConfigGenerator ### Basic Commands -CentralConfigGenerator provides three main commands: +CentralConfigGenerator provides four main commands: ```bash # Generate Directory.Build.props file with common project properties @@ -46,6 +48,9 @@ central-config build [options] # Generate Directory.Packages.props file for centralized package versions central-config packages [options] +# Generate Directory.Packages.props with enhanced version analysis +central-config packages-enhanced [options] + # Generate both files in one command central-config all [options] ``` @@ -80,14 +85,18 @@ central-config packages central-config packages -d C:\Projects\MySolution -v ``` -#### Generate Both Files +#### Enhanced Package Analysis ```bash -# Generate both files in one command -central-config all - -# Generate both files with all options -central-config all -d C:\Projects\MySolution -o -v +# Use enhanced package analysis with visual conflict resolution +central-config packages-enhanced -d C:\Projects\MySolution -v + +# This will: +# - Detect version conflicts across projects +# - Show visual analysis of conflicts +# - Suggest resolutions based on semantic versioning +# - Check for compatibility issues +# - Ask for confirmation before proceeding ``` ## How It Works @@ -116,6 +125,22 @@ The `packages` command: 4. Generates a `Directory.Packages.props` file with these package versions 5. Removes version attributes from `PackageReference` elements in project files +### Enhanced Package Analysis + +The `packages-enhanced` command provides advanced features: + +1. **Semantic Version Analysis**: Uses NuGet.Versioning for accurate version comparisons +2. **Conflict Detection**: Identifies and visualizes version conflicts across projects +3. **Version Range Support**: Handles version ranges (e.g., `[1.0.0,2.0.0)`) +4. **Pre-release Detection**: Warns about pre-release packages in production code +5. **Compatibility Checking**: Identifies known issues with specific package versions +6. **Visual Reports**: Provides clear, color-coded reports of analysis results +7. **Multiple Resolution Strategies**: Offers different approaches to resolve conflicts: + - Highest version (default) + - Lowest version + - Most common version + - Manual resolution + ### Understanding the Generated Files #### Directory.Build.props @@ -146,12 +171,64 @@ The `packages` command: ``` +## Enhanced Features + +### Version Conflict Resolution + +When multiple projects reference the same package with different versions, the enhanced analyzer: + +1. Detects all version conflicts +2. Shows a detailed conflict report +3. Applies resolution strategy (highest version by default) +4. Asks for confirmation before proceeding + +Example output: +``` +Package Analysis Summary +┌─────────────────────────┬─────────┐ +│ Metric │ Value │ +├─────────────────────────┼─────────┤ +│ Total Packages │ 12 │ +│ Packages with Conflicts │ 3 │ +│ Warnings │ 5 │ +└─────────────────────────┴─────────┘ + +Version Conflicts Detected: +┌─────────────────────┬──────────────────┬─────────────┬─────────────┐ +│ Package │ Project │ Version │ Type │ +├─────────────────────┼──────────────────┼─────────────┼─────────────┤ +│ Newtonsoft.Json │ Project1.csproj │ 11.0.2 │ Release │ +│ Newtonsoft.Json │ Project2.csproj │ 13.0.3 │ Release │ +└─────────────────────┴──────────────────┴─────────────┴─────────────┘ +``` + +### Semantic Versioning Support + +The enhanced analyzer properly handles: + +- Pre-release versions (e.g., `1.0.0-beta.1`) +- Build metadata (e.g., `1.0.0+build.123`) +- Version ranges (e.g., `[1.0.0,2.0.0)`) +- Floating versions (e.g., `1.0.*`) + +### Compatibility Warnings + +The tool can warn about: + +- Known security vulnerabilities in specific versions +- Performance issues in certain package versions +- Significantly outdated packages +- Pre-release packages in production code + ## Benefits - **Consistent Configuration**: Ensure all projects use the same framework versions, language features, and code quality settings - **Simplified Updates**: Update package versions or project settings in a single location - **Reduced Duplication**: Remove redundant configuration from individual project files - **Improved Maintainability**: Make your solution more maintainable by centralizing common settings +- **Version Conflict Resolution**: Automatically detect and resolve package version conflicts +- **Better Version Management**: Use semantic versioning for accurate version comparisons +- **Visual Analysis**: See clear, color-coded reports of package analysis results ## Common Scenarios @@ -167,11 +244,27 @@ cd MySolution central-config all -v ``` +### Resolving Version Conflicts + +When you have multiple projects with conflicting package versions: + +```bash +# Use enhanced package analysis to detect and resolve conflicts +central-config packages-enhanced -v + +# The tool will: +# 1. Show all version conflicts +# 2. Propose resolutions +# 3. Ask for confirmation +# 4. Update all project files +``` + ## Limitations - Projects with highly customized or conflicting settings may require manual adjustment after running CentralConfigGenerator - For properties to be included in Directory.Build.props, they must appear with identical values in most projects -- Version conflicts in packages will be resolved by selecting the highest version found +- Version conflicts in packages will be resolved by selecting the highest version found (configurable in enhanced mode) +- Some version formats (like variables) cannot be automatically resolved ## Local installation diff --git a/VERSION_MANAGEMENT.md b/VERSION_MANAGEMENT.md new file mode 100644 index 0000000..8ba89c6 --- /dev/null +++ b/VERSION_MANAGEMENT.md @@ -0,0 +1,200 @@ +# Advanced Version Management with NuGet.Versioning + +This document explains how CentralConfigGenerator uses NuGet.Versioning to provide advanced version management capabilities. + +## Overview + +CentralConfigGenerator uses the `NuGet.Versioning` package to provide sophisticated version handling that goes beyond basic .NET version comparison. This integration enables proper semantic versioning support, version range handling, and accurate package version conflict resolution. + +## Key Features + +### 1. Semantic Versioning Support + +The tool properly handles semantic versioning (SemVer) including: + +- Major.Minor.Patch versions (e.g., `1.2.3`) +- Pre-release tags (e.g., `1.0.0-beta.1`, `2.0.0-rc.2`) +- Build metadata (e.g., `1.0.0+build.123`) + +#### Example: +```xml + + + + +``` + +### 2. Version Range Handling + +Supports NuGet's version range syntax: + +- Exact version: `1.0.0` +- Minimum version: `1.0.0` +- Exact range: `[1.0.0]` +- Minimum inclusive: `[1.0.0,)` +- Maximum inclusive: `(,1.0.0]` +- Range: `[1.0.0,2.0.0)` +- Floating version: `1.0.*` + +#### Example: +```xml + + + + +``` + +### 3. Conflict Resolution Strategies + +The enhanced package analyzer provides multiple strategies for resolving version conflicts: + +1. **Highest Version (Default)**: Selects the highest version among all referenced versions +2. **Lowest Version**: Selects the lowest version (useful for maximum compatibility) +3. **Most Common**: Selects the version used by most projects +4. **Manual**: Prompts for manual intervention when automatic resolution isn't suitable + +#### Example: +```csharp +// Using the version conflict resolver +var resolver = new VersionConflictResolver(); +var resolvedVersion = resolver.Resolve( + "Newtonsoft.Json", + new[] { "11.0.2", "12.0.3", "13.0.1" }, + VersionResolutionStrategy.Highest +); +// Result: "13.0.1" +``` + +### 4. Compatibility Checking + +The tool can check for known issues with specific package versions: + +- Security vulnerabilities +- Performance regressions +- Breaking changes +- Deprecated versions + +#### Example output: +``` +Warnings: +┌─────────┬─────────────────────┬─────────────────────────────────────────────┐ +│ Level │ Package │ Message │ +├─────────┼─────────────────────┼─────────────────────────────────────────────┤ +│ Warning │ Newtonsoft.Json │ Known security vulnerability in version 9.x │ +│ Info │ System.Text.Json │ Pre-release version detected │ +│ Warning │ EntityFramework │ This version is significantly outdated │ +└─────────┴─────────────────────┴─────────────────────────────────────────────┘ +``` + +## Implementation Details + +### Version Parsing + +The tool uses `NuGetVersion.TryParse()` instead of the basic `Version.TryParse()`: + +```csharp +// Old approach (limited) +if (Version.TryParse(versionStr, out var version)) +{ + // Only handles numeric versions like "1.0.0" +} + +// New approach (comprehensive) +if (NuGetVersion.TryParse(versionStr, out var nugetVersion)) +{ + // Handles "1.0.0-beta.1+build.123" and more +} +``` + +### Version Comparison + +NuGet.Versioning provides accurate version comparison following SemVer rules: + +```csharp +var v1 = NuGetVersion.Parse("1.0.0-alpha"); +var v2 = NuGetVersion.Parse("1.0.0-beta"); +var v3 = NuGetVersion.Parse("1.0.0"); + +// Correct ordering: alpha < beta < release +Console.WriteLine(v1 < v2); // True +Console.WriteLine(v2 < v3); // True +``` + +### Handling Special Cases + +The tool handles various special version formats: + +1. **Variables**: `$(VersionPrefix)$(VersionSuffix)` +2. **Properties**: `$(MyPackageVersion)` +3. **Wildcards**: `1.0.*` +4. **Floating**: `1.*` + +These are preserved in the output when they cannot be resolved to specific versions. + +## Visual Reporting + +The enhanced package analyzer provides detailed visual reports using Spectre.Console: + +``` +Package Analysis Summary +┌─────────────────────────┬─────────┐ +│ Metric │ Value │ +├─────────────────────────┼─────────┤ +│ Total Packages │ 25 │ +│ Packages with Conflicts │ 5 │ +│ Warnings │ 8 │ +└─────────────────────────┴─────────┘ + +Resolved Package Versions: +┌─────────────────────────────┬─────────────────┐ +│ Package │ Version │ +├─────────────────────────────┼─────────────────┤ +│ Microsoft.Extensions.Logging│ 8.0.0 │ +│ Newtonsoft.Json │ 13.0.3 │ +│ NuGet.Versioning │ 6.7.0 │ +└─────────────────────────────┴─────────────────┘ +``` + +## Best Practices + +1. **Use Semantic Versioning**: Follow SemVer conventions for your package versions +2. **Avoid Floating Versions**: Use specific versions in production code +3. **Review Conflicts**: Always review detected conflicts before accepting resolutions +4. **Check Compatibility**: Pay attention to compatibility warnings +5. **Test After Migration**: Test your solution after centralizing package versions + +## Troubleshooting + +### Common Issues + +1. **Unparseable Versions**: Some version formats (like variables) cannot be parsed + - Solution: These are preserved as-is in the output + +2. **Complex Version Ranges**: Overlapping ranges might cause unexpected resolutions + - Solution: Review the resolution and adjust manually if needed + +3. **Pre-release Dependencies**: Pre-release packages in production code + - Solution: The tool warns about these; consider using stable versions + +### Debug Information + +Run with verbose logging to see detailed version analysis: + +```bash +central-config packages-enhanced -v +``` + +This will show: +- Each version comparison +- Resolution decisions +- Parsing failures +- Compatibility check results + +## API Reference + +### Key Classes + +1. `EnhancedPackageAnalyzer`: Main analyzer with conflict detection +2. `VersionConflictResolver`: Resolves version conflicts using various strategies +3. `VersionCompatibilityChecker`: Checks for known version issues +4. `VersionConflictVisualizer`: Creates visual reports of analysis results