From 576d49bf871d8a8aeb88f61ea0539e96f2407665 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Fri, 12 Apr 2024 12:57:26 -0700 Subject: [PATCH 01/13] Add analyze command to NuGetUpdater --- .../Commands/AnalyzeCommand.cs | 35 ++ .../Commands/DiscoverCommand.cs | 6 +- .../Analyze/AnalyzeWorkerTestBase.cs | 0 .../Analyze/AnalysisResult.cs | 11 + .../Analyze/AnalyzeWorker.cs | 245 ++++++++++ .../Analyze/CompatabilityChecker.cs | 146 ++++++ .../Analyze/DependencyFinder.cs | 45 ++ .../Analyze/DependencyInfo.cs | 12 + .../NuGetUpdater.Core/Analyze/NuGetContext.cs | 41 ++ .../NuGetUpdater.Core/Analyze/Requirement.cs | 99 ++++ .../Analyze/RequirementConverter.cs | 17 + .../Analyze/SecurityVulnerability.cs | 11 + .../SecurityVulnerabilityExtensions.cs | 36 ++ .../Analyze/VersionFinder.cs | 66 +++ .../Analyze/VersionResult.cs | 36 ++ .../nuget/analysis/analysis_json_reader.rb | 63 +++ .../nuget/analysis/dependency_analysis.rb | 0 .../nuget/discovery/discovery_json_reader.rb | 8 +- .../dependabot/nuget/http_response_helpers.rb | 19 - nuget/lib/dependabot/nuget/native_helpers.rb | 56 +++ nuget/lib/dependabot/nuget/nuget_client.rb | 223 --------- nuget/lib/dependabot/nuget/update_checker.rb | 202 ++------ .../update_checker/compatibility_checker.rb | 116 ----- .../nuget/update_checker/dependency_finder.rb | 292 ------------ .../nuget/update_checker/nupkg_fetcher.rb | 221 --------- .../nuget/update_checker/nuspec_fetcher.rb | 110 ----- .../nuget/update_checker/property_updater.rb | 195 -------- .../nuget/update_checker/repository_finder.rb | 442 ----------------- .../update_checker/requirements_updater.rb | 21 +- .../nuget/update_checker/tfm_comparer.rb | 34 -- .../nuget/update_checker/tfm_finder.rb | 30 -- .../nuget/update_checker/version_finder.rb | 450 ------------------ 32 files changed, 980 insertions(+), 2308 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs create mode 100644 nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb create mode 100644 nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb delete mode 100644 nuget/lib/dependabot/nuget/http_response_helpers.rb delete mode 100644 nuget/lib/dependabot/nuget/nuget_client.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/property_updater.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/repository_finder.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb delete mode 100644 nuget/lib/dependabot/nuget/update_checker/version_finder.rb diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs new file mode 100644 index 00000000000..e6129eb6067 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs @@ -0,0 +1,35 @@ +using System.CommandLine; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Cli.Commands; + +internal static class AnalyzeCommand +{ + internal static readonly Option DependencyFilePathOption = new("--dependency-file-path") { IsRequired = true }; + internal static readonly Option DiscoveryFilePathOption = new("--discovery-file-path") { IsRequired = true }; + internal static readonly Option AnalysisFolderOption = new("--analysis-folder-path") { IsRequired = true }; + internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); + + internal static Command GetCommand(Action setExitCode) + { + Command command = new("analyze", "Determines how to update a dependency based on the workspace discovery information.") + { + DependencyFilePathOption, + DiscoveryFilePathOption, + AnalysisFolderOption, + VerboseOption + }; + + command.TreatUnmatchedTokensAsErrors = true; + + command.SetHandler(async (discoveryPath, dependencyPath, analysisDirectory, verbose) => + { + var worker = new AnalyzeWorker(new Logger(verbose)); + await worker.RunAsync(discoveryPath.FullName, dependencyPath.FullName, analysisDirectory.FullName); + }, DiscoveryFilePathOption, DependencyFilePathOption, AnalysisFolderOption, VerboseOption); + + return command; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index c3c371937c2..a88b46356de 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -7,9 +7,9 @@ namespace NuGetUpdater.Cli.Commands; internal static class DiscoverCommand { - internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; + internal static readonly Option RepoRootOption = new("--repo-root") { IsRequired = true }; internal static readonly Option WorkspaceOption = new("--workspace") { IsRequired = true }; - internal static readonly Option OutputOption = new("--output", () => DiscoveryWorker.DiscoveryResultFileName) { IsRequired = false }; + internal static readonly Option OutputOption = new("--output") { IsRequired = true }; internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); internal static Command GetCommand(Action setExitCode) @@ -27,7 +27,7 @@ internal static Command GetCommand(Action setExitCode) command.SetHandler(async (repoRoot, workspace, outputPath, verbose) => { var worker = new DiscoveryWorker(new Logger(verbose)); - await worker.RunAsync(repoRoot.FullName, workspace.FullName, outputPath); + await worker.RunAsync(repoRoot.FullName, workspace.FullName, outputPath.FullName); }, RepoRootOption, WorkspaceOption, OutputOption, VerboseOption); return command; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs new file mode 100644 index 00000000000..802017b046f --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record AnalysisResult +{ + public required string UpdatedVersion { get; init; } + public bool CanUpdate { get; init; } + public bool VersionComesFromMultiDependencyProperty { get; init; } + public required ImmutableArray UpdatedDependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs new file mode 100644 index 00000000000..1c428b24b62 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -0,0 +1,245 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +using NuGetUpdater.Analyzer; +using NuGetUpdater.Core.Discover; + +namespace NuGetUpdater.Core.Analyze; + +public partial class AnalyzeWorker +{ + public const string AnalysisDirectoryName = "./.dependabot/analysis"; + + private readonly Logger _logger; + + internal static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new RequirementConverter() }, + }; + + public AnalyzeWorker(Logger logger) + { + _logger = logger; + } + + public async Task RunAsync(string discoveryPath, string dependencyPath, string analysisDirectory) + { + var discovery = LoadDiscovery(discoveryPath); + var dependencyInfo = LoadDependencyInfo(dependencyPath); + + var nugetContext = CreateNuGetContext(); + + var currentVersion = NuGetVersion.Parse(dependencyInfo.Version); + var projectFrameworks = FindProjectFrameworksForDependency(discovery, dependencyInfo); + var versions = await VersionFinder.GetVersionsAsync( + dependencyInfo.Name, + currentVersion.IsPrerelease, + nugetContext, + _logger, + CancellationToken.None); + + ImmutableArray updatedDependencies = []; + var updatedVersion = await FindUpdatedVersionAsync( + discovery, + dependencyInfo, + currentVersion, + projectFrameworks, + versions, + nugetContext); + if (updatedVersion is not null) + { + // Determine updated peer dependencies + var source = versions.GetPackageSources(updatedVersion).First(); + var packageId = new PackageIdentity(dependencyInfo.Name, updatedVersion); + + // Create distinct list of dependencies taking the highest version of each + Dictionary dependencies = []; + foreach (var tfm in projectFrameworks) + { + var dependenciesForTfm = await DependencyFinder.GetDependenciesAsync( + source, + packageId, + tfm, + nugetContext, + _logger, + CancellationToken.None); + + foreach (var dependency in dependenciesForTfm) + { + if (dependencies.TryGetValue(dependency.Id, out PackageDependency? value) && + value.VersionRange.MinVersion! < dependency.VersionRange.MinVersion!) + { + dependencies[dependency.Id] = dependency; + } + else + { + dependencies.Add(dependency.Id, dependency); + } + } + } + + // Filter dependencies by whether any project references them + updatedDependencies = dependencies + .Where(dep => discovery.Projects.Any(p => p.Dependencies.Any(d => d.Name.Equals(dep.Key, StringComparison.OrdinalIgnoreCase)))) + .Select(dep => new Dependency(dep.Key, dep.Value.VersionRange.MinVersion!.ToNormalizedString(), DependencyType.Unknown)) + .ToImmutableArray(); + } + + var result = new AnalysisResult + { + UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version, + CanUpdate = updatedVersion is not null, + VersionComesFromMultiDependencyProperty = false, //TODO: Provide correct value + UpdatedDependencies = updatedDependencies, + }; + + await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result); + } + + internal static WorkspaceDiscoveryResult LoadDiscovery(string discoveryPath) + { + if (!File.Exists(discoveryPath)) + { + throw new FileNotFoundException("Discovery file not found.", discoveryPath); + } + + var discoveryJson = File.ReadAllText(discoveryPath); + var discovery = JsonSerializer.Deserialize(discoveryJson, SerializerOptions); + if (discovery is null) + { + throw new InvalidOperationException("Discovery file is empty."); + } + + return discovery; + } + + internal static DependencyInfo LoadDependencyInfo(string dependencyPath) + { + if (!File.Exists(dependencyPath)) + { + throw new FileNotFoundException("Dependency info file not found.", dependencyPath); + } + + var dependencyInfoJson = File.ReadAllText(dependencyPath); + var dependencyInfo = JsonSerializer.Deserialize(dependencyInfoJson, SerializerOptions); + if (dependencyInfo is null) + { + throw new InvalidOperationException("Dependency info file is empty."); + } + + return dependencyInfo; + } + + internal static NuGetContext CreateNuGetContext() + { + var nugetContext = new NuGetContext(); + if (!Directory.Exists(nugetContext.TempPackageDirectory)) + { + Directory.CreateDirectory(nugetContext.TempPackageDirectory); + } + + return nugetContext; + } + + internal async Task FindUpdatedVersionAsync( + WorkspaceDiscoveryResult discovery, + DependencyInfo dependencyInfo, + NuGetVersion currentVersion, + ImmutableArray projectFrameworks, + VersionResult versions, + NuGetContext nugetContext) + { + var allVersions = versions.GetVersions(); + + var filteredVersions = allVersions + .Where(version => version > currentVersion) // filter lower versions + .Where(version => !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) // filter prerelease + .Where(version => !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version))) // filter ignored + .Where(version => !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version))); // filter vulnerable + + var orderedVersions = dependencyInfo.IsVulnerable + ? filteredVersions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. + : filteredVersions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. + + return await FindFirstCompatibleVersion( + dependencyInfo.Name, + currentVersion, + versions, + orderedVersions, + projectFrameworks, + nugetContext, + _logger); + } + + internal static ImmutableArray FindProjectFrameworksForDependency(WorkspaceDiscoveryResult discovery, DependencyInfo dependencyInfo) + { + return discovery.Projects + .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(tfm => NuGetFramework.Parse(tfm)) + .ToImmutableArray(); + } + + internal static async Task FindFirstCompatibleVersion( + string packageId, + NuGetVersion currentVersion, + VersionResult versions, + IEnumerable orderedVersions, + ImmutableArray projectFrameworks, + NuGetContext context, + Logger logger) + { + var source = versions.GetPackageSources(currentVersion).First(); + var isCompatible = await CompatibilityChecker.CheckAsync( + source, + new(packageId, currentVersion), + projectFrameworks, + context, + logger, + CancellationToken.None); + if (!isCompatible) + { + // If the current package is incompatible, then don't check for compatibility. + return orderedVersions.First(); + } + + foreach (var version in orderedVersions) + { + source = versions.GetPackageSources(version).First(); + isCompatible = await CompatibilityChecker.CheckAsync( + source, + new(packageId, version), + projectFrameworks, + context, + logger, + CancellationToken.None); + + if (isCompatible) + { + return version; + } + } + + // Could not find a compatible version + return null; + } + + internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result) + { + if (!Directory.Exists(analysisDirectory)) + { + Directory.CreateDirectory(analysisDirectory); + } + + var resultPath = Path.Combine(analysisDirectory, $"{dependencyName}.json"); + var resultJson = JsonSerializer.Serialize(result, SerializerOptions); + await File.WriteAllTextAsync(path: resultPath, resultJson); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs new file mode 100644 index 00000000000..9f50a980971 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -0,0 +1,146 @@ +using System.Collections.Immutable; + +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.FrameworkChecker; + +namespace NuGetUpdater.Analyzer; + +using PackageInfo = (bool IsDevDependency, ImmutableArray Frameworks); +using PackageReaders = (IAsyncPackageCoreReader CoreReader, IAsyncPackageContentReader ContentReader); + +internal static class CompatibilityChecker +{ + public static async Task CheckAsync( + PackageSource source, + PackageIdentity package, + ImmutableArray projectFrameworks, + NuGetContext context, + Logger logger, + CancellationToken cancellationToken) + { + var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync( + source, + package, + context, + cancellationToken); + + // development dependencies are packages such as analyzers which need to be compatible with the compiler not the + // project itself, but some packages that report themselves as development dependencies still contain target + // framework dependencies and should be checked for compatibility through the regular means + if (isDevDependency && packageFrameworks.Length == 0) + { + return true; + } + + if (packageFrameworks.Length == 0 || projectFrameworks.Length == 0) + { + return false; + } + + var compatibilityService = new FrameworkCompatibilityService(); + var compatibleFrameworks = compatibilityService.GetCompatibleFrameworks(packageFrameworks); + + var incompatibleFrameworks = projectFrameworks.Where(f => !compatibleFrameworks.Contains(f)).ToArray(); + if (incompatibleFrameworks.Length > 0) + { + logger.Log($"The package {package} is not compatible. Incompatible project frameworks: {string.Join(", ", incompatibleFrameworks.Select(f => f.GetShortFolderName()))}"); + return false; + } + + return true; + } + + internal static async Task GetPackageInfoAsync( + PackageSource source, + PackageIdentity package, + NuGetContext context, + CancellationToken cancellationToken) + { + var tempPackagePath = GetTempPackagePath(package, context); + var readers = File.Exists(tempPackagePath) + ? ReadPackage(tempPackagePath) + : await DownloadPackageAsync(source, package, context, cancellationToken); + + var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken); + var reader = new NuspecReader(nuspecStream); + + var isDevDependency = reader.GetDevelopmentDependency(); + + var tfms = reader.GetDependencyGroups() + .Select(d => d.TargetFramework) + .ToImmutableArray(); + if (tfms.Length == 0) + { + // If the nuspec doesn't have any dependency groups, + // try to get the TargetFramework from files in the lib folder. + var libItems = (await readers.ContentReader.GetLibItemsAsync(cancellationToken)).ToList(); + if (libItems.Count == 0) + { + // If there is no lib folder in this package, then assume it is a dev dependency. + isDevDependency = true; + } + + tfms = libItems.Select(item => item.TargetFramework) + .Distinct() + .ToImmutableArray(); + } + + // The interfaces we given are not disposable but the underlying type can be. + // This will ensure we dispose of any resources that need to be cleaned up. + (readers.CoreReader as IDisposable)?.Dispose(); + (readers.ContentReader as IDisposable)?.Dispose(); + + return (isDevDependency, tfms); + } + + internal static PackageReaders ReadPackage(string tempPackagePath) + { + var stream = new FileStream( + tempPackagePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 4096); + PackageArchiveReader archiveReader = new(stream); + return (archiveReader, archiveReader); + } + + internal static async Task DownloadPackageAsync( + PackageSource source, + PackageIdentity package, + NuGetContext context, + CancellationToken cancellationToken) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}"); + } + + var downloader = await feed.GetPackageDownloaderAsync( + package, + context.SourceCacheContext, + context.Logger, + cancellationToken); + + var tempPackagePath = GetTempPackagePath(package, context); + var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken); + if (!isDownloaded) + { + throw new Exception("Failed to download package"); + } + + return (downloader.CoreReader, downloader.ContentReader); + } + + internal static string GetTempPackagePath(PackageIdentity package, NuGetContext context) + => Path.Combine(context.TempPackageDirectory, package.Id + "." + package.Version + ".nupkg"); +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs new file mode 100644 index 00000000000..e9c15ddfd3e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs @@ -0,0 +1,45 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +using NuGetUpdater.Core; + +namespace NuGetUpdater.Analyzer; + +internal static class DependencyFinder +{ + public static async Task> GetDependenciesAsync( + PackageSource source, + PackageIdentity package, + NuGetFramework framework, + NuGetContext context, + Logger logger, + CancellationToken cancellationToken) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + throw new NotSupportedException($"Failed to get DependencyInfoResource for {source.SourceUri}"); + } + + var dependencyInfo = await feed.ResolvePackage( + package, + framework, + context.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (dependencyInfo is null) + { + throw new Exception($"Failed to resolve package {package} from {source.SourceUri}"); + } + + return dependencyInfo.Dependencies + .ToImmutableArray(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs new file mode 100644 index 00000000000..e04b96e17f1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record DependencyInfo +{ + public required string Name { get; init; } + public required string Version { get; init; } + public required bool IsVulnerable { get; init; } + public ImmutableArray IgnoredVersions { get; init; } + public ImmutableArray Vulnerabilities { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs new file mode 100644 index 00000000000..9da109dbf43 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs @@ -0,0 +1,41 @@ +using System.Collections.Immutable; + +using NuGet.CommandLine; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; + +namespace NuGetUpdater.Analyzer; + +internal record NuGetContext : IDisposable +{ + public SourceCacheContext SourceCacheContext { get; } + public string CurrentDirectory { get; } + public ISettings Settings { get; } + public IMachineWideSettings MachineWideSettings { get; } + public ImmutableArray PackageSources { get; } + public ILogger Logger { get; } + public string TempPackageDirectory { get; } + + public NuGetContext(string? currentDirectory = null, ILogger? logger = null) + { + SourceCacheContext = new SourceCacheContext(); + CurrentDirectory = currentDirectory ?? Environment.CurrentDirectory; + MachineWideSettings = new CommandLineMachineWideSettings(); + Settings = NuGet.Configuration.Settings.LoadDefaultSettings( + CurrentDirectory, + configFileName: null, + MachineWideSettings); + var sourceProvider = new PackageSourceProvider(Settings); + PackageSources = sourceProvider.LoadPackageSources() + .Where(p => p.IsEnabled) + .ToImmutableArray(); + Logger = logger ?? NullLogger.Instance; + TempPackageDirectory = Path.Combine(Path.GetTempPath(), ".dependabot", "packages"); + } + + public void Dispose() + { + SourceCacheContext?.Dispose(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs new file mode 100644 index 00000000000..78d36207bab --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs @@ -0,0 +1,99 @@ +using System.Collections.Immutable; + +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +/// +/// A Requirement is a set of one or more version restrictions. It supports a +/// few (=, !=, >, <, >=, <=, ~>) different restriction operators. +/// +/// +/// See Gem::Version for a description on how versions and requirements work +/// together in RubyGems. +/// +public class Requirement +{ + private static readonly ImmutableDictionary> Operators = new Dictionary>() + { + ["="] = (v, r) => v == r, + ["!="] = (v, r) => v != r, + [">"] = (v, r) => v > r, + ["<"] = (v, r) => v < r, + [">="] = (v, r) => v >= r, + ["<="] = (v, r) => v <= r, + ["~>"] = (v, r) => v >= r && v.Version < Bump(r), + }.ToImmutableDictionary(); + + public static Requirement Parse(string requirement) + { + var parts = requirement.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0 || parts.Length > 2) + { + throw new ArgumentException("Invalid requirement string", nameof(requirement)); + } + + var op = parts.Length == 1 ? "=" : parts[0]; + var version = NuGetVersion.Parse(parts[^1]); + + return new Requirement(op, version); + } + + public string Operator { get; } + public NuGetVersion Version { get; } + + public Requirement(string op, NuGetVersion version) + { + if (!Operators.ContainsKey(op)) + { + throw new ArgumentException("Invalid operator", nameof(op)); + } + + Operator = op; + Version = version; + } + + public override string ToString() + { + return $"{Operator} {Version}"; + } + + public bool IsSatisfiedBy(NuGetVersion version) + { + return Operators[Operator](version, Version); + } + + private static readonly Dictionary BumpMap = []; + /// + /// Return a new version object where the next to the last revision + /// number is one greater (e.g., 5.3.1 => 5.4). + /// + /// + /// This logic intended to be similar to RubyGems Gem::Version#bump + /// + public static Version Bump(NuGetVersion version) + { + if (BumpMap.TryGetValue(version.OriginalVersion!, out var bumpedVersion)) + { + return bumpedVersion; + } + + var versionParts = version.OriginalVersion! // Get the original string this version was created from + .Split('-')[0] // Get the version part without pre-release + .Split('.') // Split into Major.Minor.Patch.Revision if present + .Select(int.Parse) + .ToArray(); + + if (versionParts.Length > 1) + { + versionParts = versionParts[..^1]; // Remove the last part + } + + versionParts[^1]++; // Increment the new last part + + bumpedVersion = new Version(string.Join('.', versionParts)); + BumpMap[version.OriginalVersion!] = bumpedVersion; + + return bumpedVersion; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs new file mode 100644 index 00000000000..361643f67fa --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGetUpdater.Core.Analyze; + +public class RequirementConverter : JsonConverter +{ + public override Requirement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Requirement.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, Requirement value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs new file mode 100644 index 00000000000..e2edb0541c1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record SecurityVulnerability +{ + public required string DependencyName { get; init; } + public required string PackageManager { get; init; } + public required ImmutableArray VulnerableVersions { get; init; } + public required ImmutableArray SafeVersions { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs new file mode 100644 index 00000000000..3123e5ac55d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs @@ -0,0 +1,36 @@ +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +public static class SecurityVulnerabilityExtensions +{ + // This logic taken from Dependabot security_advisory.rb + public static bool IsVulnerable(this SecurityVulnerability vulnerability, NuGetVersion version) + { + var inSafeRange = vulnerability.SafeVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inSafeRange) + { + // If version is known safe for this advisory, it's not vulnerable + return false; + } + + var inVulnerableRange = vulnerability.VulnerableVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inVulnerableRange) + { + // If in the vulnerable range and not known safe, it's vulnerable + return true; + } + + if (vulnerability.VulnerableVersions.Length > 0) + { + // If a vulnerable range present but not met, it's not vulnerable + return false; + } + + // Finally, if no vulnerable range provided, but a safe range provided, + // and this versions isn't included (checked earlier), it's vulnerable + return vulnerability.SafeVersions.Length > 0; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs new file mode 100644 index 00000000000..63f263eebc6 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -0,0 +1,66 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +using NuGetUpdater.Core; + +namespace NuGetUpdater.Analyzer; + +internal static class VersionFinder +{ + public static async Task GetVersionsAsync( + string packageId, + bool includePrerelease, + NuGetContext context, + Logger logger, + CancellationToken cancellationToken) + { + VersionResult result = new(); + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? context.PackageSources + : context.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + // $"Failed to get MetadataResource for {source.SourceUri}" + continue; + } + + var existsInFeed = await feed.Exists( + packageId, + includePrerelease, + includeUnlisted: false, + context.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (!existsInFeed) + { + continue; + } + + var feedVersions = await feed.GetVersions( + packageId, + includePrerelease, + includeUnlisted: false, + context.SourceCacheContext, + NullLogger.Instance, + CancellationToken.None); + + result.AddRange(source, feedVersions); + } + + return result; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs new file mode 100644 index 00000000000..7e1b4e07640 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +using NuGet.Configuration; +using NuGet.Versioning; + +namespace NuGetUpdater.Analyzer; + +internal class VersionResult +{ + private readonly Dictionary> _versions = new(); + + public void AddRange(PackageSource source, IEnumerable versions) + { + foreach (var version in versions) + { + if (_versions.ContainsKey(version)) + { + _versions[version].Add(source); + } + else + { + _versions.Add(version, [source]); + } + } + } + + public ImmutableArray GetPackageSources(NuGetVersion version) + { + return [.. _versions[version]]; + } + + public ImmutableArray GetVersions() + { + return [.. _versions.Keys]; + } +} diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb new file mode 100644 index 00000000000..044b33a9dfb --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/dependency" +require "dependabot/nuget/discovery/discovery_json_reader" +require "json" +require "sorbet-runtime" + +module Dependabot + module Nuget + class AnalysisJsonReader + extend T::Sig + + sig { returns(String) } + def self.temp_directory + File.join(DiscoveryJsonReader.temp_directory, "analysis") + end + + sig { params(dependency_name: String).returns(String) } + def self.analysis_file_path(dependency_name:) + File.join(temp_directory, "#{dependency_name}.json") + end + + sig { params(dependency_name: String).returns(T.nilable(DependencyFile)) } + def self.analysis_json(dependency_name:) + file_path = analysis_file_path(dependency_name: dependency_name) + return unless File.exist?(file_path) + + DependencyFile.new( + name: Pathname.new(file_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(file_path) + ) + end + + sig { params(analysis_json: DependencyFile).void } + def initialize(analysis_json:) + @analysis_json = analysis_json + end + + sig { returns(Dependabot::Nuget::Version) } + def updated_version + Version.new("1.0.0") + end + + sig { returns(T::Boolean) } + def can_update? + true + end + + sig { returns(T::Boolean) } + def version_comes_from_multi_dependency_property? + false + end + + sig { returns(T::Array[Dependabot::Dependency]) } + def updated_dependencies + [] + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index b1926fe590c..d38f38aeac8 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -11,16 +11,16 @@ module Nuget class DiscoveryJsonReader extend T::Sig - DISCOVERY_JSON_PATH = ".dependabot/discovery.json" + DISCOVERY_JSON_FILENAME = "discovery.json" sig { returns(String) } - private_class_method def self.temp_directory - Dir.tmpdir + def self.temp_directory + File.join(Dir.tmpdir, ".dependabot") end sig { returns(String) } def self.discovery_file_path - File.join(temp_directory, DISCOVERY_JSON_PATH) + File.join(temp_directory, DISCOVERY_JSON_FILENAME) end sig { returns(T.nilable(DependencyFile)) } diff --git a/nuget/lib/dependabot/nuget/http_response_helpers.rb b/nuget/lib/dependabot/nuget/http_response_helpers.rb deleted file mode 100644 index 2e751f985e8..00000000000 --- a/nuget/lib/dependabot/nuget/http_response_helpers.rb +++ /dev/null @@ -1,19 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -module Dependabot - module Nuget - module HttpResponseHelpers - extend T::Sig - - sig { params(string: String).returns(String) } - def self.remove_wrapping_zero_width_chars(string) - string.force_encoding("UTF-8").encode - .gsub(/\A[\u200B-\u200D\uFEFF]/, "") - .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index 4047feb226d..a201e66f3ae 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -110,6 +110,62 @@ def self.run_nuget_discover_tool(repo_root:, workspace_path:, output_path:, cred end end + sig do + params(discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String).returns([String, String]) + end + def self.get_nuget_analyze_tool_command(discovery_file_path:, dependency_file_path:, + analysis_folder_path:) + exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") + command_parts = [ + exe_path, + "analyze", + "--discovery-file-path", + discovery_file_path, + "--dependency-file-path", + dependency_file_path, + "--analysis-folder-path", + analysis_folder_path, + "--verbose" + ].compact + + command = Shellwords.join(command_parts) + + fingerprint = [ + exe_path, + "analyze", + "--discovery-file-path", + "", + "--dependency-file-path", + "", + "--analysis-folder-path", + "", + "--verbose" + ].compact.join(" ") + + [command, fingerprint] + end + + sig do + params( + discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String, credentials: T::Array[Dependabot::Credential] + ).void + end + def self.run_nuget_analyze_tool(discovery_file_path:, dependency_file_path:, + analysis_folder_path:, credentials:) + (command, fingerprint) = get_nuget_analyze_tool_command(discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path) + + puts "running NuGet analyze:\n" + command + + NuGetConfigCredentialHelpers.patch_nuget_config_for_action(credentials) do + output = SharedHelpers.run_shell_command(command, allow_unsafe_shell_command: true, fingerprint: fingerprint) + puts output + end + end + sig do params(repo_root: String, proj_path: String, dependency: Dependency, is_transitive: T::Boolean).returns([String, String]) diff --git a/nuget/lib/dependabot/nuget/nuget_client.rb b/nuget/lib/dependabot/nuget/nuget_client.rb deleted file mode 100644 index 6fe1dbea41e..00000000000 --- a/nuget/lib/dependabot/nuget/nuget_client.rb +++ /dev/null @@ -1,223 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/http_response_helpers" -require "dependabot/nuget/update_checker/repository_finder" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NugetClient - extend T::Sig - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - def self.get_package_versions(dependency_name, repository_details) - repository_type = repository_details.fetch(:repository_type) - if repository_type == "v3" - get_package_versions_v3(dependency_name, repository_details) - elsif repository_type == "v2" - get_package_versions_v2(dependency_name, repository_details) - elsif repository_type == "local" - get_package_versions_local(dependency_name, repository_details) - else - raise "Unknown repository type: #{repository_type}" - end - end - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_package_versions_local(dependency_name, repository_details) - url = repository_details.fetch(:base_url) - raise "Local repo #{url} doesn't exist or isn't a directory" unless File.exist?(url) && File.directory?(url) - - package_dir = File.join(url, dependency_name) - - versions = Set.new - return versions unless File.exist?(package_dir) && File.directory?(package_dir) - - Dir.each_child(package_dir) do |child| - versions.add(child) if File.directory?(File.join(package_dir, child)) - end - - versions - end - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_package_versions_v3(dependency_name, repository_details) - # Use the registration URL if possible because it is fast and correct - if repository_details[:registration_url] - get_versions_from_registration_v3(repository_details) - # use the search API if not because it is slow but correct - elsif repository_details[:search_url] - get_versions_from_search_url_v3(repository_details, dependency_name) - # Otherwise, use the versions URL (fast but wrong because it includes unlisted versions) - elsif repository_details[:versions_url] - get_versions_from_versions_url_v3(repository_details) - else - raise "No version sources were available for #{dependency_name} in #{repository_details}" - end - end - - sig do - params(dependency_name: String, repository_details: T::Hash[Symbol, String]) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_package_versions_v2(dependency_name, repository_details) - doc = execute_xml_nuget_request(repository_details.fetch(:versions_url), repository_details) - return unless doc - - # v2 APIs can differ, but all tested have this title value set to the name of the package - title_nodes = doc.xpath("/feed/entry/title") - matching_versions = Set.new - title_nodes.each do |title_node| - return nil unless title_node.text - - next unless title_node.text.casecmp?(dependency_name) - - version_node = title_node.parent.xpath("properties/Version") - matching_versions << version_node.text if version_node && version_node.text - end - - matching_versions - end - - sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) } - private_class_method def self.get_versions_from_versions_url_v3(repository_details) - body = execute_json_nuget_request(repository_details.fetch(:versions_url), repository_details) - ver_array = T.let(body&.fetch("versions"), T.nilable(T::Array[String])) - ver_array&.to_set - end - - sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) } - private_class_method def self.get_versions_from_registration_v3(repository_details) - url = repository_details.fetch(:registration_url) - body = execute_json_nuget_request(url, repository_details) - - return unless body - - pages = body.fetch("items") - versions = T.let(Set.new, T::Set[String]) - pages.each do |page| - items = page["items"] - if items - # inlined entries - get_versions_from_inline_page(items, versions) - else - # paged entries - page_url = page["@id"] - page_body = execute_json_nuget_request(page_url, repository_details) - next unless page_body - - items = page_body.fetch("items") - items.each do |item| - catalog_entry = item.fetch("catalogEntry") - versions << catalog_entry.fetch("version") if catalog_entry["listed"] == true - end - end - end - - versions - end - - sig { params(items: T::Array[T::Hash[String, T.untyped]], versions: T::Set[String]).void } - private_class_method def self.get_versions_from_inline_page(items, versions) - items.each do |item| - catalog_entry = item["catalogEntry"] - - # a package is considered listed if the `listed` property is either `true` or missing - listed_property = catalog_entry["listed"] - is_listed = listed_property.nil? || listed_property == true - if is_listed - vers = catalog_entry["version"] - versions << vers - end - end - end - - sig do - params(repository_details: T::Hash[Symbol, String], dependency_name: String) - .returns(T.nilable(T::Set[String])) - end - private_class_method def self.get_versions_from_search_url_v3(repository_details, dependency_name) - search_url = repository_details.fetch(:search_url) - body = execute_json_nuget_request(search_url, repository_details) - - body&.fetch("data") - &.find { |d| d.fetch("id").casecmp(dependency_name.downcase).zero? } - &.fetch("versions") - &.map { |d| d.fetch("version") } - &.to_set - end - - sig do - params(url: String, repository_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) - end - private_class_method def self.execute_xml_nuget_request(url, repository_details) - response = execute_nuget_request_internal( - url: url, - auth_header: repository_details.fetch(:auth_header), - repository_url: repository_details.fetch(:repository_url) - ) - return unless response.status == 200 - - doc = Nokogiri::XML(response.body) - doc.remove_namespaces! - doc - end - - sig do - params(url: String, - repository_details: T::Hash[Symbol, T.untyped]) - .returns(T.nilable(T::Hash[T.untyped, T.untyped])) - end - private_class_method def self.execute_json_nuget_request(url, repository_details) - response = execute_nuget_request_internal( - url: url, - auth_header: repository_details.fetch(:auth_header), - repository_url: repository_details.fetch(:repository_url) - ) - return unless response.status == 200 - - body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body) - JSON.parse(body) - end - - sig do - params(url: String, auth_header: T::Hash[Symbol, T.untyped], repository_url: String).returns(Excon::Response) - end - private_class_method def self.execute_nuget_request_internal(url:, auth_header:, repository_url:) - cache = CacheManager.cache("dependency_url_search_cache") - if cache[url].nil? - response = Dependabot::RegistryClient.get( - url: url, - headers: auth_header - ) - - if [401, 402, 403].include?(response.status) - raise Dependabot::PrivateSourceAuthenticationFailure, repository_url - end - - cache[url] = response if !CacheManager.caching_disabled? && response.status == 200 - else - response = cache[url] - end - - response - rescue Excon::Error::Timeout, Excon::Error::Socket - repo_url = repository_url - raise if repo_url == Dependabot::Nuget::RepositoryFinder::DEFAULT_REPOSITORY_URL - - raise PrivateSourceTimedOut, repo_url - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index a6e4f49e55a..847e9a066b6 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -1,7 +1,8 @@ # typed: strict # frozen_string_literal: true -require "dependabot/nuget/file_parser" +require "dependabot/nuget/analysis/analysis_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "dependabot/update_checkers" require "dependabot/update_checkers/base" require "sorbet-runtime" @@ -11,12 +12,7 @@ module Nuget class UpdateChecker < Dependabot::UpdateCheckers::Base extend T::Sig - require_relative "update_checker/version_finder" - require_relative "update_checker/property_updater" require_relative "update_checker/requirements_updater" - require_relative "update_checker/dependency_finder" - - PROPERTY_REGEX = /\$\((?.*?)\)/ sig { override.returns(T.nilable(String)) } def latest_version @@ -25,7 +21,7 @@ def latest_version # if no update sources have the requisite package, then we can only assume that the current version is correct @latest_version = T.let( - latest_version_details&.fetch(:version)&.to_s || dependency.version, + update_analysis.updated_version.to_s, T.nilable(String) ) end @@ -39,14 +35,14 @@ def latest_resolvable_version sig { override.returns(Dependabot::Nuget::Version) } def lowest_security_fix_version - lowest_security_fix_version_details&.fetch(:version) + update_analysis.updated_version end sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_resolvable_security_fix_version return nil if version_comes_from_multi_dependency_property? - lowest_security_fix_version + update_analysis.updated_version end sig { override.returns(NilClass) } @@ -59,175 +55,79 @@ def latest_resolvable_version_with_no_unlock def updated_requirements RequirementsUpdater.new( requirements: dependency.requirements, - latest_version: preferred_resolvable_version_details&.fetch(:version, nil)&.to_s, - source_details: preferred_resolvable_version_details&.slice(:nuspec_url, :repo_url, :source_url) + latest_version: update_analysis.updated_version.to_s ).updated_requirements end sig { returns(T::Boolean) } def up_to_date? - # No need to update transitive dependencies unless they have a vulnerability. - return true if !dependency.top_level? && !vulnerable? - - # If any requirements have an uninterpolated property in them then - # that property couldn't be found, and we assume that the dependency - # is up-to-date - return true unless requirements_unlocked_or_can_be? - - super + !update_analysis.can_update? end sig { returns(T::Boolean) } def requirements_unlocked_or_can_be? - # If any requirements have an uninterpolated property in them then - # that property couldn't be found, and the requirement therefore - # cannot be unlocked (since we can't update that property) - dependency.requirements.none? do |req| - req.fetch(:requirement)&.match?(PROPERTY_REGEX) - end + update_analysis.can_update? end private - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def preferred_resolvable_version_details - # If this dependency is vulnerable, prefer trying to update to the - # lowest_resolvable_security_fix_version. Otherwise update all the way - # to the latest_resolvable_version. - return lowest_security_fix_version_details if vulnerable? + sig { returns(AnalysisJsonReader) } + def update_analysis + @update_analysis ||= T.let(request_analysis, T.nilable(AnalysisJsonReader)) + end + + sig { returns(String) } + def dependency_file_path + File.join(DiscoveryJsonReader.temp_directory, "dependency", "#{dependency.name}.json") + end + + sig { returns(AnalysisJsonReader) } + def request_analysis + discovery_file_path = DiscoveryJsonReader.discovery_file_path + analysis_folder_path = AnalysisJsonReader.temp_directory + + dependency_info = { + Name: dependency.name, + Version: dependency.version.to_s, + IsVulnerable: vulnerable?, + IgnoredVersions: ignored_versions.map do |i| + i == ">= 0" ? "*" : i + end, + Vulnerabilities: security_advisories.map do |vulnerability| + { + DependencyName: vulnerability.dependency_name, + PackageManager: vulnerability.package_manager, + VulnerableVersions: vulnerability.vulnerable_versions.map(&:to_s), + SafeVersions: vulnerability.safe_versions.map(&:to_s) + } + end + }.to_json + File.write(dependency_file_path, dependency_info) - latest_version_details + NativeHelpers.run_nuget_analyze_tool(discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path, + credentials: credentials) + + analysis_json = AnalysisJsonReader.analysis_json(dependency_name: dependency.name) + + AnalysisJsonReader.new(analysis_json: T.must(analysis_json)) end sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? # We always want a full unlock since any package update could update peer dependencies as well. - return true unless version_comes_from_multi_dependency_property? - - property_updater.update_possible? + true end sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock - return property_updater.updated_dependencies if version_comes_from_multi_dependency_property? - - puts "Finding updated dependencies for #{dependency.name}." - - updated_dependency = Dependency.new( - name: dependency.name, - version: latest_version, - requirements: updated_requirements, - previous_version: dependency.version, - previous_requirements: dependency.requirements, - package_manager: dependency.package_manager - ) - updated_dependencies = [updated_dependency] - updated_dependencies += DependencyFinder.new( - dependency: updated_dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: @repo_contents_path - ).updated_peer_dependencies - updated_dependencies - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def preferred_version_details - return lowest_security_fix_version_details if vulnerable? - - latest_version_details - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def latest_version_details - @latest_version_details ||= - T.let( - version_finder.latest_version_details, - T.nilable(T::Hash[Symbol, T.untyped]) - ) - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def lowest_security_fix_version_details - @lowest_security_fix_version_details ||= - T.let( - version_finder.lowest_security_fix_version_details, - T.nilable(T::Hash[Symbol, T.untyped]) - ) - end - - sig { returns(Dependabot::Nuget::UpdateChecker::VersionFinder) } - def version_finder - @version_finder ||= - T.let( - VersionFinder.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored, - security_advisories: security_advisories, - repo_contents_path: @repo_contents_path - ), - T.nilable(Dependabot::Nuget::UpdateChecker::VersionFinder) - ) - end - - sig { returns(Dependabot::Nuget::UpdateChecker::PropertyUpdater) } - def property_updater - @property_updater ||= - T.let( - PropertyUpdater.new( - dependency: dependency, - dependency_files: dependency_files, - target_version_details: latest_version_details, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored, - repo_contents_path: @repo_contents_path - ), - T.nilable(Dependabot::Nuget::UpdateChecker::PropertyUpdater) - ) + update_analysis.updated_dependencies end sig { returns(T::Boolean) } def version_comes_from_multi_dependency_property? - declarations_using_a_property.any? do |requirement| - property_name = requirement.fetch(:metadata).fetch(:property_name) - - all_property_based_dependencies.any? do |dep| - next false if dep.name == dependency.name - - dep.requirements.any? do |req| - req.dig(:metadata, :property_name) == property_name - end - end - end - end - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def declarations_using_a_property - @declarations_using_a_property ||= - T.let( - dependency.requirements - .select { |req| req.dig(:metadata, :property_name) }, - T.nilable(T::Array[T::Hash[Symbol, T.untyped]]) - ) - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def all_property_based_dependencies - @all_property_based_dependencies ||= - T.let( - Nuget::FileParser.new( - dependency_files: dependency_files, - repo_contents_path: repo_contents_path, - source: nil - ).parse.select do |dep| - dep.requirements.any? { |req| req.dig(:metadata, :property_name) } - end, - T.nilable(T::Array[Dependabot::Dependency]) - ) + update_analysis.version_comes_from_multi_dependency_property? end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb deleted file mode 100644 index 4f67de874e3..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb +++ /dev/null @@ -1,116 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" - -module Dependabot - module Nuget - class CompatibilityChecker - extend T::Sig - - require_relative "nuspec_fetcher" - require_relative "nupkg_fetcher" - require_relative "tfm_finder" - require_relative "tfm_comparer" - - sig do - params( - dependency_urls: T::Array[T::Hash[Symbol, String]], - dependency: Dependabot::Dependency - ).void - end - def initialize(dependency_urls:, dependency:) - @dependency_urls = dependency_urls - @dependency = dependency - end - - sig { params(version: String).returns(T::Boolean) } - def compatible?(version) - nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version) - return false unless nuspec_xml - - # development dependencies are packages such as analyzers which need to be compatible with the compiler not the - # project itself, but some packages that report themselves as development dependencies still contain target - # framework dependencies and should be checked for compatibility through the regular means - return true if pure_development_dependency?(nuspec_xml) - - package_tfms = parse_package_tfms(nuspec_xml) - package_tfms = fetch_package_tfms(version) if package_tfms.empty? - # nil is a special return value that indicates that the package is likely a development dependency - return true if package_tfms.nil? - return false if package_tfms.empty? - - return false if project_tfms.nil? || project_tfms&.empty? - - TfmComparer.are_frameworks_compatible?(T.must(project_tfms), package_tfms) - end - - private - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - attr_reader :dependency_urls - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { params(nuspec_xml: Nokogiri::XML::Document).returns(T::Boolean) } - def pure_development_dependency?(nuspec_xml) - contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip - return false unless contents # no `developmentDependency` element - - self_reports_as_development_dependency = contents.casecmp?("true") - return false unless self_reports_as_development_dependency - - # even though a package self-reports as a development dependency, it might not be if it has dependency groups - # with a target framework - dependency_groups_with_target_framework = - nuspec_xml.at_xpath("/package/metadata/dependencies/group[@targetFramework]") - dependency_groups_with_target_framework.to_a.empty? - end - - sig { params(nuspec_xml: Nokogiri::XML::Document).returns(T::Array[String]) } - def parse_package_tfms(nuspec_xml) - nuspec_xml.xpath("//dependencies/group").filter_map { |group| group.attribute("targetFramework") } - end - - sig { returns(T.nilable(T::Array[String])) } - def project_tfms - @project_tfms ||= T.let(TfmFinder.frameworks(dependency), T.nilable(T::Array[String])) - end - - sig { params(dependency_version: String).returns(T.nilable(T::Array[String])) } - def fetch_package_tfms(dependency_version) - cache = CacheManager.cache("compatibility_checker_tfms_cache") - key = "#{dependency.name}::#{dependency_version}" - - cache[key] ||= begin - nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version) - return [] unless nupkg_buffer - - # Parse tfms from the folders beneath the lib folder - folder_name = "lib/" - tfms = Set.new - Zip::File.open_buffer(nupkg_buffer) do |zip| - lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) } - # If there is no lib folder in this package, assume it is a development dependency - return nil if lib_file_entries.empty? - - lib_file_entries.each do |entry| - _, tfm = entry.name.split("/").first(2) - - # some zip compressors create empty directory entries (in this case `lib/`) which can cause the string - # split to return `nil`, so we have to explicitly guard against that - tfms << tfm if tfm - end - end - - tfms.to_a - end - - cache[key] - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb b/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb deleted file mode 100644 index 0341e0aa80f..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb +++ /dev/null @@ -1,292 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" -require "sorbet-runtime" -require "stringio" -require "zip" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/version" - -module Dependabot - module Nuget - class UpdateChecker < Dependabot::UpdateCheckers::Base - class DependencyFinder - extend T::Sig - - require_relative "requirements_updater" - require_relative "nuspec_fetcher" - - sig { returns(T::Hash[String, T.untyped]) } - def self.transitive_dependencies_cache - CacheManager.cache("dependency_finder_transitive_dependencies") - end - - sig { returns(T::Hash[String, T.untyped]) } - def self.updated_peer_dependencies_cache - CacheManager.cache("dependency_finder_updated_peer_dependencies") - end - - sig { returns(T::Hash[String, T.untyped]) } - def self.fetch_dependencies_cache - CacheManager.cache("dependency_finder_fetch_dependencies") - end - - sig do - params( - dependency: Dependabot::Dependency, - dependency_files: T::Array[Dependabot::DependencyFile], - credentials: T::Array[Dependabot::Credential], - repo_contents_path: T.nilable(String) - ).void - end - def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @repo_contents_path = repo_contents_path - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def transitive_dependencies - key = "#{dependency.name.downcase}::#{dependency.version}" - cache = DependencyFinder.transitive_dependencies_cache - - unless cache[key] - begin - # first do a quick sanity check on the version string; if it can't be parsed, an exception will be raised - _ = Version.new(dependency.version) - - cache[key] = fetch_transitive_dependencies( - @dependency.name, - T.must(@dependency.version) - ).map do |dependency_info| - package_name = dependency_info["packageName"] - target_version = dependency_info["version"] - - Dependency.new( - name: package_name, - version: target_version.to_s, - requirements: [], # Empty requirements for transitive dependencies - package_manager: @dependency.package_manager - ) - end - rescue StandardError - # if anything happened above, there are no meaningful dependencies that can be derived - cache[key] = [] - end - end - - cache[key] - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def updated_peer_dependencies - key = "#{dependency.name.downcase}::#{dependency.version}" - cache = DependencyFinder.updated_peer_dependencies_cache - - cache[key] ||= fetch_transitive_dependencies( - @dependency.name, - T.must(@dependency.version) - ).filter_map do |dependency_info| - package_name = dependency_info["packageName"] - target_version = dependency_info["version"] - - # Find the Dependency object for the peer dependency. We will not return - # dependencies that are not referenced from dependency files. - peer_dependency = top_level_dependencies.find { |d| d.name == package_name } - next unless peer_dependency - next unless target_version > peer_dependency.numeric_version - - # Use version finder to determine the source details for the peer dependency. - target_version_details = version_finder(peer_dependency).versions.find do |v| - v.fetch(:version) == target_version - end - next unless target_version_details - - Dependency.new( - name: peer_dependency.name, - version: target_version_details.fetch(:version).to_s, - requirements: updated_requirements(peer_dependency, target_version_details), - previous_version: peer_dependency.version, - previous_requirements: peer_dependency.requirements, - package_manager: peer_dependency.package_manager, - metadata: { information_only: true } # Instruct updater to not directly update this dependency - ) - end - - cache[key] - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T.nilable(String)) } - attr_reader :repo_contents_path - - sig do - params( - dep: Dependabot::Dependency, - target_version_details: T::Hash[Symbol, T.untyped] - ) - .returns(T::Array[T::Hash[String, T.untyped]]) - end - def updated_requirements(dep, target_version_details) - @updated_requirements ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) - @updated_requirements[dep.name] ||= - RequirementsUpdater.new( - requirements: dep.requirements, - latest_version: target_version_details.fetch(:version).to_s, - source_details: target_version_details.slice(:nuspec_url, :repo_url, :source_url) - ).updated_requirements - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def top_level_dependencies - @top_level_dependencies ||= - T.let( - Nuget::FileParser.new( - dependency_files: dependency_files, - repo_contents_path: repo_contents_path, - source: nil - ).parse.select(&:top_level?), - T.nilable(T::Array[Dependabot::Dependency]) - ) - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def nuget_configs - @nuget_configs ||= - T.let( - @dependency_files.select { |f| f.name.match?(/nuget\.config$/i) }, - T.nilable(T::Array[Dependabot::DependencyFile]) - ) - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def dependency_urls - @dependency_urls ||= - T.let( - RepositoryFinder.new( - dependency: @dependency, - credentials: @credentials, - config_files: nuget_configs - ) - .dependency_urls - .select { |url| url.fetch(:repository_type) == "v3" }, - T.nilable(T::Array[T::Hash[Symbol, String]]) - ) - end - - sig { params(package_id: String, package_version: String).returns(T::Array[T::Hash[String, T.untyped]]) } - def fetch_transitive_dependencies(package_id, package_version) - all_dependencies = {} - fetch_transitive_dependencies_impl(package_id, package_version, all_dependencies) - all_dependencies.map { |_, dependency_info| dependency_info } - end - - sig { params(package_id: String, package_version: String, all_dependencies: T::Hash[String, T.untyped]).void } - def fetch_transitive_dependencies_impl(package_id, package_version, all_dependencies) - dependencies = fetch_dependencies(package_id, package_version) - return unless dependencies.any? - - dependencies.each do |dependency| - next if dependency.nil? - - dependency_id = dependency["packageName"] - dependency_version_range = dependency["versionRange"] - - nuget_version_range_regex = /[\[(](\d+(\.\d+)*(-\w+(\.\d+)*)?)/ - nuget_version_range_match_data = nuget_version_range_regex.match(dependency_version_range) - - dependency_version = if nuget_version_range_match_data.nil? - dependency_version_range - else - nuget_version_range_match_data[1] - end - - dependency["version"] = Version.new(dependency_version) - - current_dependency = all_dependencies[dependency_id.downcase] - next unless current_dependency.nil? || current_dependency["version"] < dependency["version"] - - all_dependencies[dependency_id.downcase] = dependency - fetch_transitive_dependencies_impl(dependency_id, dependency_version, all_dependencies) - end - end - - sig { params(package_id: String, package_version: String).returns(T::Array[T::Hash[String, T.untyped]]) } - def fetch_dependencies(package_id, package_version) - key = "#{package_id.downcase}::#{package_version}" - cache = DependencyFinder.fetch_dependencies_cache - - cache[key] ||= begin - nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, package_id, package_version) - if nuspec_xml.nil? - [] - else - read_dependencies_from_nuspec(nuspec_xml) - end - end - - cache[key] - end - - sig { params(nuspec_xml: Nokogiri::XML::Document).returns(T::Array[T::Hash[String, String]]) } - def read_dependencies_from_nuspec(nuspec_xml) # rubocop:disable Metrics/PerceivedComplexity - # we want to exclude development dependencies from the lookup - allowed_attributes = %w(all compile native runtime) - - nuspec_xml_dependencies = nuspec_xml.xpath("//dependencies/child::node()/dependency").select do |dependency| - include_attr = dependency.attribute("include") - exclude_attr = dependency.attribute("exclude") - - if include_attr.nil? && exclude_attr.nil? - true - elsif include_attr - include_values = include_attr.value.split(",").map(&:strip) - include_values.any? { |element1| allowed_attributes.any? { |element2| element1.casecmp?(element2) } } - else - exclude_values = exclude_attr.value.split(",").map(&:strip) - exclude_values.none? { |element1| allowed_attributes.any? { |element2| element1.casecmp?(element2) } } - end - end - - dependency_list = [] - nuspec_xml_dependencies.each do |dependency| - next unless dependency.attribute("version") - - dependency_list << { - "packageName" => dependency.attribute("id").value, - "versionRange" => dependency.attribute("version").value - } - end - - dependency_list - end - - sig { params(dep: Dependabot::Dependency).returns(Dependabot::Nuget::UpdateChecker::VersionFinder) } - def version_finder(dep) - VersionFinder.new( - dependency: dep, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: [], - raise_on_ignored: false, - security_advisories: [], - repo_contents_path: repo_contents_path - ) - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb b/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb deleted file mode 100644 index b9f53f9d1eb..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +++ /dev/null @@ -1,221 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" -require "stringio" -require "sorbet-runtime" -require "zip" - -require "dependabot/nuget/http_response_helpers" - -module Dependabot - module Nuget - class NupkgFetcher - extend T::Sig - - require_relative "repository_finder" - - sig do - params( - dependency_urls: T::Array[T::Hash[Symbol, String]], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.fetch_nupkg_buffer(dependency_urls, package_id, package_version) - # check all repositories for the first one that has the nupkg - dependency_urls.reduce(T.let(nil, T.nilable(String))) do |nupkg_buffer, repository_details| - nupkg_buffer || fetch_nupkg_buffer_from_repository(repository_details, package_id, package_version) - end - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: T.nilable(String), - package_version: T.nilable(String) - ) - .returns(T.nilable(String)) - end - def self.fetch_nupkg_url_from_repository(repository_details, package_id, package_version) - return unless package_id && package_version && !package_version.empty? - - feed_url = repository_details[:repository_url] - repository_type = repository_details[:repository_type] - - package_url = if repository_type == "v2" - get_nuget_v2_package_url(repository_details, package_id, package_version) - elsif repository_type == "v3" - get_nuget_v3_package_url(repository_details, package_id, package_version) - else - raise Dependabot::DependencyFileNotResolvable, "Unexpected NuGet feed format: #{feed_url}" - end - - package_url - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.fetch_nupkg_buffer_from_repository(repository_details, package_id, package_version) - package_url = fetch_nupkg_url_from_repository(repository_details, package_id, package_version) - return unless package_url - - auth_header = repository_details[:auth_header] - fetch_stream(package_url, auth_header) - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.get_nuget_v3_package_url(repository_details, package_id, package_version) - base_url = repository_details[:base_url] - unless base_url - return get_nuget_v3_package_url_from_search(repository_details, package_id, - package_version) - end - - base_url = base_url.delete_suffix("/") - package_id_downcased = package_id.downcase - "#{base_url}/#{package_id_downcased}/#{package_version}/#{package_id_downcased}.#{package_version}.nupkg" - end - - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.get_nuget_v3_package_url_from_search(repository_details, package_id, package_version) - search_url = repository_details[:search_url] - return nil unless search_url - - # get search result - search_result_response = fetch_url(search_url, repository_details) - return nil unless search_result_response&.status == 200 - - search_response_body = HttpResponseHelpers.remove_wrapping_zero_width_chars(T.must(search_result_response).body) - search_results = JSON.parse(search_response_body) - - # find matching package and version - package_search_result = search_results&.[]("data")&.find { |d| package_id.casecmp?(d&.[]("id")) } - version_search_result = package_search_result&.[]("versions")&.find do |v| - package_version.casecmp?(v&.[]("version")) - end - registration_leaf_url = version_search_result&.[]("@id") - return nil unless registration_leaf_url - - registration_leaf_response = fetch_url(registration_leaf_url, repository_details) - return nil unless registration_leaf_response - return nil unless registration_leaf_response.status == 200 - - registration_leaf_response_body = - HttpResponseHelpers.remove_wrapping_zero_width_chars(registration_leaf_response.body) - registration_leaf = JSON.parse(registration_leaf_response_body) - - # finally, get the .nupkg url - registration_leaf&.[]("packageContent") - end - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/CyclomaticComplexity - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: String, - package_version: String - ) - .returns(T.nilable(String)) - end - def self.get_nuget_v2_package_url(repository_details, package_id, package_version) - # get package XML - base_url = repository_details[:base_url].delete_suffix("/") - package_url = "#{base_url}/Packages(Id='#{package_id}',Version='#{package_version}')" - response = fetch_url(package_url, repository_details) - return nil unless response&.status == 200 - - # find relevant element - doc = Nokogiri::XML(T.must(response).body) - doc.remove_namespaces! - - content_element = doc.xpath("/entry/content") - nupkg_url = content_element&.attribute("src")&.value - nupkg_url - end - - sig do - params( - stream_url: String, - auth_header: T::Hash[String, String], - max_redirects: Integer - ) - .returns(T.nilable(String)) - end - def self.fetch_stream(stream_url, auth_header, max_redirects = 5) - current_url = stream_url - current_redirects = 0 - - loop do - # Directly download the stream without any additional settings _except_ for `omit_default_port: true` which - # is necessary to not break the URL signing that some NuGet feeds use. - response = Excon.get( - current_url, - headers: auth_header, - omit_default_port: true - ) - - # redirect the HTTP response as appropriate based on documentation here: - # https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections - case response.status - when 200 - return response.body - when 301, 302, 303, 307, 308 - current_redirects += 1 - return nil if current_redirects > max_redirects - - current_url = T.must(response.headers["Location"]) - else - return nil - end - end - end - - sig do - params( - url: String, - repository_details: T::Hash[Symbol, T.untyped] - ) - .returns(T.nilable(Excon::Response)) - end - def self.fetch_url(url, repository_details) - fetch_url_with_auth(url, repository_details.fetch(:auth_header)) - end - - sig { params(url: String, auth_header: T::Hash[T.any(String, Symbol), T.untyped]).returns(Excon::Response) } - def self.fetch_url_with_auth(url, auth_header) - cache = CacheManager.cache("nupkg_fetcher_cache") - cache[url] ||= Dependabot::RegistryClient.get( - url: url, - headers: auth_header - ) - - cache[url] - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb b/nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb deleted file mode 100644 index d07d4412331..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +++ /dev/null @@ -1,110 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "nokogiri" -require "stringio" -require "sorbet-runtime" -require "zip" - -module Dependabot - module Nuget - class NuspecFetcher - extend T::Sig - - require_relative "nupkg_fetcher" - require_relative "repository_finder" - - sig do - params( - dependency_urls: T::Array[T::Hash[Symbol, String]], - package_id: String, - package_version: T.nilable(String) - ) - .returns(T.nilable(Nokogiri::XML::Document)) - end - def self.fetch_nuspec(dependency_urls, package_id, package_version) - # check all repositories for the first one that has the nuspec - dependency_urls.reduce(T.let(nil, T.nilable(Nokogiri::XML::Document))) do |nuspec_xml, repository_details| - nuspec_xml || fetch_nuspec_from_repository(repository_details, package_id, package_version) - end - end - - sig do - params( - repository_details: T::Hash[Symbol, T.untyped], - package_id: T.nilable(String), - package_version: T.nilable(String) - ) - .returns(T.nilable(Nokogiri::XML::Document)) - end - def self.fetch_nuspec_from_repository(repository_details, package_id, package_version) - return unless package_id && package_version && !package_version.empty? - - feed_url = repository_details[:repository_url] - auth_header = repository_details[:auth_header] - - nuspec_xml = nil - - if feed_supports_nuspec_download?(feed_url) - # we can use the normal nuget apis to get the nuspec and list out the dependencies - base_url = repository_details[:base_url].delete_suffix("/") - package_id_downcased = package_id.downcase - nuspec_url = "#{base_url}/#{package_id_downcased}/#{package_version}/#{package_id_downcased}.nuspec" - - nuspec_response = Dependabot::RegistryClient.get( - url: nuspec_url, - headers: auth_header - ) - - return unless nuspec_response.status == 200 - - nuspec_response_body = remove_invalid_characters(nuspec_response.body) - nuspec_xml = Nokogiri::XML(nuspec_response_body) - else - # no guarantee we can directly query the .nuspec; fall back to extracting it from the .nupkg - package_data = NupkgFetcher.fetch_nupkg_buffer_from_repository(repository_details, package_id, - package_version) - return if package_data.nil? - - nuspec_string = extract_nuspec(package_data, package_id) - nuspec_xml = Nokogiri::XML(nuspec_string) - end - - nuspec_xml.remove_namespaces! - nuspec_xml - end - - sig { params(feed_url: String).returns(T::Boolean) } - def self.feed_supports_nuspec_download?(feed_url) - feed_regexs = [ - # nuget - %r{https://api\.nuget\.org/v3/index\.json}, - # azure devops - %r{https://pkgs\.dev\.azure\.com/(?[^/]+)/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json}, - %r{https://pkgs\.dev\.azure\.com/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json(?)}, - %r{https://(?[^\.\/]+)\.pkgs\.visualstudio\.com/_packaging/(?[^/]+)/nuget/v3/index\.json(?)} - ] - feed_regexs.any? { |reg| reg.match(feed_url) } - end - - sig { params(zip_stream: String, package_id: String).returns(T.nilable(String)) } - def self.extract_nuspec(zip_stream, package_id) - Zip::File.open_buffer(zip_stream) do |zip| - nuspec_entry = zip.find { |entry| entry.name == "#{package_id}.nuspec" } - return nuspec_entry.get_input_stream.read if nuspec_entry - end - nil - end - - sig { params(string: String).returns(String) } - def self.remove_invalid_characters(string) - string.dup - .force_encoding(Encoding::UTF_8) - .encode - .scrub("") - .gsub(/\A[\u200B-\u200D\uFEFF]/, "") - .gsub(/[\u200B-\u200D\uFEFF]\Z/, "") - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb b/nuget/lib/dependabot/nuget/update_checker/property_updater.rb deleted file mode 100644 index 836c20fa78f..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb +++ /dev/null @@ -1,195 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/file_parser" - -module Dependabot - module Nuget - class UpdateChecker < Dependabot::UpdateCheckers::Base - class PropertyUpdater - extend T::Sig - - require_relative "version_finder" - require_relative "requirements_updater" - require_relative "dependency_finder" - - sig do - params( - dependency: Dependabot::Dependency, - dependency_files: T::Array[Dependabot::DependencyFile], - credentials: T::Array[Dependabot::Credential], - target_version_details: T.nilable(T::Hash[Symbol, String]), - ignored_versions: T::Array[String], - repo_contents_path: T.nilable(String), - raise_on_ignored: T::Boolean - ).void - end - def initialize(dependency:, dependency_files:, credentials:, - target_version_details:, ignored_versions:, - repo_contents_path:, raise_on_ignored: false) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @ignored_versions = ignored_versions - @raise_on_ignored = raise_on_ignored - @target_version = T.let( - target_version_details&.fetch(:version), - T.nilable(T.any(String, Dependabot::Nuget::Version)) - ) - @source_details = T.let( - target_version_details&.slice(:nuspec_url, :repo_url, :source_url), - T.nilable(T::Hash[Symbol, String]) - ) - @repo_contents_path = repo_contents_path - end - - sig { returns(T::Boolean) } - def update_possible? - return false unless target_version - - @update_possible ||= T.let( - dependencies_using_property.all? do |dep| - versions = VersionFinder.new( - dependency: dep, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored, - security_advisories: [], - repo_contents_path: repo_contents_path - ).versions.map { |v| v.fetch(:version) } - - versions.include?(target_version) || versions.none? - end, - T.nilable(T::Boolean) - ) - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def updated_dependencies - raise "Update not possible!" unless update_possible? - - @updated_dependencies ||= T.let( - begin - dependencies = T.let({}, T::Hash[String, Dependabot::Dependency]) - - dependencies_using_property.each do |dep| - # Only keep one copy of each dependency, the one with the highest target version. - visited_dependency = dependencies[dep.name.downcase] - next unless visited_dependency.nil? || T.must(visited_dependency.numeric_version) < target_version - - updated_dependency = Dependency.new( - name: dep.name, - version: target_version.to_s, - requirements: updated_requirements(dep), - previous_version: dep.version, - previous_requirements: dep.requirements, - package_manager: dep.package_manager - ) - dependencies[updated_dependency.name.downcase] = updated_dependency - # Add peer dependencies to the list of updated dependencies. - process_updated_peer_dependencies(updated_dependency, dependencies) - end - - dependencies.map { |_, dependency| dependency } - end, - T.nilable(T::Array[Dependabot::Dependency]) - ) - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T.nilable(T.any(String, Dependabot::Nuget::Version))) } - attr_reader :target_version - - sig { returns(T.nilable(T::Hash[Symbol, String])) } - attr_reader :source_details - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T::Array[String]) } - attr_reader :ignored_versions - - sig { returns(T.nilable(String)) } - attr_reader :repo_contents_path - - sig do - params( - dependency: Dependabot::Dependency, - dependencies: T::Hash[String, Dependabot::Dependency] - ) - .returns(T::Array[Dependabot::Dependency]) - end - def process_updated_peer_dependencies(dependency, dependencies) - DependencyFinder.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path - ).updated_peer_dependencies.each do |peer_dependency| - # Only keep one copy of each dependency, the one with the highest target version. - visited_dependency = dependencies[peer_dependency.name.downcase] - unless visited_dependency.nil? || - T.must(visited_dependency.numeric_version) < peer_dependency.numeric_version - next - end - - dependencies[peer_dependency.name.downcase] = peer_dependency - end - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def dependencies_using_property - @dependencies_using_property ||= - T.let( - Nuget::FileParser.new( - dependency_files: dependency_files, - repo_contents_path: repo_contents_path, - source: nil - ).parse.select do |dep| - dep.requirements.any? do |r| - r.dig(:metadata, :property_name) == property_name - end - end, - T.nilable(T::Array[Dependabot::Dependency]) - ) - end - - sig { returns(String) } - def property_name - @property_name ||= T.let( - dependency.requirements - .find { |r| r.dig(:metadata, :property_name) } - &.dig(:metadata, :property_name), - T.nilable(String) - ) - - raise "No requirement with a property name!" unless @property_name - - @property_name - end - - sig { params(dep: Dependabot::Dependency).returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def updated_requirements(dep) - @updated_requirements ||= T.let({}, T.nilable(T::Hash[String, T::Array[T::Hash[Symbol, T.untyped]]])) - @updated_requirements[dep.name] ||= - RequirementsUpdater.new( - requirements: dep.requirements, - latest_version: target_version, - source_details: source_details - ).updated_requirements - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb b/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb deleted file mode 100644 index 162e491659e..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb +++ /dev/null @@ -1,442 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "excon" -require "nokogiri" -require "sorbet-runtime" - -require "dependabot/errors" -require "dependabot/update_checkers/base" -require "dependabot/registry_client" -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/http_response_helpers" - -module Dependabot - module Nuget - class RepositoryFinder - extend T::Sig - - DEFAULT_REPOSITORY_URL = "https://api.nuget.org/v3/index.json" - DEFAULT_REPOSITORY_API_KEY = "nuget.org" - - sig do - params( - dependency: Dependabot::Dependency, - credentials: T::Array[Dependabot::Credential], - config_files: T::Array[Dependabot::DependencyFile] - ).void - end - def initialize(dependency:, credentials:, config_files: []) - @dependency = dependency - @credentials = credentials - @config_files = config_files - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def dependency_urls - find_dependency_urls - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def known_repositories - return @known_repositories if @known_repositories - - @known_repositories ||= T.let([], T.nilable(T::Array[T::Hash[Symbol, String]])) - @known_repositories += credential_repositories - @known_repositories += config_file_repositories - - @known_repositories << { url: DEFAULT_REPOSITORY_URL, token: nil } if @known_repositories.empty? - - @known_repositories = @known_repositories.map do |repo| - { url: URI::DEFAULT_PARSER.escape(repo[:url]), token: repo[:token] } - end - @known_repositories.uniq - end - - sig { params(dependency_name: String).returns(T::Hash[Symbol, T.untyped]) } - def self.get_default_repository_details(dependency_name) - { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json", - repository_url: DEFAULT_REPOSITORY_URL, - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "#{dependency_name.downcase}/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=#{dependency_name.downcase}&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - } - end - - private - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :config_files - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def find_dependency_urls - @find_dependency_urls ||= - T.let( - known_repositories.flat_map do |details| - if details.fetch(:url) == DEFAULT_REPOSITORY_URL - # Save a request for the default URL, since we already know how - # it addresses packages - next default_repository_details - end - - build_url_for_details(details) - end.compact.uniq, - T.nilable(T::Array[T::Hash[Symbol, T.untyped]]) - ) - end - - sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def build_url_for_details(repo_details) - url = repo_details.fetch(:url) - url_obj = URI.parse(url) - if url_obj.is_a?(URI::HTTP) - details = build_url_for_details_remote(repo_details) - elsif url_obj.is_a?(URI::File) - details = { - base_url: url, - repository_type: "local" - } - end - - details - end - - sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def build_url_for_details_remote(repo_details) - response = get_repo_metadata(repo_details) - check_repo_response(response, repo_details) - return unless response.status == 200 - - body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body) - parsed_json = JSON.parse(body) - base_url = base_url_from_v3_metadata(parsed_json) - search_url = search_url_from_v3_metadata(parsed_json) - registration_url = registration_url_from_v3_metadata(parsed_json) - - details = { - base_url: base_url, - repository_url: repo_details.fetch(:url), - auth_header: auth_header_for_token(repo_details.fetch(:token)), - repository_type: "v3" - } - if base_url - details[:versions_url] = - File.join(base_url, dependency.name.downcase, "index.json") - end - if search_url - details[:search_url] = - search_url + "?q=#{dependency.name.downcase}&prerelease=true&semVerLevel=2.0.0" - end - - if registration_url - details[:registration_url] = File.join(registration_url, dependency.name.downcase, "index.json") - end - - details - rescue JSON::ParserError - build_v2_url(T.must(response), repo_details) - rescue Excon::Error::Timeout, Excon::Error::Socket - handle_timeout(repo_metadata_url: repo_details.fetch(:url)) - end - - sig { params(repo_details: T::Hash[Symbol, T.untyped]).returns(Excon::Response) } - def get_repo_metadata(repo_details) - url = repo_details.fetch(:url) - cache = CacheManager.cache("repo_finder_metadatacache") - if cache[url] - cache[url] - else - result = Dependabot::RegistryClient.get( - url: url, - headers: auth_header_for_token(repo_details.fetch(:token)) - ) - cache[url] = result - result - end - end - - sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) } - def base_url_from_v3_metadata(metadata) - metadata - .fetch("resources", []) - .find { |r| r.fetch("@type") == "PackageBaseAddress/3.0.0" } - &.fetch("@id") - end - - sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) } - def registration_url_from_v3_metadata(metadata) - allowed_registration_types = %w( - RegistrationsBaseUrl - RegistrationsBaseUrl/3.0.0-beta - RegistrationsBaseUrl/3.0.0-rc - RegistrationsBaseUrl/3.4.0 - RegistrationsBaseUrl/3.6.0 - ) - metadata - .fetch("resources", []) - .find { |r| allowed_registration_types.find { |s| r.fetch("@type") == s } } - &.fetch("@id") - end - - sig { params(metadata: T::Hash[String, T::Array[T::Hash[String, T.untyped]]]).returns(T.nilable(String)) } - def search_url_from_v3_metadata(metadata) - # allowable values from here: https://learn.microsoft.com/en-us/nuget/api/search-query-service-resource#versioning - allowed_search_types = %w( - SearchQueryService - SearchQueryService/3.0.0-beta - SearchQueryService/3.0.0-rc - SearchQueryService/3.5.0 - ) - metadata - .fetch("resources", []) - .find { |r| allowed_search_types.find { |s| r.fetch("@type") == s } } - &.fetch("@id") - end - - sig do - params( - response: Excon::Response, - repo_details: T::Hash[Symbol, T.untyped] - ) - .returns(T::Hash[Symbol, T.untyped]) - end - def build_v2_url(response, repo_details) - doc = Nokogiri::XML(response.body) - - doc.remove_namespaces! - base_url = doc.at_xpath("service")&.attributes - &.fetch("base", nil)&.value - - base_url ||= repo_details.fetch(:url) - - { - base_url: base_url, - repository_url: base_url, - versions_url: File.join( - base_url.delete_suffix("/"), - "FindPackagesById()?id='#{dependency.name}'" - ), - auth_header: auth_header_for_token(repo_details.fetch(:token)), - repository_type: "v2" - } - end - - sig { params(response: Excon::Response, details: T::Hash[Symbol, T.untyped]).void } - def check_repo_response(response, details) - return unless [401, 402, 403].include?(response.status) - raise if details.fetch(:url) == DEFAULT_REPOSITORY_URL - - raise PrivateSourceAuthenticationFailure, details.fetch(:url) - end - - sig { params(repo_metadata_url: String).returns(T.noreturn) } - def handle_timeout(repo_metadata_url:) - raise if repo_metadata_url == DEFAULT_REPOSITORY_URL - - raise PrivateSourceTimedOut, repo_metadata_url - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def credential_repositories - @credential_repositories ||= - T.let( - credentials - .select { |cred| cred["type"] == "nuget_feed" && cred["url"] } - .map { |c| { url: c.fetch("url"), token: c["token"] } }, - T.nilable(T::Array[T::Hash[Symbol, String]]) - ) - end - - sig { returns(T::Array[T::Hash[Symbol, String]]) } - def config_file_repositories - config_files.flat_map { |file| repos_from_config_file(file) } - end - - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - # rubocop:disable Metrics/MethodLength - # rubocop:disable Metrics/AbcSize - sig { params(config_file: Dependabot::DependencyFile).returns(T::Array[T::Hash[Symbol, String]]) } - def repos_from_config_file(config_file) - doc = Nokogiri::XML(config_file.content) - doc.remove_namespaces! - # analogous to having a root config with the default repository - base_sources = [{ url: DEFAULT_REPOSITORY_URL, key: "nuget.org" }] - - sources = T.let([], T::Array[T::Hash[Symbol, String]]) - - # regular package sources - doc.css("configuration > packageSources").children.each do |node| - if node.name == "clear" - sources.clear - base_sources.clear - else - key = node.attribute("key")&.value&.strip || node.at_xpath("./key")&.content&.strip - url = node.attribute("value")&.value&.strip || node.at_xpath("./value")&.content&.strip - url = expand_windows_style_environment_variables(url) if url - - # if the path isn't absolute it's relative to the nuget.config file - if url - unless url.include?("://") || Pathname.new(url).absolute? - url = Pathname(config_file.directory).join(url).to_path - end - sources << { url: url, key: key } - end - end - end - - # signed package sources - # https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#trustedsigners-section - doc.xpath("/configuration/trustedSigners/repository").each do |node| - name = node.attribute("name")&.value&.strip - service_index = node.attribute("serviceIndex")&.value&.strip - sources << { url: service_index, key: name } - end - - sources += base_sources # TODO: quirky overwrite behavior - disabled_sources = disabled_sources(doc) - sources.reject! do |s| - disabled_sources.include?(s[:key]) - end - - sources.reject! do |s| - known_urls = credential_repositories.map { |cr| cr.fetch(:url) } - known_urls.include?(s.fetch(:url)) - end - - # filter out based on packageSourceMapping - package_mapping_elements = doc.xpath("/configuration/packageSourceMapping/packageSource/package[@pattern]") - matching_package_elements = package_mapping_elements.select do |package_element| - pattern = package_element.attribute("pattern").value - - # reusing this function for a case insensitive GLOB pattern patch (e.g., "Microsoft.Azure.*") - File.fnmatch(pattern, @dependency.name, File::FNM_CASEFOLD) - end - longest_matching_package_element = matching_package_elements.max_by do |package_element| - package_element.attribute("pattern").value.length - end - matching_key = longest_matching_package_element&.parent&.attribute("key")&.value - if matching_key - # found a matching source, only keep that one - sources.select! { |s| s.fetch(:key) == matching_key } - end - - add_config_file_credentials(sources: sources, doc: doc) - sources.each { |details| details.delete(:key) } - - sources - end - # rubocop:enable Metrics/AbcSize - # rubocop:enable Metrics/MethodLength - # rubocop:enable Metrics/PerceivedComplexity - # rubocop:enable Metrics/CyclomaticComplexity - - sig { returns(T::Hash[Symbol, T.untyped]) } - def default_repository_details - RepositoryFinder.get_default_repository_details(dependency.name) - end - - # rubocop:disable Metrics/PerceivedComplexity - sig { params(doc: Nokogiri::XML::Document).returns(T::Array[String]) } - def disabled_sources(doc) - doc.css("configuration > disabledPackageSources > add").filter_map do |node| - value = node.attribute("value")&.value || - node.at_xpath("./value")&.content - - if value&.strip&.downcase == "true" - node.attribute("key")&.value&.strip || - node.at_xpath("./key")&.content&.strip - end - end - end - # rubocop:enable Metrics/PerceivedComplexity - - # rubocop:disable Metrics/PerceivedComplexity - sig do - params( - sources: T::Array[T::Hash[Symbol, T.nilable(String)]], - doc: Nokogiri::XML::Document - ) - .void - end - def add_config_file_credentials(sources:, doc:) - sources.each do |source_details| - key = source_details.fetch(:key) - next source_details[:token] = nil unless key - next source_details[:token] = nil if key.match?(/^\d/) - - tag = key.gsub(" ", "_x0020_") - creds_nodes = doc.css("configuration > packageSourceCredentials " \ - "> #{tag} > add") - - username = - creds_nodes - .find { |n| n.attribute("key")&.value == "Username" } - &.attribute("value")&.value - password = - creds_nodes - .find { |n| n.attribute("key")&.value == "ClearTextPassword" } - &.attribute("value")&.value - - # NOTE: We have to look for plain text passwords, as we have no - # way of decrypting encrypted passwords. For the same reason we - # don't fetch API keys from the nuget.config at all. - next source_details[:token] = nil unless username && password - - expanded_username = expand_windows_style_environment_variables(username) - expanded_password = expand_windows_style_environment_variables(password) - source_details[:token] = "#{expanded_username}:#{expanded_password}" - rescue Nokogiri::XML::XPath::SyntaxError - # Any non-ascii characters in the tag with cause a syntax error - next source_details[:token] = nil - end - end - # rubocop:enable Metrics/PerceivedComplexity - - sig { params(string: String).returns(String) } - def expand_windows_style_environment_variables(string) - # NuGet.Config files can have Windows-style environment variables that need to be replaced - # https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#using-environment-variables - string.gsub(/%([^%]+)%/) do - environment_variable_name = T.must(::Regexp.last_match(1)) - environment_variable_value = ENV.fetch(environment_variable_name, nil) - if environment_variable_value - environment_variable_value - else - # report that the variable couldn't be expanded, then replace it as-is - Dependabot.logger.warn <<~WARN - The variable '%#{environment_variable_name}%' could not be expanded in NuGet.Config - WARN - "%#{environment_variable_name}%" - end - end - end - - sig { params(token: T.nilable(String)).returns(T::Hash[String, String]) } - def auth_header_for_token(token) - return {} unless token - - if token.include?(":") - encoded_token = Base64.encode64(token).delete("\n") - { "Authorization" => "Basic #{encoded_token}" } - elsif Base64.decode64(token).ascii_only? && - Base64.decode64(token).include?(":") - { "Authorization" => "Basic #{token.delete("\n")}" } - else - { "Authorization" => "Bearer #{token}" } - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb b/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb index fe600921c1c..d475bc87d1f 100644 --- a/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb +++ b/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb @@ -20,14 +20,12 @@ class RequirementsUpdater sig do params( requirements: T::Array[T::Hash[Symbol, T.untyped]], - latest_version: T.nilable(T.any(String, Dependabot::Nuget::Version)), - source_details: T.nilable(T::Hash[Symbol, T.untyped]) + latest_version: T.nilable(T.any(String, Dependabot::Nuget::Version)) ) .void end - def initialize(requirements:, latest_version:, source_details:) + def initialize(requirements:, latest_version:) @requirements = requirements - @source_details = source_details return unless latest_version @latest_version = T.let(version_class.new(latest_version), Dependabot::Nuget::Version) @@ -59,7 +57,7 @@ def updated_requirements next req if new_req == req.fetch(:requirement) - req.merge(requirement: new_req, source: updated_source) + req.merge(requirement: new_req) end end @@ -71,9 +69,6 @@ def updated_requirements sig { returns(T.nilable(Dependabot::Nuget::Version)) } attr_reader :latest_version - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - attr_reader :source_details - sig { returns(T.class_of(Dependabot::Nuget::Version)) } def version_class Dependabot::Nuget::Version @@ -93,16 +88,6 @@ def update_wildcard_requirement(req_string) version + wildcard_section end - - sig { returns(T::Hash[Symbol, T.untyped]) } - def updated_source - { - type: "nuget_repo", - url: source_details&.fetch(:repo_url), - nuspec_url: source_details&.fetch(:nuspec_url), - source_url: source_details&.fetch(:source_url) - } - end end end end diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb deleted file mode 100644 index 0cfbe08faf9..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_comparer.rb +++ /dev/null @@ -1,34 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/version" -require "dependabot/nuget/requirement" -require "dependabot/nuget/native_helpers" -require "dependabot/shared_helpers" - -module Dependabot - module Nuget - class TfmComparer - extend T::Sig - - sig { params(project_tfms: T::Array[String], package_tfms: T::Array[String]).returns(T::Boolean) } - def self.are_frameworks_compatible?(project_tfms, package_tfms) - return false if package_tfms.empty? - return false if project_tfms.empty? - - key = "project_ftms:#{project_tfms.sort.join(',')}:package_tfms:#{package_tfms.sort.join(',')}".downcase - - @cached_framework_check ||= T.let({}, T.nilable(T::Hash[String, T::Boolean])) - unless @cached_framework_check.key?(key) - @cached_framework_check[key] = - NativeHelpers.run_nuget_framework_check(project_tfms, - package_tfms) - end - T.must(@cached_framework_check[key]) - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb deleted file mode 100644 index aadb0e83efd..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb +++ /dev/null @@ -1,30 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/discovery/discovery_json_reader" - -module Dependabot - module Nuget - class TfmFinder - extend T::Sig - - sig { params(dependency: Dependency).returns(T::Array[String]) } - def self.frameworks(dependency) - discovery_json = DiscoveryJsonReader.discovery_json - return [] unless discovery_json - - workspace = DiscoveryJsonReader.new( - discovery_json: discovery_json - ).workspace_discovery - return [] unless workspace - - workspace.projects.select do |project| - all_dependencies = project.dependencies + project.referenced_project_paths.flat_map do |ref| - workspace.projects.find { |p| p.file_path == ref }&.dependencies || [] - end - all_dependencies.any? { |d| d.name.casecmp?(dependency.name) } - end.flat_map(&:target_frameworks).uniq - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb b/nuget/lib/dependabot/nuget/update_checker/version_finder.rb deleted file mode 100644 index 8b66eff3d1b..00000000000 --- a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb +++ /dev/null @@ -1,450 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "sorbet-runtime" - -require "dependabot/nuget/version" -require "dependabot/nuget/requirement" -require "dependabot/update_checkers/base" -require "dependabot/update_checkers/version_filters" -require "dependabot/nuget/nuget_client" - -module Dependabot - module Nuget - class UpdateChecker < Dependabot::UpdateCheckers::Base - # rubocop:disable Metrics/ClassLength - class VersionFinder - extend T::Sig - - require_relative "compatibility_checker" - require_relative "repository_finder" - - NUGET_RANGE_REGEX = /[\(\[].*,.*[\)\]]/ - - sig do - params( - dependency: Dependabot::Dependency, - dependency_files: T::Array[Dependabot::DependencyFile], - credentials: T::Array[Dependabot::Credential], - ignored_versions: T::Array[String], - security_advisories: T::Array[Dependabot::SecurityAdvisory], - repo_contents_path: T.nilable(String), - raise_on_ignored: T::Boolean - ).void - end - def initialize(dependency:, - dependency_files:, - credentials:, - ignored_versions:, - security_advisories:, - repo_contents_path:, - raise_on_ignored: false) - @dependency = dependency - @dependency_files = dependency_files - @credentials = credentials - @ignored_versions = ignored_versions - @raise_on_ignored = raise_on_ignored - @security_advisories = security_advisories - @repo_contents_path = repo_contents_path - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def latest_version_details - @latest_version_details ||= - T.let( - begin - possible_versions = versions - possible_versions = filter_prereleases(possible_versions) - possible_versions = filter_ignored_versions(possible_versions) - - find_highest_compatible_version(possible_versions) - end, - T.nilable(T::Hash[Symbol, T.untyped]) - ) - end - - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def lowest_security_fix_version_details - @lowest_security_fix_version_details ||= - T.let( - begin - possible_versions = versions - possible_versions = filter_prereleases(possible_versions) - possible_versions = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions( - possible_versions, security_advisories - ) - possible_versions = filter_ignored_versions(possible_versions) - possible_versions = filter_lower_versions(possible_versions) - - find_lowest_compatible_version(possible_versions) - end, - T.nilable(T::Hash[Symbol, T.untyped]) - ) - end - - sig { returns(T::Array[T::Hash[Symbol, T.nilable(T.any(Dependabot::Version, String))]]) } - def versions - available_v3_versions + available_v2_versions - end - - sig { returns(Dependabot::Dependency) } - attr_reader :dependency - - sig { returns(T::Array[Dependabot::DependencyFile]) } - attr_reader :dependency_files - - sig { returns(T::Array[Dependabot::Credential]) } - attr_reader :credentials - - sig { returns(T::Array[String]) } - attr_reader :ignored_versions - - sig { returns(T::Array[Dependabot::SecurityAdvisory]) } - attr_reader :security_advisories - - sig { returns(T.nilable(String)) } - attr_reader :repo_contents_path - - private - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_highest_compatible_version(possible_versions) - # sorted versions descending - sorted_versions = possible_versions.sort_by { |v| v.fetch(:version) }.reverse - find_compatible_version(sorted_versions) - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_lowest_compatible_version(possible_versions) - # sorted versions ascending - sorted_versions = possible_versions.sort_by { |v| v.fetch(:version) } - find_compatible_version(sorted_versions) - end - - sig do - params(sorted_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def find_compatible_version(sorted_versions) - # By checking the first version separately, we can avoid additional network requests - first_version = sorted_versions.first - return unless first_version - # If the current package version is incompatible, then we don't enforce compatibility. - # It could appear incompatible because they are ignoring NU1701 or the package is poorly authored. - return first_version unless version_compatible?(dependency.version) - - # once sorted by version, the best we can do is search every package, because it's entirely possible for there - # to be incompatible packages both with a higher and lower version number, so no smart searching can be done. - sorted_versions.find { |v| version_compatible?(v.fetch(:version)) } - end - - sig { params(version: T.nilable(T.any(Dependabot::Version, String))).returns(T::Boolean) } - def version_compatible?(version) - str_version_compatible?(version.to_s) - end - - sig { params(version: String).returns(T::Boolean) } - def str_version_compatible?(version) - compatibility_checker.compatible?(version) - end - - sig { returns(Dependabot::Nuget::CompatibilityChecker) } - def compatibility_checker - @compatibility_checker ||= - T.let( - CompatibilityChecker.new( - dependency_urls: dependency_urls, - dependency: dependency - ), - T.nilable(Dependabot::Nuget::CompatibilityChecker) - ) - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T::Array[T::Hash[Symbol, T.untyped]]) - end - def filter_prereleases(possible_versions) - filtered = possible_versions.reject do |d| - version = d.fetch(:version) - version.prerelease? && !related_to_current_pre?(version) - end - if possible_versions.count > filtered.count - Dependabot.logger.info("Filtered out #{possible_versions.count - filtered.count} pre-release versions") - end - filtered - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T::Array[T::Hash[Symbol, T.untyped]]) - end - def filter_ignored_versions(possible_versions) - filtered = possible_versions - - ignored_versions.each do |req| - ignore_req = requirement_class.new(parse_requirement_string(req)) - filtered = - filtered - .reject { |v| ignore_req.satisfied_by?(v.fetch(:version)) } - end - - if @raise_on_ignored && filter_lower_versions(filtered).empty? && - filter_lower_versions(possible_versions).any? - raise AllVersionsIgnored - end - - if possible_versions.count > filtered.count - Dependabot.logger.info("Filtered out #{possible_versions.count - filtered.count} ignored versions") - end - - filtered - end - - sig do - params(possible_versions: T::Array[T::Hash[Symbol, T.untyped]]) - .returns(T::Array[T::Hash[Symbol, T.untyped]]) - end - def filter_lower_versions(possible_versions) - return possible_versions unless dependency.numeric_version - - possible_versions.select do |v| - v.fetch(:version) > dependency.numeric_version - end - end - - sig { params(string: String).returns(T::Array[String]) } - def parse_requirement_string(string) - return [string] if string.match?(NUGET_RANGE_REGEX) - - string.split(",").map(&:strip) - end - - sig { returns(T::Array[T::Hash[Symbol, T.any(Dependabot::Version, String, NilClass)]]) } - def available_v3_versions - v3_nuget_listings.flat_map do |listing| - listing - .fetch("versions", []) - .map do |v| - listing_details = listing.fetch("listing_details") - nuspec_url = listing_details - .fetch(:versions_url, nil) - &.gsub(/index\.json$/, "#{v}/#{sanitized_name}.nuspec") - - { - version: version_class.new(v), - nuspec_url: nuspec_url, - source_url: nil, - repo_url: listing_details.fetch(:repository_url) - } - end - end - end - - sig { returns(T::Array[T::Hash[Symbol, T.any(Dependabot::Version, String, NilClass)]]) } - def available_v2_versions - v2_nuget_listings.flat_map do |listing| - body = listing.fetch("xml_body", []) - doc = Nokogiri::XML(body) - doc.remove_namespaces! - - doc.xpath("/feed/entry").filter_map do |entry| - listed = entry.at_xpath("./properties/Listed")&.content&.strip - next if listed&.casecmp("false")&.zero? - - entry_details = dependency_details_from_v2_entry(entry) - entry_details.merge( - repo_url: listing.fetch("listing_details") - .fetch(:repository_url) - ) - end - end - end - - sig do - params(entry: Nokogiri::XML::Element) - .returns(T::Hash[Symbol, T.any(Dependabot::Version, String, NilClass)]) - end - def dependency_details_from_v2_entry(entry) - version = entry.at_xpath("./properties/Version").content.strip - source_urls = [] - [ - entry.at_xpath("./properties/ProjectUrl")&.content, - entry.at_xpath("./properties/ReleaseNotes")&.content - ].compact.join(" ").scan(Source::SOURCE_REGEX) do - source_urls << Regexp.last_match.to_s - end - - source_url = source_urls.find { |url| Source.from_url(url) } - source_url = Source.from_url(source_url)&.url if source_url - - { - version: version_class.new(version), - nuspec_url: nil, - source_url: source_url - } - end - - # rubocop:disable Metrics/PerceivedComplexity - sig { params(version: Dependabot::Version).returns(T::Boolean) } - def related_to_current_pre?(version) - current_version = dependency.numeric_version - if current_version&.prerelease? && - current_version.release == version.release - return true - end - - dependency.requirements.any? do |req| - reqs = parse_requirement_string(req.fetch(:requirement) || "") - return true if reqs.any?("*-*") - next unless reqs.any? { |r| r.include?("-") } - - requirement_class - .requirements_array(req.fetch(:requirement)) - .any? do |r| - r.requirements.any? { |a| a.last.release == version.release } - end - rescue Gem::Requirement::BadRequirementError - false - end - end - # rubocop:enable Metrics/PerceivedComplexity - - sig { returns(T::Array[T::Hash[String, T.untyped]]) } - def v3_nuget_listings - @v3_nuget_listings ||= - T.let( - dependency_urls - .select { |details| details.fetch(:repository_type) == "v3" } - .filter_map do |url_details| - versions = NugetClient.get_package_versions(dependency.name, url_details) - next unless versions - - { "versions" => versions, "listing_details" => url_details } - end, - T.nilable(T::Array[T::Hash[String, T.untyped]]) - ) - end - - sig { returns(T::Array[T::Hash[String, T.untyped]]) } - def v2_nuget_listings - @v2_nuget_listings ||= - T.let( - dependency_urls - .select { |details| details.fetch(:repository_type) == "v2" } - .flat_map { |url_details| fetch_paginated_v2_nuget_listings(url_details) } - .filter_map do |url_details, response| - next unless response.status == 200 - - { - "xml_body" => response.body, - "listing_details" => url_details - } - end, - T.nilable(T::Array[T::Hash[String, T.untyped]]) - ) - end - - sig do - params( - url_details: T::Hash[Symbol, T.untyped], - results: T::Hash[T::Hash[Symbol, T.untyped], Excon::Response] - ) - .returns(T::Array[T::Array[T.untyped]]) - end - def fetch_paginated_v2_nuget_listings(url_details, results = {}) - response = Dependabot::RegistryClient.get( - url: url_details[:versions_url], - headers: url_details[:auth_header] - ) - - # NOTE: Short circuit if we get a circular next link - return results.to_a if results.key?(url_details) - - results[url_details] = response - - if (link_href = fetch_v2_next_link_href(response.body)) - url_details = url_details.dup - # Some Nuget repositories, such as JFrog's Artifactory, URL encode the "next" href - # link in the paged results. If the href is not URL decoded, the paging parameters - # are ignored and the first page is always returned. - url_details[:versions_url] = CGI.unescape(link_href) - fetch_paginated_v2_nuget_listings(url_details, results) - end - - results.to_a - end - - sig { params(xml_body: String).returns(T.nilable(String)) } - def fetch_v2_next_link_href(xml_body) - doc = Nokogiri::XML(xml_body) - doc.remove_namespaces! - link_node = doc.xpath("/feed/link").find do |node| - rel = node.attribute("rel").value.strip - rel == "next" - end - link_node.attribute("href").value.strip if link_node - rescue Nokogiri::XML::XPath::SyntaxError - nil - end - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def dependency_urls - @dependency_urls ||= - T.let( - RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: nuget_configs - ).dependency_urls, - T.nilable(T::Array[T::Hash[Symbol, T.untyped]]) - ) - end - - sig { returns(T::Array[Dependabot::DependencyFile]) } - def nuget_configs - @nuget_configs ||= - T.let( - dependency_files.select { |f| f.name.match?(/nuget\.config$/i) }, - T.nilable(T::Array[Dependabot::DependencyFile]) - ) - end - - sig { returns(String) } - def sanitized_name - dependency.name.downcase - end - - sig { returns(T.class_of(Gem::Version)) } - def version_class - dependency.version_class - end - - sig { returns(T.class_of(Dependabot::Requirement)) } - def requirement_class - dependency.requirement_class - end - - sig { returns(T::Hash[Symbol, Integer]) } - def excon_options - # For large JSON files we sometimes need a little longer than for - # other languages. For example, see: - # https://dotnet.myget.org/F/aspnetcore-dev/api/v3/query? - # q=microsoft.aspnetcore.mvc&prerelease=true&semVerLevel=2.0.0 - { - connect_timeout: 30, - write_timeout: 30, - read_timeout: 30 - } - end - end - # rubocop:enable Metrics/ClassLength - end - end -end From 5759f311b0497627d69c913282c69edd72daade0 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Fri, 12 Apr 2024 15:31:19 -0700 Subject: [PATCH 02/13] Add first .NET test for analyze --- .../Analyze/AnalyzeWorkerTestBase.cs | 85 +++++++++++++++++++ .../Analyze/AnalyzeWorkerTests.cs | 46 ++++++++++ .../Analyze/ExpectedAnalysisResult.cs | 8 ++ .../Analyze/AnalysisResult.cs | 2 +- .../Analyze/AnalyzeWorker.cs | 1 + 5 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs index e69de29bb2d..6cbb6bb96b2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs @@ -0,0 +1,85 @@ +using System.Collections.Immutable; +using System.Text.Json; + +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Discover; +using NuGetUpdater.Core.Test.Utilities; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +using TestFile = (string Path, string Content); + +public class AnalyzeWorkerTestBase +{ + protected static async Task TestAnalyzeAsync( + WorkspaceDiscoveryResult discovery, + DependencyInfo dependencyInfo, + ExpectedAnalysisResult expectedResult) + { + var relativeDependencyPath = $"./dependabot/dependency/{dependencyInfo.Name}.json"; + + TestFile[] files = [ + (DiscoveryWorker.DiscoveryResultFileName, JsonSerializer.Serialize(discovery, AnalyzeWorker.SerializerOptions)), + (relativeDependencyPath, JsonSerializer.Serialize(dependencyInfo, AnalyzeWorker.SerializerOptions)), + ]; + + var actualResult = await RunAnalyzerAsync(dependencyInfo.Name, files, async directoryPath => + { + var discoveryPath = Path.GetFullPath(DiscoveryWorker.DiscoveryResultFileName, directoryPath); + var dependencyPath = Path.GetFullPath(relativeDependencyPath, directoryPath); + var analysisPath = Path.GetFullPath(AnalyzeWorker.AnalysisDirectoryName, directoryPath); + + var worker = new AnalyzeWorker(new Logger(verbose: true)); + await worker.RunAsync(discoveryPath, dependencyPath, analysisPath); + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + + protected static void ValidateAnalysisResult(ExpectedAnalysisResult expectedResult, AnalysisResult actualResult) + { + Assert.NotNull(actualResult); + Assert.Equal(expectedResult.UpdatedVersion, actualResult.UpdatedVersion); + Assert.Equal(expectedResult.CanUpdate, actualResult.CanUpdate); + Assert.Equal(expectedResult.VersionComesFromMultiDependencyProperty, actualResult.VersionComesFromMultiDependencyProperty); + ValidateDependencies(expectedResult.UpdatedDependencies, actualResult.UpdatedDependencies); + Assert.Equal(expectedResult.ExpectedUpdatedDependenciesCount ?? expectedResult.UpdatedDependencies.Length, actualResult.UpdatedDependencies.Length); + + return; + + void ValidateDependencies(ImmutableArray expectedDependencies, ImmutableArray actualDependencies) + { + if (expectedDependencies.IsDefault) + { + return; + } + + foreach (var expectedDependency in expectedDependencies) + { + var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); + Assert.Equal(expectedDependency.Name, actualDependency.Name); + Assert.Equal(expectedDependency.Version, actualDependency.Version); + Assert.Equal(expectedDependency.Type, actualDependency.Type); + AssertEx.Equal(expectedDependency.TargetFrameworks, actualDependency.TargetFrameworks); + Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect); + Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive); + } + } + } + + protected static async Task RunAnalyzerAsync(string dependencyName, TestFile[] files, Func action) + { + // write initial files + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + // run discovery + await action(temporaryDirectory.DirectoryPath); + + // gather results + var resultPath = Path.Join(temporaryDirectory.DirectoryPath, AnalyzeWorker.AnalysisDirectoryName, $"{dependencyName}.json"); + var resultJson = await File.ReadAllTextAsync(resultPath); + return JsonSerializer.Deserialize(resultJson, DiscoveryWorker.SerializerOptions)!; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs new file mode 100644 index 00000000000..8f0e1bd210e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -0,0 +1,46 @@ +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase +{ + [Fact] + public async Task FindUpdatedVersion() + { + await TestAnalyzeAsync( + discovery: new() + { + FilePath = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis.Common", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + UpdatedDependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown), + ], + ExpectedUpdatedDependenciesCount = 1, + } + ); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs new file mode 100644 index 00000000000..d6302fc70cf --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs @@ -0,0 +1,8 @@ +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Core.Test.Analyze; + +public record ExpectedAnalysisResult : AnalysisResult +{ + public int? ExpectedUpdatedDependenciesCount { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs index 802017b046f..5c62a0944ab 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs @@ -2,7 +2,7 @@ namespace NuGetUpdater.Core.Analyze; -public sealed record AnalysisResult +public record AnalysisResult { public required string UpdatedVersion { get; init; } public bool CanUpdate { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs index 1c428b24b62..181f15b7319 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -88,6 +88,7 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a updatedDependencies = dependencies .Where(dep => discovery.Projects.Any(p => p.Dependencies.Any(d => d.Name.Equals(dep.Key, StringComparison.OrdinalIgnoreCase)))) .Select(dep => new Dependency(dep.Key, dep.Value.VersionRange.MinVersion!.ToNormalizedString(), DependencyType.Unknown)) + .Prepend(new Dependency(dependencyInfo.Name, updatedVersion.ToNormalizedString(), DependencyType.Unknown)) .ToImmutableArray(); } From 5e61266b0f7157f881cf43b7145c1e232cb631ac Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Tue, 16 Apr 2024 23:50:36 -0700 Subject: [PATCH 03/13] Add additional .NET tests --- .../Analyze/AnalyzeWorkerTests.cs | 94 +++++- .../Analyze/CompatibilityCheckerTests.cs | 145 ++++++++++ .../Analyze/RequirementTests.cs | 69 +++++ .../SecurityVulnerabilityExtensionsTests.cs | 78 +++++ .../Analyze/VersionFinderTests.cs | 174 +++++++++++ .../Analyze/AnalyzeWorker.cs | 273 +++++++++--------- .../Analyze/CompatabilityChecker.cs | 23 +- .../Analyze/DependencyFinder.cs | 50 ++-- .../NuGetUpdater.Core/Analyze/Extensions.cs | 34 +++ .../NuGetUpdater.Core/Analyze/NuGetContext.cs | 2 +- .../NuGetUpdater.Core/Analyze/Requirement.cs | 2 +- .../Analyze/VersionFinder.cs | 47 ++- .../Analyze/VersionResult.cs | 22 +- 13 files changed, 820 insertions(+), 193 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs index 8f0e1bd210e..fd4a19057b8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -7,7 +7,7 @@ namespace NuGetUpdater.Core.Test.Analyze; public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase { [Fact] - public async Task FindUpdatedVersion() + public async Task FindsUpdatedVersion() { await TestAnalyzeAsync( discovery: new() @@ -36,10 +36,98 @@ public async Task FindUpdatedVersion() { UpdatedVersion = "4.9.2", CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, UpdatedDependencies = [ - new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown), + new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FindsUpdatedPeerDependencies() + { + await TestAnalyzeAsync( + discovery: new() + { + FilePath = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis", "4.0.1", DependencyType.PackageReference), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Microsoft.CodeAnalysis", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + + [Fact] + public async Task DeterminesMultiPropertyVersion() + { + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(RoslynPackageVersion)", "4.0.1", "RoslynPackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + discovery: new() + { + FilePath = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Workspaces", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis.Common", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), ], - ExpectedUpdatedDependenciesCount = 1, } ); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs new file mode 100644 index 00000000000..8ad02a925b1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs @@ -0,0 +1,145 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class CompatibilityCheckerTests +{ + [Fact] + public void PerformCheck_CompatiblePackage_IsCompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_IncompatiblePackage_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithPackageFrameworks_IsChecked() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithoutPackageFrameworks_IsCompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_WithoutPackageFrameworks_IsIncompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_WithoutProjectFrameworks_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = []; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs new file mode 100644 index 00000000000..6ec3cb90ac9 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs @@ -0,0 +1,69 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class RequirementTests +{ + // Supported OPs (=, !=, >, <, >=, <=, ~>) + [Theory] + [InlineData("1.0.0", "1.0.0", true)] + [InlineData("1.0.0-alpha", "1.0.0", false)] + [InlineData("1.0.0", "= 1.0.0", true)] + [InlineData("1.0.0-alpha", "= 1.0.0", false)] + [InlineData("1.0.0", "!= 1.0.1", true)] + [InlineData("1.0.0", "!= 1.0.0", false)] + [InlineData("1.0.1", "> 1.0.0", true)] + [InlineData("1.0.0-alpha", "> 1.0.0", false)] + [InlineData("1.0.0", "< 1.0.1", true)] + [InlineData("1.0.0", "< 1.0.0-alpha", false)] + [InlineData("1.0.0", ">= 1.0.0", true)] + [InlineData("1.0.1", ">= 1.0.0", true)] + [InlineData("1.0.0-alpha", ">= 1.0.0", false)] + [InlineData("1.0.0", "<= 1.0.0", true)] + [InlineData("1.0.0-alpha", "<= 1.0.0", true)] + [InlineData("1.0.1", "<= 1.0.0", false)] + [InlineData("1.0.1", "~> 1.0.0", true)] + [InlineData("1.1.0", "~> 1.0.0", false)] + [InlineData("1.1", "~> 1.0", true)] + [InlineData("2.0", "~> 1.0", false)] + [InlineData("1", "~> 1", true)] + [InlineData("2", "~> 1", false)] + public void IsSatisfiedBy(string versionString, string requirementString, bool expected) + { + var version = NuGetVersion.Parse(versionString); + var requirement = Requirement.Parse(requirementString); + + var actual = requirement.IsSatisfiedBy(version); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("> = 1.0.0")] // Invalid format + [InlineData("<>= 1.0.0")] // Invalid Operator + [InlineData(">")] // Missing version + public void Parse_ThrowsForInvalid(string requirementString) + { + Assert.Throws(() => Requirement.Parse(requirementString)); + } + + [Theory] + [InlineData("1.0.0-alpha", "1.1.0.0")] + [InlineData("1.0.0.0", "1.0.1.0")] + [InlineData("1.0.0", "1.1.0.0")] + [InlineData("1.0", "2.0.0.0")] + [InlineData("1", "2.0.0.0")] + public void Bump(string versionString, string expectedString) + { + var version = NuGetVersion.Parse(versionString); + var expected = Version.Parse(expectedString); + + var actual = Requirement.Bump(version); + + Assert.Equal(expected, actual); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs new file mode 100644 index 00000000000..1460e910f6e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs @@ -0,0 +1,78 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class SecurityVulnerabilityExtensionsTests +{ + [Fact] + public void VersionInSafeVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionInVulnerableVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } + + [Fact] + public void VersionNotInVulnerableVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionNotInSafeVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs new file mode 100644 index 00000000000..5f1ec347ac3 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs @@ -0,0 +1,174 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class VersionFinderTests +{ + [Fact] + public void VersionFilter_VersionInIgnoredVersions_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInIgnoredVersions_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionInVulnerabilities_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInVulnerabilities_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionLessThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionHigherThanCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_PreviewVersionDifferentThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1-beta"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_PreviewVersionSameAsCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs index 181f15b7319..110bbfa34f3 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -3,10 +3,8 @@ using System.Text.Json.Serialization; using NuGet.Frameworks; -using NuGet.Packaging.Core; using NuGet.Versioning; -using NuGetUpdater.Analyzer; using NuGetUpdater.Core.Discover; namespace NuGetUpdater.Core.Analyze; @@ -30,114 +28,74 @@ public AnalyzeWorker(Logger logger) public async Task RunAsync(string discoveryPath, string dependencyPath, string analysisDirectory) { - var discovery = LoadDiscovery(discoveryPath); - var dependencyInfo = LoadDependencyInfo(dependencyPath); + var discovery = await DeserializeJsonFileAsync(discoveryPath, nameof(WorkspaceDiscoveryResult)); + var dependencyInfo = await DeserializeJsonFileAsync(dependencyPath, nameof(DependencyInfo)); - var nugetContext = CreateNuGetContext(); - - var currentVersion = NuGetVersion.Parse(dependencyInfo.Version); - var projectFrameworks = FindProjectFrameworksForDependency(discovery, dependencyInfo); - var versions = await VersionFinder.GetVersionsAsync( - dependencyInfo.Name, - currentVersion.IsPrerelease, - nugetContext, - _logger, - CancellationToken.None); + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) + .ToImmutableArray(); + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + // When updating peer dependencies, we only need to consider top-level dependencies. + var projectDependencyNames = projectsWithDependency + .SelectMany(p => p.Dependencies) + .Where(d => !d.IsTransitive) + .Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + bool versionComesFromMultiDependencyProperty = DoesDependencyUseMultiDependencyProperty( + discovery, + dependencyInfo, + projectsWithDependency); - ImmutableArray updatedDependencies = []; var updatedVersion = await FindUpdatedVersionAsync( - discovery, dependencyInfo, - currentVersion, projectFrameworks, - versions, - nugetContext); - if (updatedVersion is not null) - { - // Determine updated peer dependencies - var source = versions.GetPackageSources(updatedVersion).First(); - var packageId = new PackageIdentity(dependencyInfo.Name, updatedVersion); - - // Create distinct list of dependencies taking the highest version of each - Dictionary dependencies = []; - foreach (var tfm in projectFrameworks) - { - var dependenciesForTfm = await DependencyFinder.GetDependenciesAsync( - source, - packageId, - tfm, - nugetContext, - _logger, - CancellationToken.None); - - foreach (var dependency in dependenciesForTfm) - { - if (dependencies.TryGetValue(dependency.Id, out PackageDependency? value) && - value.VersionRange.MinVersion! < dependency.VersionRange.MinVersion!) - { - dependencies[dependency.Id] = dependency; - } - else - { - dependencies.Add(dependency.Id, dependency); - } - } - } + _logger, + CancellationToken.None); - // Filter dependencies by whether any project references them - updatedDependencies = dependencies - .Where(dep => discovery.Projects.Any(p => p.Dependencies.Any(d => d.Name.Equals(dep.Key, StringComparison.OrdinalIgnoreCase)))) - .Select(dep => new Dependency(dep.Key, dep.Value.VersionRange.MinVersion!.ToNormalizedString(), DependencyType.Unknown)) - .Prepend(new Dependency(dependencyInfo.Name, updatedVersion.ToNormalizedString(), DependencyType.Unknown)) - .ToImmutableArray(); - } + var updatedDependencies = updatedVersion is not null + ? await FindUpdatedDependenciesAsync( + discovery, + projectsWithDependency, + projectFrameworks, + projectDependencyNames, + dependencyInfo, + updatedVersion, + _logger) + : []; var result = new AnalysisResult { UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version, CanUpdate = updatedVersion is not null, - VersionComesFromMultiDependencyProperty = false, //TODO: Provide correct value + VersionComesFromMultiDependencyProperty = versionComesFromMultiDependencyProperty, UpdatedDependencies = updatedDependencies, }; await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result); } - internal static WorkspaceDiscoveryResult LoadDiscovery(string discoveryPath) + internal static async Task DeserializeJsonFileAsync(string path, string fileType) { - if (!File.Exists(discoveryPath)) - { - throw new FileNotFoundException("Discovery file not found.", discoveryPath); - } + var json = File.Exists(path) + ? await File.ReadAllTextAsync(path) + : throw new FileNotFoundException($"{fileType} file not found.", path); - var discoveryJson = File.ReadAllText(discoveryPath); - var discovery = JsonSerializer.Deserialize(discoveryJson, SerializerOptions); - if (discovery is null) - { - throw new InvalidOperationException("Discovery file is empty."); - } - - return discovery; + return JsonSerializer.Deserialize(json, SerializerOptions) + ?? throw new InvalidOperationException($"{fileType} file is empty."); } - internal static DependencyInfo LoadDependencyInfo(string dependencyPath) - { - if (!File.Exists(dependencyPath)) - { - throw new FileNotFoundException("Dependency info file not found.", dependencyPath); - } - - var dependencyInfoJson = File.ReadAllText(dependencyPath); - var dependencyInfo = JsonSerializer.Deserialize(dependencyInfoJson, SerializerOptions); - if (dependencyInfo is null) - { - throw new InvalidOperationException("Dependency info file is empty."); - } - - return dependencyInfo; - } - - internal static NuGetContext CreateNuGetContext() + internal static async Task FindUpdatedVersionAsync( + DependencyInfo dependencyInfo, + ImmutableArray projectFrameworks, + Logger logger, + CancellationToken cancellationToken) { var nugetContext = new NuGetContext(); if (!Directory.Exists(nugetContext.TempPackageDirectory)) @@ -145,66 +103,46 @@ internal static NuGetContext CreateNuGetContext() Directory.CreateDirectory(nugetContext.TempPackageDirectory); } - return nugetContext; - } - - internal async Task FindUpdatedVersionAsync( - WorkspaceDiscoveryResult discovery, - DependencyInfo dependencyInfo, - NuGetVersion currentVersion, - ImmutableArray projectFrameworks, - VersionResult versions, - NuGetContext nugetContext) - { - var allVersions = versions.GetVersions(); - - var filteredVersions = allVersions - .Where(version => version > currentVersion) // filter lower versions - .Where(version => !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) // filter prerelease - .Where(version => !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version))) // filter ignored - .Where(version => !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version))); // filter vulnerable + var currentVersion = NuGetVersion.Parse(dependencyInfo.Version); + var versionResult = await VersionFinder.GetVersionsAsync( + dependencyInfo, + nugetContext, + cancellationToken); + var versions = versionResult.GetVersions(); var orderedVersions = dependencyInfo.IsVulnerable - ? filteredVersions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. - : filteredVersions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. + ? versions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. + : versions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. return await FindFirstCompatibleVersion( dependencyInfo.Name, currentVersion, - versions, + versionResult, orderedVersions, projectFrameworks, nugetContext, - _logger); - } - - internal static ImmutableArray FindProjectFrameworksForDependency(WorkspaceDiscoveryResult discovery, DependencyInfo dependencyInfo) - { - return discovery.Projects - .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) - .SelectMany(p => p.TargetFrameworks) - .Distinct() - .Select(tfm => NuGetFramework.Parse(tfm)) - .ToImmutableArray(); + logger, + cancellationToken); } internal static async Task FindFirstCompatibleVersion( string packageId, NuGetVersion currentVersion, - VersionResult versions, + VersionResult versionResult, IEnumerable orderedVersions, ImmutableArray projectFrameworks, - NuGetContext context, - Logger logger) + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) { - var source = versions.GetPackageSources(currentVersion).First(); + var source = versionResult.GetPackageSources(currentVersion).First(); var isCompatible = await CompatibilityChecker.CheckAsync( source, new(packageId, currentVersion), projectFrameworks, - context, + nugetContext, logger, - CancellationToken.None); + cancellationToken); if (!isCompatible) { // If the current package is incompatible, then don't check for compatibility. @@ -213,14 +151,14 @@ internal static ImmutableArray FindProjectFrameworksForDependenc foreach (var version in orderedVersions) { - source = versions.GetPackageSources(version).First(); + source = versionResult.GetPackageSources(version).First(); isCompatible = await CompatibilityChecker.CheckAsync( source, new(packageId, version), projectFrameworks, - context, + nugetContext, logger, - CancellationToken.None); + cancellationToken); if (isCompatible) { @@ -232,6 +170,81 @@ internal static ImmutableArray FindProjectFrameworksForDependenc return null; } + internal static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + Dependency package, + Logger logger) + { + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + [package], + logger); + result.Add(framework, [.. dependencies]); + } + return result.ToImmutable(); + } + + internal static async Task> FindUpdatedDependenciesAsync( + WorkspaceDiscoveryResult discovery, + ImmutableArray projectsWithDependency, + ImmutableArray projectFrameworks, + ImmutableHashSet projectDependencyNames, + DependencyInfo dependencyInfo, + NuGetVersion updatedVersion, + Logger logger) + { + // Determine updated peer dependencies + var workspacePath = discovery.FilePath; + // We need any project path so the dependency finder can locate the nuget.config + var projectPath = projectsWithDependency.First().FilePath; + + // Create distinct list of dependencies taking the highest version of each + var dependencyResult = await DependencyFinder.GetDependenciesAsync( + workspacePath, + projectPath, + projectFrameworks, + package: new(dependencyInfo.Name, updatedVersion.ToNormalizedString(), DependencyType.Unknown), + logger); + + // Filter dependencies by whether any project references them + return dependencyResult.GetDependencies() + .Where(dep => projectDependencyNames.Contains(dep.Name)) + .ToImmutableArray(); + } + + internal static bool DoesDependencyUseMultiDependencyProperty( + WorkspaceDiscoveryResult discovery, + DependencyInfo dependencyInfo, + ImmutableArray projectsWithDependency) + { + var declarationsUsingProperty = projectsWithDependency.SelectMany(p + => p.Dependencies.Where(d => !d.IsTransitive && + d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + var allPropertyBasedDependencies = discovery.Projects.SelectMany(p + => p.Dependencies.Where(d => !d.IsTransitive && + !d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && + d.EvaluationResult is not null) + ).ToImmutableArray(); + + return declarationsUsingProperty.Any(d => + { + var property = d.EvaluationResult!.RootPropertyName!; + + return allPropertyBasedDependencies + .Where(pd => !pd.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase)) + .Any(pd => pd.EvaluationResult?.RootPropertyName == property); + }); + } + internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result) { if (!Directory.Exists(analysisDirectory)) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs index 9f50a980971..703f8199c61 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -7,10 +7,9 @@ using NuGet.Protocol; using NuGet.Protocol.Core.Types; -using NuGetUpdater.Core; using NuGetUpdater.Core.FrameworkChecker; -namespace NuGetUpdater.Analyzer; +namespace NuGetUpdater.Core.Analyze; using PackageInfo = (bool IsDevDependency, ImmutableArray Frameworks); using PackageReaders = (IAsyncPackageCoreReader CoreReader, IAsyncPackageContentReader ContentReader); @@ -21,16 +20,26 @@ internal static class CompatibilityChecker PackageSource source, PackageIdentity package, ImmutableArray projectFrameworks, - NuGetContext context, + NuGetContext nugetContext, Logger logger, CancellationToken cancellationToken) { var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync( source, package, - context, + nugetContext, cancellationToken); + return PerformCheck(package, projectFrameworks, isDevDependency, packageFrameworks, logger); + } + + internal static bool PerformCheck( + PackageIdentity package, + ImmutableArray projectFrameworks, + bool isDevDependency, + ImmutableArray packageFrameworks, + Logger logger) + { // development dependencies are packages such as analyzers which need to be compatible with the compiler not the // project itself, but some packages that report themselves as development dependencies still contain target // framework dependencies and should be checked for compatibility through the regular means @@ -60,13 +69,13 @@ internal static class CompatibilityChecker internal static async Task GetPackageInfoAsync( PackageSource source, PackageIdentity package, - NuGetContext context, + NuGetContext nugetContext, CancellationToken cancellationToken) { - var tempPackagePath = GetTempPackagePath(package, context); + var tempPackagePath = GetTempPackagePath(package, nugetContext); var readers = File.Exists(tempPackagePath) ? ReadPackage(tempPackagePath) - : await DownloadPackageAsync(source, package, context, cancellationToken); + : await DownloadPackageAsync(source, package, nugetContext, cancellationToken); var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken); var reader = new NuspecReader(nuspecStream); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs index e9c15ddfd3e..fee0d1cc2fb 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs @@ -1,45 +1,29 @@ using System.Collections.Immutable; -using NuGet.Common; -using NuGet.Configuration; using NuGet.Frameworks; -using NuGet.Packaging.Core; -using NuGet.Protocol; -using NuGet.Protocol.Core.Types; -using NuGetUpdater.Core; - -namespace NuGetUpdater.Analyzer; +namespace NuGetUpdater.Core.Analyze; internal static class DependencyFinder { - public static async Task> GetDependenciesAsync( - PackageSource source, - PackageIdentity package, - NuGetFramework framework, - NuGetContext context, - Logger logger, - CancellationToken cancellationToken) + public static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + Dependency package, + Logger logger) { - var sourceRepository = Repository.Factory.GetCoreV3(source); - var feed = await sourceRepository.GetResourceAsync(); - if (feed is null) + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) { - throw new NotSupportedException($"Failed to get DependencyInfoResource for {source.SourceUri}"); + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + [package], + logger); + result.Add(framework, [.. dependencies.Select(d => d with { IsTransitive = false })]); } - - var dependencyInfo = await feed.ResolvePackage( - package, - framework, - context.SourceCacheContext, - NullLogger.Instance, - cancellationToken); - if (dependencyInfo is null) - { - throw new Exception($"Failed to resolve package {package} from {source.SourceUri}"); - } - - return dependencyInfo.Dependencies - .ToImmutableArray(); + return result.ToImmutable(); } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs new file mode 100644 index 00000000000..8817e1c87ee --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs @@ -0,0 +1,34 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core; + +internal static class Extensions +{ + public static ImmutableArray GetDependencies(this ImmutableDictionary> dependenciesByTfm) + { + Dictionary dependencies = []; + foreach (var (_framework, dependenciesForTfm) in dependenciesByTfm) + { + foreach (var dependency in dependenciesForTfm) + { + if (dependencies.TryGetValue(dependency.Name, out Dependency? value) && + NuGetVersion.Parse(value.Version!) < NuGetVersion.Parse(dependency.Version!)) + { + dependencies[dependency.Name] = dependency with + { + TargetFrameworks = [.. value.TargetFrameworks ?? [], .. dependency.TargetFrameworks ?? []] + }; + } + else + { + dependencies.Add(dependency.Name, dependency); + } + } + } + + return [.. dependencies.Values]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs index 9da109dbf43..203176de2e1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs @@ -5,7 +5,7 @@ using NuGet.Configuration; using NuGet.Protocol.Core.Types; -namespace NuGetUpdater.Analyzer; +namespace NuGetUpdater.Core.Analyze; internal record NuGetContext : IDisposable { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs index 78d36207bab..9225b30a3b5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs @@ -91,7 +91,7 @@ public static Version Bump(NuGetVersion version) versionParts[^1]++; // Increment the new last part - bumpedVersion = new Version(string.Join('.', versionParts)); + bumpedVersion = NuGetVersion.Parse(string.Join('.', versionParts)).Version; BumpMap[version.OriginalVersion!] = bumpedVersion; return bumpedVersion; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index 63f263eebc6..52250c8decc 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -4,27 +4,29 @@ using NuGet.Configuration; using NuGet.Protocol; using NuGet.Protocol.Core.Types; +using NuGet.Versioning; -using NuGetUpdater.Core; - -namespace NuGetUpdater.Analyzer; +namespace NuGetUpdater.Core.Analyze; internal static class VersionFinder { public static async Task GetVersionsAsync( - string packageId, - bool includePrerelease, - NuGetContext context, - Logger logger, + DependencyInfo dependencyInfo, + NuGetContext nugetContext, CancellationToken cancellationToken) { - VersionResult result = new(); + var packageId = dependencyInfo.Name; + var currentVersion = NuGetVersion.Parse(dependencyInfo.Version); + var includePrerelease = currentVersion.IsPrerelease; + + var versionFilter = CreateVersionFilter(dependencyInfo, currentVersion); + VersionResult result = new(currentVersion); - var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings); + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); var sources = packageSources.Count == 0 - ? context.PackageSources - : context.PackageSources + ? nugetContext.PackageSources + : nugetContext.PackageSources .Where(p => packageSources.Contains(p.Name)) .ToImmutableArray(); @@ -42,7 +44,7 @@ internal static class VersionFinder packageId, includePrerelease, includeUnlisted: false, - context.SourceCacheContext, + nugetContext.SourceCacheContext, NullLogger.Instance, cancellationToken); if (!existsInFeed) @@ -50,17 +52,30 @@ internal static class VersionFinder continue; } - var feedVersions = await feed.GetVersions( + var feedVersions = (await feed.GetVersions( packageId, includePrerelease, includeUnlisted: false, - context.SourceCacheContext, + nugetContext.SourceCacheContext, NullLogger.Instance, - CancellationToken.None); + CancellationToken.None)).ToHashSet(); + + if (feedVersions.Contains(currentVersion)) + { + result.AddCurrentVersionSource(source); + } - result.AddRange(source, feedVersions); + result.AddRange(source, feedVersions.Where(versionFilter)); } return result; } + + internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, NuGetVersion currentVersion) + { + return version => version > currentVersion + && (!currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) + && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) + && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs index 7e1b4e07640..2c7d304ec28 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs @@ -3,11 +3,24 @@ using NuGet.Configuration; using NuGet.Versioning; -namespace NuGetUpdater.Analyzer; +namespace NuGetUpdater.Core.Analyze; internal class VersionResult { - private readonly Dictionary> _versions = new(); + private readonly Dictionary> _versions = []; + private readonly List _currentVersionSources = []; + + public NuGetVersion CurrentVersion { get; } + + public VersionResult(NuGetVersion currentVersion) + { + CurrentVersion = currentVersion; + } + + public void AddCurrentVersionSource(PackageSource source) + { + _currentVersionSources.Add(source); + } public void AddRange(PackageSource source, IEnumerable versions) { @@ -26,6 +39,11 @@ public void AddRange(PackageSource source, IEnumerable versions) public ImmutableArray GetPackageSources(NuGetVersion version) { + if (version == CurrentVersion) + { + return [.. _currentVersionSources]; + } + return [.. _versions[version]]; } From dd839ba3614d3b8f4266640adacf4b6ce417c611 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 18 Apr 2024 00:30:44 -0700 Subject: [PATCH 04/13] Progress on ruby tests --- .../Analyze/VersionFinderTests.cs | 35 +- .../Analyze/AnalyzeWorker.cs | 35 +- .../Analyze/VersionFinder.cs | 16 +- .../nuget/analysis/analysis_json_reader.rb | 29 +- .../nuget/analysis/dependency_analysis.rb | 61 ++ nuget/lib/dependabot/nuget/update_checker.rb | 67 +- .../compatibility_checker_spec.rb | 259 ----- .../update_checker/dependency_finder_spec.rb | 98 -- .../update_checker/nupkg_fetcher_spec.rb | 257 ----- .../update_checker/nuspec_fetcher_spec.rb | 55 - .../update_checker/repository_finder_spec.rb | 988 ------------------ .../nuget/update_checker/tfm_finder_spec.rb | 56 - .../update_checker/version_finder_spec.rb | 695 ------------ .../dependabot/nuget/update_checker_spec.rb | 15 + 14 files changed, 195 insertions(+), 2471 deletions(-) delete mode 100644 nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs index 5f1ec347ac3..a1617961929 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs @@ -19,7 +19,7 @@ public void VersionFilter_VersionInIgnoredVersions_ReturnsFalse() IgnoredVersions = [Requirement.Parse("< 1.0.0")], Vulnerabilities = [], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("0.9.0"); var result = filter(version); @@ -38,7 +38,7 @@ public void VersionFilter_VersionNotInIgnoredVersions_ReturnsTrue() IgnoredVersions = [Requirement.Parse("< 1.0.0")], Vulnerabilities = [], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("1.0.1"); var result = filter(version); @@ -63,7 +63,7 @@ public void VersionFilter_VersionInVulnerabilities_ReturnsFalse() VulnerableVersions = [Requirement.Parse("< 1.0.0")], }], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("0.9.0"); var result = filter(version); @@ -88,7 +88,7 @@ public void VersionFilter_VersionNotInVulnerabilities_ReturnsTrue() VulnerableVersions = [Requirement.Parse("< 1.0.0")], }], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("1.0.1"); var result = filter(version); @@ -107,7 +107,7 @@ public void VersionFilter_VersionLessThanCurrentVersion_ReturnsFalse() IgnoredVersions = [], Vulnerabilities = [], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("0.9.0"); var result = filter(version); @@ -126,7 +126,7 @@ public void VersionFilter_VersionHigherThanCurrentVersion_ReturnsTrue() IgnoredVersions = [], Vulnerabilities = [], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("1.0.1"); var result = filter(version); @@ -145,7 +145,7 @@ public void VersionFilter_PreviewVersionDifferentThanCurrentVersion_ReturnsFalse IgnoredVersions = [], Vulnerabilities = [], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("1.0.1-beta"); var result = filter(version); @@ -164,7 +164,26 @@ public void VersionFilter_PreviewVersionSameAsCurrentVersion_ReturnsTrue() IgnoredVersions = [], Vulnerabilities = [], }; - var filter = VersionFinder.CreateVersionFilter(dependencyInfo, NuGetVersion.Parse(dependencyInfo.Version)); + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_WildcardPreviewVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "*-*", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); var version = NuGetVersion.Parse("1.0.0-beta"); var result = filter(version); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs index 110bbfa34f3..ce1884e52a5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -103,8 +103,6 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi Directory.CreateDirectory(nugetContext.TempPackageDirectory); } - var currentVersion = NuGetVersion.Parse(dependencyInfo.Version); - var versionResult = await VersionFinder.GetVersionsAsync( dependencyInfo, nugetContext, @@ -116,7 +114,7 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi return await FindFirstCompatibleVersion( dependencyInfo.Name, - currentVersion, + dependencyInfo.Version, versionResult, orderedVersions, projectFrameworks, @@ -127,7 +125,7 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi internal static async Task FindFirstCompatibleVersion( string packageId, - NuGetVersion currentVersion, + string versionString, VersionResult versionResult, IEnumerable orderedVersions, ImmutableArray projectFrameworks, @@ -135,24 +133,27 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi Logger logger, CancellationToken cancellationToken) { - var source = versionResult.GetPackageSources(currentVersion).First(); - var isCompatible = await CompatibilityChecker.CheckAsync( - source, - new(packageId, currentVersion), - projectFrameworks, - nugetContext, - logger, - cancellationToken); - if (!isCompatible) + if (NuGetVersion.TryParse(versionString, out var currentVersion)) { - // If the current package is incompatible, then don't check for compatibility. - return orderedVersions.First(); + var source = versionResult.GetPackageSources(currentVersion).First(); + var isCompatible = await CompatibilityChecker.CheckAsync( + source, + new(packageId, currentVersion), + projectFrameworks, + nugetContext, + logger, + cancellationToken); + if (!isCompatible) + { + // If the current package is incompatible, then don't check for compatibility. + return orderedVersions.First(); + } } foreach (var version in orderedVersions) { - source = versionResult.GetPackageSources(version).First(); - isCompatible = await CompatibilityChecker.CheckAsync( + var source = versionResult.GetPackageSources(version).First(); + var isCompatible = await CompatibilityChecker.CheckAsync( source, new(packageId, version), projectFrameworks, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index 52250c8decc..80ee82d4ce5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -16,10 +16,11 @@ internal static class VersionFinder CancellationToken cancellationToken) { var packageId = dependencyInfo.Name; - var currentVersion = NuGetVersion.Parse(dependencyInfo.Version); + var versionRange = VersionRange.Parse(dependencyInfo.Version); + var currentVersion = versionRange.MinVersion!; var includePrerelease = currentVersion.IsPrerelease; - var versionFilter = CreateVersionFilter(dependencyInfo, currentVersion); + var versionFilter = CreateVersionFilter(dependencyInfo, versionRange); VersionResult result = new(currentVersion); var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); @@ -71,10 +72,15 @@ internal static class VersionFinder return result; } - internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, NuGetVersion currentVersion) + internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, VersionRange versionRange) { - return version => version > currentVersion - && (!currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) + // If we are floating to the aboslute latest version, we should not filter pre-release versions at all. + var currentVersion = versionRange.Float?.FloatBehavior != NuGetVersionFloatBehavior.AbsoluteLatest + ? versionRange.MinVersion + : null; + + return version => versionRange.Satisfies(version) + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); } diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb index 044b33a9dfb..77413c3cc7c 100644 --- a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -39,25 +39,24 @@ def initialize(analysis_json:) @analysis_json = analysis_json end - sig { returns(Dependabot::Nuget::Version) } - def updated_version - Version.new("1.0.0") - end + sig { returns(DependencyAnalysis) } + def dependency_analysis + @dependency_analysis ||= T.let(begin + raise Dependabot::DependencyFileNotParseable, analysis_json.path unless analysis_json.content - sig { returns(T::Boolean) } - def can_update? - true - end + Dependabot.logger.info("#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}") - sig { returns(T::Boolean) } - def version_comes_from_multi_dependency_property? - false + parsed_json = T.let(JSON.parse(T.must(analysis_json.content)), T::Hash[String, T.untyped]) + DependencyAnalysis.from_json(parsed_json) + end, T.nilable(DependencyAnalysis)) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, analysis_json.path end - sig { returns(T::Array[Dependabot::Dependency]) } - def updated_dependencies - [] - end + private + + sig { returns(DependencyFile) } + attr_reader :analysis_json end end end diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb index e69de29bb2d..759af5f270a 100644 --- a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb +++ b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb @@ -0,0 +1,61 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/version" +require "sorbet-runtime" + +module Dependabot + module Nuget + class DependencyAnalysis + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(DependencyAnalysis) } + def self.from_json(json) + updated_version = T.let(json.fetch("UpdatedVersion"), String) + can_update = T.let(json.fetch("CanUpdate"), T::Boolean) + version_comes_from_multi_dependency_property = T.let(json.fetch("VersionComesFromMultiDependencyProperty"), + T::Boolean) + updated_dependencies = T.let(json.fetch("UpdatedDependencies"), + T::Array[T::Hash[String, T.untyped]]).map do |dep| + DependencyDetails.from_json(dep) + end + + DependencyAnalysis.new(updated_version: updated_version, + can_update: can_update, + version_comes_from_multi_dependency_property: version_comes_from_multi_dependency_property, + updated_dependencies: updated_dependencies) + end + + sig do + params(updated_version: String, + can_update: T::Boolean, + version_comes_from_multi_dependency_property: T::Boolean, + updated_dependencies: T::Array[DependencyDetails]).void + end + def initialize(updated_version:, can_update:, version_comes_from_multi_dependency_property:, + updated_dependencies:) + @updated_version = updated_version + @can_update = can_update + @version_comes_from_multi_dependency_property = version_comes_from_multi_dependency_property + @updated_dependencies = updated_dependencies + end + + sig { returns(String) } + attr_reader :updated_version + + sig { returns(T::Boolean) } + attr_reader :can_update + + sig { returns(T::Boolean) } + attr_reader :version_comes_from_multi_dependency_property + + sig { returns(T::Array[DependencyDetails]) } + attr_reader :updated_dependencies + + sig { returns(Dependabot::Nuget::Version) } + def numeric_updated_version + @numeric_updated_version ||= T.let(Version.new(updated_version), T.nilable(Dependabot::Nuget::Version)) + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index 847e9a066b6..5269aaaf839 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -21,7 +21,7 @@ def latest_version # if no update sources have the requisite package, then we can only assume that the current version is correct @latest_version = T.let( - update_analysis.updated_version.to_s, + update_analysis.dependency_analysis.updated_version, T.nilable(String) ) end @@ -35,14 +35,14 @@ def latest_resolvable_version sig { override.returns(Dependabot::Nuget::Version) } def lowest_security_fix_version - update_analysis.updated_version + update_analysis.dependency_analysis.numeric_updated_version end sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_resolvable_security_fix_version return nil if version_comes_from_multi_dependency_property? - update_analysis.updated_version + update_analysis.dependency_analysis.numeric_updated_version end sig { override.returns(NilClass) } @@ -55,18 +55,18 @@ def latest_resolvable_version_with_no_unlock def updated_requirements RequirementsUpdater.new( requirements: dependency.requirements, - latest_version: update_analysis.updated_version.to_s + latest_version: update_analysis.dependency_analysis.updated_version ).updated_requirements end sig { returns(T::Boolean) } def up_to_date? - !update_analysis.can_update? + !update_analysis.dependency_analysis.can_update end sig { returns(T::Boolean) } def requirements_unlocked_or_can_be? - update_analysis.can_update? + update_analysis.dependency_analysis.can_update end private @@ -86,13 +86,25 @@ def request_analysis discovery_file_path = DiscoveryJsonReader.discovery_file_path analysis_folder_path = AnalysisJsonReader.temp_directory + write_dependency_info + + NativeHelpers.run_nuget_analyze_tool(discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path, + credentials: credentials) + + analysis_json = AnalysisJsonReader.analysis_json(dependency_name: dependency.name) + + AnalysisJsonReader.new(analysis_json: T.must(analysis_json)) + end + + sig { void } + def write_dependency_info dependency_info = { Name: dependency.name, Version: dependency.version.to_s, IsVulnerable: vulnerable?, - IgnoredVersions: ignored_versions.map do |i| - i == ">= 0" ? "*" : i - end, + IgnoredVersions: ignored_versions, Vulnerabilities: security_advisories.map do |vulnerability| { DependencyName: vulnerability.dependency_name, @@ -103,15 +115,16 @@ def request_analysis end }.to_json File.write(dependency_file_path, dependency_info) + end - NativeHelpers.run_nuget_analyze_tool(discovery_file_path: discovery_file_path, - dependency_file_path: dependency_file_path, - analysis_folder_path: analysis_folder_path, - credentials: credentials) - - analysis_json = AnalysisJsonReader.analysis_json(dependency_name: dependency.name) + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def discovered_dependencies + discovery_json = DiscoveryJsonReader.discovery_json + return Dependabot::FileParsers::Base::DependencySet.new unless discovery_json - AnalysisJsonReader.new(analysis_json: T.must(analysis_json)) + DiscoveryJsonReader.new( + discovery_json: discovery_json + ).dependency_set end sig { override.returns(T::Boolean) } @@ -122,12 +135,30 @@ def latest_version_resolvable_with_full_unlock? sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock - update_analysis.updated_dependencies + dependencies = discovered_dependencies.dependencies + update_analysis.dependency_analysis.updated_dependencies.filter_map do |dependency_details| + dep = dependencies.find { |d| d.name.casecmp(dependency_details.name)&.zero? } + next unless dep + + metadata = {} + # For peer dependencies, instruct updater to not directly update this dependency + metadata = { information_only: true } unless dependency.name.casecmp(dependency_details.name)&.zero? + + Dependency.new( + name: dep.name, + version: dependency_details.version, + requirements: dep.requirements, + previous_version: dep.version, + previous_requirements: dep.requirements, + package_manager: dep.package_manager, + metadata: metadata + ) + end end sig { returns(T::Boolean) } def version_comes_from_multi_dependency_property? - update_analysis.version_comes_from_multi_dependency_property? + update_analysis.dependency_analysis.version_comes_from_multi_dependency_property end end end diff --git a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb deleted file mode 100644 index 55d5ac05e42..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/compatibility_checker" -require "dependabot/nuget/update_checker/repository_finder" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::CompatibilityChecker do - subject(:checker) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency_urls: dependency_urls, - dependency: dependency - ) - end - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency_urls) do - Dependabot::Nuget::RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: [] - ).dependency_urls - end - - let(:credentials) do - [{ - "type" => "nuget_feed", - "url" => "https://api.nuget.org/v3/index.json", - "token" => "my:passw0rd" - }] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_name) { "Microsoft.AppCenter.Crashes" } - let(:dependency_version) { "5.0.2" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "5.0.2", groups: ["dependencies"], source: nil }] - end - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) do - <<~XML - - - uap10.0.16299 - - - - - - XML - end - - context "#compatible?" do - subject(:compatible) { checker.compatible?(version) } - - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/microsoft.appcenter.crashes/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, - version: "5.0.2" - } - }, - { - catalogEntry: { - listed: true, - version: "5.0.3" - } - } - ] - ] - }.to_json - ) - end - - context "when the `.nuspec` reports itself as a development dependency, but still has regular dependencies" do - let(:csproj_body) do - <<~XML - - - net6.0 - - - - - - XML - end - - before do - nuspec502 = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - true - - - - - - - XML - nuspec503 = nuspec502.gsub("5.0.2", "5.0.3") - nuspec601 = nuspec502.gsub("5.0.2", "6.0.1").gsub("net6.0", "net8.0") - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec502 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec503 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/6.0.1/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec601 - ) - end - - context "with a targetFramework compatible version" do - let(:version) { "5.0.3" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "with a targetFramework non-compatible version" do - let(:version) { "6.0.1" } - - it "returns the correct data" do - expect(compatible).to be_falsey - end - end - end - - context "when the `.nuspec` has groups without a `targetFramework` attribute" do - let(:version) { "5.0.3" } - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - end - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "when the `.nupkg` zip object contains an empty `lib/` entry" do - def create_nupkg_with_lib_contents(package_name, nuspec_contents, lib_subdirectories) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{package_name}.nuspec") - zio.write(nuspec_contents) - - zio.put_next_entry("lib/") # some zip files have an empty directory object entry - - lib_subdirectories.each do |lib| - zio.put_next_entry("lib/#{lib}/_._") - zio.write("fake contents") - end - end - content.rewind - content.sysread - end - - let(:csproj_body) do - <<~XML - - - net481 - - - - - - XML - end - - before do - nuspec_xml = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - - - - - - - XML - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec_xml - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.5.0.2.nupkg") - .to_return( - status: 200, - body: create_nupkg_with_lib_contents(dependency_name, nuspec_xml, ["net45"]) - ) - end - - context "checks the `.nupkg` contents" do - let(:version) { "5.0.2" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb deleted file mode 100644 index ba0431fcd5e..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb +++ /dev/null @@ -1,98 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/dependency_finder" - -RSpec.describe Dependabot::Nuget::UpdateChecker::DependencyFinder do - subject(:finder) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: "test/repo" - ) - end - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - # Can get transitive dependencies - describe "#transitive_dependencies", :vcr do - subject(:transitive_dependencies) { finder.transitive_dependencies } - - its(:length) { is_expected.to eq(34) } - end - - context "api.nuget.org is not hit if it's not in the NuGet.Config" do - let(:dependency_version) { "42.42.42" } - let(:nuget_config_body) { fixture("configs", "example.com_nuget.config") } - let(:nuget_config) { Dependabot::DependencyFile.new(name: "NuGet.Config", content: nuget_config_body) } - let(:dependency_files) { [csproj, nuget_config] } - subject(:transitive_dependencies) { finder.transitive_dependencies } - - def create_nupkg(nuspec_name, nuspec_fixture_path) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(fixture("nuspecs", nuspec_fixture_path)) - end - content.rewind - content.sysread - end - - before(:context) do - disallowed_urls = %w( - https://api.nuget.org/v3/index.json - https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.nuspec - https://api.nuget.org/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.nuspec - ) - - disallowed_urls.each do |url| - stub_request(:get, url) - .to_raise(StandardError.new("Not allowed to query `#{url}`")) - end - - stub_request(:get, "https://nuget.example.com/v3/index.json") - .to_return(status: 200, body: fixture("nuget_responses", "example_index.json")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.42.42.42.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.Extensions.DependencyModel", - "Microsoft.Extensions.DependencyModel_42.42.42_faked.nuspec")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.43.43.43.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.NETCore.Platforms", - "Microsoft.NETCore.Platforms_43.43.43_faked.nuspec")) - end - - # this test doesn't really care about the dependency count, we just need to ensure that `api.nuget.org` wasn't hit - its(:length) do - is_expected.to eq(1) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb deleted file mode 100644 index 52c86eba852..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb +++ /dev/null @@ -1,257 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nupkg_fetcher" -require "dependabot/nuget/update_checker/repository_finder" - -RSpec.describe Dependabot::Nuget::NupkgFetcher do - describe "#fetch_nupkg_url_from_repository" do - let(:dependency) { Dependabot::Dependency.new(name: package_name, requirements: [], package_manager: "nuget") } - let(:package_name) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:credentials) { [] } - let(:config_files) { [nuget_config] } - let(:nuget_config) do - Dependabot::DependencyFile.new( - name: "NuGet.config", - content: nuget_config_content - ) - end - let(:nuget_config_content) do - <<~XML - - - - - - - - XML - end - let(:repository_finder) do - Dependabot::Nuget::RepositoryFinder.new(dependency: dependency, credentials: credentials, - config_files: config_files) - end - let(:repository_details) { repository_finder.dependency_urls.first } - subject(:nupkg_url) do - described_class.fetch_nupkg_url_from_repository(repository_details, package_name, package_version) - end - - context "with a nuget feed url" do - let(:feed_url) { "https://api.nuget.org/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "nuget.index.json") - ) - end - - it { is_expected.to eq("https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with an azure feed url" do - let(:feed_url) { "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "dotnet-public.index.json") - ) - end - - it { is_expected.to eq("https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/45bacae2-5efb-47c8-91e5-8ec20c22b4f8/nuget/v3/flat2/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a github feed url" do - let(:feed_url) { "https://nuget.pkg.github.com/some-namespace/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it { is_expected.to eq("https://nuget.pkg.github.com/some-namespace/download/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a v2 feed url" do - let(:feed_url) { "https://www.nuget.org/api/v2" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: - +<<~XML - - - - Packages - - - - XML - ) - stub_request(:get, "https://www.nuget.org/api/v2/Packages(Id='Newtonsoft.Json',Version='13.0.1')") - .to_return( - status: 200, - body: - <<~XML - - - - - XML - ) - end - - it { is_expected.to eq("https://www.nuget.org/api/v2/Download/Newtonsoft.Json/13.0.1") } - end - - context "from a v3 feed that doesn't specify `PackageBaseAddress`" do - let(:feed_url) { "https://nuget.example.com/v3-without-package-base/index.json" } - - before do - # initial `index.json` response; only provides `SearchQueryService` and not `PackageBaseAddress` - stub_request(:get, feed_url) - .to_return( - status: 200, - body: { - version: "3.0.0", - resources: [ - { - "@id" => "https://nuget.example.com/query", - "@type" => "SearchQueryService" - } - ] - }.to_json - ) - # SearchQueryService - stub_request(:get, "https://nuget.example.com/query?q=newtonsoft.json&prerelease=true&semVerLevel=2.0.0") - .to_return( - status: 200, - body: { - totalHits: 2, - data: [ - # this is a false match - { - registration: "not-used", - version: "42.42.42", - versions: [ - { - version: "1.0.0", - "@id" => "not-used" - }, - { - version: "42.42.42", - "@id" => "not-used" - } - ], - id: "Newtonsoft.Json.False.Match" - }, - # this is the real one - { - registration: "not-used", - version: "13.0.1", - versions: [ - { - version: "12.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/12.0.1.json" - }, - { - version: "13.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json" - } - ], - id: "Newtonsoft.Json" - } - ] - }.to_json - ) - # registration content - stub_request(:get, "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json") - .to_return( - status: 200, - body: { - listed: true, - packageContent: "https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg", - registration: "not-used", - "@id" => "not-used" - }.to_json - ) - end - - it { is_expected.to eq("https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg") } - end - end - - describe "#fetch_nupkg_buffer" do - let(:package_id) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:repository_details) { Dependabot::Nuget::RepositoryFinder.get_default_repository_details(package_id) } - let(:dependency_urls) { [repository_details] } - subject(:nupkg_buffer) do - described_class.fetch_nupkg_buffer(dependency_urls, package_id, package_version) - end - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") - .to_return( - status: 301, - headers: { - "Location" => "https://api.nuget.org/redirect-on-301" - }, - body: "redirecting on 301" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-301") - .to_return( - status: 302, - headers: { - "Location" => "https://api.nuget.org/redirect-on-302" - }, - body: "redirecting on 302" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-302") - .to_return( - status: 303, - headers: { - "Location" => "https://api.nuget.org/redirect-on-303" - }, - body: "redirecting on 303" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-303") - .to_return( - status: 307, - headers: { - "Location" => "https://api.nuget.org/redirect-on-307" - }, - body: "redirecting on 307" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-307") - .to_return( - status: 308, - headers: { - "Location" => "https://api.nuget.org/redirect-on-308" - }, - body: "redirecting on 308" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-308") - .to_return( - status: 200, - body: "the final contents" - ) - end - - it "fetches the nupkg after multiple redirects" do - expect(nupkg_buffer.to_s).to eq("the final contents") - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb deleted file mode 100644 index d81a191eed4..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nuspec_fetcher" - -RSpec.describe Dependabot::Nuget::NuspecFetcher do - describe "#feed_supports_nuspec_download?" do - context "when checking with a azure feed url" do - let(:url) { "https://pkgs.dev.azure.com/dependabot/dependabot-test/_packaging/dependabot-feed/nuget/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with a azure feed url (no project)" do - let(:url) { "https://pkgs.dev.azure.com/dependabot/_packaging/dependabot-feed/nuget/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with a visual studio feed url" do - let(:url) { "https://dynamicscrm.pkgs.visualstudio.com/_packaging/CRM.Engineering/nuget/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with the nuget.org feed url" do - let(:url) { "https://api.nuget.org/v3/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_truthy } - end - - context "when checking with github feed url" do - let(:url) { "https://nuget.pkg.github.com/some_namespace/index.json" } - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - it { is_expected.to be_falsy } - end - end - - describe "remove_invalid_characters" do - context "when a utf-16 bom is present" do - let(:response_body) { "\xFE\xFF" } - subject(:result) { described_class.remove_invalid_characters(response_body) } - - it { is_expected.to eq("") } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb deleted file mode 100644 index cfa420097d3..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ /dev/null @@ -1,988 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/repository_finder" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::RepositoryFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - subject(:finder) do - described_class.new( - dependency: dependency, - credentials: credentials, - config_files: [config_file].compact - ) - end - let(:config_file) { nil } - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:dependency) do - Dependabot::Dependency.new( - name: "Microsoft.Extensions.DependencyModel", - version: "1.1.1", - requirements: [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }], - package_manager: "nuget" - ) - end - - describe "local path in NuGet.Config" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content, - directory: "some/directory" - ) - end - - subject(:known_repositories) { finder.known_repositories } - - it "finds all local paths" do - urls = known_repositories.map { |r| r[:url] } - expected = [ - "/some/directory/SomePath", - "/some/directory/RelativePath", - "/AbsolutePath", - "https://nuget.example.com/index.json" - ] - expect(urls).to match_array(expected) - end - end - - describe "environment variables in NuGet.Config" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - subject(:known_repositories) { finder.known_repositories } - - context "are expanded" do - before do - allow(Dependabot.logger).to receive(:warn) - ENV["FEED_URL"] = "https://nuget.example.com/index.json" - ENV["THIS_VARIBLE_EXISTS"] = "replacement-text" - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - - it "contains the expected values and warns on unavailable" do - repo = known_repositories[0] - expect(repo[:url]).to eq("https://nuget.example.com/index.json") - expect(repo[:token]).to eq("user:(head)replacement-text(mid)%THIS_VARIABLE_DOES_NOT%(tail)") - expect(Dependabot.logger).to have_received(:warn).with( - <<~WARN - The variable '%THIS_VARIABLE_DOES_NOT%' could not be expanded in NuGet.Config - WARN - ) - end - - after do - ENV.delete("THIS_VARIBLE_EXISTS") - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - end - end - - describe "dependency_urls" do - subject(:dependency_urls) { finder.dependency_urls } - - it "gets the right URL without making any requests" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - - context "with a URL passed as a credential" do - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - - context "that does not return PackageBaseAddress" do - let(:custom_repo_url) { "http://localhost:8082/artifactory/api/nuget/v3/nuget-local" } - before do - stub_request(:get, custom_repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: nil, - registration_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/registration/microsoft.extensions.dependencymodel/index.json", - repository_url: custom_repo_url, - search_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that has URLs that need to be escaped" do - let(:custom_repo_url) { "https://www.myget.org/F/exceptionless/api with spaces/v3/index.json" } - before do - stub_request(:get, "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json") - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that 404s" do - before { stub_request(:get, custom_repo_url).to_return(status: 404) } - - # TODO: Might want to raise here instead? - it { is_expected.to eq([]) } - end - - context "that 403s" do - before { stub_request(:get, custom_repo_url).to_return(status: 403) } - - it "raises a useful error" do - error_class = Dependabot::PrivateSourceAuthenticationFailure - expect { finder.dependency_urls } - .to raise_error do |error| - expect(error).to be_a(error_class) - expect(error.source).to eq(custom_repo_url) - end - end - end - - context "without a token" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: nil) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - end - - context "with a URL included in the nuget.config" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", config_file_fixture_name) - ) - end - let(:config_file_fixture_name) { "nuget.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url).to_return(status: 404) - stub_request(:get, repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - # skipped - # it "gets the right URLs" do - # expect(dependency_urls).to match_array( - # [{ - # repository_url: "https://api.nuget.org/v3/index.json", - # versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - # "microsoft.extensions.dependencymodel/index.json", - # search_url: "https://azuresearch-usnc.nuget.org/query" \ - # "?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: {}, - # repository_type: "v3" - # }, { - # repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "index.json", - # versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "flatcontainer/microsoft.extensions." \ - # "dependencymodel/index.json", - # search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "query?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - # repository_type: "v3" - # }] - # ) - # end - - context "include the default repository" do - let(:config_file_fixture_name) { "include_default_disable_ext_sources.config" } - - it "with disable external source" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }, { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that overrides the default package sources" do - let(:config_file_fixture_name) { "override_def_source_with_same_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "when the default api key of default registry is provided without clear" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - - let(:config_file_fixture_name) { "override_def_source_with_same_key_default.config" } - it "when the default api key of default registry is provided with clear" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that doesn't include the default repository" do - let(:config_file_fixture_name) { "excludes_default.config" } - - it "still includes the default repository (as it wasn't cleared)" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - - context "and clears it" do - let(:config_file_fixture_name) { "clears_default.config" } - - it "still excludes the default repository" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that has disabled package sources" do - let(:config_file_fixture_name) { "disabled_sources.config" } - - it "only includes the enabled package sources" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "that has disabled default package sources" do - let(:config_file_fixture_name) { "disabled_default_sources.config" } - - it "only includes the enable package sources" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - end - - context "that has a numeric key" do - let(:config_file_fixture_name) { "numeric_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that only provides versioned `SearchQueryService`` entries" do - let(:config_file_fixture_name) { "versioned_search.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "includes repositories in the `trustedSigners` section" do - let(:config_file_fixture_name) { "with_trustedSigners.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, - { - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "with GitHub packages url" do - let(:config_file_fixture_name) { "github.nuget.config" } - - before do - repo_url = "https://nuget.pkg.github.com/some-namespace/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://nuget.pkg.github.com/some-namespace/download", - registration_url: "https://nuget.pkg.github.com/some-namespace/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.pkg.github.com/some-namespace/index.json", - versions_url: "https://nuget.pkg.github.com/some-namespace/download/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.pkg.github.com/some-namespace/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that has a non-ascii key" do - let(:config_file_fixture_name) { "non_ascii_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "that uses the v2 API alongside the v3 API" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }] - ) - end - end - - context "that has no base url in v2 API response" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_no_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2/", - repository_url: "https://www.nuget.org/api/v2/", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice/api/v2", - repository_url: "https://www.myget.org/F/azure-appservice/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice-staging/api/v2", - repository_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - repository_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - versions_url: - "https://www.myget.org/F/fusemandistfeed/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - repository_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - versions_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }] - ) - end - end - - context "matching `packageSourceMapping` entries are honored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # `source1` and `source3` should never be queried - stub_index_json("https://nuget.example.com/source2/index.json") - end - - it "matches on the best pattern" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "non-matching `packageSourceMapping` entries are ignored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # all sources will need to be queried - stub_index_json("https://nuget.example.com/source1/index.json") - stub_index_json("https://nuget.example.com/source2/index.json") - stub_index_json("https://nuget.example.com/source3/index.json") - end - - it "returns all sources" do - expect(dependency_urls).to match_array( - [{ - base_url: "https://nuget.example.com/source1/PackageBaseAddress", - registration_url: "https://nuget.example.com/source1/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source1/index.json", - versions_url: "https://nuget.example.com/source1/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source1/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source3/PackageBaseAddress", - registration_url: "https://nuget.example.com/source3/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source3/index.json", - versions_url: "https://nuget.example.com/source3/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source3/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb deleted file mode 100644 index c3e0ba00665..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::TfmFinder do - let(:project_name) { "tfm_finder" } - let(:dependency_files) { nuget_project_dependency_files(project_name, directory: "/").reverse } - let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - subject(:frameworks) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - Dependabot::Nuget::TfmFinder.frameworks(dependency) - end - - describe "#frameworks" do - context "when checking for a transitive dependency" do - let(:dependency_requirements) { [] } - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - its(:length) { is_expected.to eq(2) } - end - - context "when checking for a top-level dependency" do - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "2.3.0", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Serilog" } - let(:dependency_version) { "2.3.0" } - - its(:length) { is_expected.to eq(1) } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb deleted file mode 100644 index 2230a9fa3b1..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb +++ /dev/null @@ -1,695 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/version_finder" -require "dependabot/nuget/update_checker/tfm_comparer" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::UpdateChecker::VersionFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - - let(:finder) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: raise_on_ignored, - security_advisories: security_advisories, - repo_contents_path: repo_contents_path - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:ignored_versions) { [] } - let(:raise_on_ignored) { false } - let(:security_advisories) { [] } - - let(:nuget_versions_url) do - "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:nuget_search_url) do - "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:version_class) { Dependabot::Nuget::Version } - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - let(:nuspec) do - fixture("nuspecs", "#{dependency_name}.#{dependency_version}.nuspec") - end - - let(:nuspec_url) do - "https://api.nuget.org/v3-flatcontainer/#{dependency_name.downcase}/#{dependency_version}/#{dependency_name.downcase}.nuspec" - end - - let(:version_instance) do - version_class.new(dependency_version) - end - - let(:expected_version_instance) do - version_class.new(expected_version) - end - - before do - stub_request(:get, nuget_versions_url) - .to_return(status: 200, body: nuget_versions) - stub_request(:get, nuget_search_url) - .to_return(status: 200, body: nuget_search_results) - end - - describe "#latest_version_details" do - subject(:latest_version_details) { finder.latest_version_details } - - let(:expected_version) { "2.1.0" } - let(:current_compatible) { true } - let(:expected_compatible) { true } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(current_compatible) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(expected_compatible) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - - context "when the returned versions is prefixed with a zero-width char" do - let(:nuget_search_results) do - fixture("nuget_responses", "search_results_zero_width.json") - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when the user wants a pre-release" do - let(:dependency_version) { "2.2.0-preview1-26216-03" } - let(:expected_version) { "2.2.0-preview2-26406-04" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - - context "for a previous version" do - let(:dependency_version) { "2.1.0-preview1-26216-03" } - let(:expected_version) { "2.1.0" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - end - end - - context "when the user wants a pre-release with wildcard" do - let(:dependency_version) { "*-*" } - let(:current_compatible) { false } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "*-*", groups: ["dependencies"], source: nil }] - end - its([:version]) do - is_expected.to eq(version_class.new("2.2.0-preview2-26406-04")) - end - end - - context "when the user is using an unfound property" do - let(:dependency_version) { "$PackageVersion_LibGit2SharpNativeBinaries" } - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "raise_on_ignored when later versions are allowed" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - - context "when the user is on the latest version" do - let(:dependency_version) { "2.1.0" } - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - end - - context "when the current version isn't known" do - let(:dependency_version) { nil } - let(:current_compatible) { false } - let(:expected_version) { nil } - let(:expected_compatible) { false } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - end - - context "when the dependency is a git dependency" do - let(:dependency_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "doesn't raise an error" do - expect { subject }.to_not raise_error - end - end - end - - context "when the user is ignoring all later versions" do - let(:ignored_versions) { ["> 1.1.1"] } - its([:version]) { is_expected.to eq(version_class.new("1.1.1")) } - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "raises an error" do - expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when the user is ignoring the latest version" do - let(:ignored_versions) { ["[2.a,3.0.0)"] } - let(:expected_version) { "1.1.2" } - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when a version range is specified using Ruby syntax" do - let(:ignored_versions) { [">= 2.a, < 3.0.0"] } - let(:expected_version) { "1.1.2" } - its([:version]) { is_expected.to eq(version_class.new("1.1.2")) } - end - - context "when the user has ignored all versions" do - let(:ignored_versions) { ["[0,)"] } - it "returns nil" do - expect(subject).to be_nil - end - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "raises an error" do - expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when an open version range is specified using Ruby syntax" do - let(:ignored_versions) { ["> 0"] } - it "returns nil" do - expect(subject).to be_nil - end - - context "raise_on_ignored" do - let(:raise_on_ignored) { true } - it "raises an error" do - expect { subject }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "with a custom repo in a nuget.config file" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "nuget.config") - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - # skipped - # its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "that uses the v2 API" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "with_v2_endpoints.config") - ) - end - - let(:custom_v3_nuget_versions_url) do - "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/" \ - "#{dependency_name}/index.json" - end - - let(:expected_version) { "4.8.1" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_v3_nuget_versions_url) - .to_return(status: 404) - - custom_v2_nuget_versions_url = - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'#{dependency_name}'" - stub_request(:get, custom_v2_nuget_versions_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_versions.xml") - ) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - end - - context "with a package that returns paginated api results when using the v2 nuget api", :vcr do - let(:dependency_files) { project_dependency_files("paginated_package_v2_api") } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "4.7.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "FakeItEasy" } - let(:dependency_version) { "4.7.1" } - let(:expected_version) { "7.3.0" } - - it "returns the expected version" do - expect(subject[:version]).to eq(expected_version_instance) - end - end - - context "with a custom repo in the credentials", :vcr do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "that does not return PackageBaseAddress" do - let(:custom_repo_url) { "http://www.myget.org/artifactory/api/nuget/v3/dependabot-nuget-local" } - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(admin password)) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - end - - context "with a version range specified" do - let(:dependency_files) { project_dependency_files("version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0, 3.0.0)", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with an open upper version range specified" do - let(:dependency_files) { project_dependency_files("open_upper_version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0-alpha,", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with a package that is implicitly referenced", :vcr do - let(:dependency_files) { project_dependency_files("implicit_reference") } - let(:dependency_requirements) do - [{ file: "implicitReference.csproj", requirement: "1.1.2-beta1.22511.2", groups: ["dependencies"], - source: nil }] - end - let(:dependency_name) { "NuGet.Protocol" } - let(:dependency_version) { "6.3.0" } - - # skipped - # it "returns the expected version" do - # expect(subject[:version]).to eq(version_class.new("6.5.0")) - # end - end - - context "when the package can't be meaninfully sorted by just version" do - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - reported_versions = [ - "2.6.1", - "2.7.1", - "3.4.0", - "3.14.0", - "4.0.1" - ] - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/nunit/index.json") - .to_return( - status: 200, - body: { - items: [ - items: reported_versions.map { |v| { catalogEntry: { listed: true, version: v } } } - ] - }.to_json - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/3.14.0/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.3.14.0_faked.nuspec")) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/4.0.1/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.4.0.1_faked.nuspec")) - end - - let(:csproj_body) do - <<~XML - - - netcoreapp3.1 - - - - - - XML - end - let(:expected_version) { version_class.new("3.14.0") } - let(:dependency_version) { "3.14.0" } - let(:dependency) do - Dependabot::Dependency.new( - name: "nunit", - version: dependency_version, - requirements: [{ file: "my.csproj", requirement: "3.14.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - - it "returns the expected version" do - expect(subject[:version]).to eq(version_class.new("3.14.0")) - end - end - - context "when `packageSourceMapping`s are specified" do - let(:csproj_body) do - <<~XML - - - net8.0 - - - - - - XML - end - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: - <<~XML - - - - - - - - - - - - - - - - - - XML - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:dependency) do - Dependabot::Dependency.new( - name: "Some.Package", - version: "1.0.0", - requirements: [{ file: "my.csproj", requirement: "1.0.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - let(:expected_version) { version_class.new("1.1.0") } - - def create_nupkg(nuspec_name, nuspec_content) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(nuspec_content) - end - content.rewind - content.sysread - end - - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - - # stub source 1 - stub_index_json("https://nuget.example.com/source1/index.json") - stub_request(:get, "https://nuget.example.com/source1/RegistrationsBaseUrl/some.package/index.json") - .to_return( - status: 200, - body: { - count: 1, - items: [ - { - count: 2, - items: [ - { - catalogEntry: { - id: "Some.Package", - version: "1.0.0" # this is what's currently installed - } - }, - { - catalogEntry: { - id: "Some.Package", - version: "1.1.0" # this is what we'd like to upgrade to - } - } - ] - } - ] - }.to_json - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.0.0/some.package.1.0.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.1.0/some.package.1.1.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - # none of the `source2` URLs should be called - end - - it "returns the expected version honoring the package source mapping" do - expect(subject[:version]).to eq(version_class.new("1.1.0")) - end - end - end - - describe "#lowest_security_fix_version_details" do - subject(:lowest_security_fix_version_details) do - finder.lowest_security_fix_version_details - end - - let(:dependency_version) { "1.1.1" } - let(:security_advisories) do - [ - Dependabot::SecurityAdvisory.new( - dependency_name: "rails", - package_manager: "nuget", - vulnerable_versions: ["< 2.0.0"] - ) - ] - end - - let(:expected_version) { "2.0.0" } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(true) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(true) - end - - its([:version]) { is_expected.to eq(version_class.new("2.0.0")) } - - context "when the user is ignoring the lowest version" do - let(:ignored_versions) { [">= 2.a, <= 2.0.0"] } - let(:expected_version) { "2.0.3" } - its([:version]) { is_expected.to eq(version_class.new("2.0.3")) } - end - end - - describe "#versions" do - subject(:versions) { finder.versions } - - it "includes the correct versions" do - expect(versions.count).to eq(21) - expect(versions.first).to eq( - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.0.0-rc2-002702/" \ - "microsoft.extensions.dependencymodel.nuspec", - repo_url: "https://api.nuget.org/v3/index.json", - source_url: nil, - version: Dependabot::Nuget::Version.new("1.0.0-rc2-002702") - ) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index f04c5da99a3..a8877bddfda 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -4,13 +4,28 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" +require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker" require "dependabot/nuget/version" require_common_spec "update_checkers/shared_examples_for_update_checkers" + RSpec.describe Dependabot::Nuget::UpdateChecker do it_behaves_like "an update checker" + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end + let(:checker) do + # We have to run the FileParser first to ensure the dicovery.json is generated. + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse described_class.new( dependency: dependency, dependency_files: dependency_files, From c2bfefd3aa3f0f160e65e429d12b17406141e159 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Thu, 18 Apr 2024 23:24:34 -0700 Subject: [PATCH 05/13] More ruby test progress. --- .../EntryPointTests.Analyze.cs | 46 +++ .../EntryPointTests.Discover.cs | 8 +- .../NuGetUpdater/NuGetUpdater.Cli/Program.cs | 1 + .../Analyze/AnalyzeWorkerTests.cs | 79 ++++- .../Discover/DiscoveryWorkerTestBase.cs | 4 +- .../DiscoveryWorkerTests.DotNetToolsJson.cs | 4 +- .../DiscoveryWorkerTests.GlobalJson.cs | 4 +- .../DiscoveryWorkerTests.PackagesConfig.cs | 2 +- .../Discover/DiscoveryWorkerTests.Project.cs | 12 +- .../Discover/DiscoveryWorkerTests.cs | 8 +- .../Discover/ExpectedDiscoveryResults.cs | 4 +- .../Analyze/AnalyzeWorker.cs | 62 ++-- .../NuGetUpdater.Core/Analyze/Extensions.cs | 12 +- .../Discover/DiscoveryWorker.cs | 2 +- .../Discover/WorkspaceDiscoveryResult.cs | 4 +- .../Utilities/MSBuildHelper.cs | 2 + .../nuget/analysis/analysis_json_reader.rb | 2 + .../nuget/discovery/workspace_discovery.rb | 12 +- .../nuget/nuget_config_credential_helpers.rb | 5 +- nuget/lib/dependabot/nuget/update_checker.rb | 8 + .../dependabot/nuget/nuget_client_spec.rb | 137 -------- .../requirements_updater_spec.rb | 30 +- .../dependabot/nuget/update_checker_spec.rb | 305 ++---------------- .../fixtures/csproj/property_version.csproj | 2 +- 24 files changed, 254 insertions(+), 501 deletions(-) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs delete mode 100644 nuget/spec/dependabot/nuget/nuget_client_spec.rb diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs new file mode 100644 index 00000000000..d44c4073b72 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs @@ -0,0 +1,46 @@ +using System.Text; + +using NuGetUpdater.Core.Test.Analyze; + +using Xunit; + +namespace NuGetUpdater.Cli.Test; + +using TestFile = (string Path, string Content); + +public partial class EntryPointTests +{ + public class Analyze : AnalyzeWorkerTestBase + { + private static async Task Run(Func getArgs, string dependencyName, TestFile[] initialFiles, ExpectedAnalysisResult expectedResult) + { + var actualResult = await RunAnalyzerAsync(dependencyName, initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); + + try + { + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + } + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 2ccd2458e40..2a51046e765 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -77,7 +77,7 @@ public async Task WithSolution() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -136,7 +136,7 @@ public async Task WithProject() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -195,7 +195,7 @@ public async Task WithDirectory() }, expectedResult: new() { - FilePath = workspacePath, + Path = workspacePath, Projects = [ new() { @@ -221,6 +221,8 @@ public async Task WithDirectory() { var actualResult = await RunDiscoveryAsync(initialFiles, async path => { + expectedResult = expectedResult with { Path = Path.Combine(path, expectedResult.Path) }; + var sb = new StringBuilder(); var writer = new StringWriter(sb); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs index 772fb9bf683..ff0235509b4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs @@ -15,6 +15,7 @@ internal static async Task Main(string[] args) { FrameworkCheckCommand.GetCommand(setExitCode), DiscoverCommand.GetCommand(setExitCode), + AnalyzeCommand.GetCommand(setExitCode), UpdateCommand.GetCommand(setExitCode), }; command.TreatUnmatchedTokensAsErrors = true; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs index fd4a19057b8..221867faf2c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -12,7 +12,7 @@ public async Task FindsUpdatedVersion() await TestAnalyzeAsync( discovery: new() { - FilePath = "/", + Path = "/", Projects = [ new() { @@ -50,7 +50,7 @@ public async Task FindsUpdatedPeerDependencies() await TestAnalyzeAsync( discovery: new() { - FilePath = "/", + Path = "/", Projects = [ new() { @@ -84,7 +84,6 @@ public async Task FindsUpdatedPeerDependencies() ); } - [Fact] public async Task DeterminesMultiPropertyVersion() { @@ -92,7 +91,7 @@ public async Task DeterminesMultiPropertyVersion() await TestAnalyzeAsync( discovery: new() { - FilePath = "/", + Path = "/", Projects = [ new() { @@ -131,4 +130,76 @@ public async Task DeterminesMultiPropertyVersion() } ); } + + [Fact] + public async Task ReturnsUpToDate_ForMissingVersionProperty() + { + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "$(MissingPackageVersion)", DependencyType.PackageReference, EvaluationResult: new EvaluationResult(EvaluationResultType.PropertyNotFound, "$(MissingPackageVersion)", "$(MissingPackageVersion)", "$(MissingPackageVersion)", ErrorMessage: null)), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis", + Version = "$(MissingPackageVersion)", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "$(MissingPackageVersion)", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } + + [Fact] + public async Task ReturnsUpToDate_ForMissingDependency() + { + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.0.1", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 36e5807374e..00e16d4ffc6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -20,6 +20,8 @@ public class DiscoveryWorkerTestBase { var actualResult = await RunDiscoveryAsync(files, async directoryPath => { + expectedResult = expectedResult with { Path = Path.Combine(directoryPath, expectedResult.Path) }; + var worker = new DiscoveryWorker(new Logger(verbose: true)); await worker.RunAsync(directoryPath, workspacePath, DiscoveryWorker.DiscoveryResultFileName); }); @@ -30,7 +32,7 @@ public class DiscoveryWorkerTestBase protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult) { Assert.NotNull(actualResult); - Assert.Equal(expectedResult.FilePath.NormalizePathToUnix(), actualResult.FilePath.NormalizePathToUnix()); + Assert.Equal(expectedResult.Path.NormalizePathToUnix(), actualResult.Path.NormalizePathToUnix()); ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps); ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson); ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs index ce0cffdda4c..70c6852a27d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs @@ -35,7 +35,7 @@ public async Task DiscoversDependencies() ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", @@ -77,7 +77,7 @@ public async Task ReportsFailure() ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs index dd0b0ee8bac..65577d5cce1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs @@ -25,7 +25,7 @@ public async Task DiscoversDependencies() ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", @@ -57,7 +57,7 @@ public async Task ReportsFailure() ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs index a0dcab83d5f..fc05e4e4943 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs @@ -38,7 +38,7 @@ public async Task DiscoversDependencies() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs index 5dfa34eb784..ed30c96c525 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -39,7 +39,7 @@ public async Task ReturnsPackageReferencesMissingVersions() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -110,7 +110,7 @@ public async Task WithDirectoryPackagesProps() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -188,7 +188,7 @@ public async Task WithDirectoryBuildPropsAndTargets() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -293,7 +293,7 @@ public async Task WithPackagesProps() ], expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 3, Projects = [ new() @@ -357,7 +357,7 @@ public async Task ReturnsDependenciesThatCannotBeEvaluated() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -397,7 +397,7 @@ public async Task NoDependenciesReturnedIfNoTargetFrameworkCanBeResolved() ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [] }); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index 9351ca51aea..c8218c41e77 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -29,7 +29,7 @@ public async Task TestProjectFiles(string projectPath) }, expectedResult: new() { - FilePath = "src", + Path = "src", Projects = [ new() { @@ -85,7 +85,7 @@ public async Task TestPackageConfig() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -139,7 +139,7 @@ public async Task TestProps() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -258,7 +258,7 @@ public async Task TestRepo() }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index 4fd2094d7b2..e5d65ad09dd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -4,9 +4,9 @@ namespace NuGetUpdater.Core.Test.Discover; -public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult +public record ExpectedWorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public int? ExpectedProjectCount { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs index ce1884e52a5..fcbcd1e9f91 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -31,11 +31,14 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a var discovery = await DeserializeJsonFileAsync(discoveryPath, nameof(WorkspaceDiscoveryResult)); var dependencyInfo = await DeserializeJsonFileAsync(dependencyPath, nameof(DependencyInfo)); + _logger.Log($"Starting analysis of {dependencyInfo.Name}..."); + // We need to find all projects which have the given dependency. Even in cases that they // have it transitively may require that peer dependencies be updated in the project. var projectsWithDependency = discovery.Projects .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) .ToImmutableArray(); + var foundDependency = projectsWithDependency.Length > 0; var projectFrameworks = projectsWithDependency .SelectMany(p => p.TargetFrameworks) .Distinct() @@ -48,27 +51,37 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a .Select(d => d.Name) .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); - bool versionComesFromMultiDependencyProperty = DoesDependencyUseMultiDependencyProperty( - discovery, - dependencyInfo, - projectsWithDependency); + bool versionComesFromMultiDependencyProperty = false; + NuGetVersion? updatedVersion = null; + ImmutableArray updatedDependencies = []; - var updatedVersion = await FindUpdatedVersionAsync( - dependencyInfo, - projectFrameworks, - _logger, - CancellationToken.None); - - var updatedDependencies = updatedVersion is not null - ? await FindUpdatedDependenciesAsync( + if (foundDependency) + { + _logger.Log($" Calculating multi-dependency property."); + versionComesFromMultiDependencyProperty = DoesDependencyUseMultiDependencyProperty( discovery, - projectsWithDependency, - projectFrameworks, - projectDependencyNames, dependencyInfo, - updatedVersion, - _logger) - : []; + projectsWithDependency); + + _logger.Log($" Finding updated version."); + updatedVersion = await FindUpdatedVersionAsync( + dependencyInfo, + projectFrameworks, + _logger, + CancellationToken.None); + + _logger.Log($" Finding updated peer dependencies."); + updatedDependencies = updatedVersion is not null + ? await FindUpdatedDependenciesAsync( + discovery, + projectsWithDependency, + projectFrameworks, + projectDependencyNames, + dependencyInfo, + updatedVersion, + _logger) + : []; + } var result = new AnalysisResult { @@ -78,7 +91,9 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a UpdatedDependencies = updatedDependencies, }; - await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result); + await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result, _logger); + + _logger.Log($"Analysis complete."); } internal static async Task DeserializeJsonFileAsync(string path, string fileType) @@ -202,9 +217,9 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi Logger logger) { // Determine updated peer dependencies - var workspacePath = discovery.FilePath; + var workspacePath = discovery.Path; // We need any project path so the dependency finder can locate the nuget.config - var projectPath = projectsWithDependency.First().FilePath; + var projectPath = Path.Combine(workspacePath, projectsWithDependency.First().FilePath); // Create distinct list of dependencies taking the highest version of each var dependencyResult = await DependencyFinder.GetDependenciesAsync( @@ -246,7 +261,7 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi }); } - internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result) + internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result, Logger logger) { if (!Directory.Exists(analysisDirectory)) { @@ -254,6 +269,9 @@ internal static async Task WriteResultsAsync(string analysisDirectory, string de } var resultPath = Path.Combine(analysisDirectory, $"{dependencyName}.json"); + + logger.Log($" Writing analysis result to [{resultPath}]."); + var resultJson = JsonSerializer.Serialize(result, SerializerOptions); await File.WriteAllTextAsync(path: resultPath, resultJson); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs index 8817e1c87ee..8089fd5e78f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs @@ -14,13 +14,15 @@ public static ImmutableArray GetDependencies(this ImmutableDictionar { foreach (var dependency in dependenciesForTfm) { - if (dependencies.TryGetValue(dependency.Name, out Dependency? value) && - NuGetVersion.Parse(value.Version!) < NuGetVersion.Parse(dependency.Version!)) + if (dependencies.TryGetValue(dependency.Name, out Dependency? value)) { - dependencies[dependency.Name] = dependency with + if (NuGetVersion.Parse(value.Version!) < NuGetVersion.Parse(dependency.Version!)) { - TargetFrameworks = [.. value.TargetFrameworks ?? [], .. dependency.TargetFrameworks ?? []] - }; + dependencies[dependency.Name] = dependency with + { + TargetFrameworks = [.. value.TargetFrameworks ?? [], .. dependency.TargetFrameworks ?? []] + }; + } } else { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index adc4e59987f..733b0a32315 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -72,7 +72,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out var result = new WorkspaceDiscoveryResult { - FilePath = repoRootPath != workspacePath ? Path.GetRelativePath(repoRootPath, workspacePath) : string.Empty, + Path = workspacePath, DotNetToolsJson = dotNetToolsJsonDiscovery, GlobalJson = globalJsonDiscovery, DirectoryPackagesProps = directoryPackagesPropsDiscovery, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index cae5259bb4c..9f38552b529 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -2,9 +2,9 @@ namespace NuGetUpdater.Core.Discover; -public sealed record WorkspaceDiscoveryResult : IDiscoveryResult +public sealed record WorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 2c658021ac6..30d3335b9df 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -538,6 +538,8 @@ private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buil true + + false """); diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb index 77413c3cc7c..a8591745df2 100644 --- a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "dependabot/dependency" +require "dependabot/nuget/analysis/dependency_analysis" require "dependabot/nuget/discovery/discovery_json_reader" require "json" require "sorbet-runtime" @@ -45,6 +46,7 @@ def dependency_analysis raise Dependabot::DependencyFileNotParseable, analysis_json.path unless analysis_json.content Dependabot.logger.info("#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}") + puts "#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}" parsed_json = T.let(JSON.parse(T.must(analysis_json.content)), T::Hash[String, T.untyped]) DependencyAnalysis.from_json(parsed_json) diff --git a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb index 29827953c74..f07b876dc8f 100644 --- a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb @@ -13,7 +13,7 @@ class WorkspaceDiscovery sig { params(json: T::Hash[String, T.untyped]).returns(WorkspaceDiscovery) } def self.from_json(json) - file_path = T.let(json.fetch("FilePath"), String) + path = T.let(json.fetch("Path"), String) projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| ProjectDiscovery.from_json(project) end @@ -25,7 +25,7 @@ def self.from_json(json) dotnet_tools_json = DependencyFileDiscovery .from_json(T.let(json.fetch("DotNetToolsJson"), T.nilable(T::Hash[String, T.untyped]))) - WorkspaceDiscovery.new(file_path: file_path, + WorkspaceDiscovery.new(path: path, projects: projects, directory_packages_props: directory_packages_props, global_json: global_json, @@ -33,14 +33,14 @@ def self.from_json(json) end sig do - params(file_path: String, + params(path: String, projects: T::Array[ProjectDiscovery], directory_packages_props: T.nilable(DirectoryPackagesPropsDiscovery), global_json: T.nilable(DependencyFileDiscovery), dotnet_tools_json: T.nilable(DependencyFileDiscovery)).void end - def initialize(file_path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) - @file_path = file_path + def initialize(path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) + @path = path @projects = projects @directory_packages_props = directory_packages_props @global_json = global_json @@ -48,7 +48,7 @@ def initialize(file_path:, projects:, directory_packages_props:, global_json:, d end sig { returns(String) } - attr_reader :file_path + attr_reader :path sig { returns(T::Array[ProjectDiscovery]) } attr_reader :projects diff --git a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb index c1ac11f2a02..ad8eee731b2 100644 --- a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb +++ b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb @@ -69,12 +69,13 @@ def self.patch_nuget_config_for_action(credentials, &_block) begin yield rescue StandardError => e - Dependabot.logger.error( + log_message = <<~LOG_MESSAGE Block argument of NuGetConfigCredentialHelpers::patch_nuget_config_for_action causes an exception #{e}: #{e.message} LOG_MESSAGE - ) + Dependabot.logger.error(log_message) + puts log_message ensure restore_user_nuget_config end diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index 5269aaaf839..e781a59103d 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -114,6 +114,14 @@ def write_dependency_info } end }.to_json + dependency_directory = File.dirname(dependency_file_path) + + begin + Dir.mkdir(dependency_directory) + rescue StandardError + nil? + end + File.write(dependency_file_path, dependency_info) end diff --git a/nuget/spec/dependabot/nuget/nuget_client_spec.rb b/nuget/spec/dependabot/nuget/nuget_client_spec.rb deleted file mode 100644 index bfe148b1ccc..00000000000 --- a/nuget/spec/dependabot/nuget/nuget_client_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/nuget/nuget_client" - -RSpec.describe Dependabot::Nuget::NugetClient do - describe "#get_package_versions" do - let(:dependency_name) { "Some.Dependency" } - - subject(:package_versions) do - Dependabot::Nuget::NugetClient.get_package_versions(dependency_name, repository_details) - end - - context "package versions from local" do - let(:repository_details) do - nuget_dir = File.join(File.dirname(__FILE__), "..", "..", "fixtures", "nuget_responses", "local_repo") - base_url = File.expand_path(nuget_dir) - - { - base_url: base_url, - repository_type: "local" - } - end - - it "expects to crawl the directory" do - expect(package_versions).to eq(Set["1.0.0", "1.1.0"]) - end - end - - context "package versions _might_ have the `listed` flag" do - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, # nuget.org provides this flag and it should be honored - version: "0.1.0" - } - }, - { - catalogEntry: { - listed: false, # if this is ever false, the package should not be included - version: "0.1.1" - } - }, - { - catalogEntry: { - # e.g., github doesn't have the `listed` flag, but should still be returned - version: "0.1.2" - } - } - ] - ] - }.to_json - ) - end - - let(:repository_details) do - { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "#{dependency_name.downcase}/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=#{dependency_name.downcase}&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["0.1.0", "0.1.2"]) - end - end - - context "versions can be retrieved from v2 apis" do - before do - stub_request(:get, "https://www.nuget.org/api/v2/FindPackagesById()?id=%27Some.Dependency%27") - .to_return( - status: 200, - body: - <<~XML - - - - - Some.Dependency - - - 1.0.0.0 - - - - - Some.Dependency - - 1.1.0.0 - - - - - Some.Dependency.But.The.Wrong.One - - 1.2.0.0 - - - - - XML - ) - end - - let(:repository_details) do - { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: "https://www.nuget.org/api/v2/FindPackagesById()?id='#{dependency_name}'", - auth_header: {}, - repository_type: "v2" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["1.0.0.0", "1.1.0.0"]) - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb b/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb index 097d1b09113..1833279b291 100644 --- a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb @@ -8,8 +8,7 @@ let(:updater) do described_class.new( requirements: requirements, - latest_version: latest_version, - source_details: source_details + latest_version: latest_version ) end @@ -114,26 +113,12 @@ file: "my.csproj", requirement: "23.6-jre", groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + source: nil }, { file: "another/my.csproj", requirement: "[23.6-jre]", groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + source: nil }] ) end @@ -147,14 +132,7 @@ file: "my.csproj", requirement: "23.6-jre", groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + source: nil }, { file: "another/my.csproj", requirement: "[23.0,)", diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index a8877bddfda..62febbd541f 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -6,6 +6,7 @@ require "dependabot/dependency_file" require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker" +require "dependabot/nuget/requirement" require "dependabot/nuget/version" require_common_spec "update_checkers/shared_examples_for_update_checkers" @@ -68,14 +69,6 @@ let(:version_class) { Dependabot::Nuget::Version } - def nuspec_url(name, version) - "https://api.nuget.org/v3-flatcontainer/#{name.downcase}/#{version}/#{name.downcase}.nuspec" - end - - def registration_index_url(name) - "https://api.nuget.org/v3/registration5-gz-semver2/#{name.downcase}/index.json" - end - describe "up_to_date?" do subject(:up_to_date?) { checker.up_to_date? } @@ -108,51 +101,6 @@ def registration_index_url(name) end end - describe "#latest_version" do - subject { checker.latest_version } - - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:latest_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) - - expect(checker.latest_version).to eq("1.2.3") - end - - context "the package could not be found on any source" do - before do - stub_request(:get, registration_index_url("microsoft.extensions.dependencymodel")) - .to_return(status: 404) - end - - it "reports the current version" do - expect(checker.latest_version).to eq("1.1.1") - end - end - end - - describe "#lowest_security_fix_version" do - subject { checker.lowest_security_fix_version } - - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:lowest_security_fix_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) - - expect(checker.lowest_security_fix_version).to eq("1.2.3") - end - end - describe "#latest_resolvable_version" do subject(:latest_resolvable_version) { checker.latest_resolvable_version } it { is_expected.to be_nil } @@ -163,8 +111,10 @@ def registration_index_url(name) it { is_expected.to be_nil } end - describe "#can_update?(requirements_to_unlock: :all)" do - subject(:can_update) { checker.can_update?(requirements_to_unlock: :all) } + describe "#requirements_unlocked_or_can_be?" do + subject(:requirements_unlocked_or_can_be) do + checker.requirements_unlocked_or_can_be? + end context "with a property dependency" do let(:dependency_requirements) do @@ -179,109 +129,27 @@ def registration_index_url(name) let(:dependency_name) { "Nuke.Common" } let(:dependency_version) { "0.1.434" } - context "that is used for multiple dependencies" do - let(:csproj_body) do - fixture("csproj", "property_version.csproj") - end - - context "where all dependencies can update to the latest version" do - before do - allow(checker).to receive(:all_property_based_dependencies).and_return( - [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ] - ) - - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:latest_version).and_return("0.9.0") - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - end + it { is_expected.to eq(true) } - it { is_expected.to eq(true) } + context "whose property couldn't be found" do + let(:dependency_requirements) do + [{ + requirement: "$(NukeVersion)", + file: "my.csproj", + groups: ["dependencies"], + source: nil, + metadata: { property_name: "NukeVersion" } + }] end + let(:dependency_version) { "$(NukeVersion)" } - context "where not all dependencies can update to the latest version" do - before do - allow(checker).to receive(:all_property_based_dependencies).and_return( - [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ] - ) - - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:latest_version).and_return("0.9.0") - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(false) - end - - it { is_expected.to eq(false) } - end + it { is_expected.to eq(false) } end end end - describe "#updated_requirements" do - subject(:updated_requirements) { checker.updated_requirements } - - let(:target_version) { "2.1.0" } - - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:latest_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] - ) - end + describe "#lowest_security_fix_version" do + subject { checker.lowest_security_fix_version } context "with a security vulnerability" do let(:target_version) { "2.0.0" } @@ -296,113 +164,19 @@ def registration_index_url(name) ] end - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:lowest_security_fix_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] - ) - end + it { is_expected.to eq(target_version) } context "the security vulnerability excludes all compatible packages" do let(:target_version) { "1.1.1" } let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad - subject(:updated_requirement_version) { updated_requirements[0].fetch(:requirement) } - - before do - # only vulnerable versions are returned - stub_request(:get, registration_index_url(dependency_name)) - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - version: "1.1.1" # the currently installed version, but it's vulnerable - } - }, - { - catalogEntry: { - version: "3.0.0" # newer version, but it's still vulnerable - } - } - ] - ] - }.to_json - ) - end it "reports the currently installed version" do - expect(updated_requirement_version).to eq(target_version) + is_expected.to eq(dependency_version) end end end end - describe "#requirements_unlocked_or_can_be?" do - subject(:requirements_unlocked_or_can_be) do - checker.requirements_unlocked_or_can_be? - end - - context "with a property dependency" do - let(:dependency_requirements) do - [{ - requirement: "0.1.434", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - end - let(:dependency_name) { "Nuke.Common" } - let(:dependency_version) { "0.1.434" } - - it { is_expected.to eq(true) } - - context "whose property couldn't be found" do - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - end - let(:dependency_version) { "$(NukeVersion)" } - - it { is_expected.to eq(false) } - end - end - end - describe "#updated_dependencies(requirements_to_unlock: :all)" do subject(:updated_dependencies) do checker.updated_dependencies(requirements_to_unlock: :all) @@ -427,34 +201,17 @@ def registration_index_url(name) end context "where all dependencies can update to the latest version" do - before do - allow(checker).to receive(:latest_version).and_return("0.9.0") - allow(checker).to receive(:all_property_based_dependencies).and_return( - [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ] - ) - end - it "delegates to PropertyUpdater" do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - expect(dummy_property_updater).to receive(:updated_dependencies).and_return([dependency]) - - subject + is_expected.to eq([ + Dependabot::Dependency.new( + name: dependency_name, + version: "6.3.0", + previous_version: dependency_version, + requirements: dependency_requirements, + previous_requirements: dependency_requirements, + package_manager: "nuget" + ) + ]) end end end diff --git a/nuget/spec/fixtures/csproj/property_version.csproj b/nuget/spec/fixtures/csproj/property_version.csproj index 62557f88433..4d8198bc238 100644 --- a/nuget/spec/fixtures/csproj/property_version.csproj +++ b/nuget/spec/fixtures/csproj/property_version.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net5.0 false False From e08b1a83b3687d250ba8a1d4af6f1b5130dbf500 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 01:25:50 -0700 Subject: [PATCH 06/13] Handle multi-dependency properties --- .../Analyze/AnalyzeWorkerTests.cs | 3 +- .../Analyze/AnalyzeWorker.cs | 271 +++++++++++++----- .../Analyze/CompatabilityChecker.cs | 64 +++-- .../Analyze/DependencyFinder.cs | 11 +- .../Analyze/VersionFinder.cs | 99 ++++++- .../FrameworkChecker/CompatabilityChecker.cs | 2 - .../FrameworkCompatibilityService.cs | 3 - .../FrameworkChecker/SupportedFrameworks.cs | 3 - .../Updater/BindingRedirectManager.cs | 6 - .../Updater/BindingRedirectResolver.cs | 5 - .../Updater/PackagesConfigUpdater.cs | 5 - .../WebApplicationTargetsConditionPatcher.cs | 4 - .../Updater/XmlFilePreAndPostProcessor.cs | 2 - .../Utilities/HashSetExtensions.cs | 2 - .../NuGetUpdater.Core/Utilities/JsonHelper.cs | 4 - .../NuGetUpdater.Core/Utilities/Logger.cs | 3 - .../NuGetUpdater.Core/Utilities/PathHelper.cs | 4 - .../Utilities/ProcessExtensions.cs | 3 - .../Utilities/XmlExtensions.cs | 4 - 19 files changed, 356 insertions(+), 142 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs index 221867faf2c..a20caf91b55 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -106,7 +106,7 @@ public async Task DeterminesMultiPropertyVersion() FilePath = "./project2.csproj", TargetFrameworks = ["net8.0"], Dependencies = [ - new("Microsoft.CodeAnalysis.Workspaces", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult), ], }, ], @@ -126,6 +126,7 @@ public async Task DeterminesMultiPropertyVersion() VersionComesFromMultiDependencyProperty = true, UpdatedDependencies = [ new("Microsoft.CodeAnalysis.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), ], } ); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs index fcbcd1e9f91..3815f8b5b40 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -2,6 +2,7 @@ using System.Text.Json; using System.Text.Json.Serialization; +using NuGet.Configuration; using NuGet.Frameworks; using NuGet.Versioning; @@ -9,6 +10,8 @@ namespace NuGetUpdater.Core.Analyze; +using MultiDependency = (string PropertyName, ImmutableArray TargetFrameworks, ImmutableHashSet DependencyNames); + public partial class AnalyzeWorker { public const string AnalysisDirectoryName = "./.dependabot/analysis"; @@ -38,35 +41,56 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a var projectsWithDependency = discovery.Projects .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) .ToImmutableArray(); - var foundDependency = projectsWithDependency.Length > 0; var projectFrameworks = projectsWithDependency .SelectMany(p => p.TargetFrameworks) .Distinct() .Select(NuGetFramework.Parse) .ToImmutableArray(); - // When updating peer dependencies, we only need to consider top-level dependencies. - var projectDependencyNames = projectsWithDependency - .SelectMany(p => p.Dependencies) - .Where(d => !d.IsTransitive) - .Select(d => d.Name) - .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + var propertyBasedDependencies = discovery.Projects.SelectMany(p + => p.Dependencies.Where(d => !d.IsTransitive && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); - bool versionComesFromMultiDependencyProperty = false; + bool canUpdate = false; + bool usesMultiDependencyProperty = false; NuGetVersion? updatedVersion = null; ImmutableArray updatedDependencies = []; - if (foundDependency) + bool isUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency); + if (isUpdateNecessary) { - _logger.Log($" Calculating multi-dependency property."); - versionComesFromMultiDependencyProperty = DoesDependencyUseMultiDependencyProperty( + var nugetContext = new NuGetContext(); + if (!Directory.Exists(nugetContext.TempPackageDirectory)) + { + Directory.CreateDirectory(nugetContext.TempPackageDirectory); + } + + _logger.Log($" Determining multi-dependency property."); + var multiDependencies = DetermineMultiDependencyDetails( discovery, - dependencyInfo, - projectsWithDependency); + dependencyInfo.Name, + propertyBasedDependencies); + + usesMultiDependencyProperty = multiDependencies.Any(md => md.DependencyNames.Count > 1); + var dependenciesToUpdate = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.DependencyNames) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + : [dependencyInfo.Name]; + var applicableTargetFrameworks = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.TargetFrameworks) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(NuGetFramework.Parse) + .ToImmutableArray() + : projectFrameworks; _logger.Log($" Finding updated version."); updatedVersion = await FindUpdatedVersionAsync( dependencyInfo, - projectFrameworks, + dependenciesToUpdate, + applicableTargetFrameworks, + nugetContext, _logger, CancellationToken.None); @@ -74,12 +98,11 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a updatedDependencies = updatedVersion is not null ? await FindUpdatedDependenciesAsync( discovery, - projectsWithDependency, - projectFrameworks, - projectDependencyNames, - dependencyInfo, + dependenciesToUpdate, updatedVersion, - _logger) + nugetContext, + _logger, + CancellationToken.None) : []; } @@ -87,7 +110,7 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a { UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version, CanUpdate = updatedVersion is not null, - VersionComesFromMultiDependencyProperty = versionComesFromMultiDependencyProperty, + VersionComesFromMultiDependencyProperty = usesMultiDependencyProperty, UpdatedDependencies = updatedDependencies, }; @@ -96,6 +119,26 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a _logger.Log($"Analysis complete."); } + private static bool IsUpdateNecessary(DependencyInfo dependencyInfo, ImmutableArray projectsWithDependency) + { + if (projectsWithDependency.Length == 0) + { + return false; + } + + // We will even attempt to update transitive dependencies if the dependency is vulnerable. + if (dependencyInfo.IsVulnerable) + { + return true; + } + + // Since the dependency is not vulnerable, we only need to update if it is not transitive. + return projectsWithDependency.Any(p => + p.Dependencies.Any(d => + d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && + !d.IsTransitive)); + } + internal static async Task DeserializeJsonFileAsync(string path, string fileType) { var json = File.Exists(path) @@ -108,28 +151,74 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi internal static async Task FindUpdatedVersionAsync( DependencyInfo dependencyInfo, + ImmutableHashSet packageIds, ImmutableArray projectFrameworks, + NuGetContext nugetContext, Logger logger, CancellationToken cancellationToken) { - var nugetContext = new NuGetContext(); - if (!Directory.Exists(nugetContext.TempPackageDirectory)) - { - Directory.CreateDirectory(nugetContext.TempPackageDirectory); - } - var versionResult = await VersionFinder.GetVersionsAsync( dependencyInfo, nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + dependencyInfo.Version, + versionResult, + projectFrameworks, + findLowestVersion: dependencyInfo.IsVulnerable, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetVersion version, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + packageIds.First(), + version, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + version.ToNormalizedString(), + versionResult, + projectFrameworks, + findLowestVersion, + nugetContext, + logger, cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + ImmutableArray projectFrameworks, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { var versions = versionResult.GetVersions(); - var orderedVersions = dependencyInfo.IsVulnerable + var orderedVersions = findLowestVersion ? versions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. : versions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. return await FindFirstCompatibleVersion( - dependencyInfo.Name, - dependencyInfo.Version, + packageIds, + versionString, versionResult, orderedVersions, projectFrameworks, @@ -139,7 +228,7 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi } internal static async Task FindFirstCompatibleVersion( - string packageId, + ImmutableHashSet packageIds, string versionString, VersionResult versionResult, IEnumerable orderedVersions, @@ -150,14 +239,14 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi { if (NuGetVersion.TryParse(versionString, out var currentVersion)) { - var source = versionResult.GetPackageSources(currentVersion).First(); - var isCompatible = await CompatibilityChecker.CheckAsync( - source, - new(packageId, currentVersion), + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + currentVersion, projectFrameworks, nugetContext, logger, cancellationToken); + if (!isCompatible) { // If the current package is incompatible, then don't check for compatibility. @@ -167,10 +256,9 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi foreach (var version in orderedVersions) { - var source = versionResult.GetPackageSources(version).First(); - var isCompatible = await CompatibilityChecker.CheckAsync( - source, - new(packageId, version), + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + version, projectFrameworks, nugetContext, logger, @@ -186,6 +274,31 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi return null; } + internal static async Task AreAllPackagesCompatibleAsync( + ImmutableHashSet packageIds, + NuGetVersion currentVersion, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + var isCompatible = await CompatibilityChecker.CheckAsync( + new(packageId, currentVersion), + projectFrameworks, + nugetContext, + logger, + cancellationToken); + if (!isCompatible) + { + return false; + } + } + + return true; + } + internal static async Task>> GetDependenciesAsync( string workspacePath, string projectPath, @@ -209,13 +322,35 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi internal static async Task> FindUpdatedDependenciesAsync( WorkspaceDiscoveryResult discovery, - ImmutableArray projectsWithDependency, - ImmutableArray projectFrameworks, - ImmutableHashSet projectDependencyNames, - DependencyInfo dependencyInfo, + ImmutableHashSet packageIds, NuGetVersion updatedVersion, - Logger logger) + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) { + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => packageIds.Contains(d.Name))) + .ToImmutableArray(); + if (projectsWithDependency.Length == 0) + { + return []; + } + + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + + // When updating peer dependencies, we only need to consider top-level dependencies. + var projectDependencyNames = projectsWithDependency + .SelectMany(p => p.Dependencies) + .Where(d => !d.IsTransitive) + .Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + // Determine updated peer dependencies var workspacePath = discovery.Path; // We need any project path so the dependency finder can locate the nuget.config @@ -226,39 +361,47 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi workspacePath, projectPath, projectFrameworks, - package: new(dependencyInfo.Name, updatedVersion.ToNormalizedString(), DependencyType.Unknown), + packageIds, + updatedVersion, logger); // Filter dependencies by whether any project references them return dependencyResult.GetDependencies() - .Where(dep => projectDependencyNames.Contains(dep.Name)) + .Where(d => projectDependencyNames.Contains(d.Name)) .ToImmutableArray(); } - internal static bool DoesDependencyUseMultiDependencyProperty( + internal static ImmutableArray DetermineMultiDependencyDetails( WorkspaceDiscoveryResult discovery, - DependencyInfo dependencyInfo, - ImmutableArray projectsWithDependency) + string packageId, + ImmutableArray propertyBasedDependencies) { - var declarationsUsingProperty = projectsWithDependency.SelectMany(p - => p.Dependencies.Where(d => !d.IsTransitive && - d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && - d.EvaluationResult?.RootPropertyName is not null) - ).ToImmutableArray(); - var allPropertyBasedDependencies = discovery.Projects.SelectMany(p - => p.Dependencies.Where(d => !d.IsTransitive && - !d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && - d.EvaluationResult is not null) + var packageDeclarationsUsingProperty = discovery.Projects + .SelectMany(p => + p.Dependencies.Where(d => !d.IsTransitive && + d.Name.Equals(packageId, StringComparison.OrdinalIgnoreCase) && + d.EvaluationResult?.RootPropertyName is not null) ).ToImmutableArray(); - return declarationsUsingProperty.Any(d => - { - var property = d.EvaluationResult!.RootPropertyName!; + return packageDeclarationsUsingProperty + .Select(d => d.EvaluationResult!.RootPropertyName!) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(property => + { + // Find all dependencies that use the same property + var packages = propertyBasedDependencies + .Where(d => property.Equals(d.EvaluationResult?.RootPropertyName, StringComparison.OrdinalIgnoreCase)); + + // Combine all their target frameworks + var tfms = packages.SelectMany(d => d.TargetFrameworks ?? []) + .Distinct() + .ToImmutableArray(); + + var packageIds = packages.Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); - return allPropertyBasedDependencies - .Where(pd => !pd.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase)) - .Any(pd => pd.EvaluationResult?.RootPropertyName == property); - }); + return (property, tfms, packageIds); + }).ToImmutableArray(); } internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result, Logger logger) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs index 703f8199c61..29aa2fa88ba 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; +using NuGet.Common; using NuGet.Configuration; using NuGet.Frameworks; using NuGet.Packaging; @@ -17,7 +18,6 @@ namespace NuGetUpdater.Core.Analyze; internal static class CompatibilityChecker { public static async Task CheckAsync( - PackageSource source, PackageIdentity package, ImmutableArray projectFrameworks, NuGetContext nugetContext, @@ -25,7 +25,6 @@ internal static class CompatibilityChecker CancellationToken cancellationToken) { var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync( - source, package, nugetContext, cancellationToken); @@ -67,7 +66,6 @@ internal static class CompatibilityChecker } internal static async Task GetPackageInfoAsync( - PackageSource source, PackageIdentity package, NuGetContext nugetContext, CancellationToken cancellationToken) @@ -75,7 +73,7 @@ internal static class CompatibilityChecker var tempPackagePath = GetTempPackagePath(package, nugetContext); var readers = File.Exists(tempPackagePath) ? ReadPackage(tempPackagePath) - : await DownloadPackageAsync(source, package, nugetContext, cancellationToken); + : await DownloadPackageAsync(package, nugetContext, cancellationToken); var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken); var reader = new NuspecReader(nuspecStream); @@ -122,32 +120,56 @@ internal static PackageReaders ReadPackage(string tempPackagePath) } internal static async Task DownloadPackageAsync( - PackageSource source, PackageIdentity package, NuGetContext context, CancellationToken cancellationToken) { - var sourceRepository = Repository.Factory.GetCoreV3(source); - var feed = await sourceRepository.GetResourceAsync(); - if (feed is null) + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(package.Id).ToHashSet(); + var sources = packageSources.Count == 0 + ? context.PackageSources + : context.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) { - throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}"); - } + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}"); + } - var downloader = await feed.GetPackageDownloaderAsync( - package, - context.SourceCacheContext, - context.Logger, - cancellationToken); + var exists = await feed.DoesPackageExistAsync( + package.Id, + package.Version, + context.SourceCacheContext, + NullLogger.Instance, + cancellationToken); - var tempPackagePath = GetTempPackagePath(package, context); - var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken); - if (!isDownloaded) - { - throw new Exception("Failed to download package"); + if (!exists) + { + continue; + } + + var downloader = await feed.GetPackageDownloaderAsync( + package, + context.SourceCacheContext, + context.Logger, + cancellationToken); + + var tempPackagePath = GetTempPackagePath(package, context); + var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken); + if (!isDownloaded) + { + throw new Exception($"Failed to download package [{package.Id}/{package.Version}] from [${source.SourceUri}]"); + } + + return (downloader.CoreReader, downloader.ContentReader); } - return (downloader.CoreReader, downloader.ContentReader); + throw new Exception($"Package [{package.Id}/{package.Version}] does not exist in any of the configured sources."); } internal static string GetTempPackagePath(PackageIdentity package, NuGetContext context) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs index fee0d1cc2fb..966bad986b2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs @@ -1,6 +1,7 @@ using System.Collections.Immutable; using NuGet.Frameworks; +using NuGet.Versioning; namespace NuGetUpdater.Core.Analyze; @@ -10,9 +11,15 @@ internal static class DependencyFinder string workspacePath, string projectPath, IEnumerable frameworks, - Dependency package, + ImmutableHashSet packageIds, + NuGetVersion version, Logger logger) { + var versionString = version.ToNormalizedString(); + var packages = packageIds + .Select(id => new Dependency(id, versionString, DependencyType.Unknown)) + .ToImmutableArray(); + var result = ImmutableDictionary.CreateBuilder>(); foreach (var framework in frameworks) { @@ -20,7 +27,7 @@ internal static class DependencyFinder workspacePath, projectPath, framework.ToString(), - [package], + packages, logger); result.Add(framework, [.. dependencies.Select(d => d with { IsTransitive = false })]); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index 80ee82d4ce5..e67a3ed7b08 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -2,6 +2,7 @@ using NuGet.Common; using NuGet.Configuration; +using NuGet.Packaging.Core; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; @@ -10,17 +11,41 @@ namespace NuGetUpdater.Core.Analyze; internal static class VersionFinder { - public static async Task GetVersionsAsync( + public static Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionFilter = CreateVersionFilter(currentVersion); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static Task GetVersionsAsync( DependencyInfo dependencyInfo, NuGetContext nugetContext, + Logger logger, CancellationToken cancellationToken) { var packageId = dependencyInfo.Name; var versionRange = VersionRange.Parse(dependencyInfo.Version); var currentVersion = versionRange.MinVersion!; - var includePrerelease = currentVersion.IsPrerelease; - var versionFilter = CreateVersionFilter(dependencyInfo, versionRange); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static async Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + Func versionFilter, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = currentVersion.IsPrerelease; VersionResult result = new(currentVersion); var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); @@ -37,7 +62,7 @@ internal static class VersionFinder var feed = await sourceRepository.GetResourceAsync(); if (feed is null) { - // $"Failed to get MetadataResource for {source.SourceUri}" + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); continue; } @@ -84,4 +109,70 @@ internal static class VersionFinder && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); } + + internal static Func CreateVersionFilter(NuGetVersion currentVersion) + { + return version => version > currentVersion + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version); + } + + public static async Task DoVersionsExistAsync( + IEnumerable packageIds, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + if (!await DoesVersionExistAsync(packageId, version, nugetContext, logger, cancellationToken)) + { + return false; + } + } + + return true; + } + + public static async Task DoesVersionExistAsync( + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = version.IsPrerelease; + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + new PackageIdentity(packageId, version), + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (existsInFeed) + { + return true; + } + } + + return false; + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs index e49f4c9faf3..7457bd20346 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs @@ -1,5 +1,3 @@ -using System.Linq; - using NuGet.Frameworks; namespace NuGetUpdater.Core.FrameworkChecker; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs index e3e6642e159..349df7b7e6e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using NuGetGallery.Frameworks; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs index 9ded2bf4f2b..429fa041c07 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using static NuGet.Frameworks.FrameworkConstants; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs index 81fde97a042..6907bb79589 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs @@ -1,10 +1,4 @@ extern alias CoreV2; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; using CoreV2::NuGet.Runtime; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs index 79c57646861..a0f4adee27a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs @@ -1,10 +1,5 @@ extern alias CoreV2; - -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs index 237579ec9c9..8b036d82289 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.Language.Xml; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs index 8468cdb8c56..5d7cce01a8c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs index 5b9af19eda4..39423fb04c7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs @@ -1,5 +1,3 @@ -using System; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs index 41e8051a030..7dc0d277fca 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; - namespace NuGetUpdater.Core.Utilities; public static class HashSetExtensions diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs index 4e9527a2f1a..a31d8876f36 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs index a658bbdfe6c..f4f8eddf3f8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace NuGetUpdater.Core; public sealed class Logger diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs index 88380e31d02..bec844c1497 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - namespace NuGetUpdater.Core; internal static class PathHelper diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs index c7b5da87580..7fd5172e18a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs @@ -1,8 +1,5 @@ -using System; using System.Diagnostics; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace NuGetUpdater.Core; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs index 1a38affa7d7..a747bf54a57 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core; From 8fc77da2d5931e1bf8573349f10a55cfb0b76610 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 02:12:11 -0700 Subject: [PATCH 07/13] Fix up tests --- .../Analyze/AnalyzeWorkerTests.cs | 51 +++++++++++++- .../Analyze/AnalyzeWorker.cs | 12 ++++ .../Analyze/VersionFinder.cs | 3 +- .../nuget/analysis/analysis_json_reader.rb | 1 - .../dependabot/nuget/update_checker_spec.rb | 67 +++++++++---------- 5 files changed, 94 insertions(+), 40 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs index a20caf91b55..ee1612ec462 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -98,7 +98,7 @@ public async Task DeterminesMultiPropertyVersion() FilePath = "./project.csproj", TargetFrameworks = ["net8.0"], Dependencies = [ - new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult), + new("Microsoft.CodeAnalysis.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), ], }, new() @@ -106,7 +106,7 @@ public async Task DeterminesMultiPropertyVersion() FilePath = "./project2.csproj", TargetFrameworks = ["net8.0"], Dependencies = [ - new("Microsoft.CodeAnalysis.Workspaces.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult), + new("Microsoft.CodeAnalysis.Workspaces.Common", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), ], }, ], @@ -132,6 +132,53 @@ public async Task DeterminesMultiPropertyVersion() ); } + [Fact] + public async Task FailsToUpdateMultiPropertyVersion() + { + // Roslyn packages and System.Memory happen to share some versions but would fail to update in sync with each other. + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(RoslynPackageVersion)", "4.5.0", "RoslynPackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Microsoft.CodeAnalysis.Common", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("System.Memory", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Microsoft.CodeAnalysis.Common", + Version = "4.5.0", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.5.0", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [], + } + ); + } + + [Fact] public async Task ReturnsUpToDate_ForMissingVersionProperty() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs index 3815f8b5b40..89efb2c1336 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -104,6 +104,12 @@ public async Task RunAsync(string discoveryPath, string dependencyPath, string a _logger, CancellationToken.None) : []; + + //TODO: At this point we should add the peer dependencies to a queue where + // we will analyze them one by one to see if they themselves are part of a + // multi-dependency property. Basically looping this if-body until we have + // emptied the queue and have a complete list of updated dependencies. We + // should track the dependenciesToUpdate as they have already been analyzed. } var result = new AnalysisResult @@ -256,6 +262,12 @@ internal static async Task DeserializeJsonFileAsync(string path, string fi foreach (var version in orderedVersions) { + var existsForAll = await VersionFinder.DoVersionsExistAsync(packageIds, version, nugetContext, logger, cancellationToken); + if (!existsForAll) + { + continue; + } + var isCompatible = await AreAllPackagesCompatibleAsync( packageIds, version, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index e67a3ed7b08..0170165b7cb 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -104,7 +104,8 @@ internal static class VersionFinder ? versionRange.MinVersion : null; - return version => versionRange.Satisfies(version) + return version => (currentVersion is null || version > currentVersion) + && versionRange.Satisfies(version) && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb index a8591745df2..5ceba566fa8 100644 --- a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -46,7 +46,6 @@ def dependency_analysis raise Dependabot::DependencyFileNotParseable, analysis_json.path unless analysis_json.content Dependabot.logger.info("#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}") - puts "#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}" parsed_json = T.let(JSON.parse(T.must(analysis_json.content)), T::Hash[String, T.untyped]) DependencyAnalysis.from_json(parsed_json) diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index 62febbd541f..b9cfbc153dc 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -23,7 +23,7 @@ end let(:checker) do - # We have to run the FileParser first to ensure the dicovery.json is generated. + # We have to run the FileParser first to ensure the discovery.json is generated. Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, source: source, repo_contents_path: repo_contents_path).parse @@ -112,6 +112,10 @@ end describe "#requirements_unlocked_or_can_be?" do + let(:csproj_body) do + fixture("csproj", "property_version.csproj") + end + subject(:requirements_unlocked_or_can_be) do checker.requirements_unlocked_or_can_be? end @@ -130,49 +134,32 @@ let(:dependency_version) { "0.1.434" } it { is_expected.to eq(true) } - - context "whose property couldn't be found" do - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] - end - let(:dependency_version) { "$(NukeVersion)" } - - it { is_expected.to eq(false) } - end end end describe "#lowest_security_fix_version" do subject { checker.lowest_security_fix_version } - context "with a security vulnerability" do - let(:target_version) { "2.0.0" } - let(:vulnerable_versions) { ["< 2.0.0"] } - let(:security_advisories) do - [ - Dependabot::SecurityAdvisory.new( - dependency_name: dependency_name, - package_manager: "nuget", - vulnerable_versions: vulnerable_versions - ) - ] - end + let(:target_version) { "2.0.0" } + let(:vulnerable_versions) { ["< 2.0.0"] } + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: dependency_name, + package_manager: "nuget", + vulnerable_versions: vulnerable_versions + ) + ] + end - it { is_expected.to eq(target_version) } + it { is_expected.to eq(target_version) } - context "the security vulnerability excludes all compatible packages" do - let(:target_version) { "1.1.1" } - let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad + context "the security vulnerability excludes all compatible packages" do + let(:target_version) { "1.1.1" } + let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad - it "reports the currently installed version" do - is_expected.to eq(dependency_version) - end + it "reports the currently installed version" do + is_expected.to eq(target_version) end end end @@ -204,7 +191,15 @@ it "delegates to PropertyUpdater" do is_expected.to eq([ Dependabot::Dependency.new( - name: dependency_name, + name: "Nuke.CodeGeneration", + version: "6.3.0", + previous_version: dependency_version, + requirements: dependency_requirements, + previous_requirements: dependency_requirements, + package_manager: "nuget" + ), + Dependabot::Dependency.new( + name: "Nuke.Common", version: "6.3.0", previous_version: dependency_version, requirements: dependency_requirements, From d950160c7a9b9f47eecdbb6c1353ca1c067d0c57 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 16:05:08 -0700 Subject: [PATCH 08/13] Fix up tests. --- .../EntryPointTests.Analyze.cs | 2 - .../EntryPointTests.Discover.cs | 8 +- .../EntryPointTests.FrameworkCheck.cs | 4 - .../EntryPointTests.Update.cs | 233 +++++++++--------- .../Updater/BindingRedirectManager.cs | 1 + .../Updater/BindingRedirectResolver.cs | 1 + .../spec/dependabot/nuget/file_parser_spec.rb | 2 +- .../dependabot/nuget/native_helpers_spec.rb | 1 - 8 files changed, 125 insertions(+), 127 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs index d44c4073b72..d16f60658cb 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs @@ -2,8 +2,6 @@ using NuGetUpdater.Core.Test.Analyze; -using Xunit; - namespace NuGetUpdater.Cli.Test; using TestFile = (string Path, string Content); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index 2a51046e765..8a6b3bced45 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -1,7 +1,7 @@ -using System.Collections.Immutable; using System.Text; using NuGetUpdater.Core; +using NuGetUpdater.Core.Discover; using NuGetUpdater.Core.Test.Discover; using Xunit; @@ -25,6 +25,8 @@ public async Task WithSolution() path, "--workspace", path, + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) ], new[] { @@ -107,6 +109,8 @@ public async Task WithProject() path, "--workspace", path, + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) ], new[] { @@ -166,6 +170,8 @@ public async Task WithDirectory() path, "--workspace", Path.Combine(path, workspacePath), + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) ], new[] { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs index 0bc2f50d8df..280affb9801 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - using Xunit; namespace NuGetUpdater.Cli.Test; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs index 42199f092cb..b17d1e3384e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; using System.Text; -using System.Threading.Tasks; using NuGetUpdater.Core; using NuGetUpdater.Core.Test; @@ -13,14 +10,14 @@ namespace NuGetUpdater.Cli.Test; public partial class EntryPointTests { - public class Update : UpdateWorkerTestBase + public class Update : UpdateWorkerTestBase + { + [Fact] + public async Task WithSolution() { - [Fact] - public async Task WithSolution() - { - await Run(path => - [ - "update", + await Run(path => + [ + "update", "--repo-root", path, "--solution-or-project", @@ -32,8 +29,8 @@ public async Task WithSolution() "--previous-version", "7.0.1", ], - [ - ("path/to/solution.sln", """ + [ + ("path/to/solution.sln", """ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.22705.0 @@ -79,9 +76,9 @@ public async Task WithSolution() """) - ], - [ - ("path/to/my.csproj", """ + ], + [ + ("path/to/my.csproj", """ @@ -105,15 +102,15 @@ public async Task WithSolution() """) - ]); - } + ]); + } - [Fact] - public async Task WithProject() - { - await Run(path => - [ - "update", + [Fact] + public async Task WithProject() + { + await Run(path => + [ + "update", "--repo-root", path, "--solution-or-project", @@ -125,9 +122,9 @@ public async Task WithProject() "--previous-version", "7.0.1", "--verbose" - ], - [ - ("path/to/my.csproj", """ + ], + [ + ("path/to/my.csproj", """ @@ -150,9 +147,9 @@ public async Task WithProject() """) - ], - [ - ("path/to/my.csproj", """ + ], + [ + ("path/to/my.csproj", """ @@ -176,15 +173,15 @@ public async Task WithProject() """) - ]); - } + ]); + } - [Fact] - public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStillMatchingThePackage() - { - await Run(path => - [ - "update", + [Fact] + public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStillMatchingThePackage() + { + await Run(path => + [ + "update", "--repo-root", path, "--solution-or-project", @@ -196,10 +193,10 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill "--previous-version", "6.1.0", "--verbose" - ], - initialFiles: - [ - ("some-dir/dirs.proj", """ + ], + initialFiles: + [ + ("some-dir/dirs.proj", """ @@ -235,17 +232,17 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """), ("other-dir/Directory.Build.props", """ - + """) - ], - expectedFiles: - [ - ("some-dir/dirs.proj", """ + ], + expectedFiles: + [ + ("some-dir/dirs.proj", """ @@ -282,41 +279,41 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """), ("other-dir/Directory.Build.props", """ - + """) - ] - ); - } + ] + ); + } - [Theory] - [InlineData(null)] - [InlineData("src")] - public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? workingDirectoryPath) - { - // This is a _very_ specific scenario where the `NuGetUpdater.Cli` tool might pick up a `global.json` from - // the root of the repo under test and use it's `sdk` property when trying to locate MSBuild. To properly - // test this, it must be tested in a new process where MSBuild has not been loaded yet and the runner tool - // must be started with its working directory at the test repo's root. - using var tempDir = new TemporaryDirectory(); - var globalJsonPath = Path.Join(tempDir.DirectoryPath, "global.json"); - var srcGlobalJsonPath = Path.Join(tempDir.DirectoryPath, "src", "global.json"); - string globalJsonContent = """ + [Theory] + [InlineData(null)] + [InlineData("src")] + public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? workingDirectoryPath) + { + // This is a _very_ specific scenario where the `NuGetUpdater.Cli` tool might pick up a `global.json` from + // the root of the repo under test and use it's `sdk` property when trying to locate MSBuild. To properly + // test this, it must be tested in a new process where MSBuild has not been loaded yet and the runner tool + // must be started with its working directory at the test repo's root. + using var tempDir = new TemporaryDirectory(); + var globalJsonPath = Path.Join(tempDir.DirectoryPath, "global.json"); + var srcGlobalJsonPath = Path.Join(tempDir.DirectoryPath, "src", "global.json"); + string globalJsonContent = """ { "sdk": { "version": "99.99.99" } } """; - await File.WriteAllTextAsync(globalJsonPath, globalJsonContent); - Directory.CreateDirectory(Path.Join(tempDir.DirectoryPath, "src")); - await File.WriteAllTextAsync(srcGlobalJsonPath, globalJsonContent); - var projectPath = Path.Join(tempDir.DirectoryPath, "src", "project.csproj"); - await File.WriteAllTextAsync(projectPath, """ + await File.WriteAllTextAsync(globalJsonPath, globalJsonContent); + Directory.CreateDirectory(Path.Join(tempDir.DirectoryPath, "src")); + await File.WriteAllTextAsync(srcGlobalJsonPath, globalJsonContent); + var projectPath = Path.Join(tempDir.DirectoryPath, "src", "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ net8.0 @@ -326,10 +323,10 @@ public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? working """); - var executableName = $"NuGetUpdater.Cli{(Environment.OSVersion.Platform == PlatformID.Win32NT ? ".exe" : "")}"; - var executableArgs = string.Join(" ", - [ - "update", + var executableName = $"NuGetUpdater.Cli{(Environment.OSVersion.Platform == PlatformID.Win32NT ? ".exe" : "")}"; + var executableArgs = string.Join(" ", + [ + "update", "--repo-root", tempDir.DirectoryPath, "--solution-or-project", @@ -341,60 +338,60 @@ public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? working "--previous-version", "7.0.1", "--verbose" - ]); + ]); - // verify base run - var workingDirectory = tempDir.DirectoryPath; - if (workingDirectoryPath is not null) - { - workingDirectory = Path.Join(workingDirectory, workingDirectoryPath); - } + // verify base run + var workingDirectory = tempDir.DirectoryPath; + if (workingDirectoryPath is not null) + { + workingDirectory = Path.Join(workingDirectory, workingDirectoryPath); + } - var (exitCode, output, error) = await ProcessEx.RunAsync(executableName, executableArgs, workingDirectory: workingDirectory); - Assert.True(exitCode == 0, $"Error running update on unsupported SDK.\nSTDOUT:\n{output}\nSTDERR:\n{error}"); + var (exitCode, output, error) = await ProcessEx.RunAsync(executableName, executableArgs, workingDirectory: workingDirectory); + Assert.True(exitCode == 0, $"Error running update on unsupported SDK.\nSTDOUT:\n{output}\nSTDERR:\n{error}"); - // verify project update - var updatedProjectContents = await File.ReadAllTextAsync(projectPath); - Assert.Contains("13.0.1", updatedProjectContents); + // verify project update + var updatedProjectContents = await File.ReadAllTextAsync(projectPath); + Assert.Contains("13.0.1", updatedProjectContents); - // verify `global.json` untouched - var updatedGlobalJsonContents = await File.ReadAllTextAsync(globalJsonPath); - Assert.Contains("99.99.99", updatedGlobalJsonContents); + // verify `global.json` untouched + var updatedGlobalJsonContents = await File.ReadAllTextAsync(globalJsonPath); + Assert.Contains("99.99.99", updatedGlobalJsonContents); - // verify `src/global.json` untouched - var updatedSrcGlobalJsonContents = await File.ReadAllTextAsync(srcGlobalJsonPath); - Assert.Contains("99.99.99", updatedGlobalJsonContents); - } - - private static async Task Run(Func getArgs, (string Path, string Content)[] initialFiles, (string, string)[] expectedFiles) - { - var actualFiles = await RunUpdate(initialFiles, async path => - { - var sb = new StringBuilder(); - var writer = new StringWriter(sb); + // verify `src/global.json` untouched + var updatedSrcGlobalJsonContents = await File.ReadAllTextAsync(srcGlobalJsonPath); + Assert.Contains("99.99.99", updatedGlobalJsonContents); + } - var originalOut = Console.Out; - var originalErr = Console.Error; - Console.SetOut(writer); - Console.SetError(writer); + private static async Task Run(Func getArgs, (string Path, string Content)[] initialFiles, (string, string)[] expectedFiles) + { + var actualFiles = await RunUpdate(initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); - try - { - var args = getArgs(path); - var result = await Program.Main(args); - if (result != 0) - { - throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); - } - } - finally - { - Console.SetOut(originalOut); - Console.SetError(originalErr); - } - }); + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); - AssertContainsFiles(expectedFiles, actualFiles); + try + { + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); } + }); + + AssertContainsFiles(expectedFiles, actualFiles); } + } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs index 6907bb79589..892d506ef8f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs @@ -1,4 +1,5 @@ extern alias CoreV2; + using System.Xml.Linq; using CoreV2::NuGet.Runtime; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs index a0f4adee27a..29bdf75dbe2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs @@ -1,4 +1,5 @@ extern alias CoreV2; + using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.RegularExpressions; diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index 783f685e4b8..00c3ff1bd7c 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -551,7 +551,7 @@ expect(Dependabot.logger).to have_received(:info).with( <<~INFO Discovery JSON content: { - "FilePath": "", + "Path": "#{repo_contents_path}", "IsSuccess": true, "Projects": [ { diff --git a/nuget/spec/dependabot/nuget/native_helpers_spec.rb b/nuget/spec/dependabot/nuget/native_helpers_spec.rb index ab94a84df05..e83a2cbbf6e 100644 --- a/nuget/spec/dependabot/nuget/native_helpers_spec.rb +++ b/nuget/spec/dependabot/nuget/native_helpers_spec.rb @@ -102,7 +102,6 @@ lib_path, "--exclude", except_path, - "--verify-no-changes", "-v", "diag" ].join(" ") From 3ca614237c5eeca658ab441ac3d1fd2131f5022c Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 16:08:05 -0700 Subject: [PATCH 09/13] Make sorbet and codespell happy --- .../lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs | 2 +- nuget/lib/dependabot/nuget/update_checker.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index 0170165b7cb..2ad8a4fb07b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -99,7 +99,7 @@ internal static class VersionFinder internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, VersionRange versionRange) { - // If we are floating to the aboslute latest version, we should not filter pre-release versions at all. + // If we are floating to the absolute latest version, we should not filter pre-release versions at all. var currentVersion = versionRange.Float?.FloatBehavior != NuGetVersionFloatBehavior.AbsoluteLatest ? versionRange.MinVersion : null; diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index e781a59103d..9158dc17b9b 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -1,4 +1,4 @@ -# typed: strict +# typed: strong # frozen_string_literal: true require "dependabot/nuget/analysis/analysis_json_reader" From bef7f12ceeefb4b22260902b7899b93fb1de521b Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 16:36:59 -0700 Subject: [PATCH 10/13] Fix indentation --- .../EntryPointTests.Update.cs | 228 +++++++++--------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs index b17d1e3384e..7a4905cf71a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -10,14 +10,14 @@ namespace NuGetUpdater.Cli.Test; public partial class EntryPointTests { - public class Update : UpdateWorkerTestBase - { - [Fact] - public async Task WithSolution() + public class Update : UpdateWorkerTestBase { - await Run(path => - [ - "update", + [Fact] + public async Task WithSolution() + { + await Run(path => + [ + "update", "--repo-root", path, "--solution-or-project", @@ -29,8 +29,8 @@ public async Task WithSolution() "--previous-version", "7.0.1", ], - [ - ("path/to/solution.sln", """ + [ + ("path/to/solution.sln", """ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 VisualStudioVersion = 14.0.22705.0 @@ -76,9 +76,9 @@ public async Task WithSolution() """) - ], - [ - ("path/to/my.csproj", """ + ], + [ + ("path/to/my.csproj", """ @@ -102,15 +102,15 @@ public async Task WithSolution() """) - ]); - } + ]); + } - [Fact] - public async Task WithProject() - { - await Run(path => - [ - "update", + [Fact] + public async Task WithProject() + { + await Run(path => + [ + "update", "--repo-root", path, "--solution-or-project", @@ -122,9 +122,9 @@ public async Task WithProject() "--previous-version", "7.0.1", "--verbose" - ], - [ - ("path/to/my.csproj", """ + ], + [ + ("path/to/my.csproj", """ @@ -147,9 +147,9 @@ public async Task WithProject() """) - ], - [ - ("path/to/my.csproj", """ + ], + [ + ("path/to/my.csproj", """ @@ -173,15 +173,15 @@ public async Task WithProject() """) - ]); - } + ]); + } - [Fact] - public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStillMatchingThePackage() - { - await Run(path => - [ - "update", + [Fact] + public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStillMatchingThePackage() + { + await Run(path => + [ + "update", "--repo-root", path, "--solution-or-project", @@ -193,10 +193,10 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill "--previous-version", "6.1.0", "--verbose" - ], - initialFiles: - [ - ("some-dir/dirs.proj", """ + ], + initialFiles: + [ + ("some-dir/dirs.proj", """ @@ -239,10 +239,10 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """) - ], - expectedFiles: - [ - ("some-dir/dirs.proj", """ + ], + expectedFiles: + [ + ("some-dir/dirs.proj", """ @@ -251,7 +251,7 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """), ("some-dir/project1/project.csproj", - """ + """ Exe @@ -286,34 +286,34 @@ public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStill """) - ] - ); - } + ] + ); + } - [Theory] - [InlineData(null)] - [InlineData("src")] - public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? workingDirectoryPath) - { - // This is a _very_ specific scenario where the `NuGetUpdater.Cli` tool might pick up a `global.json` from - // the root of the repo under test and use it's `sdk` property when trying to locate MSBuild. To properly - // test this, it must be tested in a new process where MSBuild has not been loaded yet and the runner tool - // must be started with its working directory at the test repo's root. - using var tempDir = new TemporaryDirectory(); - var globalJsonPath = Path.Join(tempDir.DirectoryPath, "global.json"); - var srcGlobalJsonPath = Path.Join(tempDir.DirectoryPath, "src", "global.json"); - string globalJsonContent = """ + [Theory] + [InlineData(null)] + [InlineData("src")] + public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? workingDirectoryPath) + { + // This is a _very_ specific scenario where the `NuGetUpdater.Cli` tool might pick up a `global.json` from + // the root of the repo under test and use it's `sdk` property when trying to locate MSBuild. To properly + // test this, it must be tested in a new process where MSBuild has not been loaded yet and the runner tool + // must be started with its working directory at the test repo's root. + using var tempDir = new TemporaryDirectory(); + var globalJsonPath = Path.Join(tempDir.DirectoryPath, "global.json"); + var srcGlobalJsonPath = Path.Join(tempDir.DirectoryPath, "src", "global.json"); + string globalJsonContent = """ { "sdk": { "version": "99.99.99" } } """; - await File.WriteAllTextAsync(globalJsonPath, globalJsonContent); - Directory.CreateDirectory(Path.Join(tempDir.DirectoryPath, "src")); - await File.WriteAllTextAsync(srcGlobalJsonPath, globalJsonContent); - var projectPath = Path.Join(tempDir.DirectoryPath, "src", "project.csproj"); - await File.WriteAllTextAsync(projectPath, """ + await File.WriteAllTextAsync(globalJsonPath, globalJsonContent); + Directory.CreateDirectory(Path.Join(tempDir.DirectoryPath, "src")); + await File.WriteAllTextAsync(srcGlobalJsonPath, globalJsonContent); + var projectPath = Path.Join(tempDir.DirectoryPath, "src", "project.csproj"); + await File.WriteAllTextAsync(projectPath, """ net8.0 @@ -323,10 +323,10 @@ public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? working """); - var executableName = $"NuGetUpdater.Cli{(Environment.OSVersion.Platform == PlatformID.Win32NT ? ".exe" : "")}"; - var executableArgs = string.Join(" ", - [ - "update", + var executableName = $"NuGetUpdater.Cli{(Environment.OSVersion.Platform == PlatformID.Win32NT ? ".exe" : "")}"; + var executableArgs = string.Join(" ", + [ + "update", "--repo-root", tempDir.DirectoryPath, "--solution-or-project", @@ -338,60 +338,60 @@ public async Task UpdaterDoesNotUseRepoGlobalJsonForMSBuildTasks(string? working "--previous-version", "7.0.1", "--verbose" - ]); + ]); - // verify base run - var workingDirectory = tempDir.DirectoryPath; - if (workingDirectoryPath is not null) - { - workingDirectory = Path.Join(workingDirectory, workingDirectoryPath); - } + // verify base run + var workingDirectory = tempDir.DirectoryPath; + if (workingDirectoryPath is not null) + { + workingDirectory = Path.Join(workingDirectory, workingDirectoryPath); + } - var (exitCode, output, error) = await ProcessEx.RunAsync(executableName, executableArgs, workingDirectory: workingDirectory); - Assert.True(exitCode == 0, $"Error running update on unsupported SDK.\nSTDOUT:\n{output}\nSTDERR:\n{error}"); + var (exitCode, output, error) = await ProcessEx.RunAsync(executableName, executableArgs, workingDirectory: workingDirectory); + Assert.True(exitCode == 0, $"Error running update on unsupported SDK.\nSTDOUT:\n{output}\nSTDERR:\n{error}"); - // verify project update - var updatedProjectContents = await File.ReadAllTextAsync(projectPath); - Assert.Contains("13.0.1", updatedProjectContents); + // verify project update + var updatedProjectContents = await File.ReadAllTextAsync(projectPath); + Assert.Contains("13.0.1", updatedProjectContents); - // verify `global.json` untouched - var updatedGlobalJsonContents = await File.ReadAllTextAsync(globalJsonPath); - Assert.Contains("99.99.99", updatedGlobalJsonContents); + // verify `global.json` untouched + var updatedGlobalJsonContents = await File.ReadAllTextAsync(globalJsonPath); + Assert.Contains("99.99.99", updatedGlobalJsonContents); - // verify `src/global.json` untouched - var updatedSrcGlobalJsonContents = await File.ReadAllTextAsync(srcGlobalJsonPath); - Assert.Contains("99.99.99", updatedGlobalJsonContents); - } + // verify `src/global.json` untouched + var updatedSrcGlobalJsonContents = await File.ReadAllTextAsync(srcGlobalJsonPath); + Assert.Contains("99.99.99", updatedGlobalJsonContents); + } - private static async Task Run(Func getArgs, (string Path, string Content)[] initialFiles, (string, string)[] expectedFiles) - { - var actualFiles = await RunUpdate(initialFiles, async path => - { - var sb = new StringBuilder(); - var writer = new StringWriter(sb); + private static async Task Run(Func getArgs, (string Path, string Content)[] initialFiles, (string, string)[] expectedFiles) + { + var actualFiles = await RunUpdate(initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); - var originalOut = Console.Out; - var originalErr = Console.Error; - Console.SetOut(writer); - Console.SetError(writer); + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); - try - { - var args = getArgs(path); - var result = await Program.Main(args); - if (result != 0) - { - throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); - } - } - finally - { - Console.SetOut(originalOut); - Console.SetError(originalErr); - } - }); + try + { + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + } + }); - AssertContainsFiles(expectedFiles, actualFiles); + AssertContainsFiles(expectedFiles, actualFiles); + } } - } } From fbd7e51880379512b9629fe16d47348b8073c64f Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 16:37:09 -0700 Subject: [PATCH 11/13] Fix requirement parsing --- .../Analyze/RequirementTests.cs | 6 +++--- .../NuGetUpdater.Core/Analyze/Requirement.cs | 12 +++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs index 6ec3cb90ac9..f2150ae11f0 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs @@ -14,19 +14,19 @@ public class RequirementTests [InlineData("1.0.0-alpha", "1.0.0", false)] [InlineData("1.0.0", "= 1.0.0", true)] [InlineData("1.0.0-alpha", "= 1.0.0", false)] - [InlineData("1.0.0", "!= 1.0.1", true)] + [InlineData("1.0.0", "!=1.0.1", true)] [InlineData("1.0.0", "!= 1.0.0", false)] [InlineData("1.0.1", "> 1.0.0", true)] [InlineData("1.0.0-alpha", "> 1.0.0", false)] [InlineData("1.0.0", "< 1.0.1", true)] - [InlineData("1.0.0", "< 1.0.0-alpha", false)] + [InlineData("1.0.0", "<1.0.0-alpha", false)] [InlineData("1.0.0", ">= 1.0.0", true)] [InlineData("1.0.1", ">= 1.0.0", true)] [InlineData("1.0.0-alpha", ">= 1.0.0", false)] [InlineData("1.0.0", "<= 1.0.0", true)] [InlineData("1.0.0-alpha", "<= 1.0.0", true)] [InlineData("1.0.1", "<= 1.0.0", false)] - [InlineData("1.0.1", "~> 1.0.0", true)] + [InlineData("1.0.1", "~>1.0.0", true)] [InlineData("1.1.0", "~> 1.0.0", false)] [InlineData("1.1", "~> 1.0", true)] [InlineData("2.0", "~> 1.0", false)] diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs index 9225b30a3b5..63695e787c3 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs @@ -27,12 +27,18 @@ public class Requirement public static Requirement Parse(string requirement) { - var parts = requirement.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - if (parts.Length == 0 || parts.Length > 2) + var splitIndex = requirement.LastIndexOfAny(['=', '>', '<']); + + // Throw if the requirement is all operator and no version. + if (splitIndex == requirement.Length - 1) { - throw new ArgumentException("Invalid requirement string", nameof(requirement)); + throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement)); } + string[] parts = splitIndex == -1 + ? [requirement.Trim()] + : [requirement[..(splitIndex + 1)].Trim(), requirement[(splitIndex + 1)..].Trim()]; + var op = parts.Length == 1 ? "=" : parts[0]; var version = NuGetVersion.Parse(parts[^1]); From ac0b8e28d33b0a7557abaf215cbd55f3e5accb73 Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Mon, 22 Apr 2024 16:37:15 -0700 Subject: [PATCH 12/13] Fix line too long --- .../dependabot/nuget/analysis/dependency_analysis.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb index 759af5f270a..78f682653a9 100644 --- a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb +++ b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb @@ -20,10 +20,12 @@ def self.from_json(json) DependencyDetails.from_json(dep) end - DependencyAnalysis.new(updated_version: updated_version, - can_update: can_update, - version_comes_from_multi_dependency_property: version_comes_from_multi_dependency_property, - updated_dependencies: updated_dependencies) + DependencyAnalysis.new( + updated_version: updated_version, + can_update: can_update, + version_comes_from_multi_dependency_property: version_comes_from_multi_dependency_property, + updated_dependencies: updated_dependencies + ) end sig do From 85a78af1bf99f087b0a6d61c97b59c0c744979ae Mon Sep 17 00:00:00 2001 From: Joey Robichaud Date: Tue, 23 Apr 2024 00:46:32 -0700 Subject: [PATCH 13/13] Add Nuspec locator class so we can return proper source information --- .../Analyze/NuspecLocatorTests.cs | 98 +++++++++ .../Analyze/NuspecLocator.cs | 195 ++++++++++++++++++ .../Analyze/VersionFinder.cs | 14 ++ 3 files changed, 307 insertions(+) create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs create mode 100644 nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs new file mode 100644 index 00000000000..307b007c7cc --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/NuspecLocatorTests.cs @@ -0,0 +1,98 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +using TestFile = (string Path, string Content); + +public class NuspecLocatorTests +{ + internal const string NuGetOrgFeedUrl = "https://api.nuget.org/v3/index.json"; + internal const string DotNetToolsFeedUrl = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json"; + + [Fact] + public async Task LocateNuspec_ReturnsUrl_ForNuGetOrgFeed() + { + await TestLocateAsync( + [NuGetOrgFeedUrl], + "Microsoft.CodeAnalysis.Common", + NuGetVersion.Parse("4.9.2"), + "https://api.nuget.org/v3-flatcontainer/microsoft.codeanalysis.common/4.9.2/microsoft.codeanalysis.common.nuspec"); + } + + [Fact] + public async Task LocateNuspec_ReturnsUrl_ForAzureArtifactsFeed() + { + await TestLocateAsync( + [NuGetOrgFeedUrl, DotNetToolsFeedUrl], + "Microsoft.CodeAnalysis.Common", + NuGetVersion.Parse("4.11.0-1.24219.1"), + "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/d1622942-d16f-48e5-bc83-96f4539e7601/nuget/v3/flat2/microsoft.codeanalysis.common/4.11.0-1.24219.1/microsoft.codeanalysis.common.nuspec"); + } + + [Fact] + public async Task LocateNuspec_ReturnsNull_ForInvalidFeed() + { + await TestLocateAsync( + ["https:://invalid-feed-url"], + "Microsoft.CodeAnalysis.Common", + NuGetVersion.Parse("4.9.2"), + null); + } + + [Fact] + public async Task LocateNuspec_ReturnsNull_ForInvalidPackage() + { + await TestLocateAsync( + [NuGetOrgFeedUrl], + "Microsoft.CodeAnalysis.Invalid", + NuGetVersion.Parse("4.9.2"), + null); + } + + protected static async Task TestLocateAsync( + string[] feedUrls, + string packageId, + NuGetVersion version, + string? expectedResult) + { + var currentDirectory = Environment.CurrentDirectory; + + TestFile[] files = [ + ("./nuget.config", $""" + + + + + {string.Join(Environment.NewLine, feedUrls.Select((url, index) => $""))} + + + """), + ]; + + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + try + { + Environment.CurrentDirectory = temporaryDirectory.DirectoryPath; + var nugetContext = new NuGetContext(); + var logger = new Logger(verbose: true); + + var actual = await NuspecLocator.LocateNuspecAsync( + packageId, + version, + nugetContext, + logger, + CancellationToken.None); + + Assert.Equal(expectedResult, actual); + } + finally + { + Environment.CurrentDirectory = currentDirectory; + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs new file mode 100644 index 00000000000..c35bc0987c2 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuspecLocator.cs @@ -0,0 +1,195 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static partial class NuspecLocator +{ + internal static readonly ImmutableArray SupportedFeedRegexes = [ + // nuget + NuGetOrgRegex(), + // azure devops + AzureArtifactsOrgProjectRegex(), + AzureArtifactsOrgRegex(), + VisualStudioRegex(), + ]; + internal static readonly Dictionary BaseUrlCache = []; + internal static readonly JsonSerializerOptions JsonSerializerOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static async Task LocateNuspecAsync( + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var nuspecUrl = await LocateNuspecAsync(source, packageId, version, nugetContext, logger, cancellationToken); + if (nuspecUrl is not null) + { + return nuspecUrl; + } + } + + return null; + } + + public static async Task LocateNuspecAsync( + PackageSource source, + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var isSupported = DoesFeedSupportNuspecDownload(source); + if (!isSupported) + { + return null; + } + + var baseUrl = await GetBaseUrlAsync(source, nugetContext, cancellationToken); + if (baseUrl is null) + { + return null; + } + + var packageExists = await DoesPackageExistInFeedAsync( + source, + packageId, + version, + nugetContext, + logger, + cancellationToken); + if (!packageExists) + { + return null; + } + + return $"{baseUrl.TrimEnd('/')}/{packageId.ToLowerInvariant()}/{version.ToNormalizedString().ToLowerInvariant()}/{packageId.ToLowerInvariant()}.nuspec"; + } + + public static async Task GetBaseUrlAsync( + PackageSource source, + NuGetContext nugetContext, + CancellationToken cancellationToken) + { + if (BaseUrlCache.TryGetValue(source.SourceUri, out var baseUrl)) + { + return baseUrl; + } + + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + return null; + } + + var httpSourceCacheContext = HttpSourceCacheContext.Create(nugetContext.SourceCacheContext, isFirstAttempt: true); + var request = new HttpSourceCachedRequest(source.SourceUri.AbsoluteUri, "source_uri", httpSourceCacheContext); + var result = await feed.HttpSource.GetAsync( + request, + async result => + { + try + { + return await GetV3BaseUrlAsync(result.Stream); + } + catch (JsonException) + { + // V2 endpoint perhaps + return null; + } + }, + nugetContext.Logger, + cancellationToken); + + BaseUrlCache[source.SourceUri] = result; + + return result; + } + + internal static async Task GetV3BaseUrlAsync(Stream stream) + { + return (await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptions)) + ?.Resources + .FirstOrDefault(r => r.Type == "PackageBaseAddress/3.0.0") + ?.Id; + } + + internal static bool DoesFeedSupportNuspecDownload(PackageSource source) + { + var feedUrl = source.SourceUri.AbsoluteUri; + return SupportedFeedRegexes.Any(r => r.IsMatch(feedUrl)); + } + + internal static async Task DoesPackageExistInFeedAsync( + PackageSource source, + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + return false; + } + + var existsInFeed = await feed.Exists( + new PackageIdentity(packageId, version), + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + + return existsInFeed; + } + + private class RepoMetadataResult + { + public required ResourceInfo[] Resources { get; set; } + } + + private class ResourceInfo + { + [JsonPropertyName("@type")] + public required string Type { get; set; } + [JsonPropertyName("@id")] + public required string Id { get; set; } + } + + [GeneratedRegex(@"https://api\.nuget\.org/v3/index\.json")] + private static partial Regex NuGetOrgRegex(); + [GeneratedRegex(@"https://pkgs\.dev\.azure\.com/(?[^/]+)/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json")] + private static partial Regex AzureArtifactsOrgProjectRegex(); + [GeneratedRegex(@"https://pkgs\.dev\.azure\.com/(?[^/]+)/_packaging/(?[^/]+)/nuget/v3/index\.json(?)")] + private static partial Regex AzureArtifactsOrgRegex(); + [GeneratedRegex(@"https://(?[^\.\/]+)\.pkgs\.visualstudio\.com/_packaging/(?[^/]+)/nuget/v3/index\.json(?)")] + private static partial Regex VisualStudioRegex(); +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs index 2ad8a4fb07b..09a8c72e95f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using System.Text.Json; using NuGet.Common; using NuGet.Configuration; @@ -11,6 +12,17 @@ namespace NuGetUpdater.Core.Analyze; internal static class VersionFinder { + internal static readonly System.Text.Json.JsonSerializerOptions JsonSerializerOptions = CreateJsonSerializerOptions(); + + private static System.Text.Json.JsonSerializerOptions CreateJsonSerializerOptions() + { + var options = new System.Text.Json.JsonSerializerOptions() + { + PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase + }; + return options; + } + public static Task GetVersionsAsync( string packageId, NuGetVersion currentVersion, @@ -45,6 +57,8 @@ internal static class VersionFinder Logger logger, CancellationToken cancellationToken) { + var url = await NuspecLocator.LocateNuspecAsync(packageId, currentVersion, nugetContext, logger, cancellationToken); + var includePrerelease = currentVersion.IsPrerelease; VersionResult result = new(currentVersion);