From 5edc7a106e7e7f6f9b87f27425408e6ab4199919 Mon Sep 17 00:00:00 2001 From: "Brett V. Forsgren" Date: Fri, 6 Dec 2024 16:42:23 -0700 Subject: [PATCH] remove old update checker and stale code --- .../nuget/analysis/analysis_json_reader.rb | 2 +- .../nuget/analysis/dependency_analysis.rb | 6 +- .../nuget/discovery/dependency_details.rb | 13 +- .../discovery/dependency_file_discovery.rb | 20 +- .../directory_packages_props_discovery.rb | 43 -- .../nuget/discovery/discovery_json_reader.rb | 243 +++++++-- .../nuget/discovery/project_discovery.rb | 49 +- .../nuget/discovery/workspace_discovery.rb | 33 +- nuget/lib/dependabot/nuget/file_fetcher.rb | 4 +- nuget/lib/dependabot/nuget/file_parser.rb | 4 +- nuget/lib/dependabot/nuget/file_updater.rb | 26 +- .../dependabot/nuget/http_response_helpers.rb | 19 - .../native_dependency_details.rb | 102 ---- .../native_dependency_file_discovery.rb | 122 ----- .../native_discovery_json_reader.rb | 266 ---------- .../native_evaluation_details.rb | 63 --- .../native_project_discovery.rb | 104 ---- .../native_property_details.rb | 43 -- .../native_workspace_discovery.rb | 61 --- .../native_requirements_updater.rb | 105 ---- .../native_update_checker.rb | 214 -------- nuget/lib/dependabot/nuget/nuget_client.rb | 223 --------- nuget/lib/dependabot/nuget/update_checker.rb | 302 +++++------- .../update_checker/compatibility_checker.rb | 116 ----- .../nuget/update_checker/dependency_finder.rb | 297 ----------- .../nuget/update_checker/nupkg_fetcher.rb | 221 --------- .../nuget/update_checker/nuspec_fetcher.rb | 110 ----- .../nuget/update_checker/property_updater.rb | 196 -------- .../nuget/update_checker/repository_finder.rb | 466 ------------------ .../update_checker/requirements_updater.rb | 50 +- .../nuget/update_checker/tfm_comparer.rb | 34 -- .../nuget/update_checker/tfm_finder.rb | 30 -- .../nuget/update_checker/version_finder.rb | 449 ----------------- .../dependabot/nuget/file_fetcher_spec.rb | 10 +- .../spec/dependabot/nuget/file_parser_spec.rb | 17 +- .../dependabot/nuget/file_updater_spec.rb | 26 +- nuget/spec/dependabot/nuget/github_helpers.rb | 135 ----- .../dependabot/nuget/nuget_search_stubs.rb | 139 ------ .../update_checker/repository_finder_spec.rb | 147 ------ .../requirements_updater_spec.rb} | 6 +- .../dependabot/nuget/update_checker_spec.rb | 18 +- nuget/spec/spec_helper.rb | 3 - 42 files changed, 469 insertions(+), 4068 deletions(-) delete mode 100644 nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb delete mode 100644 nuget/lib/dependabot/nuget/http_response_helpers.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb delete mode 100644 nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb delete mode 100644 nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb delete mode 100644 nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.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 delete mode 100644 nuget/spec/dependabot/nuget/github_helpers.rb delete mode 100644 nuget/spec/dependabot/nuget/nuget_search_stubs.rb delete mode 100644 nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb rename nuget/spec/dependabot/nuget/{native_update_checker/native_requirements_updater_spec.rb => update_checker/requirements_updater_spec.rb} (95%) diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb index eb3cff36e4..c3e67e14dc 100644 --- a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -3,7 +3,7 @@ require "dependabot/dependency" require "dependabot/nuget/analysis/dependency_analysis" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "json" require "sorbet-runtime" diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb index 01e5022270..1dfc76705f 100644 --- a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb +++ b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb @@ -20,7 +20,7 @@ def self.from_json(json) T::Boolean) updated_dependencies = T.let(json.fetch("UpdatedDependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| - NativeDependencyDetails.from_json(dep) + DependencyDetails.from_json(dep) end DependencyAnalysis.new( @@ -35,7 +35,7 @@ def self.from_json(json) params(updated_version: String, can_update: T::Boolean, version_comes_from_multi_dependency_property: T::Boolean, - updated_dependencies: T::Array[NativeDependencyDetails]).void + updated_dependencies: T::Array[DependencyDetails]).void end def initialize(updated_version:, can_update:, version_comes_from_multi_dependency_property:, updated_dependencies:) @@ -54,7 +54,7 @@ def initialize(updated_version:, can_update:, version_comes_from_multi_dependenc sig { returns(T::Boolean) } attr_reader :version_comes_from_multi_dependency_property - sig { returns(T::Array[NativeDependencyDetails]) } + sig { returns(T::Array[DependencyDetails]) } attr_reader :updated_dependencies sig { returns(Dependabot::Nuget::Version) } diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb index f96ac5d2b4..6cc6dbe935 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_details.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_details.rb @@ -22,6 +22,7 @@ def self.from_json(json) is_transitive = T.let(json.fetch("IsTransitive"), T::Boolean) is_override = T.let(json.fetch("IsOverride"), T::Boolean) is_update = T.let(json.fetch("IsUpdate"), T::Boolean) + info_url = T.let(json.fetch("InfoUrl"), T.nilable(String)) DependencyDetails.new(name: name, version: version, @@ -32,7 +33,8 @@ def self.from_json(json) is_direct: is_direct, is_transitive: is_transitive, is_override: is_override, - is_update: is_update) + is_update: is_update, + info_url: info_url) end sig do @@ -45,10 +47,11 @@ def self.from_json(json) is_direct: T::Boolean, is_transitive: T::Boolean, is_override: T::Boolean, - is_update: T::Boolean).void + is_update: T::Boolean, + info_url: T.nilable(String)).void end def initialize(name:, version:, type:, evaluation:, target_frameworks:, is_dev_dependency:, is_direct:, - is_transitive:, is_override:, is_update:) + is_transitive:, is_override:, is_update:, info_url:) @name = name @version = version @type = type @@ -59,6 +62,7 @@ def initialize(name:, version:, type:, evaluation:, target_frameworks:, is_dev_d @is_transitive = is_transitive @is_override = is_override @is_update = is_update + @info_url = info_url end sig { returns(String) } @@ -90,6 +94,9 @@ def initialize(name:, version:, type:, evaluation:, target_frameworks:, is_dev_d sig { returns(T::Boolean) } attr_reader :is_update + + sig { returns(T.nilable(String)) } + attr_reader :info_url end end end diff --git a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb index db4c8ddfc2..5a2e84110e 100644 --- a/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/dependency_file_discovery.rb @@ -9,11 +9,14 @@ module Nuget class DependencyFileDiscovery extend T::Sig - sig { params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(DependencyFileDiscovery)) } - def self.from_json(json) + sig do + params(json: T.nilable(T::Hash[String, T.untyped]), + directory: String).returns(T.nilable(DependencyFileDiscovery)) + end + def self.from_json(json, directory) return nil if json.nil? - file_path = T.let(json.fetch("FilePath"), String) + file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| DependencyDetails.from_json(dep) end @@ -38,7 +41,7 @@ def initialize(file_path:, dependencies:) attr_reader :dependencies sig { overridable.returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize + def dependency_set # rubocop:disable Metrics/PerceivedComplexity dependency_set = Dependabot::FileParsers::Base::DependencySet.new file_name = Pathname.new(file_path).cleanpath.to_path @@ -62,14 +65,7 @@ def dependency_set # rubocop:disable Metrics/PerceivedComplexity,Metrics/Cycloma # Exclude any dependencies which reference an item type next if dependency.name.include?("@(") - dependency_file_name = file_name - if dependency.type == "PackagesConfig" - dir_name = File.dirname(file_name) - dependency_file_name = "packages.config" - dependency_file_name = File.join(dir_name, "packages.config") unless dir_name == "." - end - - dependency_set << build_dependency(dependency_file_name, dependency) + dependency_set << build_dependency(file_name, dependency) end dependency_set diff --git a/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb b/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb deleted file mode 100644 index b48d65557a..0000000000 --- a/nuget/lib/dependabot/nuget/discovery/directory_packages_props_discovery.rb +++ /dev/null @@ -1,43 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/discovery/dependency_details" -require "sorbet-runtime" - -module Dependabot - module Nuget - class DirectoryPackagesPropsDiscovery < DependencyFileDiscovery - extend T::Sig - - sig do - params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(DirectoryPackagesPropsDiscovery)) - end - def self.from_json(json) - return nil if json.nil? - - file_path = T.let(json.fetch("FilePath"), String) - is_transitive_pinning_enabled = T.let(json.fetch("IsTransitivePinningEnabled"), T::Boolean) - dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| - DependencyDetails.from_json(dep) - end - - DirectoryPackagesPropsDiscovery.new(file_path: file_path, - is_transitive_pinning_enabled: is_transitive_pinning_enabled, - dependencies: dependencies) - end - - sig do - params(file_path: String, - is_transitive_pinning_enabled: T::Boolean, - dependencies: T::Array[DependencyDetails]).void - end - def initialize(file_path:, is_transitive_pinning_enabled:, dependencies:) - super(file_path: file_path, dependencies: dependencies) - @is_transitive_pinning_enabled = is_transitive_pinning_enabled - end - - sig { returns(T::Boolean) } - attr_reader :is_transitive_pinning_enabled - end - end -end diff --git a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb index 749e60397d..f74a0f5386 100644 --- a/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb +++ b/nuget/lib/dependabot/nuget/discovery/discovery_json_reader.rb @@ -2,6 +2,8 @@ # frozen_string_literal: true require "dependabot/dependency" +require "dependabot/file_parsers/base/dependency_set" +require "dependabot/nuget/cache_manager" require "dependabot/nuget/discovery/workspace_discovery" require "json" require "sorbet-runtime" @@ -11,37 +13,199 @@ module Nuget class DiscoveryJsonReader extend T::Sig - DISCOVERY_JSON_PATH = ".dependabot/discovery.json" + sig { returns(T::Hash[String, DiscoveryJsonReader]) } + def self.cache_directory_to_discovery_json_reader + CacheManager.cache("cache_directory_to_discovery_json_reader") + end - sig { returns(String) } - private_class_method def self.temp_directory - Dir.tmpdir + sig { returns(T::Hash[String, DiscoveryJsonReader]) } + def self.cache_dependency_file_paths_to_discovery_json_reader + CacheManager.cache("cache_dependency_file_paths_to_discovery_json_reader") + end + + sig { returns(T::Hash[String, String]) } + def self.cache_dependency_file_paths_to_discovery_json_path + CacheManager.cache("cache_dependency_file_paths_to_discovery_json_path") + end + + sig { void } + def self.testonly_clear_caches + cache_directory_to_discovery_json_reader.clear + cache_dependency_file_paths_to_discovery_json_reader.clear + cache_dependency_file_paths_to_discovery_json_path.clear + end + + sig { void } + def self.testonly_clear_discovery_files + # this will get recreated when necessary + FileUtils.rm_rf(discovery_directory) + end + + # Runs NuGet dependency discovery in the given directory and returns a new instance of DiscoveryJsonReader. + # The location of the resultant JSON file is saved. + sig do + params( + repo_contents_path: String, + directory: String, + credentials: T::Array[Dependabot::Credential] + ).returns(DiscoveryJsonReader) + end + def self.run_discovery_in_directory(repo_contents_path:, directory:, credentials:) + # run discovery + job_file_path = ENV.fetch("DEPENDABOT_JOB_PATH") + discovery_json_path = discovery_file_path_from_workspace_path(directory) + unless File.exist?(discovery_json_path) + NativeHelpers.run_nuget_discover_tool(job_path: job_file_path, + repo_root: repo_contents_path, + workspace_path: directory, + output_path: discovery_json_path, + credentials: credentials) + + Dependabot.logger.info("Discovery JSON content: #{File.read(discovery_json_path)}") + end + load_discovery_for_directory(repo_contents_path: repo_contents_path, directory: directory) + end + + # Loads NuGet dependency discovery for the given directory and returns a new instance of DiscoveryJsonReader and + # caches the resultant object. + sig { params(repo_contents_path: String, directory: String).returns(DiscoveryJsonReader) } + def self.load_discovery_for_directory(repo_contents_path:, directory:) + cache_directory_to_discovery_json_reader[directory] ||= begin + discovery_json_reader = discovery_json_reader(repo_contents_path: repo_contents_path, + workspace_path: directory) + cache_directory_to_discovery_json_reader[directory] = discovery_json_reader + dependency_file_cache_key = cache_key_from_dependency_file_paths(discovery_json_reader.dependency_file_paths) + cache_dependency_file_paths_to_discovery_json_reader[dependency_file_cache_key] = discovery_json_reader + discovery_file_path = discovery_file_path_from_workspace_path(directory) + cache_dependency_file_paths_to_discovery_json_path[dependency_file_cache_key] = discovery_file_path + + discovery_json_reader + end + end + + # Retrieves the cached DiscoveryJsonReader object for the given dependency file paths. + sig { params(dependency_file_paths: T::Array[String]).returns(DiscoveryJsonReader) } + def self.load_discovery_for_dependency_file_paths(dependency_file_paths) + dependency_file_cache_key = cache_key_from_dependency_file_paths(dependency_file_paths) + T.must(cache_dependency_file_paths_to_discovery_json_reader[dependency_file_cache_key]) + end + + # Retrieves the cached location of the discovery JSON file for the given dependency file paths. + sig { params(dependency_file_paths: T::Array[String]).returns(String) } + def self.get_discovery_json_path_for_dependency_file_paths(dependency_file_paths) + dependency_file_cache_key = cache_key_from_dependency_file_paths(dependency_file_paths) + T.must(cache_dependency_file_paths_to_discovery_json_path[dependency_file_cache_key]) + end + + sig { params(repo_contents_path: String, dependency_file: Dependabot::DependencyFile).returns(String) } + def self.dependency_file_path(repo_contents_path:, dependency_file:) + dep_file_path = Pathname.new(File.join(dependency_file.directory, dependency_file.name)).cleanpath.to_path + dep_file_path.delete_prefix("#{repo_contents_path}/") end sig { returns(String) } - def self.discovery_file_path - File.join(temp_directory, DISCOVERY_JSON_PATH) + def self.discovery_map_file_path + File.join(discovery_directory, "discovery_map.json") end - sig { returns(T.nilable(DependencyFile)) } - def self.discovery_json - return unless File.exist?(discovery_file_path) + sig { params(workspace_path: String).returns(String) } + def self.discovery_file_path_from_workspace_path(workspace_path) + # Given an update directory (also known as a workspace path), this function returns the path where the discovery + # JSON file is located. This function is called both by methods that need to write the discovery JSON file and + # by methods that need to read the discovery JSON file. This function is also called by multiple processes so + # we need a way to retain the data. This is accomplished by the following steps: + # 1. Check a well-known file for a mapping of workspace_path => discovery file path. If found, return it. + # 2. If the path is not found, generate a new path, save it to the well-known file, and return the value. + discovery_map_contents = File.exist?(discovery_map_file_path) ? File.read(discovery_map_file_path) : "{}" + discovery_map = T.let(JSON.parse(discovery_map_contents), T::Hash[String, String]) + + discovery_json_path = discovery_map[workspace_path] + if discovery_json_path + Dependabot.logger.info("Discovery JSON path for workspace path [#{workspace_path}] found in file " \ + "[#{discovery_map_file_path}] at location [#{discovery_json_path}]") + return discovery_json_path + end + + # no discovery JSON path found; generate a new one, but first find a suitable location + discovery_json_counter = 1 + new_discovery_json_path = "" + loop do + new_discovery_json_path = File.join(discovery_directory, "discovery.#{discovery_json_counter}.json") + break unless File.exist?(new_discovery_json_path) - DependencyFile.new( + discovery_json_counter += 1 + end + + discovery_map[workspace_path] = new_discovery_json_path + + File.write(discovery_map_file_path, discovery_map.to_json) + Dependabot.logger.info("Discovery JSON path for workspace path [#{workspace_path}] created for file " \ + "[#{discovery_map_file_path}] at location [#{new_discovery_json_path}]") + new_discovery_json_path + end + + sig { params(dependency_file_paths: T::Array[String]).returns(String) } + def self.cache_key_from_dependency_file_paths(dependency_file_paths) + dependency_file_paths.sort.join(",") + end + + sig { returns(String) } + def self.discovery_directory + t = File.join(Dir.home, ".dependabot") + FileUtils.mkdir_p(t) + t + end + + sig { params(repo_contents_path: String, workspace_path: String).returns(DiscoveryJsonReader) } + def self.discovery_json_reader(repo_contents_path:, workspace_path:) + discovery_file_path = discovery_file_path_from_workspace_path(workspace_path) + discovery_json = DependencyFile.new( name: Pathname.new(discovery_file_path).cleanpath.to_path, - directory: temp_directory, + directory: discovery_directory, type: "file", content: File.read(discovery_file_path) ) + DiscoveryJsonReader.new(repo_contents_path: repo_contents_path, discovery_json: discovery_json) end - sig { params(discovery_json: DependencyFile).void } - def initialize(discovery_json:) + sig { returns(T.nilable(WorkspaceDiscovery)) } + attr_reader :workspace_discovery + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + attr_reader :dependency_set + + sig { returns(T::Array[String]) } + attr_reader :dependency_file_paths + + sig { params(repo_contents_path: String, discovery_json: DependencyFile).void } + def initialize(repo_contents_path:, discovery_json:) + @repo_contents_path = repo_contents_path @discovery_json = discovery_json + @workspace_discovery = T.let(read_workspace_discovery, T.nilable(Dependabot::Nuget::WorkspaceDiscovery)) + @dependency_set = T.let(read_dependency_set, Dependabot::FileParsers::Base::DependencySet) + @dependency_file_paths = T.let(read_dependency_file_paths, T::Array[String]) + end + + private + + sig { returns(String) } + attr_reader :repo_contents_path + + sig { returns(DependencyFile) } + attr_reader :discovery_json + + sig { returns(T.nilable(WorkspaceDiscovery)) } + def read_workspace_discovery + return nil unless discovery_json.content + + parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) + WorkspaceDiscovery.from_json(parsed_json) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, discovery_json.path end sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set + def read_dependency_set dependency_set = Dependabot::FileParsers::Base::DependencySet.new return dependency_set unless workspace_discovery @@ -49,9 +213,6 @@ def dependency_set workspace_result.projects.each do |project| dependency_set += project.dependency_set end - if workspace_result.directory_packages_props - dependency_set += T.must(workspace_result.directory_packages_props).dependency_set - end if workspace_result.dotnet_tools_json dependency_set += T.must(workspace_result.dotnet_tools_json).dependency_set end @@ -60,22 +221,46 @@ def dependency_set dependency_set end - sig { returns(T.nilable(WorkspaceDiscovery)) } - def workspace_discovery - @workspace_discovery ||= T.let(begin - return nil unless discovery_json.content + sig { returns(T::Array[String]) } + def read_dependency_file_paths + dependency_file_paths = T.let([], T::Array[T.nilable(String)]) + dependency_file_paths << dependency_file_path_from_repo_path("global.json") if workspace_discovery&.global_json + if workspace_discovery&.dotnet_tools_json + dependency_file_paths << dependency_file_path_from_repo_path(".config/dotnet-tools.json") + end - parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) - WorkspaceDiscovery.from_json(parsed_json) - end, T.nilable(WorkspaceDiscovery)) - rescue JSON::ParserError - raise Dependabot::DependencyFileNotParseable, discovery_json.path + projects = workspace_discovery&.projects || [] + projects.each do |project| + dependency_file_paths << dependency_file_path_from_repo_path(project.file_path) + dependency_file_paths += project.imported_files.map do |f| + dependency_file_path_from_project_path(project.file_path, f) + end + dependency_file_paths += project.additional_files.map do |f| + dependency_file_path_from_project_path(project.file_path, f) + end + end + + deduped_dependency_file_paths = T.let(Set.new(dependency_file_paths.compact), T::Set[String]) + result = deduped_dependency_file_paths.sort + result end - private + sig { params(path_parts: String).returns(T.nilable(String)) } + def dependency_file_path_from_repo_path(*path_parts) + path_parts = path_parts.map { |p| p.delete_prefix("/").delete_suffix("/") } + normalized_repo_path = Pathname.new(path_parts.join("/")).cleanpath.to_path.delete_prefix("/") + full_path = Pathname.new(File.join(repo_contents_path, normalized_repo_path)).cleanpath.to_path + return unless File.exist?(full_path) - sig { returns(DependencyFile) } - attr_reader :discovery_json + normalized_repo_path = "/#{normalized_repo_path}" unless normalized_repo_path.start_with?("/") + normalized_repo_path + end + + sig { params(project_path: String, relative_file_path: String).returns(T.nilable(String)) } + def dependency_file_path_from_project_path(project_path, relative_file_path) + project_directory = File.dirname(project_path) + dependency_file_path_from_repo_path(project_directory, relative_file_path) + end end end end diff --git a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb index 37fbd1c4b7..600ca21f48 100644 --- a/nuget/lib/dependabot/nuget/discovery/project_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/project_discovery.rb @@ -10,41 +10,68 @@ module Nuget class ProjectDiscovery < DependencyFileDiscovery extend T::Sig + # rubocop:disable Metrics/AbcSize sig do - params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(ProjectDiscovery)) + override.params(json: T.nilable(T::Hash[String, T.untyped]), + directory: String).returns(T.nilable(ProjectDiscovery)) end - def self.from_json(json) + def self.from_json(json, directory) return nil if json.nil? - file_path = T.let(json.fetch("FilePath"), String) + file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) properties = T.let(json.fetch("Properties"), T::Array[T::Hash[String, T.untyped]]).map do |prop| PropertyDetails.from_json(prop) end target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) referenced_project_paths = T.let(json.fetch("ReferencedProjectPaths"), T::Array[String]) - dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| - DependencyDetails.from_json(dep) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).filter_map do |dep| + details = DependencyDetails.from_json(dep) + next unless details.version # can't do anything without a version + + version = T.must(details.version) + next unless version.length.positive? # can't do anything with an empty version + + next if version.include? "," # can't do anything with a range + + next if version.include? "*" # can't do anything with a wildcard + + details end + imported_files = T.let(json.fetch("ImportedFiles"), T::Array[String]) + additional_files = T.let(json.fetch("AdditionalFiles"), T::Array[String]) ProjectDiscovery.new(file_path: file_path, properties: properties, target_frameworks: target_frameworks, referenced_project_paths: referenced_project_paths, - dependencies: dependencies) + dependencies: dependencies, + imported_files: imported_files, + additional_files: additional_files) end + # rubocop:enable Metrics/AbcSize sig do params(file_path: String, properties: T::Array[PropertyDetails], target_frameworks: T::Array[String], referenced_project_paths: T::Array[String], - dependencies: T::Array[DependencyDetails]).void + dependencies: T::Array[DependencyDetails], + imported_files: T::Array[String], + additional_files: T::Array[String]).void end - def initialize(file_path:, properties:, target_frameworks:, referenced_project_paths:, dependencies:) + def initialize(file_path:, + properties:, + target_frameworks:, + referenced_project_paths:, + dependencies:, + imported_files:, + additional_files:) super(file_path: file_path, dependencies: dependencies) @properties = properties @target_frameworks = target_frameworks @referenced_project_paths = referenced_project_paths + @imported_files = imported_files + @additional_files = additional_files end sig { returns(T::Array[PropertyDetails]) } @@ -56,6 +83,12 @@ def initialize(file_path:, properties:, target_frameworks:, referenced_project_p sig { returns(T::Array[String]) } attr_reader :referenced_project_paths + sig { returns(T::Array[String]) } + attr_reader :imported_files + + sig { returns(T::Array[String]) } + attr_reader :additional_files + sig { override.returns(Dependabot::FileParsers::Base::DependencySet) } def dependency_set if target_frameworks.empty? && file_path.end_with?("proj") diff --git a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb index 29827953c7..8e14db3c1c 100644 --- a/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb +++ b/nuget/lib/dependabot/nuget/discovery/workspace_discovery.rb @@ -2,8 +2,8 @@ # frozen_string_literal: true require "dependabot/nuget/discovery/dependency_file_discovery" -require "dependabot/nuget/discovery/directory_packages_props_discovery" require "dependabot/nuget/discovery/project_discovery" +require "dependabot/nuget/native_helpers" require "sorbet-runtime" module Dependabot @@ -13,49 +13,44 @@ 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) + Dependabot::Nuget::NativeHelpers.ensure_no_errors(json) + + path = T.let(json.fetch("Path"), String) + path = "/" + path unless path.start_with?("/") projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| - ProjectDiscovery.from_json(project) + ProjectDiscovery.from_json(project, path) end - directory_packages_props = DirectoryPackagesPropsDiscovery - .from_json(T.let(json.fetch("DirectoryPackagesProps"), - T.nilable(T::Hash[String, T.untyped]))) global_json = DependencyFileDiscovery - .from_json(T.let(json.fetch("GlobalJson"), T.nilable(T::Hash[String, T.untyped]))) + .from_json(T.let(json.fetch("GlobalJson"), T.nilable(T::Hash[String, T.untyped])), path) dotnet_tools_json = DependencyFileDiscovery - .from_json(T.let(json.fetch("DotNetToolsJson"), T.nilable(T::Hash[String, T.untyped]))) + .from_json(T.let(json.fetch("DotNetToolsJson"), + T.nilable(T::Hash[String, T.untyped])), path) - WorkspaceDiscovery.new(file_path: file_path, + WorkspaceDiscovery.new(path: path, projects: projects, - directory_packages_props: directory_packages_props, global_json: global_json, dotnet_tools_json: dotnet_tools_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:, global_json:, dotnet_tools_json:) + @path = path @projects = projects - @directory_packages_props = directory_packages_props @global_json = global_json @dotnet_tools_json = dotnet_tools_json end sig { returns(String) } - attr_reader :file_path + attr_reader :path sig { returns(T::Array[ProjectDiscovery]) } attr_reader :projects - sig { returns(T.nilable(DirectoryPackagesPropsDiscovery)) } - attr_reader :directory_packages_props - sig { returns(T.nilable(DependencyFileDiscovery)) } attr_reader :global_json diff --git a/nuget/lib/dependabot/nuget/file_fetcher.rb b/nuget/lib/dependabot/nuget/file_fetcher.rb index 5dacfd983e..0b64718562 100644 --- a/nuget/lib/dependabot/nuget/file_fetcher.rb +++ b/nuget/lib/dependabot/nuget/file_fetcher.rb @@ -3,7 +3,7 @@ require "dependabot/file_fetchers" require "dependabot/file_fetchers/base" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "dependabot/nuget/native_helpers" require "set" require "sorbet-runtime" @@ -26,7 +26,7 @@ def self.required_files_message sig { override.returns(T::Array[DependencyFile]) } def fetch_files - discovery_json_reader = NativeDiscoveryJsonReader.run_discovery_in_directory( + discovery_json_reader = DiscoveryJsonReader.run_discovery_in_directory( repo_contents_path: T.must(repo_contents_path), directory: directory, credentials: credentials diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index 8022071b37..055a30f987 100644 --- a/nuget/lib/dependabot/nuget/file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser.rb @@ -4,7 +4,7 @@ require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "dependabot/nuget/native_helpers" require "sorbet-runtime" @@ -28,7 +28,7 @@ def parse def dependencies @dependencies ||= T.let(begin directory = source&.directory || "/" - discovery_json_reader = NativeDiscoveryJsonReader.run_discovery_in_directory( + discovery_json_reader = DiscoveryJsonReader.run_discovery_in_directory( repo_contents_path: T.must(repo_contents_path), directory: directory, credentials: credentials diff --git a/nuget/lib/dependabot/nuget/file_updater.rb b/nuget/lib/dependabot/nuget/file_updater.rb index 39393b9ae5..6bbef86d7f 100644 --- a/nuget/lib/dependabot/nuget/file_updater.rb +++ b/nuget/lib/dependabot/nuget/file_updater.rb @@ -4,9 +4,9 @@ require "dependabot/dependency_file" require "dependabot/file_updaters" require "dependabot/file_updaters/base" -require "dependabot/nuget/native_discovery/native_dependency_details" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" -require "dependabot/nuget/native_discovery/native_workspace_discovery" +require "dependabot/nuget/discovery/dependency_details" +require "dependabot/nuget/discovery/discovery_json_reader" +require "dependabot/nuget/discovery/workspace_discovery" require "dependabot/nuget/native_helpers" require "dependabot/shared_helpers" require "sorbet-runtime" @@ -57,7 +57,7 @@ def updated_dependency_files try_update_projects(dependency) || try_update_json(dependency) end updated_files = dependency_files.filter_map do |f| - dependency_file_path = NativeDiscoveryJsonReader.dependency_file_path( + dependency_file_path = DiscoveryJsonReader.dependency_file_path( repo_contents_path: T.must(repo_contents_path), dependency_file: f ) @@ -97,7 +97,7 @@ def try_update_projects(dependency) # run update for each project file project_files.each do |project_file| project_dependencies = project_dependencies(project_file) - dependency_file_path = NativeDiscoveryJsonReader.dependency_file_path( + dependency_file_path = DiscoveryJsonReader.dependency_file_path( repo_contents_path: T.must(repo_contents_path), dependency_file: project_file ) @@ -128,7 +128,7 @@ def try_update_json(dependency) # We just need to feed the updater a project file, grab the first project_file = T.must(project_files.first) - dependency_file_path = NativeDiscoveryJsonReader.dependency_file_path( + dependency_file_path = DiscoveryJsonReader.dependency_file_path( repo_contents_path: T.must(repo_contents_path), dependency_file: project_file ) @@ -168,13 +168,13 @@ def testonly_update_tooling_calls @update_tooling_calls end - sig { returns(T.nilable(NativeWorkspaceDiscovery)) } + sig { returns(T.nilable(WorkspaceDiscovery)) } def workspace dependency_file_paths = dependency_files.map do |f| - NativeDiscoveryJsonReader.dependency_file_path(repo_contents_path: T.must(repo_contents_path), - dependency_file: f) + DiscoveryJsonReader.dependency_file_path(repo_contents_path: T.must(repo_contents_path), + dependency_file: f) end - NativeDiscoveryJsonReader.load_discovery_for_dependency_file_paths(dependency_file_paths).workspace_discovery + DiscoveryJsonReader.load_discovery_for_dependency_file_paths(dependency_file_paths).workspace_discovery end sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[String]) } @@ -182,7 +182,7 @@ def referenced_project_paths(project_file) workspace&.projects&.find { |p| p.file_path == project_file.name }&.referenced_project_paths || [] end - sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[NativeDependencyDetails]) } + sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[DependencyDetails]) } def project_dependencies(project_file) workspace&.projects&.find do |p| full_project_file_path = File.join(project_file.directory, project_file.name) @@ -190,12 +190,12 @@ def project_dependencies(project_file) end&.dependencies || [] end - sig { returns(T::Array[NativeDependencyDetails]) } + sig { returns(T::Array[DependencyDetails]) } def global_json_dependencies workspace&.global_json&.dependencies || [] end - sig { returns(T::Array[NativeDependencyDetails]) } + sig { returns(T::Array[DependencyDetails]) } def dotnet_tools_json_dependencies workspace&.dotnet_tools_json&.dependencies || [] end 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 2e751f985e..0000000000 --- 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_discovery/native_dependency_details.rb b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb deleted file mode 100644 index 94cdecf0da..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb +++ /dev/null @@ -1,102 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/native_discovery/native_evaluation_details" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeDependencyDetails - extend T::Sig - - sig { params(json: T::Hash[String, T.untyped]).returns(NativeDependencyDetails) } - def self.from_json(json) - name = T.let(json.fetch("Name"), String) - version = T.let(json.fetch("Version"), T.nilable(String)) - type = T.let(json.fetch("Type"), String) - evaluation = NativeEvaluationDetails - .from_json(T.let(json.fetch("EvaluationResult"), T.nilable(T::Hash[String, T.untyped]))) - target_frameworks = T.let(json.fetch("TargetFrameworks"), T.nilable(T::Array[String])) - is_dev_dependency = T.let(json.fetch("IsDevDependency"), T::Boolean) - is_direct = T.let(json.fetch("IsDirect"), T::Boolean) - is_transitive = T.let(json.fetch("IsTransitive"), T::Boolean) - is_override = T.let(json.fetch("IsOverride"), T::Boolean) - is_update = T.let(json.fetch("IsUpdate"), T::Boolean) - info_url = T.let(json.fetch("InfoUrl"), T.nilable(String)) - - NativeDependencyDetails.new(name: name, - version: version, - type: type, - evaluation: evaluation, - target_frameworks: target_frameworks, - is_dev_dependency: is_dev_dependency, - is_direct: is_direct, - is_transitive: is_transitive, - is_override: is_override, - is_update: is_update, - info_url: info_url) - end - - sig do - params(name: String, - version: T.nilable(String), - type: String, - evaluation: T.nilable(NativeEvaluationDetails), - target_frameworks: T.nilable(T::Array[String]), - is_dev_dependency: T::Boolean, - is_direct: T::Boolean, - is_transitive: T::Boolean, - is_override: T::Boolean, - is_update: T::Boolean, - info_url: T.nilable(String)).void - end - def initialize(name:, version:, type:, evaluation:, target_frameworks:, is_dev_dependency:, is_direct:, - is_transitive:, is_override:, is_update:, info_url:) - @name = name - @version = version - @type = type - @evaluation = evaluation - @target_frameworks = target_frameworks - @is_dev_dependency = is_dev_dependency - @is_direct = is_direct - @is_transitive = is_transitive - @is_override = is_override - @is_update = is_update - @info_url = info_url - end - - sig { returns(String) } - attr_reader :name - - sig { returns(T.nilable(String)) } - attr_reader :version - - sig { returns(String) } - attr_reader :type - - sig { returns(T.nilable(NativeEvaluationDetails)) } - attr_reader :evaluation - - sig { returns(T.nilable(T::Array[String])) } - attr_reader :target_frameworks - - sig { returns(T::Boolean) } - attr_reader :is_dev_dependency - - sig { returns(T::Boolean) } - attr_reader :is_direct - - sig { returns(T::Boolean) } - attr_reader :is_transitive - - sig { returns(T::Boolean) } - attr_reader :is_override - - sig { returns(T::Boolean) } - attr_reader :is_update - - sig { returns(T.nilable(String)) } - attr_reader :info_url - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb deleted file mode 100644 index 012cc84a3e..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb +++ /dev/null @@ -1,122 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/native_discovery/native_dependency_details" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeDependencyFileDiscovery - extend T::Sig - - sig do - params(json: T.nilable(T::Hash[String, T.untyped]), - directory: String).returns(T.nilable(NativeDependencyFileDiscovery)) - end - def self.from_json(json, directory) - return nil if json.nil? - - file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) - dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| - NativeDependencyDetails.from_json(dep) - end - - NativeDependencyFileDiscovery.new(file_path: file_path, - dependencies: dependencies) - end - - sig do - params(file_path: String, - dependencies: T::Array[NativeDependencyDetails]).void - end - def initialize(file_path:, dependencies:) - @file_path = file_path - @dependencies = dependencies - end - - sig { returns(String) } - attr_reader :file_path - - sig { returns(T::Array[NativeDependencyDetails]) } - attr_reader :dependencies - - sig { overridable.returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set # rubocop:disable Metrics/PerceivedComplexity - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - - file_name = Pathname.new(file_path).cleanpath.to_path - dependencies.each do |dependency| - next if dependency.name.casecmp("Microsoft.NET.Sdk")&.zero? - - # If the version string was evaluated it must have been successfully resolved - if dependency.evaluation && dependency.evaluation&.result_type != "Success" - logger.warn "Dependency '#{dependency.name}' excluded due to unparsable version: #{dependency.version}" - next - end - - # Exclude any dependencies using version ranges or wildcards - next if dependency.version&.include?(",") || - dependency.version&.include?("*") - - # Exclude any dependencies specified using interpolation - next if dependency.name.include?("%(") || - dependency.version&.include?("%(") - - # Exclude any dependencies which reference an item type - next if dependency.name.include?("@(") - - dependency_set << build_dependency(file_name, dependency) - end - - dependency_set - end - - private - - sig { returns(::Logger) } - def logger - Dependabot.logger - end - - sig { params(file_name: String, dependency_details: NativeDependencyDetails).returns(Dependabot::Dependency) } - def build_dependency(file_name, dependency_details) - requirement = build_requirement(file_name, dependency_details) - requirements = requirement.nil? ? [] : [requirement] - - version = dependency_details.version&.gsub(/[\(\)\[\]]/, "")&.strip - version = nil if version&.empty? - - Dependency.new( - name: dependency_details.name, - version: version, - package_manager: "nuget", - requirements: requirements - ) - end - - sig do - params(file_name: String, dependency_details: NativeDependencyDetails) - .returns(T.nilable(T::Hash[Symbol, T.untyped])) - end - def build_requirement(file_name, dependency_details) - return if dependency_details.is_transitive - - version = dependency_details.version - version = nil if version&.empty? - - requirement = { - requirement: version, - file: file_name, - groups: [dependency_details.is_dev_dependency ? "devDependencies" : "dependencies"], - source: nil - } - - property_name = dependency_details.evaluation&.root_property_name - return requirement unless property_name - - requirement[:metadata] = { property_name: property_name } - requirement - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb b/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb deleted file mode 100644 index 7bcaadb865..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb +++ /dev/null @@ -1,266 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/dependency" -require "dependabot/file_parsers/base/dependency_set" -require "dependabot/nuget/cache_manager" -require "dependabot/nuget/native_discovery/native_workspace_discovery" -require "json" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeDiscoveryJsonReader - extend T::Sig - - sig { returns(T::Hash[String, NativeDiscoveryJsonReader]) } - def self.cache_directory_to_discovery_json_reader - CacheManager.cache("cache_directory_to_discovery_json_reader") - end - - sig { returns(T::Hash[String, NativeDiscoveryJsonReader]) } - def self.cache_dependency_file_paths_to_discovery_json_reader - CacheManager.cache("cache_dependency_file_paths_to_discovery_json_reader") - end - - sig { returns(T::Hash[String, String]) } - def self.cache_dependency_file_paths_to_discovery_json_path - CacheManager.cache("cache_dependency_file_paths_to_discovery_json_path") - end - - sig { void } - def self.testonly_clear_caches - cache_directory_to_discovery_json_reader.clear - cache_dependency_file_paths_to_discovery_json_reader.clear - cache_dependency_file_paths_to_discovery_json_path.clear - end - - sig { void } - def self.testonly_clear_discovery_files - # this will get recreated when necessary - FileUtils.rm_rf(discovery_directory) - end - - # Runs NuGet dependency discovery in the given directory and returns a new instance of NativeDiscoveryJsonReader. - # The location of the resultant JSON file is saved. - sig do - params( - repo_contents_path: String, - directory: String, - credentials: T::Array[Dependabot::Credential] - ).returns(NativeDiscoveryJsonReader) - end - def self.run_discovery_in_directory(repo_contents_path:, directory:, credentials:) - # run discovery - job_file_path = ENV.fetch("DEPENDABOT_JOB_PATH") - discovery_json_path = discovery_file_path_from_workspace_path(directory) - unless File.exist?(discovery_json_path) - NativeHelpers.run_nuget_discover_tool(job_path: job_file_path, - repo_root: repo_contents_path, - workspace_path: directory, - output_path: discovery_json_path, - credentials: credentials) - - Dependabot.logger.info("Discovery JSON content: #{File.read(discovery_json_path)}") - end - load_discovery_for_directory(repo_contents_path: repo_contents_path, directory: directory) - end - - # Loads NuGet dependency discovery for the given directory and returns a new instance of - # NativeDiscoveryJsonReader and caches the resultant object. - sig { params(repo_contents_path: String, directory: String).returns(NativeDiscoveryJsonReader) } - def self.load_discovery_for_directory(repo_contents_path:, directory:) - cache_directory_to_discovery_json_reader[directory] ||= begin - discovery_json_reader = discovery_json_reader(repo_contents_path: repo_contents_path, - workspace_path: directory) - cache_directory_to_discovery_json_reader[directory] = discovery_json_reader - dependency_file_cache_key = cache_key_from_dependency_file_paths(discovery_json_reader.dependency_file_paths) - cache_dependency_file_paths_to_discovery_json_reader[dependency_file_cache_key] = discovery_json_reader - discovery_file_path = discovery_file_path_from_workspace_path(directory) - cache_dependency_file_paths_to_discovery_json_path[dependency_file_cache_key] = discovery_file_path - - discovery_json_reader - end - end - - # Retrieves the cached NativeDiscoveryJsonReader object for the given dependency file paths. - sig { params(dependency_file_paths: T::Array[String]).returns(NativeDiscoveryJsonReader) } - def self.load_discovery_for_dependency_file_paths(dependency_file_paths) - dependency_file_cache_key = cache_key_from_dependency_file_paths(dependency_file_paths) - T.must(cache_dependency_file_paths_to_discovery_json_reader[dependency_file_cache_key]) - end - - # Retrieves the cached location of the discovery JSON file for the given dependency file paths. - sig { params(dependency_file_paths: T::Array[String]).returns(String) } - def self.get_discovery_json_path_for_dependency_file_paths(dependency_file_paths) - dependency_file_cache_key = cache_key_from_dependency_file_paths(dependency_file_paths) - T.must(cache_dependency_file_paths_to_discovery_json_path[dependency_file_cache_key]) - end - - sig { params(repo_contents_path: String, dependency_file: Dependabot::DependencyFile).returns(String) } - def self.dependency_file_path(repo_contents_path:, dependency_file:) - dep_file_path = Pathname.new(File.join(dependency_file.directory, dependency_file.name)).cleanpath.to_path - dep_file_path.delete_prefix("#{repo_contents_path}/") - end - - sig { returns(String) } - def self.discovery_map_file_path - File.join(discovery_directory, "discovery_map.json") - end - - sig { params(workspace_path: String).returns(String) } - def self.discovery_file_path_from_workspace_path(workspace_path) - # Given an update directory (also known as a workspace path), this function returns the path where the discovery - # JSON file is located. This function is called both by methods that need to write the discovery JSON file and - # by methods that need to read the discovery JSON file. This function is also called by multiple processes so - # we need a way to retain the data. This is accomplished by the following steps: - # 1. Check a well-known file for a mapping of workspace_path => discovery file path. If found, return it. - # 2. If the path is not found, generate a new path, save it to the well-known file, and return the value. - discovery_map_contents = File.exist?(discovery_map_file_path) ? File.read(discovery_map_file_path) : "{}" - discovery_map = T.let(JSON.parse(discovery_map_contents), T::Hash[String, String]) - - discovery_json_path = discovery_map[workspace_path] - if discovery_json_path - Dependabot.logger.info("Discovery JSON path for workspace path [#{workspace_path}] found in file " \ - "[#{discovery_map_file_path}] at location [#{discovery_json_path}]") - return discovery_json_path - end - - # no discovery JSON path found; generate a new one, but first find a suitable location - discovery_json_counter = 1 - new_discovery_json_path = "" - loop do - new_discovery_json_path = File.join(discovery_directory, "discovery.#{discovery_json_counter}.json") - break unless File.exist?(new_discovery_json_path) - - discovery_json_counter += 1 - end - - discovery_map[workspace_path] = new_discovery_json_path - - File.write(discovery_map_file_path, discovery_map.to_json) - Dependabot.logger.info("Discovery JSON path for workspace path [#{workspace_path}] created for file " \ - "[#{discovery_map_file_path}] at location [#{new_discovery_json_path}]") - new_discovery_json_path - end - - sig { params(dependency_file_paths: T::Array[String]).returns(String) } - def self.cache_key_from_dependency_file_paths(dependency_file_paths) - dependency_file_paths.sort.join(",") - end - - sig { returns(String) } - def self.discovery_directory - t = File.join(Dir.home, ".dependabot") - FileUtils.mkdir_p(t) - t - end - - sig { params(repo_contents_path: String, workspace_path: String).returns(NativeDiscoveryJsonReader) } - def self.discovery_json_reader(repo_contents_path:, workspace_path:) - discovery_file_path = discovery_file_path_from_workspace_path(workspace_path) - discovery_json = DependencyFile.new( - name: Pathname.new(discovery_file_path).cleanpath.to_path, - directory: discovery_directory, - type: "file", - content: File.read(discovery_file_path) - ) - NativeDiscoveryJsonReader.new(repo_contents_path: repo_contents_path, discovery_json: discovery_json) - end - - sig { returns(T.nilable(NativeWorkspaceDiscovery)) } - attr_reader :workspace_discovery - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - attr_reader :dependency_set - - sig { returns(T::Array[String]) } - attr_reader :dependency_file_paths - - sig { params(repo_contents_path: String, discovery_json: DependencyFile).void } - def initialize(repo_contents_path:, discovery_json:) - @repo_contents_path = repo_contents_path - @discovery_json = discovery_json - @workspace_discovery = T.let(read_workspace_discovery, T.nilable(Dependabot::Nuget::NativeWorkspaceDiscovery)) - @dependency_set = T.let(read_dependency_set, Dependabot::FileParsers::Base::DependencySet) - @dependency_file_paths = T.let(read_dependency_file_paths, T::Array[String]) - end - - private - - sig { returns(String) } - attr_reader :repo_contents_path - - sig { returns(DependencyFile) } - attr_reader :discovery_json - - sig { returns(T.nilable(NativeWorkspaceDiscovery)) } - def read_workspace_discovery - return nil unless discovery_json.content - - parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) - NativeWorkspaceDiscovery.from_json(parsed_json) - rescue JSON::ParserError - raise Dependabot::DependencyFileNotParseable, discovery_json.path - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def read_dependency_set - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - return dependency_set unless workspace_discovery - - workspace_result = T.must(workspace_discovery) - workspace_result.projects.each do |project| - dependency_set += project.dependency_set - end - if workspace_result.dotnet_tools_json - dependency_set += T.must(workspace_result.dotnet_tools_json).dependency_set - end - dependency_set += T.must(workspace_result.global_json).dependency_set if workspace_result.global_json - - dependency_set - end - - sig { returns(T::Array[String]) } - def read_dependency_file_paths - dependency_file_paths = T.let([], T::Array[T.nilable(String)]) - dependency_file_paths << dependency_file_path_from_repo_path("global.json") if workspace_discovery&.global_json - if workspace_discovery&.dotnet_tools_json - dependency_file_paths << dependency_file_path_from_repo_path(".config/dotnet-tools.json") - end - - projects = workspace_discovery&.projects || [] - projects.each do |project| - dependency_file_paths << dependency_file_path_from_repo_path(project.file_path) - dependency_file_paths += project.imported_files.map do |f| - dependency_file_path_from_project_path(project.file_path, f) - end - dependency_file_paths += project.additional_files.map do |f| - dependency_file_path_from_project_path(project.file_path, f) - end - end - - deduped_dependency_file_paths = T.let(Set.new(dependency_file_paths.compact), T::Set[String]) - result = deduped_dependency_file_paths.sort - result - end - - sig { params(path_parts: String).returns(T.nilable(String)) } - def dependency_file_path_from_repo_path(*path_parts) - path_parts = path_parts.map { |p| p.delete_prefix("/").delete_suffix("/") } - normalized_repo_path = Pathname.new(path_parts.join("/")).cleanpath.to_path.delete_prefix("/") - full_path = Pathname.new(File.join(repo_contents_path, normalized_repo_path)).cleanpath.to_path - return unless File.exist?(full_path) - - normalized_repo_path = "/#{normalized_repo_path}" unless normalized_repo_path.start_with?("/") - normalized_repo_path - end - - sig { params(project_path: String, relative_file_path: String).returns(T.nilable(String)) } - def dependency_file_path_from_project_path(project_path, relative_file_path) - project_directory = File.dirname(project_path) - dependency_file_path_from_repo_path(project_directory, relative_file_path) - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb b/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb deleted file mode 100644 index 37e13d67ad..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb +++ /dev/null @@ -1,63 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeEvaluationDetails - extend T::Sig - - sig { params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(NativeEvaluationDetails)) } - def self.from_json(json) - return nil if json.nil? - - result_type = T.let(json.fetch("ResultType"), String) - original_value = T.let(json.fetch("OriginalValue"), String) - evaluated_value = T.let(json.fetch("EvaluatedValue"), String) - root_property_name = T.let(json.fetch("RootPropertyName", nil), T.nilable(String)) - error_message = T.let(json.fetch("ErrorMessage", nil), T.nilable(String)) - - NativeEvaluationDetails.new(result_type: result_type, - original_value: original_value, - evaluated_value: evaluated_value, - root_property_name: root_property_name, - error_message: error_message) - end - - sig do - params(result_type: String, - original_value: String, - evaluated_value: String, - root_property_name: T.nilable(String), - error_message: T.nilable(String)).void - end - def initialize(result_type:, - original_value:, - evaluated_value:, - root_property_name:, - error_message:) - @result_type = result_type - @original_value = original_value - @evaluated_value = evaluated_value - @root_property_name = root_property_name - @error_message = error_message - end - - sig { returns(String) } - attr_reader :result_type - - sig { returns(String) } - attr_reader :original_value - - sig { returns(String) } - attr_reader :evaluated_value - - sig { returns(T.nilable(String)) } - attr_reader :root_property_name - - sig { returns(T.nilable(String)) } - attr_reader :error_message - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb deleted file mode 100644 index b5a1b87771..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb +++ /dev/null @@ -1,104 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/native_discovery/native_dependency_details" -require "dependabot/nuget/native_discovery/native_property_details" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeProjectDiscovery < NativeDependencyFileDiscovery - extend T::Sig - - # rubocop:disable Metrics/AbcSize - sig do - override.params(json: T.nilable(T::Hash[String, T.untyped]), - directory: String).returns(T.nilable(NativeProjectDiscovery)) - end - def self.from_json(json, directory) - return nil if json.nil? - - file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) - properties = T.let(json.fetch("Properties"), T::Array[T::Hash[String, T.untyped]]).map do |prop| - NativePropertyDetails.from_json(prop) - end - target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) - referenced_project_paths = T.let(json.fetch("ReferencedProjectPaths"), T::Array[String]) - dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).filter_map do |dep| - details = NativeDependencyDetails.from_json(dep) - next unless details.version # can't do anything without a version - - version = T.must(details.version) - next unless version.length.positive? # can't do anything with an empty version - - next if version.include? "," # can't do anything with a range - - next if version.include? "*" # can't do anything with a wildcard - - details - end - imported_files = T.let(json.fetch("ImportedFiles"), T::Array[String]) - additional_files = T.let(json.fetch("AdditionalFiles"), T::Array[String]) - - NativeProjectDiscovery.new(file_path: file_path, - properties: properties, - target_frameworks: target_frameworks, - referenced_project_paths: referenced_project_paths, - dependencies: dependencies, - imported_files: imported_files, - additional_files: additional_files) - end - # rubocop:enable Metrics/AbcSize - - sig do - params(file_path: String, - properties: T::Array[NativePropertyDetails], - target_frameworks: T::Array[String], - referenced_project_paths: T::Array[String], - dependencies: T::Array[NativeDependencyDetails], - imported_files: T::Array[String], - additional_files: T::Array[String]).void - end - def initialize(file_path:, - properties:, - target_frameworks:, - referenced_project_paths:, - dependencies:, - imported_files:, - additional_files:) - super(file_path: file_path, dependencies: dependencies) - @properties = properties - @target_frameworks = target_frameworks - @referenced_project_paths = referenced_project_paths - @imported_files = imported_files - @additional_files = additional_files - end - - sig { returns(T::Array[NativePropertyDetails]) } - attr_reader :properties - - sig { returns(T::Array[String]) } - attr_reader :target_frameworks - - sig { returns(T::Array[String]) } - attr_reader :referenced_project_paths - - sig { returns(T::Array[String]) } - attr_reader :imported_files - - sig { returns(T::Array[String]) } - attr_reader :additional_files - - sig { override.returns(Dependabot::FileParsers::Base::DependencySet) } - def dependency_set - if target_frameworks.empty? && file_path.end_with?("proj") - Dependabot.logger.warn("Excluding project file '#{file_path}' due to unresolvable target framework") - dependency_set = Dependabot::FileParsers::Base::DependencySet.new - return dependency_set - end - - super - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb b/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb deleted file mode 100644 index aa29f5c48e..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb +++ /dev/null @@ -1,43 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativePropertyDetails - extend T::Sig - - sig { params(json: T::Hash[String, T.untyped]).returns(NativePropertyDetails) } - def self.from_json(json) - name = T.let(json.fetch("Name"), String) - value = T.let(json.fetch("Value"), String) - source_file_path = T.let(json.fetch("SourceFilePath"), String) - - NativePropertyDetails.new(name: name, - value: value, - source_file_path: source_file_path) - end - - sig do - params(name: String, - value: String, - source_file_path: String).void - end - def initialize(name:, value:, source_file_path:) - @name = name - @value = value - @source_file_path = source_file_path - end - - sig { returns(String) } - attr_reader :name - - sig { returns(String) } - attr_reader :value - - sig { returns(String) } - attr_reader :source_file_path - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb deleted file mode 100644 index f11a35ac09..0000000000 --- a/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb +++ /dev/null @@ -1,61 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/native_discovery/native_dependency_file_discovery" -require "dependabot/nuget/native_discovery/native_project_discovery" -require "dependabot/nuget/native_helpers" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeWorkspaceDiscovery - extend T::Sig - - sig { params(json: T::Hash[String, T.untyped]).returns(NativeWorkspaceDiscovery) } - def self.from_json(json) - Dependabot::Nuget::NativeHelpers.ensure_no_errors(json) - - path = T.let(json.fetch("Path"), String) - path = "/" + path unless path.start_with?("/") - projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| - NativeProjectDiscovery.from_json(project, path) - end - global_json = NativeDependencyFileDiscovery - .from_json(T.let(json.fetch("GlobalJson"), T.nilable(T::Hash[String, T.untyped])), path) - dotnet_tools_json = NativeDependencyFileDiscovery - .from_json(T.let(json.fetch("DotNetToolsJson"), - T.nilable(T::Hash[String, T.untyped])), path) - - NativeWorkspaceDiscovery.new(path: path, - projects: projects, - global_json: global_json, - dotnet_tools_json: dotnet_tools_json) - end - - sig do - params(path: String, - projects: T::Array[NativeProjectDiscovery], - global_json: T.nilable(NativeDependencyFileDiscovery), - dotnet_tools_json: T.nilable(NativeDependencyFileDiscovery)).void - end - def initialize(path:, projects:, global_json:, dotnet_tools_json:) - @path = path - @projects = projects - @global_json = global_json - @dotnet_tools_json = dotnet_tools_json - end - - sig { returns(String) } - attr_reader :path - - sig { returns(T::Array[NativeProjectDiscovery]) } - attr_reader :projects - - sig { returns(T.nilable(NativeDependencyFileDiscovery)) } - attr_reader :global_json - - sig { returns(T.nilable(NativeDependencyFileDiscovery)) } - attr_reader :dotnet_tools_json - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb b/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb deleted file mode 100644 index ac8264e9c5..0000000000 --- a/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb +++ /dev/null @@ -1,105 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -####################################################################### -# For more details on Dotnet version constraints, see: # -# https://docs.microsoft.com/en-us/nuget/reference/package-versioning # -####################################################################### - -require "sorbet-runtime" - -require "dependabot/update_checkers/base" -require "dependabot/nuget/native_discovery/native_dependency_details" -require "dependabot/nuget/version" - -module Dependabot - module Nuget - class NativeUpdateChecker < Dependabot::UpdateCheckers::Base - class NativeRequirementsUpdater - extend T::Sig - - sig do - params( - requirements: T::Array[T::Hash[Symbol, T.untyped]], - dependency_details: T.nilable(Dependabot::Nuget::NativeDependencyDetails) - ) - .void - end - def initialize(requirements:, dependency_details:) - @requirements = requirements - @dependency_details = dependency_details - end - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def updated_requirements - return requirements unless clean_version - - # NOTE: Order is important here. The FileUpdater needs the updated - # requirement at index `i` to correspond to the previous requirement - # at the same index. - requirements.map do |req| - next req if req.fetch(:requirement).nil? - next req if req.fetch(:requirement).include?(",") - - new_req = - if req.fetch(:requirement).include?("*") - update_wildcard_requirement(req.fetch(:requirement)) - else - # Since range requirements are excluded by the line above we can - # replace anything that looks like a version with the new - # version - req[:requirement].sub( - /#{Nuget::Version::VERSION_PATTERN}/o, - clean_version.to_s - ) - end - - next req if new_req == req.fetch(:requirement) - - new_source = req[:source]&.dup - unless @dependency_details.nil? - new_source = { - type: "nuget_repo", - source_url: @dependency_details.info_url - } - end - - req.merge({ requirement: new_req, source: new_source }) - end - end - - private - - sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } - attr_reader :requirements - - sig { returns(T.class_of(Dependabot::Nuget::Version)) } - def version_class - Dependabot::Nuget::Version - end - - sig { returns(T.nilable(Dependabot::Nuget::Version)) } - def clean_version - return unless @dependency_details&.version - - version_class.new(@dependency_details.version) - end - - sig { params(req_string: String).returns(String) } - def update_wildcard_requirement(req_string) - return req_string if req_string == "*-*" - - return req_string if req_string == "*" - - precision = T.must(req_string.split("*").first).split(/\.|\-/).count - wildcard_section = req_string.partition(/(?=[.\-]\*)/).last - - version_parts = T.must(clean_version).segments.first(precision) - version = version_parts.join(".") - - version + wildcard_section - end - end - end - end -end diff --git a/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb b/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb deleted file mode 100644 index 10fe1034c5..0000000000 --- a/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb +++ /dev/null @@ -1,214 +0,0 @@ -# typed: strong -# frozen_string_literal: true - -require "dependabot/nuget/analysis/analysis_json_reader" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" -require "dependabot/update_checkers" -require "dependabot/update_checkers/base" -require "sorbet-runtime" - -module Dependabot - module Nuget - class NativeUpdateChecker < Dependabot::UpdateCheckers::Base - extend T::Sig - - require_relative "native_requirements_updater" - - sig { override.returns(T.nilable(String)) } - def latest_version - # No need to find latest version for transitive dependencies unless they have a vulnerability. - return dependency.version if !dependency.top_level? && !vulnerable? - - # 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.dependency_analysis.updated_version, - T.nilable(String) - ) - end - - sig { override.returns(T.nilable(T.any(String, Gem::Version))) } - def latest_resolvable_version - # We always want a full unlock since any package update could update peer dependencies as well. - # To force a full unlock instead of an own unlock, we return nil. - nil - end - - sig { override.returns(Dependabot::Nuget::Version) } - def lowest_security_fix_version - update_analysis.dependency_analysis.numeric_updated_version - end - - sig { override.returns(T.nilable(Dependabot::Nuget::Version)) } - def lowest_resolvable_security_fix_version - return nil if version_comes_from_multi_dependency_property? - - update_analysis.dependency_analysis.numeric_updated_version - end - - sig { override.returns(NilClass) } - def latest_resolvable_version_with_no_unlock - # Irrelevant, since Nuget has a single dependency file - nil - end - - sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } - def updated_requirements - dep_details = updated_dependency_details.find { |d| d.name.casecmp?(dependency.name) } - NativeRequirementsUpdater.new( - requirements: dependency.requirements, - dependency_details: dep_details - ).updated_requirements - end - - sig { returns(T::Boolean) } - def up_to_date? - !update_analysis.dependency_analysis.can_update - end - - sig { returns(T::Boolean) } - def requirements_unlocked_or_can_be? - update_analysis.dependency_analysis.can_update - end - - sig { returns(T::Boolean) } - def public_latest_version_resolvable_with_full_unlock? - latest_version_resolvable_with_full_unlock? - end - - sig { returns(T::Array[Dependabot::Dependency]) } - def public_updated_dependencies_after_full_unlock - updated_dependencies_after_full_unlock - end - - private - - sig { returns(AnalysisJsonReader) } - def update_analysis - @update_analysis ||= T.let(request_analysis, T.nilable(AnalysisJsonReader)) - end - - sig { returns(String) } - def dependency_file_path - d = File.join(Dir.tmpdir, "dependency") - FileUtils.mkdir_p(d) - File.join(d, "#{dependency.name}.json") - end - - sig { returns(T::Array[String]) } - def dependency_file_paths - dependency_files.map do |file| - NativeDiscoveryJsonReader.dependency_file_path( - repo_contents_path: T.must(repo_contents_path), - dependency_file: file - ) - end - end - - sig { returns(AnalysisJsonReader) } - def request_analysis - discovery_file_path = NativeDiscoveryJsonReader.get_discovery_json_path_for_dependency_file_paths( - dependency_file_paths - ) - analysis_folder_path = AnalysisJsonReader.temp_directory - - write_dependency_info - - NativeHelpers.run_nuget_analyze_tool(repo_root: T.must(repo_contents_path), - 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, - 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 - dependency_directory = File.dirname(dependency_file_path) - - begin - Dir.mkdir(dependency_directory) - rescue StandardError - nil? - end - - Dependabot.logger.info("Writing dependency info: #{dependency_info}") - File.write(dependency_file_path, dependency_info) - end - - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def discovered_dependencies - NativeDiscoveryJsonReader.load_discovery_for_dependency_file_paths(dependency_file_paths).dependency_set - 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. - true - end - - sig { override.returns(T::Array[Dependabot::Dependency]) } - def updated_dependencies_after_full_unlock - dependencies = discovered_dependencies.dependencies - updated_dependency_details.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? - - # rebuild the new requirements with the updated dependency details - updated_reqs = dep.requirements.map do |r| - r = r.clone - r[:requirement] = dependency_details.version - r[:source] = { - type: "nuget_repo", - source_url: dependency_details.info_url - } - r - end - - Dependency.new( - name: dep.name, - version: dependency_details.version, - requirements: updated_reqs, - previous_version: dep.version, - previous_requirements: dep.requirements, - package_manager: dep.package_manager, - metadata: metadata - ) - end - end - - sig { returns(T::Array[Dependabot::Nuget::NativeDependencyDetails]) } - def updated_dependency_details - @updated_dependency_details ||= T.let(update_analysis.dependency_analysis.updated_dependencies, - T.nilable(T::Array[Dependabot::Nuget::NativeDependencyDetails])) - end - - sig { returns(T::Boolean) } - def version_comes_from_multi_dependency_property? - update_analysis.dependency_analysis.version_comes_from_multi_dependency_property - end - end - end -end - -Dependabot::UpdateCheckers.register("nuget", Dependabot::Nuget::UpdateChecker) diff --git a/nuget/lib/dependabot/nuget/nuget_client.rb b/nuget/lib/dependabot/nuget/nuget_client.rb deleted file mode 100644 index 6fe1dbea41..0000000000 --- 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 2e7dde1286..26144b699b 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -1,7 +1,8 @@ -# typed: strict +# typed: strong # 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,38 +12,22 @@ 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" - - require_relative "native_update_checker/native_update_checker" - - PROPERTY_REGEX = /\$\((?.*?)\)/ - - sig { returns(T::Boolean) } - def self.native_analysis_enabled? - Dependabot::Experiments.enabled?(:nuget_native_analysis) - end sig { override.returns(T.nilable(String)) } def latest_version - return native_update_checker.latest_version if UpdateChecker.native_analysis_enabled? - # No need to find latest version for transitive dependencies unless they have a vulnerability. return dependency.version if !dependency.top_level? && !vulnerable? # 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.dependency_analysis.updated_version, T.nilable(String) ) end sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_resolvable_version - return native_update_checker.latest_resolvable_version if UpdateChecker.native_analysis_enabled? - # We always want a full unlock since any package update could update peer dependencies as well. # To force a full unlock instead of an own unlock, we return nil. nil @@ -50,232 +35,167 @@ def latest_resolvable_version sig { override.returns(Dependabot::Nuget::Version) } def lowest_security_fix_version - return native_update_checker.lowest_security_fix_version if UpdateChecker.native_analysis_enabled? - - lowest_security_fix_version_details&.fetch(:version) + update_analysis.dependency_analysis.numeric_updated_version end sig { override.returns(T.nilable(Dependabot::Nuget::Version)) } def lowest_resolvable_security_fix_version return nil if version_comes_from_multi_dependency_property? - lowest_security_fix_version + update_analysis.dependency_analysis.numeric_updated_version end sig { override.returns(NilClass) } def latest_resolvable_version_with_no_unlock - return native_update_checker.latest_resolvable_version_with_no_unlock if UpdateChecker.native_analysis_enabled? - # Irrelevant, since Nuget has a single dependency file nil end sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements - return native_update_checker.updated_requirements if UpdateChecker.native_analysis_enabled? - + dep_details = updated_dependency_details.find { |d| d.name.casecmp?(dependency.name) } 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) + dependency_details: dep_details ).updated_requirements end sig { returns(T::Boolean) } def up_to_date? - return native_update_checker.up_to_date? if UpdateChecker.native_analysis_enabled? - - # 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.dependency_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.dependency_analysis.can_update end private - sig { returns(Dependabot::Nuget::NativeUpdateChecker) } - def native_update_checker - @native_update_checker ||= - T.let( - Dependabot::Nuget::NativeUpdateChecker.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - repo_contents_path: repo_contents_path, - ignored_versions: ignored_versions, - raise_on_ignored: raise_on_ignored, - security_advisories: security_advisories, - requirements_update_strategy: requirements_update_strategy, - dependency_group: dependency_group, - options: options - ), - T.nilable(Dependabot::Nuget::NativeUpdateChecker) - ) + sig { returns(AnalysisJsonReader) } + def update_analysis + @update_analysis ||= T.let(request_analysis, T.nilable(AnalysisJsonReader)) end - 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? - - latest_version_details + sig { returns(String) } + def dependency_file_path + d = File.join(Dir.tmpdir, "dependency") + FileUtils.mkdir_p(d) + File.join(d, "#{dependency.name}.json") end - sig { override.returns(T::Boolean) } - def latest_version_resolvable_with_full_unlock? - if UpdateChecker.native_analysis_enabled? - return native_update_checker.public_latest_version_resolvable_with_full_unlock? + sig { returns(T::Array[String]) } + def dependency_file_paths + dependency_files.map do |file| + DiscoveryJsonReader.dependency_file_path( + repo_contents_path: T.must(repo_contents_path), + dependency_file: file + ) end - - # 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? end - sig { override.returns(T::Array[Dependabot::Dependency]) } - def updated_dependencies_after_full_unlock - if UpdateChecker.native_analysis_enabled? - return native_update_checker.public_updated_dependencies_after_full_unlock - end - - 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 + sig { returns(AnalysisJsonReader) } + def request_analysis + discovery_file_path = DiscoveryJsonReader.get_discovery_json_path_for_dependency_file_paths( + dependency_file_paths ) - updated_dependencies = [updated_dependency] - updated_dependencies += DependencyFinder.new( - dependency: updated_dependency, - dependency_files: dependency_files, - ignored_versions: ignored_versions, - credentials: credentials, - repo_contents_path: @repo_contents_path - ).updated_peer_dependencies - updated_dependencies - end + analysis_folder_path = AnalysisJsonReader.temp_directory + + write_dependency_info + + NativeHelpers.run_nuget_analyze_tool(repo_root: T.must(repo_contents_path), + 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, + 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 + dependency_directory = File.dirname(dependency_file_path) - sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } - def preferred_version_details - return lowest_security_fix_version_details if vulnerable? + begin + Dir.mkdir(dependency_directory) + rescue StandardError + nil? + end - latest_version_details + Dependabot.logger.info("Writing dependency info: #{dependency_info}") + File.write(dependency_file_path, dependency_info) 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]) - ) + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def discovered_dependencies + DiscoveryJsonReader.load_discovery_for_dependency_file_paths(dependency_file_paths).dependency_set 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]) - ) + 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. + true 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 { override.returns(T::Array[Dependabot::Dependency]) } + def updated_dependencies_after_full_unlock + dependencies = discovered_dependencies.dependencies + updated_dependency_details.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? + + # rebuild the new requirements with the updated dependency details + updated_reqs = dep.requirements.map do |r| + r = r.clone + r[:requirement] = dependency_details.version + r[:source] = { + type: "nuget_repo", + source_url: dependency_details.info_url + } + r + 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) + Dependency.new( + name: dep.name, + version: dependency_details.version, + requirements: updated_reqs, + previous_version: dep.version, + previous_requirements: dep.requirements, + package_manager: dep.package_manager, + metadata: metadata ) - 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]]) - ) + sig { returns(T::Array[Dependabot::Nuget::DependencyDetails]) } + def updated_dependency_details + @updated_dependency_details ||= T.let(update_analysis.dependency_analysis.updated_dependencies, + T.nilable(T::Array[Dependabot::Nuget::DependencyDetails])) 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]) - ) + sig { returns(T::Boolean) } + def version_comes_from_multi_dependency_property? + update_analysis.dependency_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 4f67de874e..0000000000 --- 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 e940df9df9..0000000000 --- a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb +++ /dev/null @@ -1,297 +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], - ignored_versions: T::Array[String], - credentials: T::Array[Dependabot::Credential], - repo_contents_path: T.nilable(String) - ).void - end - def initialize(dependency:, dependency_files:, ignored_versions:, credentials:, repo_contents_path:) - @dependency = dependency - @dependency_files = dependency_files - @ignored_versions = ignored_versions - @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[String]) } - attr_reader :ignored_versions - - 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: 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 b9f53f9d1e..0000000000 --- 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 d07d441233..0000000000 --- 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 34663d19a3..0000000000 --- a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb +++ /dev/null @@ -1,196 +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, - ignored_versions: ignored_versions, - 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 af7181430a..0000000000 --- a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb +++ /dev/null @@ -1,466 +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 - # rubocop:disable Metrics/ClassLength - 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 = repo[:url] - begin - url = URI::DEFAULT_PARSER.parse(url).to_s - rescue URI::InvalidURIError - # e.g., the url has spaces or unacceptable symbols - url = URI::DEFAULT_PARSER.escape(url) - end - - { url: 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 - - sig { params(source_name: String).returns(String) } - def self.escape_source_name_to_element_name(source_name) - source_name.chars.map do |c| - case c - when /[A-Za-z0-9\-_.]/ - # letters, digits, hyphens, underscores, and periods are all directly allowed - c - else - # otherwise it needs to be escaped as a 4 digit hex value - "_x#{c.ord.to_s(16).rjust(4, '0')}_" - end - end.join - 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 = RepositoryFinder.escape_source_name_to_element_name(key) - 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 - # rubocop:enable Metrics/ClassLength - 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 fe600921c1..f796ba2796 100644 --- a/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb +++ b/nuget/lib/dependabot/nuget/update_checker/requirements_updater.rb @@ -9,6 +9,7 @@ require "sorbet-runtime" require "dependabot/update_checkers/base" +require "dependabot/nuget/discovery/dependency_details" require "dependabot/nuget/version" module Dependabot @@ -20,22 +21,18 @@ 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]) + dependency_details: T.nilable(Dependabot::Nuget::DependencyDetails) ) .void end - def initialize(requirements:, latest_version:, source_details:) + def initialize(requirements:, dependency_details:) @requirements = requirements - @source_details = source_details - return unless latest_version - - @latest_version = T.let(version_class.new(latest_version), Dependabot::Nuget::Version) + @dependency_details = dependency_details end sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements - return requirements unless latest_version + return requirements unless clean_version # NOTE: Order is important here. The FileUpdater needs the updated # requirement at index `i` to correspond to the previous requirement @@ -53,13 +50,21 @@ def updated_requirements # version req[:requirement].sub( /#{Nuget::Version::VERSION_PATTERN}/o, - latest_version.to_s + clean_version.to_s ) end next req if new_req == req.fetch(:requirement) - req.merge(requirement: new_req, source: updated_source) + new_source = req[:source]&.dup + unless @dependency_details.nil? + new_source = { + type: "nuget_repo", + source_url: @dependency_details.info_url + } + end + + req.merge({ requirement: new_req, source: new_source }) end end @@ -68,17 +73,18 @@ def updated_requirements sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } attr_reader :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 end + sig { returns(T.nilable(Dependabot::Nuget::Version)) } + def clean_version + return unless @dependency_details&.version + + version_class.new(@dependency_details.version) + end + sig { params(req_string: String).returns(String) } def update_wildcard_requirement(req_string) return req_string if req_string == "*-*" @@ -88,21 +94,11 @@ def update_wildcard_requirement(req_string) precision = T.must(req_string.split("*").first).split(/\.|\-/).count wildcard_section = req_string.partition(/(?=[.\-]\*)/).last - version_parts = T.must(latest_version).segments.first(precision) + version_parts = T.must(clean_version).segments.first(precision) version = version_parts.join(".") 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 0cfbe08faf..0000000000 --- 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 aadb0e83ef..0000000000 --- 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 d3c3c67a02..0000000000 --- a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb +++ /dev/null @@ -1,449 +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_reqs = parse_requirement_string(req).map { |r| requirement_class.new(r) } - filtered = - filtered - .reject { |v| ignore_reqs.any? { |r| r.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 diff --git a/nuget/spec/dependabot/nuget/file_fetcher_spec.rb b/nuget/spec/dependabot/nuget/file_fetcher_spec.rb index 80a4fed65a..b2eb1be9a1 100644 --- a/nuget/spec/dependabot/nuget/file_fetcher_spec.rb +++ b/nuget/spec/dependabot/nuget/file_fetcher_spec.rb @@ -5,7 +5,7 @@ require "dependabot/dependency_file" require "dependabot/source" require "dependabot/nuget/file_fetcher" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "json" require_common_spec "file_fetchers/shared_examples_for_file_fetchers" @@ -13,8 +13,8 @@ subject(:fetched_file_paths) do files = file_fetcher_instance.fetch_files files.map do |f| - Dependabot::Nuget::NativeDiscoveryJsonReader.dependency_file_path(repo_contents_path: repo_contents_path, - dependency_file: f) + Dependabot::Nuget::DiscoveryJsonReader.dependency_file_path(repo_contents_path: repo_contents_path, + dependency_file: f) end end let(:report_stub_debug_information) { false } # set to `true` to write method stubbing information to the screen @@ -42,7 +42,7 @@ it_behaves_like "a dependency file fetcher" def clean_common_files - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_discovery_files + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_discovery_files end def clean_repo_files @@ -59,7 +59,7 @@ def run_fetch_test(files_on_disk:, discovery_content_hash:, &_block) File.write(ENV.fetch("DEPENDABOT_JOB_PATH", nil), "unused") begin # stub call to native tool - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches allow(Dependabot::Nuget::NativeHelpers) .to receive(:run_nuget_discover_tool) .and_wrap_original do |_original_method, *args, &_block| diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index d6d0d0aecc..aa3acea007 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -6,14 +6,9 @@ require "dependabot/source" require "dependabot/nuget/file_parser" require "dependabot/nuget/version" -require_relative "nuget_search_stubs" require_common_spec "file_parsers/shared_examples_for_file_parsers" RSpec.describe Dependabot::Nuget::FileParser do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - let(:stub_native_tools) { true } # set to `false` to allow invoking the native tools during tests let(:report_stub_debug_information) { false } # set to `true` to write native tool stubbing information to the screen @@ -69,19 +64,19 @@ def ensure_job_file(&_block) end def clean_common_files - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_discovery_files + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_discovery_files end def run_parser_test(&_block) ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" clean_common_files - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches ensure_job_file do # ensure discovery files are present... - Dependabot::Nuget::NativeDiscoveryJsonReader.run_discovery_in_directory(repo_contents_path: repo_contents_path, - directory: directory, - credentials: []) + Dependabot::Nuget::DiscoveryJsonReader.run_discovery_in_directory(repo_contents_path: repo_contents_path, + directory: directory, + credentials: []) # ...create the parser... parser = Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, @@ -92,7 +87,7 @@ def run_parser_test(&_block) yield parser end ensure - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches ENV.delete("DEPENDABOT_NUGET_CACHE_DISABLED") clean_common_files end diff --git a/nuget/spec/dependabot/nuget/file_updater_spec.rb b/nuget/spec/dependabot/nuget/file_updater_spec.rb index 00d12a3325..becb6418ce 100644 --- a/nuget/spec/dependabot/nuget/file_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/file_updater_spec.rb @@ -6,16 +6,10 @@ require "dependabot/nuget/file_parser" require "dependabot/nuget/file_updater" require "dependabot/nuget/version" -require_relative "github_helpers" -require_relative "nuget_search_stubs" require "json" require_common_spec "file_updaters/shared_examples_for_file_updaters" RSpec.describe Dependabot::Nuget::FileUpdater do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - let(:stub_native_tools) { true } # set to `false` to allow invoking the native tools during tests let(:report_stub_debug_information) { false } # set to `true` to write native tool stubbing information to the screen @@ -75,14 +69,6 @@ } end - before do - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.0", "1.1.1"]) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.0.0/" \ - "microsoft.extensions.dependencymodel.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "Microsoft.Extensions.DependencyModel.1.0.0.nuspec")) - end - it_behaves_like "a dependency file updater" def ensure_job_file(&_block) @@ -100,20 +86,20 @@ def ensure_job_file(&_block) end def clean_common_files - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_discovery_files + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_discovery_files end def run_update_test(&_block) # caching is explicitly required for these tests ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches clean_common_files ensure_job_file do # ensure discovery files are present - Dependabot::Nuget::NativeDiscoveryJsonReader.run_discovery_in_directory(repo_contents_path: repo_contents_path, - directory: directory, - credentials: []) + Dependabot::Nuget::DiscoveryJsonReader.run_discovery_in_directory(repo_contents_path: repo_contents_path, + directory: directory, + credentials: []) # calling `#parse` is necessary to force `discover` which is stubbed below Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, @@ -135,7 +121,7 @@ def run_update_test(&_block) yield updater end ensure - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" clean_common_files end diff --git a/nuget/spec/dependabot/nuget/github_helpers.rb b/nuget/spec/dependabot/nuget/github_helpers.rb deleted file mode 100644 index 12382881fc..0000000000 --- a/nuget/spec/dependabot/nuget/github_helpers.rb +++ /dev/null @@ -1,135 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "digest/sha1" - -module GitHubHelpers - # rubocop:disable Metrics/MethodLength - # rubocop:disable Metrics/ParameterLists - def self.stub_requests_for_directory(stub_callback, path_on_disk, relative_path, url_base, authorization, org_name, - repo_name, branch_name) - url = "#{url_base}#{relative_path}?ref=sha" - stub_callback.call(:get, url) - .with(headers: { "Authorization" => authorization }) - .to_return( - status: 200, - body: GitHubHelpers.create_tree_object( - path_on_disk, - relative_path, - org_name, - repo_name, - branch_name - ).to_json, - headers: { "content-type" => "application/json" } - ) - - Dir.entries(path_on_disk).select { |entry| entry != "." && entry != ".." }.each do |entry| - full_path = File.join(path_on_disk, entry) - current_relative_path = relative_path == "" ? entry : File.join(relative_path, entry) - url = "#{url_base}#{current_relative_path}?ref=sha" - if File.directory?(full_path) - stub_requests_for_directory(stub_callback, full_path, current_relative_path, url_base, authorization, org_name, - repo_name, branch_name) - else - stub_callback.call(:get, url) - .with(headers: { "Authorization" => authorization }) - .to_return( - status: 200, - body: GitHubHelpers.create_file_object( - current_relative_path, - File.read(full_path), - org_name, - repo_name, - branch_name - ).to_json, - headers: { "content-type" => "application/json" } - ) - end - end - end - # rubocop:enable Metrics/ParameterLists - # rubocop:enable Metrics/MethodLength - - def self.create_file_object(path, content, org_name, repo_name, branch_name) - hash = hash_file_content(content) - obj = { - "name" => File.basename(path), - "path" => path, - "sha" => hash, - "size" => content.length, - "url" => "https://api.github.com/repos/#{org_name}/#{repo_name}/contents/#{path}?ref=#{branch_name}", - "html_url" => "https://github.com/#{org_name}/#{repo_name}/blob/#{branch_name}/#{path}", - "git_url" => "https://api.github.com/repos/#{org_name}/#{repo_name}/git/blobs/#{hash}", - "download_url" => "https://raw.githubusercontent.com/#{org_name}/#{repo_name}/#{branch_name}/#{path}", - "type" => "file", - "content" => Base64.encode64(content), - "encoding" => "base64", - "_links" => { - "self" => "https://api.github.com/repos/#{org_name}/#{repo_name}/contents/#{path}?ref=#{branch_name}", - "git" => "https://api.github.com/repos/#{org_name}/#{repo_name}/git/blobs/#{hash}", - "html" => "https://github.com/#{org_name}/#{repo_name}/blob/#{branch_name}/#{path}}" - } - } - obj - end - - # rubocop:disable Metrics/MethodLength - def self.create_tree_object(directory_path, relative_path, org_name, repo_name, branch_name) - result = - Dir.entries(directory_path).select { |entry| entry != "." && entry != ".." }.map do |entry| - path = File.join(directory_path, entry) - if File.directory?(path) - type = "dir" - obj_type = "tree" - sha = hash_tree_content(path) - size = 0 - download_url = nil - else - type = "file" - obj_type = "blob" - content = File.read(path) - sha = hash_file_content(content) - size = content.length - download_url = "https://raw.githubusercontent.com/#{org_name}/#{repo_name}/#{branch_name}/#{path}" - end - obj = { - "name" => entry, - "path" => relative_path, - "sha" => sha, - "size" => size, - "url" => "https://api.github.com/repos/#{org_name}/#{repo_name}/contents/#{path}?ref=#{branch_name}", - "html_url" => "https://github.com/#{org_name}/#{repo_name}/#{obj_type}/#{branch_name}/#{path}", - "git_url" => "https://api.github.com/repos/#{org_name}/#{repo_name}/git/#{obj_type}s/#{sha}", - "download_url" => download_url, - "type" => type, - "_links" => { - "self" => "https://api.github.com/repos/#{org_name}/#{repo_name}/contents/#{path}?ref=#{branch_name}", - "git" => "https://api.github.com/repos/#{org_name}/#{repo_name}/git/#{obj_type}s/#{sha}", - "html" => "https://github.com/#{org_name}/#{repo_name}/#{obj_type}/#{branch_name}/#{path}" - } - } - obj - end - result - end - # rubocop:enable Metrics/MethodLength - - def self.hash_file_content(content) - raw_content = "blob #{content.length}\0#{content}" - Digest::SHA1.hexdigest(raw_content) - end - - def self.hash_tree_content(directory) - tree_content = - Dir.entries(directory).select { |entry| entry != "." && entry != ".." }.map do |entry| - path = File.join(directory, entry) - if File.directory?(path) - "040000 tree #{hash_tree_content(path)}\t#{entry}" - else - content = File.read(path) - "100644 blob #{hash_file_content(content)}\t#{entry}" - end - end.join("\n") - Digest::SHA1.hexdigest("tree #{tree_content.length}\0#{tree_content}") - end -end diff --git a/nuget/spec/dependabot/nuget/nuget_search_stubs.rb b/nuget/spec/dependabot/nuget/nuget_search_stubs.rb deleted file mode 100644 index 725a048df9..0000000000 --- a/nuget/spec/dependabot/nuget/nuget_search_stubs.rb +++ /dev/null @@ -1,139 +0,0 @@ -# typed: false -# frozen_string_literal: true - -module NuGetSearchStubs - def stub_index_json(url) - shortened_url = url.delete_suffix("/index.json") - stub_request(:get, url) - .to_return( - status: 200, - body: { - resources: [ - { - "@type": "PackageBaseAddress/3.0.0", - "@id": "#{shortened_url}/PackageBaseAddress" - }, - { - "@type": "RegistrationsBaseUrl/3.6.0", - "@id": "#{shortened_url}/RegistrationsBaseUrl" - }, - { - "@type": "SearchQueryService/3.5.0", - "@id": "#{shortened_url}/SearchQueryService" - } - ] - }.to_json - ) - end - - def stub_no_search_results(name) - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/#{name}/index.json") - .to_return(status: 404, body: "") - end - - def stub_registry_v3(name, versions) - registration_json = registration_results(name, versions) - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/#{name}/index.json") - .to_return(status: 200, body: registration_json) - end - - def stub_search_results_with_versions_v3(name, versions) - stub_registry_v3(name, versions) - end - - def registration_results(name, versions) - page = { - "@id": "https://api.nuget.org/v3/registration5-semver2/#{name}/index.json#page/PAGE1", - "@type": "catalog:CatalogPage", - "count" => versions.count, - "items" => versions.map do |version| - { - "catalogEntry" => { - "@type": "PackageDetails", - "id" => name, - "listed" => true, - "version" => version - } - } - end - } - pages = [page] - response = { - "@id": "https://api.nuget.org/v3/registration5-gz-semver2/#{name}/index.json", - "count" => versions.count, - "items" => pages - } - response.to_json - end - - # rubocop:disable Metrics/MethodLength - def search_results_with_versions_v2(name, versions) - entries = versions.map do |version| - xml = <<~XML - - https://www.nuget.org/api/v2/Packages(Id='#{name}',Version='#{version}') - - - - #{name} - 2015-07-28T23:37:16Z - - FakeAuthor - - - - #{name} - #{version} - #{version} - FakeAuthor - FakeCopyright - 2015-07-28T23:37:16.85+00:00 - - FakeDescription - 42 - https://www.nuget.org/packages/#{name}/#{version} - - false - false - false - - 2015-07-28T23:37:16.85+00:00 - 2015-07-28T23:37:16.85+00:00 - FakeHash - SHA512 - 42 - https://example.com/#{name} - https://example.com/#{name} - - false - - - #{name} - 42 - - 2018-12-08T05:53:10.917+00:00 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - XML - xml = xml.split("\n").map { |line| " #{line}" }.join("\n") - xml - end.join("\n") - xml = <<~XML - - - #{versions.length} - http://schemas.datacontract.org/2004/07/ - - <updated>2023-12-05T23:35:30Z</updated> - <link rel="self" href="https://www.nuget.org/api/v2/Packages" /> - #{entries} - </feed> - XML - xml - end - # rubocop:enable Metrics/MethodLength -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 88f1e7d4ca..0000000000 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ /dev/null @@ -1,147 +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" - -RSpec.describe Dependabot::Nuget::RepositoryFinder do - describe "#escape_source_name_to_element_name" do - subject(:escaped) do - described_class.escape_source_name_to_element_name(source_name) - end - - context "when the source name needs no escaping" do - let(:source_name) { "some_source-name.1" } - - it { is_expected.to eq("some_source-name.1") } - end - - context "when the source name has a space" do - let(:source_name) { "source name" } - - it { is_expected.to eq("source_x0020_name") } - end - - context "when the source name has other characters that need to be escaped" do - let(:source_name) { "source@local" } - - it { is_expected.to eq("source_x0040_local") } - end - end - - describe "#dependency_urls" do - subject(:dependency_urls) do - described_class.new( - dependency: Dependabot::Dependency.new( - name: "Some.Package", - version: "1.0.0", - requirements: [], - package_manager: "nuget" - ), - credentials: credentials, - config_files: config_files - ).send(:dependency_urls) - end - - context "when package source name contains non-identifier characters" do - let(:credentials) { [] } - let(:config_files) do - [ - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: - <<~XML - <?xml version="1.0" encoding="utf-8"?> - <configuration> - <packageSources> - <clear /> - <!-- the `@` symbol in the name can cause issues when searching through this file --> - <add key="nuget-mirror@local" value="https://nuget.example.com/v3/index.json" /> - </packageSources> - </configuration> - XML - ) - ] - end - - before do - stub_request(:get, "https://nuget.example.com/v3/index.json") - .to_return( - status: 200, - body: { - version: "3.0.0", - resources: [ - { - "@id": "https://nuget.example.com/v3/base", - "@type": "PackageBaseAddress/3.0.0" - }, - { - "@id": "https://nuget.example.com/v3/registrations", - "@type": "RegistrationsBaseUrl" - }, - { - "@id": "https://nuget.example.com/v3/search", - "@type": "SearchQueryService/3.5.0" - } - ] - }.to_json - ) - end - - it "returns the urls" do - expect(dependency_urls).to eq( - [{ - auth_header: {}, - base_url: "https://nuget.example.com/v3/base", - registration_url: "https://nuget.example.com/v3/registrations/some.package/index.json", - repository_type: "v3", - repository_url: "https://nuget.example.com/v3/index.json", - search_url: "https://nuget.example.com/v3/search?q=some.package&prerelease=true&semVerLevel=2.0.0", - versions_url: "https://nuget.example.com/v3/base/some.package/index.json" - }] - ) - end - end - end - - describe "#known_repositories" do - subject(:url) do - dependency = Dependabot::Dependency.new( - name: "Some.Package", - version: "1.0.0", - requirements: [], - package_manager: "nuget" - ) - instance = described_class.new(dependency: dependency, credentials: credentials) - instance.known_repositories.first.fetch(:url) - end - - let(:credentials) { [{ "type" => "nuget_feed", "url" => feed_url }] } - - context "when no escaping is required" do - let(:feed_url) { "https://nuget.example.com/v3/index.json" } - - it { is_expected.to eq("https://nuget.example.com/v3/index.json") } - end - - context "when escaping is required" do - let(:feed_url) { "https://nuget.example.com/feed with spaces/v3/index.json" } - - it { is_expected.to eq("https://nuget.example.com/feed%20with%20spaces/v3/index.json") } - end - - context "when escaping has already been done" do - let(:feed_url) { "https://nuget.example.com/feed%20with%20spaces/v3/index.json" } - - it { is_expected.to eq("https://nuget.example.com/feed%20with%20spaces/v3/index.json") } - end - - context "when the feed is a relative local path" do - let(:feed_url) { "../packages" } - - it { is_expected.to eq("../packages") } - end - end -end diff --git a/nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb b/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb similarity index 95% rename from nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb rename to nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb index 573c886d5c..82230f133e 100644 --- a/nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb @@ -2,9 +2,9 @@ # frozen_string_literal: true require "spec_helper" -require "dependabot/nuget/native_update_checker/native_requirements_updater" +require "dependabot/nuget/update_checker/requirements_updater" -RSpec.describe Dependabot::Nuget::NativeUpdateChecker::NativeRequirementsUpdater do +RSpec.describe Dependabot::Nuget::UpdateChecker::RequirementsUpdater do let(:updater) do described_class.new( requirements: requirements, @@ -25,7 +25,7 @@ let(:latest_version) { "23.6-jre" } let(:info_url) { "https://nuget.example.com/some.package" } let(:dependency_details) do - Dependabot::Nuget::NativeDependencyDetails.from_json(JSON.parse({ + Dependabot::Nuget::DependencyDetails.from_json(JSON.parse({ Name: "unused", Version: latest_version, Type: "PackageReference", diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index 026d7ea8cf..8904a31ff4 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -5,7 +5,7 @@ require "dependabot/dependency" require "dependabot/dependency_file" require "dependabot/nuget/analysis/analysis_json_reader" -require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/discovery/discovery_json_reader" require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker" require "dependabot/nuget/requirement" @@ -74,10 +74,6 @@ end let(:directory) { "/" } - before do - Dependabot::Experiments.register(:nuget_native_analysis, true) - end - it_behaves_like "an update checker" def ensure_job_file(&_block) @@ -95,20 +91,20 @@ def ensure_job_file(&_block) end def clean_common_files - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_discovery_files + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_discovery_files end def run_analyze_test(&_block) # caching is explicitly required for these tests ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches clean_common_files ensure_job_file do # ensure discovery files are present - Dependabot::Nuget::NativeDiscoveryJsonReader.run_discovery_in_directory(repo_contents_path: repo_contents_path, - directory: directory, - credentials: []) + Dependabot::Nuget::DiscoveryJsonReader.run_discovery_in_directory(repo_contents_path: repo_contents_path, + directory: directory, + credentials: []) # calling `#parse` is necessary to force `discover` which is stubbed below Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, @@ -129,7 +125,7 @@ def run_analyze_test(&_block) yield checker end ensure - Dependabot::Nuget::NativeDiscoveryJsonReader.testonly_clear_caches + Dependabot::Nuget::DiscoveryJsonReader.testonly_clear_caches ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" clean_common_files end diff --git a/nuget/spec/spec_helper.rb b/nuget/spec/spec_helper.rb index a06f2f997b..a23c3fa17f 100644 --- a/nuget/spec/spec_helper.rb +++ b/nuget/spec/spec_helper.rb @@ -1,9 +1,6 @@ # typed: true # frozen_string_literal: true -# require "dependabot/experiments" -# Dependabot::Experiments.register(:nuget_native_analysis, true) - ENV["DEPENDABOT_NUGET_TEST_RUN"] = "true" ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true"