diff --git a/.github/workflows/add-to-core-project.yml b/.github/workflows/add-to-core-project.yml deleted file mode 100644 index 725aed3c16f..00000000000 --- a/.github/workflows/add-to-core-project.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Add issues to core project - -on: # yamllint disable-line rule:truthy - issues: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 - with: - project-url: https://github.com/orgs/dependabot/projects/5 - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b8e4cff77d..c62d38a01e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: BUNDLE_GEMFILE: updater/Gemfile steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - uses: ruby/setup-ruby@d5fb7a202fc07872cb44f00ba8e6197b70cb0c55 # v1.179.0 + - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 with: bundler-cache: true - run: ./bin/lint diff --git a/.github/workflows/gems-bump-version.yml b/.github/workflows/gems-bump-version.yml index f8e438fe481..a4ca0cd92d5 100644 --- a/.github/workflows/gems-bump-version.yml +++ b/.github/workflows/gems-bump-version.yml @@ -31,7 +31,7 @@ jobs: ref: "main" # bump-version.rb needs bundler - - uses: ruby/setup-ruby@d5fb7a202fc07872cb44f00ba8e6197b70cb0c55 # v1.179.0 + - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 with: # Use the version of bundler specified in `updater/Gemfile.lock`. # Otherwise the generated PR will change `BUNDLED WITH` in diff --git a/.github/workflows/gems-release-to-rubygems.yml b/.github/workflows/gems-release-to-rubygems.yml index 47b91cc8a59..8bf3b6623f3 100644 --- a/.github/workflows/gems-release-to-rubygems.yml +++ b/.github/workflows/gems-release-to-rubygems.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - uses: ruby/setup-ruby@d5fb7a202fc07872cb44f00ba8e6197b70cb0c55 # v1.179.0 + - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 - run: | [ -d ~/.gem ] || mkdir ~/.gem echo "---" > ~/.gem/credentials diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 40c652970d5..c191fea966c 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -16,6 +16,7 @@ concurrency: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKE_TEST_BRANCH: main jobs: discover: runs-on: ubuntu-latest @@ -44,7 +45,7 @@ jobs: cat filtered.json # Curl the smoke-test tests directory to get a list of tests to run - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests?ref=${{ env.SMOKE_TEST_BRANCH }} curl $URL > tests.json # Select the names that match smoke-$test*.yaml, where $test is the .text value from filtered.json @@ -84,7 +85,7 @@ jobs: - name: Download test if: steps.cache-smoke-test.outputs.cache-hit != 'true' run: | - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }} + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }}?ref=${{ env.SMOKE_TEST_BRANCH }} curl $(gh api $URL --jq .download_url) -o smoke.yaml - name: Cache Smoke Test diff --git a/.github/workflows/sorbet.yml b/.github/workflows/sorbet.yml index ba8f8a3ace0..3132cee626b 100644 --- a/.github/workflows/sorbet.yml +++ b/.github/workflows/sorbet.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - uses: ruby/setup-ruby@d5fb7a202fc07872cb44f00ba8e6197b70cb0c55 # v1.179.0 + - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 with: bundler-cache: true diff --git a/.rubocop.yml b/.rubocop.yml index 75991bfccfb..3a9503f0619 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -341,6 +341,7 @@ Style/SpecialGlobalVars: Style/SelectByRegexp: Enabled: false Sorbet/TrueSigil: + Enabled: true Exclude: - "**/spec/**/*" Sorbet/StrictSigil: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1a459f7fc83..64ea1d09a6e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -22,14 +22,6 @@ RSpec/AnyInstance: - 'updater/spec/dependabot/dependency_change_builder_spec.rb' - 'updater/spec/dependabot/file_fetcher_command_spec.rb' -# Offense count: 7 -RSpec/BeforeAfterAll: - Exclude: - - 'nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb' - - 'pub/spec/dependabot/pub/file_updater_spec.rb' - - 'pub/spec/dependabot/pub/infer_sdk_versions_spec.rb' - - 'pub/spec/dependabot/pub/update_checker_spec.rb' - # Offense count: 1286 # Configuration parameters: CountAsOne. RSpec/ExampleLength: @@ -55,12 +47,7 @@ RSpec/FilePath: # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: - - 'bundler/helpers/v2/spec/ruby_version_spec.rb' - - 'common/spec/dependabot/clients/azure_spec.rb' - 'go_modules/spec/dependabot/go_modules/file_updater_spec.rb' - - 'pub/spec/dependabot/pub/file_updater_spec.rb' - - 'pub/spec/dependabot/pub/infer_sdk_versions_spec.rb' - - 'pub/spec/dependabot/pub/update_checker_spec.rb' # Offense count: 22 RSpec/IteratedExpectation: @@ -94,11 +81,6 @@ RSpec/MessageChain: RSpec/MessageSpies: Enabled: false -# Offense count: 1 -RSpec/MultipleDescribes: - Exclude: - - 'common/spec/dependabot/errors_spec.rb' - # Offense count: 1380 RSpec/MultipleExpectations: Max: 17 diff --git a/Dockerfile.updater-core b/Dockerfile.updater-core index 4856e99be30..4ae2b1b217d 100644 --- a/Dockerfile.updater-core +++ b/Dockerfile.updater-core @@ -110,17 +110,18 @@ RUN for ecosystem in git_submodules terraform github_actions hex elm docker nuge WORKDIR $DEPENDABOT_HOME/dependabot-updater -ARG RUBYGEMS_VERSION=3.5.11 -RUN gem update --system $RUBYGEMS_VERSION - -# When bumping Bundler, need to also: -# * Regenerate `updater/Gemfile.lock` via `BUNDLE_GEMFILE=updater/Gemfile bundle lock --update --bundler` +# RubyGems & Bundler should be bumped together following these steps: +# * Bump RubyGems version below. That will also automatically update the default Bundler version. +# * Regenerate `updater/Gemfile.lock` via `BUNDLE_GEMFILE=updater/Gemfile bundle lock --update --bundler`. # * Regenerate `Gemfile.lock` via `bundle lock --update --bundler`. -ARG BUNDLER_V2_VERSION=2.5.11 +# +# Note that RubyGems & Bundler versions are currently released in sync, but +# RubyGems version is one major ahead. So when bumping to RubyGems 3.y.z, Bundler +# version will jump to 2.y.z +ARG RUBYGEMS_VERSION=3.5.14 +RUN gem update --system $RUBYGEMS_VERSION -RUN gem install bundler -v $BUNDLER_V2_VERSION --no-document && \ - rm -rf /var/lib/gems/*/cache/* && \ - bundle config set --global build.psych --with-libyaml-source-dir=$DEPENDABOT_HOME/src/libyaml/yaml-$LIBYAML_VERSION && \ +RUN bundle config set --global build.psych --with-libyaml-source-dir=$DEPENDABOT_HOME/src/libyaml/yaml-$LIBYAML_VERSION && \ bundle config set --local path 'vendor' && \ bundle config set --local frozen 'true' && \ bundle config set --local without 'development' && \ diff --git a/Gemfile.lock b/Gemfile.lock index aabf7609fa3..30bfa01e904 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,20 +1,20 @@ PATH remote: bundler specs: - dependabot-bundler (0.262.0) - dependabot-common (= 0.262.0) + dependabot-bundler (0.264.0) + dependabot-common (= 0.264.0) parallel (~> 1.24) PATH remote: cargo specs: - dependabot-cargo (0.262.0) - dependabot-common (= 0.262.0) + dependabot-cargo (0.264.0) + dependabot-common (= 0.264.0) PATH remote: common specs: - dependabot-common (0.262.0) + dependabot-common (0.264.0) aws-sdk-codecommit (~> 1.28) aws-sdk-ecr (~> 1.5) bundler (>= 1.16, < 3.0.0) @@ -37,107 +37,107 @@ PATH PATH remote: composer specs: - dependabot-composer (0.262.0) - dependabot-common (= 0.262.0) + dependabot-composer (0.264.0) + dependabot-common (= 0.264.0) PATH remote: devcontainers specs: - dependabot-devcontainers (0.262.0) - dependabot-common (= 0.262.0) + dependabot-devcontainers (0.264.0) + dependabot-common (= 0.264.0) PATH remote: docker specs: - dependabot-docker (0.262.0) - dependabot-common (= 0.262.0) + dependabot-docker (0.264.0) + dependabot-common (= 0.264.0) PATH remote: elm specs: - dependabot-elm (0.262.0) - dependabot-common (= 0.262.0) + dependabot-elm (0.264.0) + dependabot-common (= 0.264.0) PATH remote: git_submodules specs: - dependabot-git_submodules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-git_submodules (0.264.0) + dependabot-common (= 0.264.0) parseconfig (~> 1.0, < 1.1.0) PATH remote: github_actions specs: - dependabot-github_actions (0.262.0) - dependabot-common (= 0.262.0) + dependabot-github_actions (0.264.0) + dependabot-common (= 0.264.0) PATH remote: go_modules specs: - dependabot-go_modules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-go_modules (0.264.0) + dependabot-common (= 0.264.0) PATH remote: gradle specs: - dependabot-gradle (0.262.0) - dependabot-common (= 0.262.0) - dependabot-maven (= 0.262.0) + dependabot-gradle (0.264.0) + dependabot-common (= 0.264.0) + dependabot-maven (= 0.264.0) PATH remote: hex specs: - dependabot-hex (0.262.0) - dependabot-common (= 0.262.0) + dependabot-hex (0.264.0) + dependabot-common (= 0.264.0) PATH remote: maven specs: - dependabot-maven (0.262.0) - dependabot-common (= 0.262.0) + dependabot-maven (0.264.0) + dependabot-common (= 0.264.0) PATH remote: npm_and_yarn specs: - dependabot-npm_and_yarn (0.262.0) - dependabot-common (= 0.262.0) + dependabot-npm_and_yarn (0.264.0) + dependabot-common (= 0.264.0) PATH remote: nuget specs: - dependabot-nuget (0.262.0) - dependabot-common (= 0.262.0) + dependabot-nuget (0.264.0) + dependabot-common (= 0.264.0) rubyzip (>= 2.3.2, < 3.0) PATH remote: pub specs: - dependabot-pub (0.262.0) - dependabot-common (= 0.262.0) + dependabot-pub (0.264.0) + dependabot-common (= 0.264.0) PATH remote: python specs: - dependabot-python (0.262.0) - dependabot-common (= 0.262.0) + dependabot-python (0.264.0) + dependabot-common (= 0.264.0) PATH remote: silent specs: - dependabot-silent (0.262.0) - dependabot-common (= 0.262.0) + dependabot-silent (0.264.0) + dependabot-common (= 0.264.0) PATH remote: swift specs: - dependabot-swift (0.262.0) - dependabot-common (= 0.262.0) + dependabot-swift (0.264.0) + dependabot-common (= 0.264.0) PATH remote: terraform specs: - dependabot-terraform (0.262.0) - dependabot-common (= 0.262.0) + dependabot-terraform (0.264.0) + dependabot-common (= 0.264.0) GEM remote: https://rubygems.org/ @@ -417,4 +417,4 @@ DEPENDENCIES webrick (>= 1.7) BUNDLED WITH - 2.5.11 + 2.5.14 diff --git a/README.md b/README.md index 9e29f9aaf44..1e15891b067 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ It is intended as a starting point for advanced users to run a self-hosted versi ## Dependabot CLI The [Dependabot CLI](https://github.com/dependabot/cli) is a newer tool that may eventually replace [`dependabot-script`](#dependabot-script) for standalone use cases. -While it creates dependency diffs, it's currently missing the logic to turn those diffs into actual PR's. Nevertheless, it +While it creates dependency diffs, it's currently missing the logic to turn those diffs into actual PRs. Nevertheless, it may be useful for advanced users looking for examples of how to hack on Dependabot. ## Dependabot on CI diff --git a/bin/bump-version.rb b/bin/bump-version.rb index e50d7ca5efc..80b0386622c 100755 --- a/bin/bump-version.rb +++ b/bin/bump-version.rb @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# typed: false +# typed: true # frozen_string_literal: true unless %w(minor patch).include?(ARGV[0]) diff --git a/bin/dry-run.rb b/bin/dry-run.rb index 847613b572d..662953b562a 100755 --- a/bin/dry-run.rb +++ b/bin/dry-run.rb @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# typed: false +# typed: true # frozen_string_literal: true # This script executes a full update run for a given repo (optionally for a diff --git a/bundler/.rubocop.yml b/bundler/.rubocop.yml index f04967a814a..507d22b2def 100644 --- a/bundler/.rubocop.yml +++ b/bundler/.rubocop.yml @@ -5,6 +5,5 @@ inherit_mode: - Exclude Sorbet/TrueSigil: - Enabled: true Exclude: - "helpers/**/monkey_patches/*.rb" diff --git a/bundler/helpers/v2/spec/ruby_version_spec.rb b/bundler/helpers/v2/spec/ruby_version_spec.rb index 6acf0e36603..f3787a48382 100644 --- a/bundler/helpers/v2/spec/ruby_version_spec.rb +++ b/bundler/helpers/v2/spec/ruby_version_spec.rb @@ -9,13 +9,13 @@ include_context "when stubbing rubygems compact index" let(:project_name) { "ruby_version_implied" } + let(:ui) { Bundler.ui } before do - @ui = Bundler.ui Bundler.ui = Bundler::UI::Silent.new end - after { Bundler.ui = @ui } + after { Bundler.ui = ui } it "updates to the most recent version" do in_tmp_folder do diff --git a/bundler/lib/dependabot/bundler/file_fetcher.rb b/bundler/lib/dependabot/bundler/file_fetcher.rb index bcbd18add95..e25ab963653 100644 --- a/bundler/lib/dependabot/bundler/file_fetcher.rb +++ b/bundler/lib/dependabot/bundler/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" @@ -17,18 +17,21 @@ class FileFetcher < Dependabot::FileFetchers::Base require "dependabot/bundler/file_fetcher/gemspec_finder" require "dependabot/bundler/file_fetcher/path_gemspec_finder" require "dependabot/bundler/file_fetcher/child_gemfile_finder" - require "dependabot/bundler/file_fetcher/require_relative_finder" + require "dependabot/bundler/file_fetcher/included_path_finder" + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) return true if filenames.any? { |name| name.match?(%r{^[^/]*\.gemspec$}) } filenames.include?("Gemfile") || filenames.include?("gems.rb") end + sig { override.returns(String) } def self.required_files_message "Repo must contain either a Gemfile, a gemspec, or a gems.rb." end + sig { override.returns(T.nilable(T::Hash[Symbol, T.nilable(String)])) } def ecosystem_versions { package_managers: { @@ -39,41 +42,47 @@ def ecosystem_versions sig { override.returns(T::Array[DependencyFile]) } def fetch_files - fetched_files = [] - fetched_files << gemfile if gemfile - fetched_files << lockfile if gemfile && lockfile + fetched_files = T.let([], T::Array[DependencyFile]) + fetched_files << T.must(gemfile) if gemfile + fetched_files << T.must(lockfile) if gemfile && lockfile fetched_files += child_gemfiles fetched_files += gemspecs - fetched_files << ruby_version_file if ruby_version_file - fetched_files << tool_versions_file if tool_versions_file + fetched_files << T.must(ruby_version_file) if ruby_version_file + fetched_files << T.must(tool_versions_file) if tool_versions_file fetched_files += path_gemspecs - fetched_files += require_relative_files(fetched_files) + fetched_files += find_included_files(fetched_files) uniq_files(fetched_files) end private + sig { params(fetched_files: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) } def uniq_files(fetched_files) uniq_files = fetched_files.reject(&:support_file?).uniq uniq_files += fetched_files .reject { |f| uniq_files.map(&:name).include?(f.name) } end + sig { returns(T.nilable(DependencyFile)) } def gemfile return @gemfile if defined?(@gemfile) - @gemfile = fetch_file_if_present("gems.rb") || fetch_file_if_present("Gemfile") + @gemfile = T.let(fetch_file_if_present("gems.rb") || fetch_file_if_present("Gemfile"), + T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(DependencyFile)) } def lockfile return @lockfile if defined?(@lockfile) - @lockfile = fetch_file_if_present("gems.locked") || fetch_file_if_present("Gemfile.lock") + @lockfile = T.let(fetch_file_if_present("gems.locked") || fetch_file_if_present("Gemfile.lock"), + T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def gemspecs - return @gemspecs if defined?(@gemspecs) + return T.must(@gemspecs) if defined?(@gemspecs) gemspecs_paths = gemspec_directories @@ -83,11 +92,17 @@ def gemspecs .map { |f| File.join(d, f.name) } end - @gemspecs = gemspecs_paths.map { |n| fetch_file_from_host(n) } + @gemspecs ||= T.let( + gemspecs_paths.map do |n| + fetch_file_from_host(n) + end, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) rescue Octokit::NotFound [] end + sig { returns(T::Array[String]) } def gemspec_directories gemfiles = ([gemfile] + child_gemfiles).compact directories = @@ -98,18 +113,21 @@ def gemspec_directories directories.empty? ? ["."] : directories end + sig { returns(T.nilable(DependencyFile)) } def ruby_version_file return unless gemfile - @ruby_version_file ||= fetch_support_file(".ruby-version") + @ruby_version_file ||= T.let(fetch_support_file(".ruby-version"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(DependencyFile)) } def tool_versions_file return unless gemfile - @tool_versions_file ||= fetch_support_file(".tool-versions") + @tool_versions_file ||= T.let(fetch_support_file(".tool-versions"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Array[DependencyFile]) } def path_gemspecs gemspec_files = T.let([], T::Array[Dependabot::DependencyFile]) unfetchable_gems = [] @@ -141,21 +159,25 @@ def path_gemspecs gemspec_files end + sig { returns(T::Array[Pathname]) } def path_gemspec_paths fetch_path_gemspec_paths.map { |path| Pathname.new(path) } end - def require_relative_files(files) + sig { params(files: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) } + def find_included_files(files) ruby_files = files.select { |f| f.name.end_with?(".rb", "Gemfile", ".gemspec") } paths = ruby_files.flat_map do |file| - RequireRelativeFinder.new(file: file).require_relative_paths + IncludedPathFinder.new(file: file).find_included_paths end - @require_relative_files ||= + @find_included_files ||= T.let( paths.map { |path| fetch_file_from_host(path) } - .tap { |req_files| req_files.each { |f| f.support_file = true } } + .tap { |req_files| req_files.each { |f| f.support_file = true } }, + T.nilable(T::Array[DependencyFile]) + ) end sig { params(dir_path: T.any(String, Pathname)).returns(T::Array[DependencyFile]) } @@ -166,9 +188,10 @@ def fetch_gemspecs_from_directory(dir_path) .map { |fp| fetch_file_from_host(fp, fetch_submodules: true) } end + sig { returns(T::Array[String]) } def fetch_path_gemspec_paths if lockfile - parsed_lockfile = CachedLockfileParser.parse(sanitized_lockfile_content) + parsed_lockfile = CachedLockfileParser.parse(T.must(sanitized_lockfile_content)) parsed_lockfile.specs .select { |s| s.source.instance_of?(::Bundler::Source::Path) } .map { |s| s.source.path }.uniq @@ -179,26 +202,35 @@ def fetch_path_gemspec_paths end.uniq end rescue ::Bundler::LockfileError - raise Dependabot::DependencyFileNotParseable, lockfile.path + raise Dependabot::DependencyFileNotParseable, T.must(lockfile).path rescue ::Bundler::Plugin::UnknownSourceError # Quietly ignore plugin errors - we'll raise a better error during # parsing [] end + sig { returns(T::Array[DependencyFile]) } def child_gemfiles return [] unless gemfile - @child_gemfiles ||= - fetch_child_gemfiles(file: gemfile, previously_fetched_files: []) + @child_gemfiles ||= T.let( + fetch_child_gemfiles(file: T.must(gemfile), previously_fetched_files: []), + T.nilable(T::Array[DependencyFile]) + ) end # TODO: Stop sanitizing the lockfile once we have bundler 2 installed + + sig { returns T.nilable(String) } def sanitized_lockfile_content regex = FileUpdater::LockfileUpdater::LOCKFILE_ENDING - lockfile.content.gsub(regex, "") + lockfile&.content&.gsub(regex, "") end + sig do + params(file: DependencyFile, + previously_fetched_files: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) + end def fetch_child_gemfiles(file:, previously_fetched_files:) paths = ChildGemfileFinder.new(gemfile: file).child_gemfile_paths diff --git a/bundler/lib/dependabot/bundler/file_fetcher/included_path_finder.rb b/bundler/lib/dependabot/bundler/file_fetcher/included_path_finder.rb new file mode 100644 index 00000000000..41e62a4fdb9 --- /dev/null +++ b/bundler/lib/dependabot/bundler/file_fetcher/included_path_finder.rb @@ -0,0 +1,133 @@ +# typed: strict +# frozen_string_literal: true + +require "pathname" +require "parser/current" +require "dependabot/bundler/file_fetcher" +require "dependabot/errors" +require "sorbet-runtime" + +module Dependabot + module Bundler + class FileFetcher + # Finds the paths of any files included using `require_relative` and `eval` in the + # passed file. + class IncludedPathFinder + extend T::Sig + + sig { params(file: Dependabot::DependencyFile).void } + def initialize(file:) + @file = file + end + + sig { returns(T::Array[String]) } + def find_included_paths + ast = Parser::CurrentRuby.parse(file.content) + find_require_relative_paths(ast) + find_eval_paths(ast) + rescue Parser::SyntaxError + raise Dependabot::DependencyFileNotParseable, file.path + end + + private + + sig { returns(Dependabot::DependencyFile) } + attr_reader :file + + sig { params(node: T.untyped).returns(T::Array[String]) } + def find_require_relative_paths(node) + return [] unless node.is_a?(Parser::AST::Node) + + if declares_require_relative?(node) + return [] unless node.children[2].type == :str + + path = node.children[2].loc.expression.source.gsub(/['"]/, "") + path = File.join(current_dir, path) unless current_dir.nil? + path += ".rb" unless path.end_with?(".rb") + return [Pathname.new(path).cleanpath.to_path] + end + + node.children.flat_map do |child_node| + find_require_relative_paths(child_node) + end + end + + sig { params(node: T.untyped).returns(T::Array[String]) } + def find_eval_paths(node) + return [] unless node.is_a?(Parser::AST::Node) + + if declares_eval?(node) + eval_arg = node.children[2] + if eval_arg.is_a?(Parser::AST::Node) + file_read_node = find_file_read_node(eval_arg) + path = extract_path_from_file_read(file_read_node) if file_read_node + return [path] if path + end + end + + node.children.flat_map do |child_node| + find_eval_paths(child_node) + end + end + + sig { params(node: Parser::AST::Node).returns(T.nilable(Parser::AST::Node)) } + def find_file_read_node(node) + return nil unless node.is_a?(Parser::AST::Node) + + # Check if the node represents a method call (:send) + # and if the method name is :read + method_name = node.children[1] + receiver_node = node.children[0] + + if node.type == :send && method_name == :read && receiver_node.is_a?(Parser::AST::Node) + # Check if the receiver of the :read method call is :File + receiver_const = receiver_node.children[1] + return node if receiver_const == :File + end + + # Recursively search for a file read node in the children + node.children.each do |child| + next unless child.is_a?(Parser::AST::Node) + + result = find_file_read_node(child) + return result if result + end + + nil + end + + sig { params(node: Parser::AST::Node).returns(T.nilable(String)) } + def extract_path_from_file_read(node) + return nil unless node.is_a?(Parser::AST::Node) + + expand_path_node = node.children[2] + if expand_path_node.type == :send && expand_path_node.children[1] == :expand_path + path_node = expand_path_node.children[2] + return path_node.loc.expression.source.gsub(/['"]/, "") if path_node.type == :str + end + nil + end + + sig { returns(T.nilable(String)) } + def current_dir + @current_dir ||= T.let(file.name.rpartition("/").first, T.nilable(String)) + @current_dir = nil if @current_dir == "" + @current_dir + end + + sig { params(node: Parser::AST::Node).returns(T::Boolean) } + def declares_require_relative?(node) + return false unless node.is_a?(Parser::AST::Node) + + node.children[1] == :require_relative + end + + sig { params(node: Parser::AST::Node).returns(T::Boolean) } + def declares_eval?(node) + return false unless node.is_a?(Parser::AST::Node) + + node.children[1] == :eval + end + end + end + end +end diff --git a/bundler/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb b/bundler/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb index 8b5fae0d667..b2b3ba04e92 100644 --- a/bundler/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb +++ b/bundler/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb @@ -1,10 +1,11 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "pathname" require "parser/current" require "dependabot/bundler/file_fetcher" require "dependabot/errors" +require "sorbet-runtime" module Dependabot module Bundler @@ -12,36 +13,42 @@ class FileFetcher # Finds the paths of any gemspecs declared using `path: ` in the # passed Gemfile. class PathGemspecFinder + extend T::Sig + + sig { params(gemfile: Dependabot::DependencyFile).void } def initialize(gemfile:) @gemfile = gemfile end + sig { returns(T::Array[String]) } def path_gemspec_paths - ast = Parser::CurrentRuby.parse(gemfile.content) + ast = Parser::CurrentRuby.parse(gemfile&.content) find_path_gemspec_paths(ast) rescue Parser::SyntaxError - raise Dependabot::DependencyFileNotParseable, gemfile.path + raise Dependabot::DependencyFileNotParseable, T.must(gemfile).path end private + sig { returns(T.nilable(Dependabot::DependencyFile)) } attr_reader :gemfile + sig { params(node: T.untyped).returns(T::Array[T.untyped]) } def find_path_gemspec_paths(node) return [] unless node.is_a?(Parser::AST::Node) if declares_path_dependency?(node) path_node = path_node_for_gem_declaration(node) - unless path_node.type == :str - path = gemfile.path + unless path_node&.type == :str + path = gemfile&.path msg = "Dependabot only supports uninterpolated string arguments " \ "for path dependencies. Got " \ - "`#{path_node.loc.expression.source}`" - raise Dependabot::DependencyFileNotParseable.new(path, msg) + "`#{path_node&.loc&.expression&.source}`" + raise Dependabot::DependencyFileNotParseable.new(T.must(path), msg) end - path = path_node.loc.expression.source.gsub(/['"]/, "") + path = T.must(path_node).loc.expression.source.gsub(/['"]/, "") return [clean_path(path)] end @@ -50,12 +57,14 @@ def find_path_gemspec_paths(node) end end + sig { returns(T.nilable(String)) } def current_dir - @current_dir ||= gemfile.name.rpartition("/").first + @current_dir ||= T.let(gemfile&.name&.rpartition("/")&.first, T.nilable(String)) @current_dir = nil if @current_dir == "" @current_dir end + sig { params(node: Parser::AST::Node).returns(T::Boolean) } def declares_path_dependency?(node) return false unless node.is_a?(Parser::AST::Node) return false unless node.children[1] == :gem @@ -63,6 +72,7 @@ def declares_path_dependency?(node) !path_node_for_gem_declaration(node).nil? end + sig { params(path: String).returns(Pathname) } def clean_path(path) if Pathname.new(path).absolute? base_path = Pathname.new(File.expand_path(Dir.pwd)) @@ -72,6 +82,7 @@ def clean_path(path) Pathname.new(path).cleanpath end + sig { params(node: Parser::AST::Node).returns(T.nilable(Parser::AST::Node)) } def path_node_for_gem_declaration(node) return unless node.children.last.type == :hash @@ -86,6 +97,7 @@ def path_node_for_gem_declaration(node) path_hash_pair.children.last end + sig { params(node: Parser::AST::Node).returns(Symbol) } def key_from_hash_pair(node) node.children.first.children.first.to_sym end diff --git a/bundler/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb b/bundler/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb deleted file mode 100644 index cd31d72f2f4..00000000000 --- a/bundler/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +++ /dev/null @@ -1,70 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "pathname" -require "parser/current" -require "dependabot/bundler/file_fetcher" -require "dependabot/errors" -require "sorbet-runtime" - -module Dependabot - module Bundler - class FileFetcher - # Finds the paths of any files included using `require_relative` in the - # passed file. - class RequireRelativeFinder - extend T::Sig - - sig { params(file: Dependabot::DependencyFile).void } - def initialize(file:) - @file = file - end - - sig { returns(T::Array[String]) } - def require_relative_paths - ast = Parser::CurrentRuby.parse(file.content) - find_require_relative_paths(ast) - rescue Parser::SyntaxError - raise Dependabot::DependencyFileNotParseable, file.path - end - - private - - sig { returns(Dependabot::DependencyFile) } - attr_reader :file - - sig { params(node: T.untyped).returns(T::Array[T.untyped]) } - def find_require_relative_paths(node) - return [] unless node.is_a?(Parser::AST::Node) - - if declares_require_relative?(node) - return [] unless node.children[2].type == :str - - path = node.children[2].loc.expression.source.gsub(/['"]/, "") - path = File.join(current_dir, path) unless current_dir.nil? - path += ".rb" unless path.end_with?(".rb") - return [Pathname.new(path).cleanpath.to_path] - end - - node.children.flat_map do |child_node| - find_require_relative_paths(child_node) - end - end - - sig { returns(T.nilable(String)) } - def current_dir - @current_dir ||= T.let(file.name.rpartition("/").first, T.nilable(String)) - @current_dir = nil if @current_dir == "" - @current_dir - end - - sig { params(node: Parser::AST::Node).returns(T::Boolean) } - def declares_require_relative?(node) - return false unless node.is_a?(Parser::AST::Node) - - node.children[1] == :require_relative - end - end - end - end -end diff --git a/bundler/lib/dependabot/bundler/file_updater/ruby_requirement_setter.rb b/bundler/lib/dependabot/bundler/file_updater/ruby_requirement_setter.rb index 6eabb29a129..87f5376064f 100644 --- a/bundler/lib/dependabot/bundler/file_updater/ruby_requirement_setter.rb +++ b/bundler/lib/dependabot/bundler/file_updater/ruby_requirement_setter.rb @@ -12,7 +12,7 @@ class RubyRequirementSetter class RubyVersionNotFound < StandardError; end RUBY_VERSIONS = %w( - 1.8.7 1.9.3 2.0.0 2.1.10 2.2.10 2.3.8 2.4.10 2.5.9 2.6.9 2.7.6 3.0.6 3.1.4 3.2.2 3.3.1 + 1.8.7 1.9.3 2.0.0 2.1.10 2.2.10 2.3.8 2.4.10 2.5.9 2.6.9 2.7.6 3.0.6 3.1.4 3.2.2 3.3.3 ).freeze attr_reader :gemspec diff --git a/bundler/lib/dependabot/bundler/requirement.rb b/bundler/lib/dependabot/bundler/requirement.rb index bed14614ff1..dbcb5b2ae75 100644 --- a/bundler/lib/dependabot/bundler/requirement.rb +++ b/bundler/lib/dependabot/bundler/requirement.rb @@ -11,6 +11,11 @@ module Bundler class Requirement < Dependabot::Requirement extend T::Sig + sig { params(req: T::Hash[Symbol, String], version: Gem::Version).returns(T::Boolean) } + def self.satisfied_by?(req, version) + new(req[:requirement]).satisfied_by?(version) + end + # For consistency with other languages, we define a requirements array. # Ruby doesn't have an `OR` separator for requirements, so it always # contains a single element. diff --git a/bundler/lib/dependabot/bundler/update_checker.rb b/bundler/lib/dependabot/bundler/update_checker.rb index 9dd3b57df72..3c1b03eae33 100644 --- a/bundler/lib/dependabot/bundler/update_checker.rb +++ b/bundler/lib/dependabot/bundler/update_checker.rb @@ -42,9 +42,9 @@ def lowest_resolvable_security_fix_version lowest_fix = latest_version_finder(remove_git_source: false) .lowest_security_fix_version - return unless lowest_fix + return unless lowest_fix && resolvable?(lowest_fix) - resolvable?(lowest_fix) ? lowest_fix : latest_resolvable_version + lowest_fix end def latest_resolvable_version_with_no_unlock @@ -77,7 +77,7 @@ def updated_requirements def requirements_unlocked_or_can_be? return true if requirements_unlocked? - return false if requirements_update_strategy == RequirementsUpdateStrategy::LockfileOnly + return false if requirements_update_strategy.lockfile_only? dependency.specific_requirements .all? do |req| diff --git a/bundler/lib/dependabot/bundler/update_checker/force_updater.rb b/bundler/lib/dependabot/bundler/update_checker/force_updater.rb index b957dd892df..ac562921913 100644 --- a/bundler/lib/dependabot/bundler/update_checker/force_updater.rb +++ b/bundler/lib/dependabot/bundler/update_checker/force_updater.rb @@ -51,6 +51,13 @@ def update_multiple_dependencies? end def force_update + requirement = dependency.requirements.find { |req| req[:file] == gemfile.name } + manifest_requirement_not_satisfied = requirement && !Requirement.satisfied_by?(requirement, target_version) + + if manifest_requirement_not_satisfied && requirements_update_strategy.lockfile_only? + raise Dependabot::DependencyFileNotResolvable + end + in_a_native_bundler_context(error_handling: false) do |tmp_dir| updated_deps, specs = NativeHelpers.run_bundler_subprocess( bundler_version: bundler_version, @@ -67,10 +74,10 @@ def force_update } ) dependencies_from(updated_deps, specs) + rescue SharedHelpers::HelperSubprocessFailed => e + msg = e.error_class + " with message: " + e.message + raise Dependabot::DependencyFileNotResolvable, msg end - rescue SharedHelpers::HelperSubprocessFailed => e - msg = e.error_class + " with message: " + e.message - raise Dependabot::DependencyFileNotResolvable, msg end def original_dependencies diff --git a/bundler/lib/dependabot/bundler/update_checker/requirements_updater.rb b/bundler/lib/dependabot/bundler/update_checker/requirements_updater.rb index 15cffac0f50..1ce6e59cdd3 100644 --- a/bundler/lib/dependabot/bundler/update_checker/requirements_updater.rb +++ b/bundler/lib/dependabot/bundler/update_checker/requirements_updater.rb @@ -39,7 +39,7 @@ def initialize(requirements:, update_strategy:, updated_source:, end def updated_requirements - return requirements if update_strategy == RequirementsUpdateStrategy::LockfileOnly + return requirements if update_strategy.lockfile_only? requirements.map do |req| if req[:file].include?(".gemspec") @@ -102,8 +102,7 @@ def update_version_requirement(req) end def new_version_satisfies?(req) - original_req = Gem::Requirement.new(req[:requirement].split(",")) - original_req.satisfied_by?(latest_resolvable_version) + Requirement.satisfied_by?(req, latest_resolvable_version) end def update_gemfile_range(requirements) diff --git a/bundler/spec/dependabot/bundler/file_fetcher/require_relative_finder_spec.rb b/bundler/spec/dependabot/bundler/file_fetcher/included_path_finder_spec.rb similarity index 69% rename from bundler/spec/dependabot/bundler/file_fetcher/require_relative_finder_spec.rb rename to bundler/spec/dependabot/bundler/file_fetcher/included_path_finder_spec.rb index 8a2d2c47d8d..500ab6c3629 100644 --- a/bundler/spec/dependabot/bundler/file_fetcher/require_relative_finder_spec.rb +++ b/bundler/spec/dependabot/bundler/file_fetcher/included_path_finder_spec.rb @@ -4,9 +4,9 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" -require "dependabot/bundler/file_fetcher/require_relative_finder" +require "dependabot/bundler/file_fetcher/included_path_finder" -RSpec.describe Dependabot::Bundler::FileFetcher::RequireRelativeFinder do +RSpec.describe Dependabot::Bundler::FileFetcher::IncludedPathFinder do let(:finder) { described_class.new(file: file) } let(:file) do @@ -14,8 +14,8 @@ end let(:file_name) { "Gemfile" } - describe "#require_relative_paths" do - subject(:require_relative_paths) { finder.require_relative_paths } + describe "#find_included_paths" do + subject(:find_included_paths) { finder.find_included_paths } context "when the file does not include any relative paths" do let(:file_body) { bundler_project_dependency_file("gemfile", filename: "Gemfile").content } @@ -28,7 +28,7 @@ it "raises a helpful error" do suppress_output do - expect { finder.require_relative_paths }.to raise_error do |error| + expect { finder.find_included_paths }.to raise_error do |error| expect(error).to be_a(Dependabot::DependencyFileNotParseable) expect(error.file_name).to eq("Gemfile") end @@ -36,7 +36,7 @@ end end - context "when the file does include a relative path" do + context "when the file includes a require_relative path" do let(:file_body) do bundler_project_dependency_file("includes_require_relative_gemfile", filename: "nested/Gemfile").content end @@ -68,11 +68,23 @@ it { is_expected.to eq([]) } end # rubocop:enable Lint/InterpolationCheck + end - context "when dealing with a file that is already nested" do - let(:file_name) { "deeply/nested/Gemfile" } + context "when the file includes an eval statement" do + context "with File.read" do + let(:file_body) do + 'eval File.read(File.expand_path("some_other_file.rb", __dir__))' + end + + it { is_expected.to eq(["some_other_file.rb"]) } + end - it { is_expected.to eq(["deeply/some_other_file.rb"]) } + context "when the eval does not read a file" do + let(:file_body) do + 'eval "puts \'Hello, world!\'"' + end + + it { is_expected.to eq([]) } end end end diff --git a/bundler/spec/dependabot/bundler/file_updater/ruby_requirement_setter_spec.rb b/bundler/spec/dependabot/bundler/file_updater/ruby_requirement_setter_spec.rb index 72e492427df..2edffb8f4ca 100644 --- a/bundler/spec/dependabot/bundler/file_updater/ruby_requirement_setter_spec.rb +++ b/bundler/spec/dependabot/bundler/file_updater/ruby_requirement_setter_spec.rb @@ -143,7 +143,7 @@ bundler_project_dependency_file("gemfile", filename: "Gemfile").content end - it { is_expected.to include("ruby '3.3.1'\n") } + it { is_expected.to include("ruby '3.3.3'\n") } it { is_expected.to include(%(gem "business", "~> 1.4.0")) } end diff --git a/bundler/spec/dependabot/bundler/update_checker/force_updater_spec.rb b/bundler/spec/dependabot/bundler/update_checker/force_updater_spec.rb index 618b52ae8ea..0ca94d6b208 100644 --- a/bundler/spec/dependabot/bundler/update_checker/force_updater_spec.rb +++ b/bundler/spec/dependabot/bundler/update_checker/force_updater_spec.rb @@ -16,7 +16,7 @@ described_class.new( dependency: dependency, dependency_files: dependency_files, - target_version: target_version, + target_version: Gem::Version.new(target_version), requirements_update_strategy: update_strategy, credentials: [{ "type" => "git_source", @@ -328,5 +328,25 @@ .to raise_error(Dependabot::DependencyFileNotResolvable) end end + + context "when lockfile_only strategy is used and manifest would need updates" do + let(:update_strategy) { Dependabot::RequirementsUpdateStrategy::LockfileOnly } + let(:dependency_files) { bundler_project_dependency_files("lockfile_only_and_forced_updates") } + let(:target_version) { "4.0.0.beta7" } + let(:dependency_name) { "activeadmin" } + let(:requirements) do + [{ + file: "Gemfile", + requirement: "4.0.0.beta6", + groups: [:default], + source: nil + }] + end + + it "raises a resolvability error" do + expect { updater.updated_dependencies } + .to raise_error(Dependabot::DependencyFileNotResolvable) + end + end end end diff --git a/bundler/spec/dependabot/bundler/update_checker_spec.rb b/bundler/spec/dependabot/bundler/update_checker_spec.rb index 530571f84b8..83352a9e32d 100644 --- a/bundler/spec/dependabot/bundler/update_checker_spec.rb +++ b/bundler/spec/dependabot/bundler/update_checker_spec.rb @@ -420,9 +420,7 @@ context "with a latest version" do before do - allow(checker) - .to receive(:latest_version) - .and_return(target_version) + allow(checker).to receive(:latest_version).and_return(Gem::Version.new(target_version)) end context "when the force updater raises" do @@ -463,7 +461,7 @@ context "with a latest version" do before do - allow(checker).to receive(:latest_version).and_return(target_version) + allow(checker).to receive(:latest_version).and_return(Gem::Version.new(target_version)) end context "when the force updater succeeds" do diff --git a/bundler/spec/fixtures/projects/bundler1/gemfile_require_ruby_3_3/example.gemspec b/bundler/spec/fixtures/projects/bundler1/gemfile_require_ruby_3_3/example.gemspec index 306eefbba82..76e0ab9ba74 100644 --- a/bundler/spec/fixtures/projects/bundler1/gemfile_require_ruby_3_3/example.gemspec +++ b/bundler/spec/fixtures/projects/bundler1/gemfile_require_ruby_3_3/example.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.files = Dir["CHANGELOG.md", "LICENSE.txt", "README.md", "lib/**/*", "helpers/**/*"] - spec.required_ruby_version = ">= 3.3.1" + spec.required_ruby_version = ">= 3.3.3" spec.required_rubygems_version = ">= 2.6.11" spec.add_dependency 'business', '~> 1.0' diff --git a/bundler/spec/fixtures/projects/bundler1/lockfile_only_and_forced_updates/Gemfile b/bundler/spec/fixtures/projects/bundler1/lockfile_only_and_forced_updates/Gemfile new file mode 100644 index 00000000000..af3a7d5a318 --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler1/lockfile_only_and_forced_updates/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +gem 'activeadmin', '4.0.0.beta6' diff --git a/bundler/spec/fixtures/projects/bundler1/lockfile_only_and_forced_updates/Gemfile.lock b/bundler/spec/fixtures/projects/bundler1/lockfile_only_and_forced_updates/Gemfile.lock new file mode 100644 index 00000000000..914e7506a2c --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler1/lockfile_only_and_forced_updates/Gemfile.lock @@ -0,0 +1,147 @@ +GEM + remote: https://rubygems.org/ + specs: + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activeadmin (4.0.0.beta6) + arbre (~> 2.0) + csv + formtastic (>= 3.1) + formtastic_i18n (>= 0.4) + inherited_resources (~> 1.7) + kaminari (>= 1.2.1) + railties (>= 6.1) + ransack (>= 4.0) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activesupport (7.1.3.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + arbre (2.0.0) + activesupport (>= 3.0.0) + ruby2_keywords (>= 0.0.2) + base64 (0.2.0) + bigdecimal (3.1.8) + builder (3.3.0) + concurrent-ruby (1.3.1) + connection_pool (2.4.1) + crass (1.0.6) + csv (3.3.0) + drb (2.2.1) + erubi (1.12.0) + formtastic (5.0.0) + actionpack (>= 6.0.0) + formtastic_i18n (0.7.0) + has_scope (0.8.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + inherited_resources (1.14.0) + actionpack (>= 6.0) + has_scope (>= 0.6) + railties (>= 6.0) + responders (>= 2) + io-console (0.7.2) + irb (1.13.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + minitest (5.23.1) + mutex_m (0.2.0) + nokogiri (1.16.5-arm64-darwin) + racc (~> 1.4) + psych (5.1.2) + stringio + racc (1.8.0) + rack (3.0.11) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.2.1) + ransack (4.1.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n + rdoc (6.7.0) + psych (>= 4.0.0) + reline (0.5.8) + io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + ruby2_keywords (0.0.5) + stringio (3.1.0) + thor (1.3.1) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + webrick (1.8.1) + zeitwerk (2.6.15) + +PLATFORMS + ruby + +DEPENDENCIES + activeadmin (= 4.0.0.beta6) + +BUNDLED WITH + 1.17.3 diff --git a/bundler/spec/fixtures/projects/bundler2/gemfile_require_ruby_3_3/example.gemspec b/bundler/spec/fixtures/projects/bundler2/gemfile_require_ruby_3_3/example.gemspec index 0d53a8f9226..47dcde20f90 100644 --- a/bundler/spec/fixtures/projects/bundler2/gemfile_require_ruby_3_3/example.gemspec +++ b/bundler/spec/fixtures/projects/bundler2/gemfile_require_ruby_3_3/example.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |spec| spec.files = Dir["CHANGELOG.md", "LICENSE.txt", "README.md", "lib/**/*", "helpers/**/*"] - spec.required_ruby_version = ">= 3.3.1" + spec.required_ruby_version = ">= 3.3.3" spec.required_rubygems_version = ">= 3.5.3" spec.add_dependency 'business', '~> 1.0' diff --git a/bundler/spec/fixtures/projects/bundler2/lockfile_only_and_forced_updates/Gemfile b/bundler/spec/fixtures/projects/bundler2/lockfile_only_and_forced_updates/Gemfile new file mode 100644 index 00000000000..af3a7d5a318 --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler2/lockfile_only_and_forced_updates/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' +git_source(:github) { |repo| "https://github.com/#{repo}.git" } + +gem 'activeadmin', '4.0.0.beta6' diff --git a/bundler/spec/fixtures/projects/bundler2/lockfile_only_and_forced_updates/Gemfile.lock b/bundler/spec/fixtures/projects/bundler2/lockfile_only_and_forced_updates/Gemfile.lock new file mode 100644 index 00000000000..a45818f761f --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler2/lockfile_only_and_forced_updates/Gemfile.lock @@ -0,0 +1,147 @@ +GEM + remote: https://rubygems.org/ + specs: + actionpack (7.1.3.4) + actionview (= 7.1.3.4) + activesupport (= 7.1.3.4) + nokogiri (>= 1.8.5) + racc + rack (>= 2.2.4) + rack-session (>= 1.0.1) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + actionview (7.1.3.4) + activesupport (= 7.1.3.4) + builder (~> 3.1) + erubi (~> 1.11) + rails-dom-testing (~> 2.2) + rails-html-sanitizer (~> 1.6) + activeadmin (4.0.0.beta6) + arbre (~> 2.0) + csv + formtastic (>= 3.1) + formtastic_i18n (>= 0.4) + inherited_resources (~> 1.7) + kaminari (>= 1.2.1) + railties (>= 6.1) + ransack (>= 4.0) + activemodel (7.1.3.4) + activesupport (= 7.1.3.4) + activerecord (7.1.3.4) + activemodel (= 7.1.3.4) + activesupport (= 7.1.3.4) + timeout (>= 0.4.0) + activesupport (7.1.3.4) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + arbre (2.0.0) + activesupport (>= 3.0.0) + ruby2_keywords (>= 0.0.2) + base64 (0.2.0) + bigdecimal (3.1.8) + builder (3.3.0) + concurrent-ruby (1.3.1) + connection_pool (2.4.1) + crass (1.0.6) + csv (3.3.0) + drb (2.2.1) + erubi (1.12.0) + formtastic (5.0.0) + actionpack (>= 6.0.0) + formtastic_i18n (0.7.0) + has_scope (0.8.2) + actionpack (>= 5.2) + activesupport (>= 5.2) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + inherited_resources (1.14.0) + actionpack (>= 6.0) + has_scope (>= 0.6) + railties (>= 6.0) + responders (>= 2) + io-console (0.7.2) + irb (1.13.1) + rdoc (>= 4.0.0) + reline (>= 0.4.2) + kaminari (1.2.2) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.2) + kaminari-activerecord (= 1.2.2) + kaminari-core (= 1.2.2) + kaminari-actionview (1.2.2) + actionview + kaminari-core (= 1.2.2) + kaminari-activerecord (1.2.2) + activerecord + kaminari-core (= 1.2.2) + kaminari-core (1.2.2) + loofah (2.22.0) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + minitest (5.23.1) + mutex_m (0.2.0) + nokogiri (1.16.5-arm64-darwin) + racc (~> 1.4) + psych (5.1.2) + stringio + racc (1.8.0) + rack (3.0.11) + rack-session (2.0.0) + rack (>= 3.0.0) + rack-test (2.1.0) + rack (>= 1.3) + rackup (2.1.0) + rack (>= 3) + webrick (~> 1.8) + rails-dom-testing (2.2.0) + activesupport (>= 5.0.0) + minitest + nokogiri (>= 1.6) + rails-html-sanitizer (1.6.0) + loofah (~> 2.21) + nokogiri (~> 1.14) + railties (7.1.3.4) + actionpack (= 7.1.3.4) + activesupport (= 7.1.3.4) + irb + rackup (>= 1.0.0) + rake (>= 12.2) + thor (~> 1.0, >= 1.2.2) + zeitwerk (~> 2.6) + rake (13.2.1) + ransack (4.1.1) + activerecord (>= 6.1.5) + activesupport (>= 6.1.5) + i18n + rdoc (6.7.0) + psych (>= 4.0.0) + reline (0.5.8) + io-console (~> 0.5) + responders (3.1.1) + actionpack (>= 5.2) + railties (>= 5.2) + ruby2_keywords (0.0.5) + stringio (3.1.0) + thor (1.3.1) + timeout (0.4.1) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + webrick (1.8.1) + zeitwerk (2.6.15) + +PLATFORMS + arm64-darwin-23 + +DEPENDENCIES + activeadmin (= 4.0.0.beta6) + +BUNDLED WITH + 2.5.11 diff --git a/cargo/.rubocop.yml b/cargo/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/cargo/.rubocop.yml +++ b/cargo/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/cargo/Dockerfile b/cargo/Dockerfile index c799ede70bd..97b4f2d3183 100644 --- a/cargo/Dockerfile +++ b/cargo/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.78.0-bookworm as rust +FROM docker.io/library/rust:1.79.0-bookworm as rust FROM ghcr.io/dependabot/dependabot-updater-core diff --git a/cargo/lib/dependabot/cargo/update_checker.rb b/cargo/lib/dependabot/cargo/update_checker.rb index f05684c9b10..98f78e6b6c4 100644 --- a/cargo/lib/dependabot/cargo/update_checker.rb +++ b/cargo/lib/dependabot/cargo/update_checker.rb @@ -78,7 +78,7 @@ def updated_requirements end def requirements_unlocked_or_can_be? - requirements_update_strategy != RequirementsUpdateStrategy::LockfileOnly + !requirements_update_strategy.lockfile_only? end def requirements_update_strategy diff --git a/cargo/lib/dependabot/cargo/update_checker/requirements_updater.rb b/cargo/lib/dependabot/cargo/update_checker/requirements_updater.rb index 2c08acd1313..f13ec26d992 100644 --- a/cargo/lib/dependabot/cargo/update_checker/requirements_updater.rb +++ b/cargo/lib/dependabot/cargo/update_checker/requirements_updater.rb @@ -46,7 +46,7 @@ def initialize(requirements:, updated_source:, update_strategy:, end def updated_requirements - return requirements if update_strategy == RequirementsUpdateStrategy::LockfileOnly + return requirements if update_strategy.lockfile_only? # NOTE: Order is important here. The FileUpdater needs the updated # requirement at index `i` to correspond to the previous requirement @@ -142,7 +142,7 @@ def update_range_requirements(string_reqs) req.sub(VERSION_REGEX) do |old_version| if req.start_with?("<=") - target_version + update_version_string(old_version) else update_greatest_version(old_version, target_version) end diff --git a/cargo/spec/dependabot/cargo/update_checker/requirements_updater_spec.rb b/cargo/spec/dependabot/cargo/update_checker/requirements_updater_spec.rb index 9b534db8161..fd5dc5ff386 100644 --- a/cargo/spec/dependabot/cargo/update_checker/requirements_updater_spec.rb +++ b/cargo/spec/dependabot/cargo/update_checker/requirements_updater_spec.rb @@ -184,6 +184,12 @@ its([:requirement]) { is_expected.to eq(">=1.2.0, <1.6.0") } end + + context "when patch version is missing" do + let(:req_string) { "> 1.0, < 1.2" } + + its([:requirement]) { is_expected.to eq("> 1.0, < 1.6") } + end end context "with `less than equal`" do @@ -202,6 +208,12 @@ its([:requirement]) { is_expected.to eq(">=1.2.0, <=1.5.0") } end + + context "when patch version is missing" do + let(:req_string) { "> 1.0, <= 1.2" } + + its([:requirement]) { is_expected.to eq("> 1.0, <= 1.5") } + end end end diff --git a/common/lib/dependabot.rb b/common/lib/dependabot.rb index 573023fe55c..21b50aba5ba 100644 --- a/common/lib/dependabot.rb +++ b/common/lib/dependabot.rb @@ -2,5 +2,5 @@ # frozen_string_literal: true module Dependabot - VERSION = "0.262.0" + VERSION = "0.264.0" end diff --git a/common/lib/dependabot/errors.rb b/common/lib/dependabot/errors.rb index e7179ae57d6..4f6b0e968c2 100644 --- a/common/lib/dependabot/errors.rb +++ b/common/lib/dependabot/errors.rb @@ -17,6 +17,7 @@ module ErrorAttributes DEPENDENCY_GROUPS = "job-dependency-groups" JOB_ID = "job-id" PACKAGE_MANAGER = "package-manager" + SECURITY_UPDATE = "security-update" end # rubocop:disable Metrics/MethodLength @@ -191,6 +192,11 @@ def self.updater_error_details(error) "error-type": "private_source_authentication_failure", "error-detail": { source: error.source } } + when Dependabot::DependencyNotFound + { + "error-type": "dependency_not_found", + "error-detail": { source: error.source } + } when Dependabot::PrivateSourceTimedOut { "error-type": "private_source_timed_out", @@ -511,6 +517,20 @@ def initialize(environment_variable) end end + class DependencyNotFound < DependabotError + extend T::Sig + + sig { returns(String) } + attr_reader :source + + sig { params(source: T.nilable(String)).void } + def initialize(source) + @source = T.let(sanitize_source(T.must(source)), String) + msg = "The following dependency could not be found : #{@source}" + super(msg) + end + end + # Useful for JS file updaters, where the registry API sometimes returns # different results to the actual update process class InconsistentRegistryResponse < DependabotError; end diff --git a/common/lib/dependabot/file_fetchers/base.rb b/common/lib/dependabot/file_fetchers/base.rb index d714a0400d3..dadf75083a3 100644 --- a/common/lib/dependabot/file_fetchers/base.rb +++ b/common/lib/dependabot/file_fetchers/base.rb @@ -93,12 +93,13 @@ def self.required_files_message # # options supports custom feature enablement sig do - params( - source: Dependabot::Source, - credentials: T::Array[Dependabot::Credential], - repo_contents_path: T.nilable(String), - options: T::Hash[String, String] - ) + overridable + .params( + source: Dependabot::Source, + credentials: T::Array[Dependabot::Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[String, String] + ) .void end def initialize(source:, credentials:, repo_contents_path: nil, options: {}) diff --git a/common/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb b/common/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb index e89c29bc254..e42db819086 100644 --- a/common/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb +++ b/common/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb @@ -25,6 +25,10 @@ class LinkAndMentionSanitizer TEAM_MENTION_REGEX = %r{(?#{GITHUB_USERNAME})/(?#{GITHUB_USERNAME})/?} # End of string EOS_REGEX = /\z/ + + # regex to match markdown headers or links + MARKDOWN_REGEX = /\[(.+?)\]\(([^)]+)\)|\[(.+?)\]|\A#+\s+([^\s].*)/ + COMMONMARKER_OPTIONS = T.let( %i(GITHUB_PRE_LANG FULL_INFO_STRING).freeze, T::Array[Symbol] @@ -53,10 +57,16 @@ def sanitize_links_and_mentions(text:, unsafe: false, format_html: true) sanitize_links(doc) sanitize_nwo_text(doc) + render_options = if text.match?(MARKDOWN_REGEX) + COMMONMARKER_OPTIONS + else + COMMONMARKER_OPTIONS + [:HARDBREAKS] + end + mode = unsafe ? :UNSAFE : :DEFAULT - return doc.to_commonmark([mode] + COMMONMARKER_OPTIONS) unless format_html + return doc.to_commonmark([mode] + render_options) unless format_html - doc.to_html(([mode] + COMMONMARKER_OPTIONS), COMMONMARKER_EXTENSIONS) + doc.to_html(([mode] + render_options), COMMONMARKER_EXTENSIONS) end private diff --git a/common/lib/dependabot/requirements_update_strategy.rb b/common/lib/dependabot/requirements_update_strategy.rb index 248aea620fd..efb60799bff 100644 --- a/common/lib/dependabot/requirements_update_strategy.rb +++ b/common/lib/dependabot/requirements_update_strategy.rb @@ -9,5 +9,12 @@ class RequirementsUpdateStrategy < T::Enum LockfileOnly = new("lockfile_only") WidenRanges = new("widen_ranges") end + + extend T::Sig + + sig { returns(T::Boolean) } + def lockfile_only? + self == LockfileOnly + end end end diff --git a/common/lib/dependabot/shared_helpers.rb b/common/lib/dependabot/shared_helpers.rb index e57a3ccf5bd..b8033801039 100644 --- a/common/lib/dependabot/shared_helpers.rb +++ b/common/lib/dependabot/shared_helpers.rb @@ -131,7 +131,7 @@ def self.escape_command(command) params( command: String, function: String, - args: T.any(T::Array[String], T::Hash[Symbol, String]), + args: T.any(T::Array[T.any(String, T::Array[T::Hash[String, T.untyped]])], T::Hash[Symbol, String]), env: T.nilable(T::Hash[String, String]), stderr_to_stdout: T::Boolean, allow_unsafe_shell_command: T::Boolean @@ -258,13 +258,16 @@ def self.with_git_configured(credentials:, &_block) FileUtils.mkdir_p(Utils::BUMP_TMP_DIR_PATH) previous_config = ENV.fetch("GIT_CONFIG_GLOBAL", nil) + previous_terminal_prompt = ENV.fetch("GIT_TERMINAL_PROMPT", nil) begin ENV["GIT_CONFIG_GLOBAL"] = GIT_CONFIG_GLOBAL_PATH + ENV["GIT_TERMINAL_PROMPT"] = "false" configure_git_to_use_https_with_credentials(credentials, safe_directories) yield ensure ENV["GIT_CONFIG_GLOBAL"] = previous_config + ENV["GIT_TERMINAL_PROMPT"] = previous_terminal_prompt end rescue Errno::ENOSPC => e raise Dependabot::OutOfDisk, e.message diff --git a/common/spec/dependabot/clients/azure_spec.rb b/common/spec/dependabot/clients/azure_spec.rb index 73dbdedf7ee..815baf60448 100644 --- a/common/spec/dependabot/clients/azure_spec.rb +++ b/common/spec/dependabot/clients/azure_spec.rb @@ -441,26 +441,24 @@ end context "when dealing with POST" do - before do - @request_body = "request body" - end + let(:request_body) { "request body" } it "with failure count <= max_retries" do # Request succeeds on third attempt stub_request(:post, base_url) - .with(basic_auth: [username, password], body: @request_body) + .with(basic_auth: [username, password], body: request_body) .to_return({ status: 503 }, { status: 503 }, { status: 200 }) - response = client.post(base_url, @request_body) + response = client.post(base_url, request_body) expect(response.status).to eq(200) end it "with failure count > max_retries raises an error" do stub_request(:post, base_url) - .with(basic_auth: [username, password], body: @request_body) + .with(basic_auth: [username, password], body: request_body) .to_return({ status: 503 }, { status: 503 }, { status: 503 }, { status: 503 }) - expect { client.post(base_url, @request_body) } + expect { client.post(base_url, request_body) } .to raise_error(Dependabot::Clients::Azure::ServiceNotAvailable) end end diff --git a/common/spec/dependabot/dependabot_error_spec.rb b/common/spec/dependabot/dependabot_error_spec.rb new file mode 100644 index 00000000000..43e2fe9290a --- /dev/null +++ b/common/spec/dependabot/dependabot_error_spec.rb @@ -0,0 +1,221 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/errors" + +RSpec.describe Dependabot::DependabotError do + let(:error) { described_class.new(message) } + let(:message) do + "some error" + end + + describe "#message" do + subject(:error_message) { error.message } + + let(:tmp) { Dependabot::Utils::BUMP_TMP_DIR_PATH } + + it { is_expected.to eq("some error") } + + context "with dependabot temp path" do + let(:message) do + "#{tmp}/dependabot_20201218-14100-y0d218/path error" + end + + it { is_expected.to eq("dependabot_tmp_dir/path error") } + end + + context "with dependabot temp path" do + let(:message) do + "Error (/Users/x/code/dependabot-core/cargo/#{tmp}/dependabot_20201218-14100-y0d218) " \ + "failed to load https://github.com/dependabot" + end + + it do + expect(error_message).to eq( + "Error (/Users/x/code/dependabot-core/cargo/dependabot_tmp_dir) " \ + "failed to load https://github.com/dependabot" + ) + end + end + + context "without http basic auth token" do + let(:message) do + "git://user@github.com error" + end + + it { is_expected.to eq("git://user@github.com error") } + end + + context "with http basic auth missing username" do + let(:message) do + "git://:token@github.com error" + end + + it { is_expected.to eq("git://github.com error") } + end + + context "with http basic auth" do + let(:message) do + "git://user:token@github.com error" + end + + it { is_expected.to eq("git://github.com error") } + end + + context "with escaped basic auth uri" do + let(:message) do + "git://user:token%40github.com error" + end + + it { is_expected.to eq("git://github.com error") } + end + + context "with multiple uri's include @ in their fragment, but no auth" do + let(:message) do + <<~MESSAGE.strip + https://github.com/EspressoSystems/tide-disco.git#tide-disco@0.4.1 + https://github.com/EspressoSystems/tide-disco.git#tide-disco@0.4.1 + MESSAGE + end + + it { is_expected.to eq(message) } + end + end + + describe Dependabot::DependencyFileNotFound do + let(:error) { described_class.new(file_path) } + let(:file_path) { "path/to/Gemfile" } + + describe "#file_name" do + subject { error.file_name } + + it { is_expected.to eq("Gemfile") } + end + + describe "#directory" do + subject { error.directory } + + it { is_expected.to eq("/path/to") } + + context "with the root directory" do + let(:file_path) { "Gemfile" } + + it { is_expected.to eq("/") } + end + + context "with a root level file whose path starts with a slash" do + let(:file_path) { "/Gemfile" } + + it { is_expected.to eq("/") } + end + + context "with a nested file whose path starts with a slash" do + let(:file_path) { "/path/to/Gemfile" } + + it { is_expected.to eq("/path/to") } + end + end + end + + describe Dependabot::PrivateSourceAuthenticationFailure do + let(:error) { described_class.new(source) } + let(:source) { "source" } + + describe "#message" do + subject(:error_message) { error.message } + + it do + expect(error_message).to eq( + "The following source could not be reached as it requires authentication (and any provided details were " \ + "invalid or lacked the required permissions): source" + ) + end + + context "when source includes something that looks like a path" do + let(:source) do + "npm.fury.io/token123/path" + end + + it do + expect(error_message).to eq( + "The following source could not be reached as it requires authentication (and any provided details were " \ + "invalid or lacked the required permissions): npm.fury.io/" + ) + end + end + end + end + + describe Dependabot::PrivateSourceTimedOut do + let(:error) { described_class.new(source) } + let(:source) { "source" } + + describe "#message" do + subject(:error_message) { error.message } + + it do + expect(error_message).to eq( + "The following source timed out: source" + ) + end + + context "when source includes something that looks like a path" do + let(:source) do + "npm.fury.io/token123/path" + end + + it do + expect(error_message).to eq( + "The following source timed out: npm.fury.io/" + ) + end + end + end + end + + describe Dependabot::PrivateSourceCertificateFailure do + let(:error) { described_class.new(source) } + let(:source) { "source" } + + describe "#message" do + subject(:error_message) { error.message } + + it do + expect(error_message).to eq( + "Could not verify the SSL certificate for source" + ) + end + + context "when source includes something that looks like a path" do + let(:source) do + "npm.fury.io/token123/path" + end + + it do + expect(error_message).to eq( + "Could not verify the SSL certificate for npm.fury.io/" + ) + end + end + end + end + + describe Dependabot::GitDependenciesNotReachable do + let(:error) { described_class.new(dependency_url) } + let(:dependency_url) do + "https://x-access-token:token@bitbucket.org/gocardless/" + end + + describe "#message" do + subject(:error_message) { error.message } + + it do + expect(error_message).to eq( + "The following git URLs could not be retrieved: " \ + "https://bitbucket.org/gocardless/" + ) + end + end + end +end diff --git a/common/spec/dependabot/errors_spec.rb b/common/spec/dependabot/errors_spec.rb deleted file mode 100644 index 210435506ca..00000000000 --- a/common/spec/dependabot/errors_spec.rb +++ /dev/null @@ -1,221 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/errors" - -RSpec.describe Dependabot::DependabotError do - let(:error) { described_class.new(message) } - let(:message) do - "some error" - end - - describe "#message" do - subject(:err_message) { error.message } - - let(:tmp) { Dependabot::Utils::BUMP_TMP_DIR_PATH } - - it { is_expected.to eq("some error") } - - context "with dependabot temp path" do - let(:message) do - "#{tmp}/dependabot_20201218-14100-y0d218/path error" - end - - it { is_expected.to eq("dependabot_tmp_dir/path error") } - end - - context "with dependabot temp path" do - let(:message) do - "Error (/Users/x/code/dependabot-core/cargo/#{tmp}/dependabot_20201218-14100-y0d218) " \ - "failed to load https://github.com/dependabot" - end - - it do - expect(err_message).to eq( - "Error (/Users/x/code/dependabot-core/cargo/dependabot_tmp_dir) " \ - "failed to load https://github.com/dependabot" - ) - end - end - - context "without http basic auth token" do - let(:message) do - "git://user@github.com error" - end - - it { is_expected.to eq("git://user@github.com error") } - end - - context "with http basic auth missing username" do - let(:message) do - "git://:token@github.com error" - end - - it { is_expected.to eq("git://github.com error") } - end - - context "with http basic auth" do - let(:message) do - "git://user:token@github.com error" - end - - it { is_expected.to eq("git://github.com error") } - end - - context "with escaped basic auth uri" do - let(:message) do - "git://user:token%40github.com error" - end - - it { is_expected.to eq("git://github.com error") } - end - - context "with multiple uri's include @ in their fragment, but no auth" do - let(:message) do - <<~MESSAGE.strip - https://github.com/EspressoSystems/tide-disco.git#tide-disco@0.4.1 - https://github.com/EspressoSystems/tide-disco.git#tide-disco@0.4.1 - MESSAGE - end - - it { is_expected.to eq(message) } - end - end -end - -RSpec.describe Dependabot::DependencyFileNotFound do - let(:error) { described_class.new(file_path) } - let(:file_path) { "path/to/Gemfile" } - - describe "#file_name" do - subject { error.file_name } - - it { is_expected.to eq("Gemfile") } - end - - describe "#directory" do - subject { error.directory } - - it { is_expected.to eq("/path/to") } - - context "with the root directory" do - let(:file_path) { "Gemfile" } - - it { is_expected.to eq("/") } - end - - context "with a root level file whose path starts with a slash" do - let(:file_path) { "/Gemfile" } - - it { is_expected.to eq("/") } - end - - context "with a nested file whose path starts with a slash" do - let(:file_path) { "/path/to/Gemfile" } - - it { is_expected.to eq("/path/to") } - end - end -end - -RSpec.describe Dependabot::PrivateSourceAuthenticationFailure do - let(:error) { described_class.new(source) } - let(:source) { "source" } - - describe "#message" do - subject(:err_message) { error.message } - - it do - expect(err_message).to eq( - "The following source could not be reached as it requires authentication (and any provided details were " \ - "invalid or lacked the required permissions): source" - ) - end - - context "when source includes something that looks like a path" do - let(:source) do - "npm.fury.io/token123/path" - end - - it do - expect(err_message).to eq( - "The following source could not be reached as it requires authentication (and any provided details were " \ - "invalid or lacked the required permissions): npm.fury.io/" - ) - end - end - end -end - -RSpec.describe Dependabot::PrivateSourceTimedOut do - let(:error) { described_class.new(source) } - let(:source) { "source" } - - describe "#message" do - subject(:err_message) { error.message } - - it do - expect(err_message).to eq( - "The following source timed out: source" - ) - end - - context "when source includes something that looks like a path" do - let(:source) do - "npm.fury.io/token123/path" - end - - it do - expect(err_message).to eq( - "The following source timed out: npm.fury.io/" - ) - end - end - end -end - -RSpec.describe Dependabot::PrivateSourceCertificateFailure do - let(:error) { described_class.new(source) } - let(:source) { "source" } - - describe "#message" do - subject(:err_message) { error.message } - - it do - expect(err_message).to eq( - "Could not verify the SSL certificate for source" - ) - end - - context "when source includes something that looks like a path" do - let(:source) do - "npm.fury.io/token123/path" - end - - it do - expect(err_message).to eq( - "Could not verify the SSL certificate for npm.fury.io/" - ) - end - end - end -end - -RSpec.describe Dependabot::GitDependenciesNotReachable do - let(:error) { described_class.new(dependency_url) } - let(:dependency_url) do - "https://x-access-token:token@bitbucket.org/gocardless/" - end - - describe "#message" do - subject(:err_message) { error.message } - - it do - expect(err_message).to eq( - "The following git URLs could not be retrieved: " \ - "https://bitbucket.org/gocardless/" - ) - end - end -end diff --git a/common/spec/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer_spec.rb b/common/spec/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer_spec.rb index d047ca775b5..697cbaedc3e 100644 --- a/common/spec/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer_spec.rb +++ b/common/spec/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer_spec.rb @@ -129,7 +129,7 @@ it "sanitizes the mention" do expect(sanitize_links_and_mentions).to eq( - "

@command\nThanks to " \ + "

@command
\nThanks to " \ "@\u200Bfeelepxyz" \ "@other " \ "@\u200Bescape

\n" @@ -142,7 +142,7 @@ it "sanitizes the mention" do expect(sanitize_links_and_mentions).to eq( - "

@command \n @test " \ + "

@command
\n @test " \ "@\u200Bfeelepxyz

\n" ) end @@ -350,8 +350,18 @@ it do expect(sanitize_links_and_mentions).to eq( - "

\n#144\n

\n" + "


\n#144
\n

\n" + ) + end + end + + context "when a line has softbreaks" do + let(:text) { "Soft \n break" } + + it "converts to hardbreaks" do + expect(sanitize_links_and_mentions).to eq( + "

Soft
\nbreak

\n" ) end end diff --git a/common/spec/dependabot/pull_request_creator/message_builder_spec.rb b/common/spec/dependabot/pull_request_creator/message_builder_spec.rb index 0e55ced48fe..8ece97cca72 100644 --- a/common/spec/dependabot/pull_request_creator/message_builder_spec.rb +++ b/common/spec/dependabot/pull_request_creator/message_builder_spec.rb @@ -1566,10 +1566,10 @@ def commits_details(base:, head:) "
\n" \ "Vulnerabilities fixed\n" \ "
\n" \ - "

Serious vulnerability\n" \ - "A vulnerability that allows arbitrary code\n" \ + "

Serious vulnerability
\n" \ + "A vulnerability that allows arbitrary code
\n" \ "execution.

\n" \ - "

Patched versions: > 1.5.0\n" \ + "

Patched versions: > 1.5.0
\n" \ "Unaffected versions: none

\n" \ "
\n" \ "
\n" diff --git a/composer/.rubocop.yml b/composer/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/composer/.rubocop.yml +++ b/composer/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/composer/Dockerfile b/composer/Dockerfile index 46b0010d206..11513eda85b 100644 --- a/composer/Dockerfile +++ b/composer/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/dependabot/dependabot-updater-core ARG COMPOSER_V1_VERSION=1.10.27 -ARG COMPOSER_V2_VERSION=2.7.4 +ARG COMPOSER_V2_VERSION=2.7.7 ENV COMPOSER_ALLOW_SUPERUSER=1 RUN apt-get update \ && apt-get upgrade -y \ diff --git a/composer/helpers/v2/composer.lock b/composer/helpers/v2/composer.lock index 742794027e2..b9eb43fad3a 100644 --- a/composer/helpers/v2/composer.lock +++ b/composer/helpers/v2/composer.lock @@ -84,16 +84,16 @@ }, { "name": "composer/class-map-generator", - "version": "1.1.1", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "8286a62d243312ed99b3eee20d5005c961adb311" + "reference": "b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", - "reference": "8286a62d243312ed99b3eee20d5005c961adb311", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3", + "reference": "b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3", "shasum": "" }, "require": { @@ -137,7 +137,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.1" + "source": "https://github.com/composer/class-map-generator/tree/1.3.4" }, "funding": [ { @@ -153,28 +153,28 @@ "type": "tidelift" } ], - "time": "2024-03-15T12:53:41+00:00" + "time": "2024-06-12T14:13:04+00:00" }, { "name": "composer/composer", - "version": "2.7.4", + "version": "2.7.7", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "a625e50598e12171d3f60b1149eb530690c43474" + "reference": "291942978f39435cf904d33739f98d7d4eca7b23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/a625e50598e12171d3f60b1149eb530690c43474", - "reference": "a625e50598e12171d3f60b1149eb530690c43474", + "url": "https://api.github.com/repos/composer/composer/zipball/291942978f39435cf904d33739f98d7d4eca7b23", + "reference": "291942978f39435cf904d33739f98d7d4eca7b23", "shasum": "" }, "require": { "composer/ca-bundle": "^1.0", - "composer/class-map-generator": "^1.0", + "composer/class-map-generator": "^1.3.3", "composer/metadata-minifier": "^1.0", "composer/pcre": "^2.1 || ^3.1", - "composer/semver": "^3.2.5", + "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", "justinrainbow/json-schema": "^5.2.11", @@ -193,11 +193,11 @@ "symfony/process": "^5.4 || ^6.0 || ^7" }, "require-dev": { - "phpstan/phpstan": "^1.9.3", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1", - "phpstan/phpstan-symfony": "^1.2.10", + "phpstan/phpstan": "^1.11.0", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpstan/phpstan-symfony": "^1.4.0", "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1" }, "suggest": { @@ -251,7 +251,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.4" + "source": "https://github.com/composer/composer/tree/2.7.7" }, "funding": [ { @@ -267,7 +267,7 @@ "type": "tidelift" } ], - "time": "2024-04-22T19:17:03+00:00" + "time": "2024-06-10T20:11:12+00:00" }, { "name": "composer/metadata-minifier", @@ -340,16 +340,16 @@ }, { "name": "composer/pcre", - "version": "3.1.3", + "version": "3.1.4", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + "reference": "04229f163664973f68f38f6f73d917799168ef24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", + "reference": "04229f163664973f68f38f6f73d917799168ef24", "shasum": "" }, "require": { @@ -391,7 +391,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" + "source": "https://github.com/composer/pcre/tree/3.1.4" }, "funding": [ { @@ -407,7 +407,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T10:26:25+00:00" + "time": "2024-05-27T13:40:54+00:00" }, { "name": "composer/semver", @@ -572,16 +572,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -618,7 +618,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -634,19 +634,19 @@ "type": "tidelift" } ], - "time": "2024-03-26T18:29:49+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "justinrainbow/json-schema", "version": "v5.2.13", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", + "url": "https://github.com/jsonrainbow/json-schema.git", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", "shasum": "" }, @@ -701,8 +701,8 @@ "schema" ], "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/v5.2.13" }, "time": "2023-09-26T02:20:38+00:00" }, @@ -806,16 +806,16 @@ }, { "name": "react/promise", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { @@ -867,7 +867,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.1.0" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { @@ -875,7 +875,7 @@ "type": "open_collective" } ], - "time": "2023-11-16T16:21:57+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "seld/jsonlint", @@ -1052,16 +1052,16 @@ }, { "name": "symfony/console", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e" + "reference": "aa73115c0c24220b523625bfcfa655d7d73662dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", - "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", + "url": "https://api.github.com/repos/symfony/console/zipball/aa73115c0c24220b523625bfcfa655d7d73662dd", + "reference": "aa73115c0c24220b523625bfcfa655d7d73662dd", "shasum": "" }, "require": { @@ -1131,7 +1131,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.36" + "source": "https://github.com/symfony/console/tree/v5.4.40" }, "funding": [ { @@ -1147,7 +1147,7 @@ "type": "tidelift" } ], - "time": "2024-02-20T16:33:57+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1218,16 +1218,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.38", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "899330a01056077271e2f614c7b28b0379a671eb" + "reference": "26dd9912df6940810ea00f8f53ad48d6a3424995" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/899330a01056077271e2f614c7b28b0379a671eb", - "reference": "899330a01056077271e2f614c7b28b0379a671eb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/26dd9912df6940810ea00f8f53ad48d6a3424995", + "reference": "26dd9912df6940810ea00f8f53ad48d6a3424995", "shasum": "" }, "require": { @@ -1236,6 +1236,9 @@ "symfony/polyfill-mbstring": "~1.8", "symfony/polyfill-php80": "^1.16" }, + "require-dev": { + "symfony/process": "^5.4|^6.4" + }, "type": "library", "autoload": { "psr-4": { @@ -1262,7 +1265,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.38" + "source": "https://github.com/symfony/filesystem/tree/v5.4.40" }, "funding": [ { @@ -1278,20 +1281,20 @@ "type": "tidelift" } ], - "time": "2024-03-21T08:05:07+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/finder", - "version": "v5.4.35", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435" + "reference": "f51cff4687547641c7d8180d74932ab40b2205ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/abe6d6f77d9465fed3cd2d029b29d03b56b56435", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435", + "url": "https://api.github.com/repos/symfony/finder/zipball/f51cff4687547641c7d8180d74932ab40b2205ce", + "reference": "f51cff4687547641c7d8180d74932ab40b2205ce", "shasum": "" }, "require": { @@ -1325,7 +1328,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.35" + "source": "https://github.com/symfony/finder/tree/v5.4.40" }, "funding": [ { @@ -1341,7 +1344,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1895,16 +1898,16 @@ }, { "name": "symfony/process", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "shasum": "" }, "require": { @@ -1937,7 +1940,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.36" + "source": "https://github.com/symfony/process/tree/v5.4.40" }, "funding": [ { @@ -1953,7 +1956,7 @@ "type": "tidelift" } ], - "time": "2024-02-12T15:49:53+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/service-contracts", @@ -2040,16 +2043,16 @@ }, { "name": "symfony/string", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b" + "reference": "142877285aa974a6f7685e292ab5ba9aae86b143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e232c83622bd8cd32b794216aa29d0d266d353b", - "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b", + "url": "https://api.github.com/repos/symfony/string/zipball/142877285aa974a6f7685e292ab5ba9aae86b143", + "reference": "142877285aa974a6f7685e292ab5ba9aae86b143", "shasum": "" }, "require": { @@ -2106,7 +2109,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.36" + "source": "https://github.com/symfony/string/tree/v5.4.40" }, "funding": [ { @@ -2122,7 +2125,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T08:49:30+00:00" + "time": "2024-05-31T14:33:22+00:00" } ], "packages-dev": [ diff --git a/composer/helpers/v2/src/UpdateChecker.php b/composer/helpers/v2/src/UpdateChecker.php index 462b1a80c07..cb849941ccf 100644 --- a/composer/helpers/v2/src/UpdateChecker.php +++ b/composer/helpers/v2/src/UpdateChecker.php @@ -8,15 +8,13 @@ use Composer\Factory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; -use Composer\Package\Link; use Composer\Package\PackageInterface; -use Composer\Package\Version\VersionParser; final class UpdateChecker { public static function getLatestResolvableVersion(array $args): ?string { - [$workingDirectory, $dependencyName, $gitCredentials, $registryCredentials, $latestAllowableVersion] = $args; + [$workingDirectory, $dependencyName, $gitCredentials, $registryCredentials] = $args; $httpBasicCredentials = []; @@ -50,22 +48,10 @@ public static function getLatestResolvableVersion(array $args): ?string $io->loadConfiguration($config); } - $package = $composer->getPackage(); - - $versionParser = new VersionParser(); - - $constraint = $versionParser->parseConstraints($latestAllowableVersion); // your version constraint - $packageLink = new Link($package->getName(), $dependencyName, $constraint); - - $requires = $package->getRequires(); - $requires[$dependencyName] = $packageLink; - - $package->setRequires($requires); - $install = new Installer( $io, $config, - $package, // @phpstan-ignore-line + $composer->getPackage(), // @phpstan-ignore-line $composer->getDownloadManager(), $composer->getRepositoryManager(), $composer->getLocker(), diff --git a/composer/lib/dependabot/composer/update_checker.rb b/composer/lib/dependabot/composer/update_checker.rb index 6da407bea45..b783ff67923 100644 --- a/composer/lib/dependabot/composer/update_checker.rb +++ b/composer/lib/dependabot/composer/update_checker.rb @@ -72,7 +72,7 @@ def updated_requirements end def requirements_unlocked_or_can_be? - requirements_update_strategy != RequirementsUpdateStrategy::LockfileOnly + !requirements_update_strategy.lockfile_only? end def requirements_update_strategy diff --git a/composer/lib/dependabot/composer/update_checker/requirements_updater.rb b/composer/lib/dependabot/composer/update_checker/requirements_updater.rb index bbb7ddededa..9b70ea35d7d 100644 --- a/composer/lib/dependabot/composer/update_checker/requirements_updater.rb +++ b/composer/lib/dependabot/composer/update_checker/requirements_updater.rb @@ -48,7 +48,7 @@ def initialize(requirements:, update_strategy:, end def updated_requirements - return requirements if update_strategy == RequirementsUpdateStrategy::LockfileOnly + return requirements if update_strategy.lockfile_only? return requirements unless latest_resolvable_version requirements.map { |req| updated_requirement(req) } diff --git a/composer/lib/dependabot/composer/update_checker/version_resolver.rb b/composer/lib/dependabot/composer/update_checker/version_resolver.rb index 501e3522eaa..81fac90e6b7 100644 --- a/composer/lib/dependabot/composer/update_checker/version_resolver.rb +++ b/composer/lib/dependabot/composer/update_checker/version_resolver.rb @@ -148,8 +148,7 @@ def run_update_checker Dir.pwd, dependency.name.downcase, git_credentials, - registry_credentials, - @latest_allowable_version.to_s + registry_credentials ] ) end diff --git a/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb b/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb index 564f8188799..8e3a7c3734a 100644 --- a/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb +++ b/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb @@ -59,7 +59,6 @@ let(:dependency_name) { "phpdocumentor/reflection-docblock" } let(:dependency_version) { "2.0.4" } let(:string_req) { "2.0.4" } - let(:latest_allowable_version) { Gem::Version.new("3.3.2") } it { is_expected.to eq(Dependabot::Composer::Version.new("3.3.2")) } end @@ -69,14 +68,12 @@ let(:dependency_name) { "phpdocumentor/reflection-docblock" } let(:dependency_version) { "2.0.4" } let(:string_req) { "2.0.4" } - let(:latest_allowable_version) { Gem::Version.new("3.3.2") } it { is_expected.to eq(Dependabot::Composer::Version.new("3.3.2")) } context "when the minimum version is invalid" do let(:dependency_version) { "4.2.0" } let(:string_req) { "4.2.0" } - let(:latest_allowable_version) { Gem::Version.new("4.3.1") } it { is_expected.to be >= Dependabot::Composer::Version.new("4.3.1") } end @@ -88,7 +85,6 @@ let(:dependency_name) { "phpdocumentor/reflection-docblock" } let(:dependency_version) { "2.0.4" } let(:string_req) { "2.0.4" } - let(:latest_allowable_version) { Gem::Version.new("3.2.2") } it { is_expected.to eq(Dependabot::Composer::Version.new("3.2.2")) } end @@ -107,7 +103,7 @@ context "with a dependency that's provided by another dep" do let(:project_name) { "provided_dependency" } let(:string_req) { "^1.0" } - let(:latest_allowable_version) { Gem::Version.new("1.0.0") } + let(:latest_allowable_version) { Gem::Version.new("6.0.0") } let(:dependency_name) { "php-http/client-implementation" } let(:dependency_version) { nil } diff --git a/composer/spec/dependabot/composer/update_checker_spec.rb b/composer/spec/dependabot/composer/update_checker_spec.rb index ac5934bdef4..a95e973b531 100644 --- a/composer/spec/dependabot/composer/update_checker_spec.rb +++ b/composer/spec/dependabot/composer/update_checker_spec.rb @@ -194,12 +194,6 @@ describe "#latest_resolvable_version" do subject(:latest_resolvable_version) { checker.latest_resolvable_version } - # setting the latest allowable version to 1.22.0 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("1.22.0")) - end - it "returns a non-normalized version, following semver" do expect(latest_resolvable_version.segments.count).to eq(3) end @@ -215,7 +209,7 @@ context "when the user is ignoring the latest version" do let(:ignored_versions) { [">= 1.22.0.a, < 4.0"] } - it { is_expected.to eq(Gem::Version.new("1.22.0")) } + it { is_expected.to eq(Gem::Version.new("1.21.0")) } end context "without a lockfile" do @@ -234,12 +228,6 @@ }] end - # setting the latest allowable version to 4.3.0 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("4.3.0")) - end - it { is_expected.to be >= Gem::Version.new("4.3.0") } end @@ -256,32 +244,26 @@ }] end - # setting the latest allowable version to 5.2.45 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("5.2.45")) - end - it { is_expected.to be >= Gem::Version.new("5.2.45") } context "when as a platform requirement" do let(:project_name) { "old_php_platform" } - it { is_expected.to eq(Gem::Version.new("5.2.45")) } + it { is_expected.to eq(Gem::Version.new("5.4.36")) } context "when an extension is specified that we don't have" do let(:project_name) { "missing_extension" } it "pretends the missing extension is there" do expect(latest_resolvable_version) - .to eq(Dependabot::Composer::Version.new("5.2.45")) + .to eq(Dependabot::Composer::Version.new("5.4.36")) end end context "when the platform requirement only specifies an extension" do let(:project_name) { "bad_php" } - it { is_expected.to eq(Gem::Version.new("5.2.45")) } + it { is_expected.to eq(Gem::Version.new("5.4.36")) } end end end @@ -299,12 +281,6 @@ }] end - # setting the latest allowable version to 5.2.45 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("5.2.45")) - end - it { is_expected.to be >= Gem::Version.new("5.2.45") } end end @@ -489,8 +465,6 @@ v1_metadata_url = "https://repo.packagist.org/p/#{dependency_name.downcase}.json" # v1 url doesn't always return 404 for missing packages stub_request(:get, v1_metadata_url).to_return(status: 200, body: '{"error":{"code":404,"message":"Not Found"}}') - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("2.4.2")) end it "is between 2.0.0 and 3.0.0" do @@ -513,12 +487,6 @@ end let(:ignored_versions) { [">= 2.8.0"] } - # set latest allowable version from registry to 2.1.7 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("2.1.7")) - end - it "is the highest resolvable version" do expect(latest_resolvable_version).to eq(Gem::Version.new("2.1.7")) end @@ -586,16 +554,12 @@ context "when there is no lockfile" do let(:project_name) { "version_conflict_on_update_without_lockfile" } - it "raises a helpful error" do - expect { latest_resolvable_version }.to raise_error(Dependabot::DependencyFileNotResolvable) - end + it { is_expected.to be_nil } context "when the conflict comes from a loose PHP version" do let(:project_name) { "version_conflict_library" } - it "raises a helpful error" do - expect { latest_resolvable_version }.to raise_error(Dependabot::DependencyFileNotResolvable) - end + it { is_expected.to be_nil } end end end @@ -686,12 +650,6 @@ }] end - # set latest allowable version from registry to 1.3.0 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("1.3.0")) - end - # Alternatively, this could raise an error. Either behaviour would be # fine - the below is just what we get with Composer at the moment # because we disabled downloading the files in @@ -770,8 +728,6 @@ status: 200, body: fixture("wpackagist_response.json") ) - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("3.0.2")) end it { is_expected.to be >= Gem::Version.new("3.0.2") } @@ -790,13 +746,7 @@ }] end - # set latest allowable version from registry to 5.2.7 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("5.2.7")) - end - - it { is_expected.to be >= Gem::Version.new("5.2.7") } + it { is_expected.to be >= Gem::Version.new("5.2.30") } end context "when a sub-dependency would block the update" do @@ -812,12 +762,6 @@ }] end - # setting the latest allowable version to 5.6.23 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("5.6.23")) - end - # 5.5.0 series and up require an update to illuminate/contracts it { is_expected.to be >= Gem::Version.new("5.6.23") } end diff --git a/devcontainers/.rubocop.yml b/devcontainers/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/devcontainers/.rubocop.yml +++ b/devcontainers/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/docker/.rubocop.yml b/docker/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/docker/.rubocop.yml +++ b/docker/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/docker/lib/dependabot/docker/tag.rb b/docker/lib/dependabot/docker/tag.rb index 9b9728a74ed..62c67632bf0 100644 --- a/docker/lib/dependabot/docker/tag.rb +++ b/docker/lib/dependabot/docker/tag.rb @@ -9,7 +9,7 @@ module Docker class Tag extend T::Sig WORDS_WITH_BUILD = /(?:(?:-[a-z]+)+-[0-9]+)+/ - VERSION_REGEX = /v?(?[0-9]+(?:\.[0-9]+)*(?:_[0-9]+|\.[a-z0-9]+|#{WORDS_WITH_BUILD}|-(?:kb)?[0-9]+)*)/i + VERSION_REGEX = /v?(?[0-9]+(?:[_.][0-9]+)*(?:\.[a-z0-9]+|#{WORDS_WITH_BUILD}|-(?:kb)?[0-9]+)*)/i VERSION_WITH_SFX = /^#{VERSION_REGEX}(?-[a-z][a-z0-9.\-]*)?$/i VERSION_WITH_PFX = /^(?[a-z][a-z0-9.\-_]*-)?#{VERSION_REGEX}$/i VERSION_WITH_PFX_AND_SFX = /^(?[a-z\-_]+-)?#{VERSION_REGEX}(?-[a-z\-]+)?$/i diff --git a/elm/.rubocop.yml b/elm/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/elm/.rubocop.yml +++ b/elm/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/github_actions/.rubocop.yml b/github_actions/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/github_actions/.rubocop.yml +++ b/github_actions/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/go_modules/.rubocop.yml b/go_modules/.rubocop.yml index fc2019d46a3..e5270530f5a 100644 --- a/go_modules/.rubocop.yml +++ b/go_modules/.rubocop.yml @@ -1 +1,4 @@ inherit_from: ../.rubocop.yml + +Sorbet/StrictSigil: + Enabled: true diff --git a/go_modules/helpers/go.mod b/go_modules/helpers/go.mod index 56db04ad0be..dcc5e088e07 100644 --- a/go_modules/helpers/go.mod +++ b/go_modules/helpers/go.mod @@ -2,4 +2,7 @@ module github.com/dependabot/dependabot-core/go_modules/helpers go 1.20 -require github.com/Masterminds/vcs v1.13.3 +require ( + github.com/Masterminds/vcs v1.13.3 + golang.org/x/mod v0.18.0 +) diff --git a/go_modules/helpers/go.sum b/go_modules/helpers/go.sum index 89365af4e39..1ab2315e792 100644 --- a/go_modules/helpers/go.sum +++ b/go_modules/helpers/go.sum @@ -1,2 +1,4 @@ github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= diff --git a/go_modules/helpers/version_test.go b/go_modules/helpers/version_test.go new file mode 100644 index 00000000000..74031f9aacc --- /dev/null +++ b/go_modules/helpers/version_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/json" + "golang.org/x/mod/semver" + "os" + "reflect" + "testing" +) + +// TestVersionComparison verifies that the ordered version fixture is sorted correctly. +func TestVersionComparison(t *testing.T) { + data, err := os.ReadFile("../spec/fixtures/ordered_versions.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + var expected []string + if err = json.Unmarshal(data, &expected); err != nil { + t.Fatalf("failed to unmarshal json: %v", err) + } + + actual := make([]string, len(expected)) + copy(actual, expected) + semver.Sort(actual) + + // The sorted order should equal the original order in the file. + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("got %v", actual) + } +} diff --git a/go_modules/lib/dependabot/go_modules/file_updater.rb b/go_modules/lib/dependabot/go_modules/file_updater.rb index f3e15e8e72d..514bcae499c 100644 --- a/go_modules/lib/dependabot/go_modules/file_updater.rb +++ b/go_modules/lib/dependabot/go_modules/file_updater.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/shared_helpers" require "dependabot/file_updaters" require "dependabot/file_updaters/base" @@ -9,16 +11,29 @@ module Dependabot module GoModules class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + require_relative "file_updater/go_mod_updater" - def initialize(dependencies:, dependency_files:, repo_contents_path: nil, - credentials:, options: {}) + sig do + override + .params( + dependencies: T::Array[Dependabot::Dependency], + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Dependabot::Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[Symbol, T.untyped] + ) + .void + end + def initialize(dependencies:, dependency_files:, credentials:, repo_contents_path: nil, options: {}) super - @goprivate = options.fetch(:goprivate, "*") + @goprivate = T.let(options.fetch(:goprivate, "*"), String) use_repo_contents_stub if repo_contents_path.nil? end + sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ /^go\.mod$/, @@ -26,25 +41,26 @@ def self.updated_files_regex ] end + sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files updated_files = [] - if go_mod && dependency_changed?(go_mod) + if go_mod && dependency_changed?(T.must(go_mod)) updated_files << updated_file( - file: go_mod, - content: file_updater.updated_go_mod_content + file: T.must(go_mod), + content: T.must(file_updater.updated_go_mod_content) ) - if go_sum && go_sum.content != file_updater.updated_go_sum_content + if go_sum && T.must(go_sum).content != file_updater.updated_go_sum_content updated_files << updated_file( - file: go_sum, - content: file_updater.updated_go_sum_content + file: T.must(go_sum), + content: T.must(file_updater.updated_go_sum_content) ) end - vendor_updater.updated_vendor_cache_files(base_directory: directory) + vendor_updater.updated_files(base_directory: T.must(directory)) .each do |file| updated_files << file end @@ -57,19 +73,22 @@ def updated_dependency_files private + sig { params(go_mod: Dependabot::DependencyFile).returns(T::Boolean) } def dependency_changed?(go_mod) # file_changed? only checks for changed requirements. Need to check for indirect dep version changes too. file_changed?(go_mod) || dependencies.any? { |dep| dep.previous_version != dep.version } end + sig { override.void } def check_required_files return if go_mod raise "No go.mod!" end + sig { returns(String) } def use_repo_contents_stub - @repo_contents_stub = true + @repo_contents_stub = T.let(true, T.nilable(T::Boolean)) @repo_contents_path = Dir.mktmpdir Dir.chdir(@repo_contents_path) do @@ -92,22 +111,27 @@ def use_repo_contents_stub end end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def go_mod - @go_mod ||= get_original_file("go.mod") + @go_mod ||= T.let(get_original_file("go.mod"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def go_sum - @go_sum ||= get_original_file("go.sum") + @go_sum ||= T.let(get_original_file("go.sum"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(String)) } def directory dependency_files.first&.directory end + sig { returns(String) } def vendor_dir File.join(repo_contents_path, directory, "vendor") end + sig { returns(Dependabot::FileUpdaters::VendorUpdater) } def vendor_updater Dependabot::FileUpdaters::VendorUpdater.new( repo_contents_path: repo_contents_path, @@ -115,22 +139,27 @@ def vendor_updater ) end + sig { returns(GoModUpdater) } def file_updater - @file_updater ||= + @file_updater ||= T.let( GoModUpdater.new( dependencies: dependencies, dependency_files: dependency_files, credentials: credentials, repo_contents_path: repo_contents_path, - directory: directory, + directory: T.must(directory), options: { tidy: tidy?, vendor: vendor?, goprivate: @goprivate } - ) + ), + T.nilable(Dependabot::GoModules::FileUpdater::GoModUpdater) + ) end + sig { returns(T::Boolean) } def tidy? !@repo_contents_stub end + sig { returns(T::Boolean) } def vendor? File.exist?(File.join(vendor_dir, "modules.txt")) end diff --git a/go_modules/lib/dependabot/go_modules/metadata_finder.rb b/go_modules/lib/dependabot/go_modules/metadata_finder.rb index e5e007a8d70..c848e57a7f7 100644 --- a/go_modules/lib/dependabot/go_modules/metadata_finder.rb +++ b/go_modules/lib/dependabot/go_modules/metadata_finder.rb @@ -1,4 +1,4 @@ -# typed: strict +# typed: strong # frozen_string_literal: true require "sorbet-runtime" diff --git a/go_modules/lib/dependabot/go_modules/native_helpers.rb b/go_modules/lib/dependabot/go_modules/native_helpers.rb index fd955f37513..dbd08d3e523 100644 --- a/go_modules/lib/dependabot/go_modules/native_helpers.rb +++ b/go_modules/lib/dependabot/go_modules/native_helpers.rb @@ -1,18 +1,25 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module GoModules module NativeHelpers + extend T::Sig + + sig { returns(String) } def self.helper_path clean_path(File.join(native_helpers_root, "go_modules/bin/helper")) end + sig { returns(String) } def self.native_helpers_root default_path = File.join(__dir__, "../../../helpers/install-dir") ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path) end + sig { params(path: String).returns(String) } def self.clean_path(path) Pathname.new(path).cleanpath.to_path end diff --git a/go_modules/lib/dependabot/go_modules/path_converter.rb b/go_modules/lib/dependabot/go_modules/path_converter.rb index 97715287011..42901f57a36 100644 --- a/go_modules/lib/dependabot/go_modules/path_converter.rb +++ b/go_modules/lib/dependabot/go_modules/path_converter.rb @@ -1,19 +1,32 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/go_modules/native_helpers" module Dependabot module GoModules module PathConverter + extend T::Sig + + sig do + params(path: String) + .returns( + T.nilable(String) + ) + end def self.git_url_for_path(path) # Save a query by manually converting golang.org/x names import_path = path.gsub(%r{^golang\.org/x}, "github.com/golang") - SharedHelpers.run_helper_subprocess( - command: NativeHelpers.helper_path, - function: "getVcsRemoteForImport", - args: { import: import_path } + T.cast( + SharedHelpers.run_helper_subprocess( + command: NativeHelpers.helper_path, + function: "getVcsRemoteForImport", + args: { import: import_path } + ), + T.nilable(String) ) end end diff --git a/go_modules/lib/dependabot/go_modules/resolvability_errors.rb b/go_modules/lib/dependabot/go_modules/resolvability_errors.rb index 0c1820df613..398f0a89cdb 100644 --- a/go_modules/lib/dependabot/go_modules/resolvability_errors.rb +++ b/go_modules/lib/dependabot/go_modules/resolvability_errors.rb @@ -1,11 +1,16 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module GoModules module ResolvabilityErrors + extend T::Sig + GITHUB_REPO_REGEX = %r{github.com/[^:@]*} + sig { params(message: String, goprivate: T.untyped).void } def self.handle(message, goprivate:) mod_path = message.scan(GITHUB_REPO_REGEX).last unless mod_path && message.include?("If this is a private repository") @@ -17,9 +22,10 @@ def self.handle(message, goprivate:) SharedHelpers.in_a_temporary_directory do File.write("go.mod", "module dummy\n") + mod_path = T.cast(mod_path, String) mod_split = mod_path.split("/") repo_path = if mod_split.size > 3 - mod_split[0..2].join("/") + T.must(mod_split[0..2]).join("/") else mod_path end diff --git a/go_modules/lib/dependabot/go_modules/update_checker.rb b/go_modules/lib/dependabot/go_modules/update_checker.rb index 1a884dd7884..b1d37f4fb5e 100644 --- a/go_modules/lib/dependabot/go_modules/update_checker.rb +++ b/go_modules/lib/dependabot/go_modules/update_checker.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/update_checkers" require "dependabot/update_checkers/base" require "dependabot/shared_helpers" @@ -10,8 +12,11 @@ module Dependabot module GoModules class UpdateChecker < Dependabot::UpdateCheckers::Base + extend T::Sig + require_relative "update_checker/latest_version_finder" + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_resolvable_version latest_version_finder.latest_version end @@ -19,25 +24,30 @@ def latest_resolvable_version # This is currently used to short-circuit latest_resolvable_version, # with the assumption that it'll be quicker than checking # resolvability. As this is quite quick in Go anyway, we just alias. + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_version latest_resolvable_version end + sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_resolvable_security_fix_version raise "Dependency not vulnerable!" unless vulnerable? lowest_security_fix_version end + sig { override.returns(Dependabot::Version) } def lowest_security_fix_version latest_version_finder.lowest_security_fix_version end + sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) } def latest_resolvable_version_with_no_unlock # Irrelevant, since Go modules uses a single dependency file nil end + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements dependency.requirements.map do |req| req.merge(requirement: latest_version) @@ -46,8 +56,9 @@ def updated_requirements private + sig { returns(Dependabot::GoModules::UpdateChecker::LatestVersionFinder) } def latest_version_finder - @latest_version_finder ||= + @latest_version_finder ||= T.let( LatestVersionFinder.new( dependency: dependency, dependency_files: dependency_files, @@ -56,23 +67,29 @@ def latest_version_finder security_advisories: security_advisories, raise_on_ignored: raise_on_ignored, goprivate: options.fetch(:goprivate, "*") - ) + ), + T.nilable(Dependabot::GoModules::UpdateChecker::LatestVersionFinder) + ) end + sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? # Full unlock checks aren't implemented for Go (yet) false end + sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock raise NotImplementedError end # Go only supports semver and semver-compliant pseudo-versions, so it can't be a SHA. + sig { returns(T::Boolean) } def existing_version_is_sha? false end + sig { params(tag: T.nilable(T::Hash[Symbol, String])).returns(T.untyped) } def version_from_tag(tag) # To compare with the current version we either use the commit SHA # (if that's what the parser picked up) or the tag name. @@ -81,6 +98,7 @@ def version_from_tag(tag) tag&.fetch(:tag) end + sig { returns(T::Hash[Symbol, T.untyped]) } def default_source { type: "default", source: dependency.name } end diff --git a/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb b/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb index 8f2f06007f7..d73232ffecc 100644 --- a/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb +++ b/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb @@ -1,7 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "excon" +require "sorbet-runtime" require "dependabot/go_modules/update_checker" require "dependabot/update_checkers/version_filters" @@ -9,7 +10,6 @@ require "dependabot/errors" require "dependabot/go_modules/requirement" require "dependabot/go_modules/resolvability_errors" -require "sorbet-runtime" module Dependabot module GoModules @@ -17,26 +17,47 @@ class UpdateChecker class LatestVersionFinder extend T::Sig - RESOLVABILITY_ERROR_REGEXES = [ - # Package url/proxy doesn't include any redirect meta tags - /no go-import meta tags/, - # Package url 404s - /404 Not Found/, - /Repository not found/, - /unrecognized import path/, - /malformed module path/, - # (Private) module could not be fetched - /module .*: git ls-remote .*: exit status 128/m - ].freeze + RESOLVABILITY_ERROR_REGEXES = T.let( + [ + # Package url/proxy doesn't include any redirect meta tags + /no go-import meta tags/, + # Package url 404s + /404 Not Found/, + /Repository not found/, + /unrecognized import path/, + /malformed module path/, + # (Private) module could not be fetched + /module .*: git ls-remote .*: exit status 128/m + ].freeze, + T::Array[Regexp] + ) # The module was retracted from the proxy # OR the version of Go required is greater than what Dependabot supports # OR other go.mod version errors INVALID_VERSION_REGEX = /(go: loading module retractions for)|(version "[^"]+" invalid)/m PSEUDO_VERSION_REGEX = /\b\d{14}-[0-9a-f]{12}$/ - def initialize(dependency:, dependency_files:, credentials:, - ignored_versions:, security_advisories:, raise_on_ignored: false, - goprivate:) + 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], + goprivate: String, + raise_on_ignored: T::Boolean + ) + .void + end + def initialize( + dependency:, + dependency_files:, + credentials:, + ignored_versions:, + security_advisories:, + goprivate:, + raise_on_ignored: false + ) @dependency = dependency @dependency_files = dependency_files @credentials = credentials @@ -46,32 +67,45 @@ def initialize(dependency:, dependency_files:, credentials:, @goprivate = goprivate end + sig { returns(T.nilable(Dependabot::Version)) } def latest_version - @latest_version ||= fetch_latest_version + @latest_version ||= T.let(fetch_latest_version, T.nilable(Dependabot::Version)) end + sig { returns(Dependabot::Version) } def lowest_security_fix_version - @lowest_security_fix_version ||= fetch_lowest_security_fix_version + @lowest_security_fix_version ||= T.let(fetch_lowest_security_fix_version, T.nilable(Dependabot::Version)) end private + sig { returns(Dependabot::Dependency) } attr_reader :dependency + + sig { returns(T::Array[Dependabot::DependencyFile]) } attr_reader :dependency_files + + sig { returns(T::Array[Dependabot::Credential]) } attr_reader :credentials + + sig { returns(T::Array[String]) } attr_reader :ignored_versions + + sig { returns(T::Array[Dependabot::SecurityAdvisory]) } attr_reader :security_advisories + sig { returns(T.nilable(Dependabot::Version)) } def fetch_latest_version candidate_versions = available_versions candidate_versions = filter_prerelease_versions(candidate_versions) candidate_versions = filter_ignored_versions(candidate_versions) # Adding the psuedo-version to the list to avoid downgrades - candidate_versions << dependency.version if PSEUDO_VERSION_REGEX.match?(dependency.version) + candidate_versions << version_class.new(dependency.version) if PSEUDO_VERSION_REGEX.match?(dependency.version) candidate_versions.max end + sig { returns(Dependabot::Version) } def fetch_lowest_security_fix_version relevant_versions = available_versions relevant_versions = filter_prerelease_versions(relevant_versions) @@ -80,13 +114,15 @@ def fetch_lowest_security_fix_version relevant_versions = filter_ignored_versions(relevant_versions) relevant_versions = filter_lower_versions(relevant_versions) - relevant_versions.min + T.must(relevant_versions.min) end + sig { returns(T::Array[Dependabot::Version]) } def available_versions - @available_versions ||= fetch_available_versions + @available_versions ||= T.let(fetch_available_versions, T.nilable(T::Array[Dependabot::Version])) end + sig { returns(T::Array[Dependabot::Version]) } def fetch_available_versions SharedHelpers.in_a_temporary_directory do SharedHelpers.with_git_configured(credentials: credentials) do @@ -124,26 +160,29 @@ def fetch_available_versions ResolvabilityErrors.handle(e.message, goprivate: @goprivate) end + sig { params(error: StandardError).returns(T::Boolean) } def transitory_failure?(error) return true if error.message.include?("EOF") error.message.include?("Internal Server Error") end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def go_mod - @go_mod ||= dependency_files.find { |f| f.name == "go.mod" } + @go_mod ||= T.let(dependency_files.find { |f| f.name == "go.mod" }, T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Hash[String, T.untyped]) } def parse_manifest SharedHelpers.in_a_temporary_directory do - File.write("go.mod", go_mod.content) + File.write("go.mod", T.must(go_mod).content) json = SharedHelpers.run_shell_command("go mod edit -json") JSON.parse(json) || {} end end - sig { params(versions_array: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) } + sig { params(versions_array: T::Array[Dependabot::Version]).returns(T::Array[Dependabot::Version]) } def filter_prerelease_versions(versions_array) return versions_array if wants_prerelease? @@ -154,6 +193,7 @@ def filter_prerelease_versions(versions_array) filtered end + sig { params(versions_array: T::Array[Dependabot::Version]).returns(T::Array[Dependabot::Version]) } def filter_lower_versions(versions_array) return versions_array unless dependency.numeric_version @@ -161,7 +201,7 @@ def filter_lower_versions(versions_array) .select { |version| version > dependency.numeric_version } end - sig { params(versions_array: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) } + sig { params(versions_array: T::Array[Dependabot::Version]).returns(T::Array[Dependabot::Version]) } def filter_ignored_versions(versions_array) filtered = versions_array .reject { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } } @@ -176,22 +216,28 @@ def filter_ignored_versions(versions_array) filtered end + sig { returns(T::Boolean) } def wants_prerelease? - @wants_prerelease ||= + @wants_prerelease ||= T.let( begin current_version = dependency.numeric_version - current_version&.prerelease? - end + !current_version&.prerelease?.nil? + end, + T.nilable(T::Boolean) + ) end + sig { returns(T::Array[Dependabot::Requirement]) } def ignore_requirements ignored_versions.flat_map { |req| requirement_class.requirements_array(req) } end + sig { returns(T.class_of(Dependabot::Requirement)) } def requirement_class dependency.requirement_class end + sig { returns(T.class_of(Dependabot::Version)) } def version_class dependency.version_class end diff --git a/go_modules/lib/dependabot/go_modules/version.rb b/go_modules/lib/dependabot/go_modules/version.rb index 59297b768bd..48ba5fb8a88 100644 --- a/go_modules/lib/dependabot/go_modules/version.rb +++ b/go_modules/lib/dependabot/go_modules/version.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true # Go pre-release versions use 1.0.1-rc1 syntax, which Gem::Version @@ -6,6 +6,8 @@ # alteration. # Best docs are at https://github.com/Masterminds/semver +require "sorbet-runtime" + require "dependabot/version" require "dependabot/utils" @@ -19,6 +21,7 @@ class Version < Dependabot::Version '(\+incompatible)?' ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ + sig { override.params(version: VersionParameter).returns(T::Boolean) } def self.correct?(version) version = version.gsub(/^v/, "") if version.is_a?(String) version = version.to_s.split("+").first if version.to_s.include?("+") @@ -26,34 +29,40 @@ def self.correct?(version) super(version) end + sig { override.params(version: VersionParameter).void } def initialize(version) - @version_string = version.to_s.gsub(/^v/, "") + @version_string = T.let(version.to_s.gsub(/^v/, ""), String) version = version.gsub(/^v/, "") if version.is_a?(String) version = version.to_s.split("+").first if version.to_s.include?("+") + @prerelease = T.let(nil, T.nilable(String)) version, @prerelease = version.to_s.split("-", 2) if version.to_s.include?("-") super end + sig { returns(String) } def inspect # :nodoc: "#<#{self.class} #{@version_string.inspect}>" end + sig { returns(String) } def to_s @version_string end + sig { params(other: Object).returns(T.nilable(Integer)) } def <=>(other) result = super(other) return if result.nil? return result unless result.zero? - other = self.class.new(other) unless other.is_a?(Version) + other = self.class.new(other.to_s) unless other.is_a?(Version) compare_prerelease(@prerelease || "", T.unsafe(other).prerelease || "") end protected + sig { returns(T.nilable(String)) } attr_reader :prerelease private @@ -62,6 +71,7 @@ def <=>(other) # see https://github.com/golang/mod/blob/fa1ba4269bda724bb9f01ec381fbbaf031e45833/semver/semver.go#L333 # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity + sig { params(left: T.untyped, right: T.untyped).returns(Integer) } def compare_prerelease(left, right) return 0 if left == right return 1 if left == "" @@ -98,12 +108,14 @@ def compare_prerelease(left, right) # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity + sig { params(data: String).returns(T.untyped) } def next_ident(data) i = 0 i += 1 while i < data.length && data[i] != "." [data[0..i], data[i..-1]] end + sig { params(data: T.untyped).returns(T::Boolean) } def num?(data) i = 0 i += 1 while i < data.length && data[i] >= "0" && data[i] <= "9" diff --git a/go_modules/script/ci-test b/go_modules/script/ci-test index a9cf3203e11..811461cbb03 100755 --- a/go_modules/script/ci-test +++ b/go_modules/script/ci-test @@ -2,5 +2,8 @@ set -e +pushd helpers +go test ./... +popd bundle install bundle exec turbo_tests --verbose diff --git a/go_modules/spec/dependabot/go_modules/version_spec.rb b/go_modules/spec/dependabot/go_modules/version_spec.rb index 4159ac35903..ad5eeb3e3f1 100644 --- a/go_modules/spec/dependabot/go_modules/version_spec.rb +++ b/go_modules/spec/dependabot/go_modules/version_spec.rb @@ -148,46 +148,8 @@ expect(described_class.new("v1.0.1-0.20231231120000-abcdefabcdef")).to be < described_class.new("v1.0.1") end - # Tested against the following Go program: - # package main - # - # import ( - # "golang.org/x/mod/semver" - # "log" - # "reflect" - # ) - # - # func main() { - # expected := []string{ - # "v1.0.0", - # "v1.0.1-1", - # "v1.0.1-2", - # "v1.0.1", - # "v1.1.0-rc.6", - # "v1.1.0-rc5", - # "v1.1.0-rc6", - # "v1.1.0", - # } - # actual := make([]string, len(expected)) - # copy(actual, expected) - # semver.Sort(actual) - # if !reflect.DeepEqual(actual, expected) { - # log.Fatalf("got %v", actual) - # } - # } - sorted_versions = [ - "v1.0.0", - "v1.0.1-1", - "v1.0.1-2", - "v1.0.1", - "v1.1.0-rc.6", - "v1.1.0-rc0", - "v1.1.0-rc5", - "v1.1.0-rc6", - "v1.1.0", - "v1.34.2-20220907172603-9a877cf260e1.1", - "v1.34.2-20220907172603-9a877cf260e1.2" - ] + # See also the companion Go program that verifies the version order matches. + sorted_versions = JSON.parse(fixture("ordered_versions.json")) sorted_versions.combination(2).each do |lhs, rhs| it "'#{lhs}' < '#{rhs}'" do expect(described_class.new(lhs)).to be < rhs diff --git a/go_modules/spec/fixtures/ordered_versions.json b/go_modules/spec/fixtures/ordered_versions.json new file mode 100644 index 00000000000..7eeed7ad7e9 --- /dev/null +++ b/go_modules/spec/fixtures/ordered_versions.json @@ -0,0 +1,12 @@ +[ + "v1.0.0", + "v1.0.1-1", + "v1.0.1-2", + "v1.0.1", + "v1.1.0-rc.6", + "v1.1.0-rc5", + "v1.1.0-rc6", + "v1.1.0", + "v1.34.2-20220907172603-9a877cf260e1.1", + "v1.34.2-20220907172603-9a877cf260e1.2" +] diff --git a/gradle/.rubocop.yml b/gradle/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/gradle/.rubocop.yml +++ b/gradle/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/gradle/lib/dependabot/gradle/file_fetcher.rb b/gradle/lib/dependabot/gradle/file_fetcher.rb index e2d70a91d2c..abbd1dc2ef3 100644 --- a/gradle/lib/dependabot/gradle/file_fetcher.rb +++ b/gradle/lib/dependabot/gradle/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" @@ -16,21 +16,39 @@ class FileFetcher < Dependabot::FileFetchers::Base require_relative "file_fetcher/settings_file_parser" SUPPORTED_BUILD_FILE_NAMES = - %w(build.gradle build.gradle.kts).freeze + T.let(%w(build.gradle build.gradle.kts).freeze, T::Array[String]) SUPPORTED_SETTINGS_FILE_NAMES = - %w(settings.gradle settings.gradle.kts).freeze + T.let(%w(settings.gradle settings.gradle.kts).freeze, T::Array[String]) # For now Gradle only supports library .toml files in the main gradle folder SUPPORTED_VERSION_CATALOG_FILE_PATH = - %w(/gradle/libs.versions.toml).freeze + T.let(%w(/gradle/libs.versions.toml).freeze, T::Array[String]) + sig do + override + .params( + source: Dependabot::Source, + credentials: T::Array[Dependabot::Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[String, String] + ) + .void + end + def initialize(source:, credentials:, repo_contents_path: nil, options: {}) + super + + @buildfile_name = T.let(nil, T.nilable(String)) + end + + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) filenames.any? do |filename| SUPPORTED_BUILD_FILE_NAMES.any? { |supported| filename.end_with?(supported) } end end + sig { override.returns(String) } def self.required_files_message "Repo must contain a build.gradle / build.gradle.kts file." end @@ -42,6 +60,7 @@ def fetch_files private + sig { params(root_dir: String).returns(T::Array[DependencyFile]) } def all_buildfiles_in_build(root_dir) files = [buildfile(root_dir), settings_file(root_dir), version_catalog_file(root_dir)].compact files += subproject_buildfiles(root_dir) @@ -50,6 +69,7 @@ def all_buildfiles_in_build(root_dir) .flat_map { |dir| all_buildfiles_in_build(dir) } end + sig { params(root_dir: String).returns(T::Array[String]) } def included_builds(root_dir) builds = [] @@ -61,7 +81,7 @@ def included_builds(root_dir) return builds unless settings_file(root_dir) builds += SettingsFileParser - .new(settings_file: settings_file(root_dir)) + .new(settings_file: T.must(settings_file(root_dir))) .included_build_paths .map { |p| clean_join([root_dir, p]) } @@ -73,17 +93,19 @@ def clean_join(parts) Pathname.new(File.join(parts)).cleanpath.to_path end + sig { params(root_dir: String).returns(T::Array[DependencyFile]) } def subproject_buildfiles(root_dir) return [] unless settings_file(root_dir) subproject_paths = SettingsFileParser - .new(settings_file: settings_file(root_dir)) + .new(settings_file: T.must(settings_file(root_dir))) .subproject_paths subproject_paths.filter_map do |path| if @buildfile_name - fetch_file_from_host(File.join(root_dir, path, @buildfile_name)) + buildfile_path = File.join(root_dir, path, @buildfile_name) + fetch_file_from_host(buildfile_path) else buildfile(File.join(root_dir, path)) end @@ -93,6 +115,7 @@ def subproject_buildfiles(root_dir) end end + sig { params(root_dir: String).returns(T.nilable(DependencyFile)) } def version_catalog_file(root_dir) return nil unless root_dir == "." @@ -100,6 +123,7 @@ def version_catalog_file(root_dir) end # rubocop:disable Metrics/PerceivedComplexity + sig { params(root_dir: String).returns(T::Array[DependencyFile]) } def dependency_script_plugins(root_dir) return [] unless buildfile(root_dir) @@ -123,6 +147,7 @@ def dependency_script_plugins(root_dir) end # rubocop:enable Metrics/PerceivedComplexity + sig { params(path: T.any(Pathname, String)).returns(T::Boolean) } def file_exists_in_submodule?(path) fetch_file_from_host(path, fetch_submodules: true) true @@ -130,20 +155,24 @@ def file_exists_in_submodule?(path) false end + sig { params(dir: String).returns(T.nilable(DependencyFile)) } def buildfile(dir) file = find_first(dir, SUPPORTED_BUILD_FILE_NAMES) || return @buildfile_name ||= File.basename(file.name) file end + sig { params(dir: String).returns(T.nilable(DependencyFile)) } def gradle_toml_file(dir) find_first(dir, SUPPORTED_VERSION_CATALOG_FILE_PATH) end + sig { params(dir: String).returns(T.nilable(DependencyFile)) } def settings_file(dir) find_first(dir, SUPPORTED_SETTINGS_FILE_NAMES) end + sig { params(dir: String, supported_names: T::Array[String]).returns(T.nilable(DependencyFile)) } def find_first(dir, supported_names) paths = supported_names .map { |name| clean_join([dir, name]) } @@ -153,10 +182,12 @@ def find_first(dir, supported_names) fetch_first_if_present(paths) end + sig { returns(T::Hash[String, DependencyFile]) } def cached_files - @cached_files ||= {} + @cached_files ||= T.let({}, T.nilable(T::Hash[String, DependencyFile])) end + sig { params(paths: T::Array[String]).returns(T.nilable(DependencyFile)) } def fetch_first_if_present(paths) paths.each do |path| file = fetch_file_if_present(path) || next diff --git a/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb b/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb index 25f25e639d0..72f1b905fb8 100644 --- a/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb +++ b/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "sorbet-runtime" @@ -11,53 +11,61 @@ class FileFetcher class SettingsFileParser extend T::Sig + sig { params(settings_file: Dependabot::DependencyFile).void } def initialize(settings_file:) @settings_file = settings_file end + sig { returns(T::Array[String]) } def included_build_paths paths = [] - comment_free_content.scan(function_regex("includeBuild")) do + comment_free_content&.scan(function_regex("includeBuild")) do arg = T.must(Regexp.last_match).named_captures.fetch("args") paths << T.must(arg).gsub(/["']/, "").strip end paths.uniq end + sig { returns(T::Array[T.nilable(String)]) } def subproject_paths subprojects = T.let([], T::Array[String]) + process_include_functions(subprojects) + subprojects.uniq.map { |name| process_subproject_name(name) } + end + + private - comment_free_content.scan(function_regex("include")) do + sig { params(subprojects: T::Array[String]).void } + def process_include_functions(subprojects) + comment_free_content&.scan(function_regex("include")) do args = T.must(Regexp.last_match).named_captures.fetch("args") args = T.must(args).split(",") args = args.filter_map { |p| p.gsub(/["']/, "").strip } - subprojects += args + subprojects.concat(args) end + end - subprojects = subprojects.uniq - - subproject_dirs = subprojects.map do |proj| - if comment_free_content.match?(project_dir_regex(proj)) - comment_free_content.match(project_dir_regex(proj)) - .named_captures.fetch("path").sub(%r{^/}, "") - else - proj.tr(":", "/").sub(%r{^/}, "") - end + sig { params(proj: String).returns(T.nilable(String)) } + def process_subproject_name(proj) + if comment_free_content&.match?(project_dir_regex(proj)) + comment_free_content&.match(project_dir_regex(proj)) + &.named_captures&.fetch("path")&.sub(%r{^/}, "") + else + proj.tr(":", "/").sub(%r{^/}, "") end - - subproject_dirs.uniq end - private - + sig { returns(Dependabot::DependencyFile) } attr_reader :settings_file + sig { returns(T.nilable(String)) } def comment_free_content settings_file.content - .gsub(%r{(?<=^|\s)//.*$}, "\n") - .gsub(%r{(?<=^|\s)/\*.*?\*/}m, "") + &.gsub(%r{(?<=^|\s)//.*$}, "\n") + &.gsub(%r{(?<=^|\s)/\*.*?\*/}m, "") end + sig { params(function_name: T.any(String, Symbol)).returns(Regexp) } def function_regex(function_name) / (?:^|\s)#{Regexp.quote(function_name)}(?:\s*\(|\s) @@ -65,6 +73,7 @@ def function_regex(function_name) /mx end + sig { params(proj: String).returns(Regexp) } def project_dir_regex(proj) prefixed_proj = Regexp.quote(":#{proj.gsub(/^:/, '')}") /['"]#{prefixed_proj}['"].*dir\s*=.*['"](?.*?)['"]/i diff --git a/gradle/lib/dependabot/gradle/file_updater.rb b/gradle/lib/dependabot/gradle/file_updater.rb index a9b9c198a6c..ca626a98836 100644 --- a/gradle/lib/dependabot/gradle/file_updater.rb +++ b/gradle/lib/dependabot/gradle/file_updater.rb @@ -56,7 +56,6 @@ def original_file def update_buildfiles_for_dependency(buildfiles:, dependency:) files = buildfiles.dup - # The UpdateChecker ensures the order of requirements is preserved # when updating, so we can zip them together in new/old pairs. reqs = dependency.requirements.zip(dependency.previous_requirements) @@ -69,6 +68,13 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:) buildfile = files.find { |f| f.name == new_req.fetch(:file) } + # Exception raised to handle issue that arises when buildfiles function (see this file) + # removes the build file that contains the dependency itself. So no build file exists to + # update dependency, This behaviour is evident for extremely small number of users + # that have added separate repos as sub-modules in parent projects + + raise DependencyFileNotResolvable, "No build file found to update the dependency" if buildfile.nil? + if new_req.dig(:metadata, :property_name) files = update_files_for_property_change(files, old_req, new_req) elsif new_req.dig(:metadata, :dependency_set) diff --git a/gradle/spec/dependabot/gradle/file_updater_spec.rb b/gradle/spec/dependabot/gradle/file_updater_spec.rb index 6fca4714f34..d32207d8fee 100644 --- a/gradle/spec/dependabot/gradle/file_updater_spec.rb +++ b/gradle/spec/dependabot/gradle/file_updater_spec.rb @@ -309,6 +309,59 @@ end end + context "with multiple sub module buildfiles" do + let(:dependency_files) { [buildfile, subproject_buildfile] } + let(:subproject_buildfile) do + Dependabot::DependencyFile.new( + name: "submodule/build.gradle", + content: fixture("buildfiles", buildfile_fixture_name) + ) + end + + context "when trying to update buildfiles" do + let(:dependency) do + Dependabot::Dependency.new( + name: "co.aikar:acf-paper", + version: "0.5.0-SNAPSHOT", + requirements: [{ + file: "build.gradle", + requirement: "0.6.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }, { + file: "app/build.gradle", + requirement: "0.6.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }], + previous_requirements: [{ + file: "build.gradle", + requirement: "0.5.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }, { + file: "app/build.gradle", + requirement: "0.5.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }], + package_manager: "gradle" + ) + end + + describe "updates the submodule/build.gradle file" do + it "raises a DependencyFileNotResolvable error" do + expect { updated_files.find { |f| f.name == "submodule/build.gradle" } } + .to raise_error(Dependabot::DependencyFileNotResolvable) + end + end + end + end + context "with a dependency name defined by a property" do let(:buildfile_fixture_name) { "name_property.gradle" } diff --git a/hex/.rubocop.yml b/hex/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/hex/.rubocop.yml +++ b/hex/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/hex/helpers/mix.lock b/hex/helpers/mix.lock index ddb949c9954..87cf2f98782 100644 --- a/hex/helpers/mix.lock +++ b/hex/helpers/mix.lock @@ -1,3 +1,3 @@ %{ - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, } diff --git a/hex/lib/dependabot/hex/file_fetcher.rb b/hex/lib/dependabot/hex/file_fetcher.rb index 22e6c221a23..4e4e9021373 100644 --- a/hex/lib/dependabot/hex/file_fetcher.rb +++ b/hex/lib/dependabot/hex/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" @@ -13,14 +13,16 @@ class FileFetcher < Dependabot::FileFetchers::Base APPS_PATH_REGEX = /apps_path:\s*"(?.*?)"/m STRING_ARG = %{(?:["'](.*?)["'])} - SUPPORTED_METHODS = %w(eval_file require_file).join("|").freeze + SUPPORTED_METHODS = T.let(%w(eval_file require_file).join("|").freeze, String) SUPPORT_FILE = /Code\.(?:#{SUPPORTED_METHODS})\(#{STRING_ARG}(?:\s*,\s*#{STRING_ARG})?\)/ PATH_DEPS_REGEX = /{.*path: ?#{STRING_ARG}.*}/ + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) filenames.include?("mix.exs") end + sig { override.returns(String) } def self.required_files_message "Repo must contain a mix.exs." end @@ -37,25 +39,28 @@ def fetch_files private + sig { returns(T.nilable(DependencyFile)) } def mixfile - @mixfile ||= fetch_file_from_host("mix.exs") + @mixfile ||= T.let(fetch_file_from_host("mix.exs"), T.nilable(Dependabot::DependencyFile)) + fetch_file_from_host("mix.exs") end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def lockfile - return @lockfile if defined?(@lockfile) - - @lockfile = fetch_lockfile + @lockfile ||= T.let(fetch_lockfile, T.nilable(DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def fetch_lockfile fetch_file_from_host("mix.lock") rescue Dependabot::DependencyFileNotFound nil end + sig { returns(T::Array[String]) } def umbrella_app_directories - apps_path = mixfile.content.match(APPS_PATH_REGEX) - &.named_captures&.fetch("path") + apps_path = T.must(T.must(mixfile).content).match(APPS_PATH_REGEX) + &.named_captures&.fetch("path") return [] unless apps_path repo_contents(dir: apps_path) @@ -63,10 +68,12 @@ def umbrella_app_directories .map { |f| File.join(apps_path, f.name) } end + sig { returns(T::Array[String]) } def sub_project_directories - mixfile.content.scan(PATH_DEPS_REGEX).flatten + T.must(T.must(mixfile).content).scan(PATH_DEPS_REGEX).flatten end + sig { returns(T::Array[Dependabot::DependencyFile]) } def subapp_mixfiles subapp_directories = [] subapp_directories += umbrella_app_directories @@ -86,15 +93,17 @@ def subapp_mixfiles [] end + sig { returns(T::Array[T.nilable(Dependabot::DependencyFile)]) } def support_files mixfiles = [mixfile] + subapp_mixfiles mixfiles.flat_map do |mixfile| - mixfile_dir = mixfile.path.to_s.delete_prefix("/").delete_suffix("/mix.exs") + mixfile_dir = mixfile&.path&.to_s&.delete_prefix("/")&.delete_suffix("/mix.exs") - mixfile.content.gsub("__DIR__", "\"#{mixfile_dir}\"").scan(SUPPORT_FILE).map do |support_file_args| - path = Pathname.new(File.join(*support_file_args.compact.reverse)) - .cleanpath.to_path + mixfile&.content&.gsub("__DIR__", "\"#{mixfile_dir}\"")&.scan(SUPPORT_FILE)&.map do |support_file_args| + path = Pathname.new(File.join(Array(support_file_args).compact.reverse)) + .cleanpath + .to_path fetch_file_from_host(path).tap { |f| f.support_file = true } end end diff --git a/maven/.rubocop.yml b/maven/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/maven/.rubocop.yml +++ b/maven/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/maven/lib/dependabot/maven/file_parser.rb b/maven/lib/dependabot/maven/file_parser.rb index 6e64d028272..ee3ce5128bb 100644 --- a/maven/lib/dependabot/maven/file_parser.rb +++ b/maven/lib/dependabot/maven/file_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "nokogiri" @@ -15,6 +15,7 @@ module Dependabot module Maven class FileParser < Dependabot::FileParsers::Base + extend T::Sig require "dependabot/file_parsers/base/dependency_set" require_relative "file_parser/property_value_finder" @@ -35,6 +36,7 @@ class FileParser < Dependabot::FileParsers::Base # Regex to get the property name from a declaration that uses a property PROPERTY_REGEX = /\$\{(?.*?)\}/ + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse dependency_set = DependencySet.new pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) } @@ -44,6 +46,7 @@ def parse private + sig { params(pom: Dependabot::DependencyFile).returns(DependencySet) } def pomfile_dependencies(pom) dependency_set = DependencySet.new @@ -70,6 +73,7 @@ def pomfile_dependencies(pom) dependency_set end + sig { params(extension: Dependabot::DependencyFile).returns(DependencySet) } def extensionfile_dependencies(extension) dependency_set = DependencySet.new @@ -89,6 +93,10 @@ def extensionfile_dependencies(extension) dependency_set end + sig do + params(pom: Dependabot::DependencyFile, + dependency_node: Nokogiri::XML::Element).returns(T.nilable(Dependabot::Dependency)) + end def dependency_from_dependency_node(pom, dependency_node) return unless (name = dependency_name(dependency_node, pom)) return if internal_dependency_names.include?(name) @@ -96,6 +104,10 @@ def dependency_from_dependency_node(pom, dependency_node) build_dependency(pom, dependency_node, name) end + sig do + params(pom: Dependabot::DependencyFile, + dependency_node: Nokogiri::XML::Element).returns(T.nilable(Dependabot::Dependency)) + end def dependency_from_plugin_node(pom, dependency_node) return unless (name = plugin_name(dependency_node, pom)) return if internal_dependency_names.include?(name) @@ -103,6 +115,10 @@ def dependency_from_plugin_node(pom, dependency_node) build_dependency(pom, dependency_node, name) end + sig do + params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element, + name: String).returns(T.nilable(Dependabot::Dependency)) + end def build_dependency(pom, dependency_node, name) property_details = { @@ -127,6 +143,10 @@ def build_dependency(pom, dependency_node, name) ) end + sig do + params(dependency_node: Nokogiri::XML::Element, + pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def dependency_name(dependency_node, pom) return unless dependency_node.at_xpath("./groupId") return unless dependency_node.at_xpath("./artifactId") @@ -143,6 +163,9 @@ def dependency_name(dependency_node, pom) ].join(":") end + sig do + params(dependency_node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def dependency_classifier(dependency_node, pom) return unless dependency_node.at_xpath("./classifier") @@ -152,6 +175,9 @@ def dependency_classifier(dependency_node, pom) ) end + sig do + params(dependency_node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def plugin_name(dependency_node, pom) return unless plugin_group_id(pom, dependency_node) return unless dependency_node.at_xpath("./artifactId") @@ -165,6 +191,7 @@ def plugin_name(dependency_node, pom) ].join(":") end + sig { params(pom: Dependabot::DependencyFile, node: Nokogiri::XML::Element).returns(T.nilable(String)) } def plugin_group_id(pom, node) return "org.apache.maven.plugins" unless node.at_xpath("./groupId") @@ -174,6 +201,9 @@ def plugin_group_id(pom, node) ) end + sig do + params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(T.nilable(String)) + end def dependency_version(pom, dependency_node) requirement = dependency_requirement(pom, dependency_node) return nil unless requirement @@ -185,6 +215,9 @@ def dependency_version(pom, dependency_node) requirement.gsub(/[\(\)\[\]]/, "").strip end + sig do + params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(T.nilable(String)) + end def dependency_requirement(pom, dependency_node) return unless dependency_node.at_xpath("./version") @@ -194,10 +227,12 @@ def dependency_requirement(pom, dependency_node) version_content.empty? ? nil : version_content end + sig { params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(T::Array[String]) } def dependency_groups(pom, dependency_node) dependency_scope(pom, dependency_node) == "test" ? ["test"] : [] end + sig { params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(String) } def dependency_scope(pom, dependency_node) return "compile" unless dependency_node.at_xpath("./scope") @@ -207,6 +242,7 @@ def dependency_scope(pom, dependency_node) scope_content.empty? ? "compile" : scope_content end + sig { params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(String) } def packaging_type(pom, dependency_node) return "pom" if dependency_node.node_name == "parent" return "jar" unless dependency_node.at_xpath("./type") @@ -217,6 +253,7 @@ def packaging_type(pom, dependency_node) evaluated_value(packaging_type_content, pom) end + sig { params(dependency_node: Nokogiri::XML::Element).returns(T.nilable(String)) } def version_property_name(dependency_node) return unless dependency_node.at_xpath("./version") @@ -228,17 +265,21 @@ def version_property_name(dependency_node) .named_captures.fetch("property") end + sig { params(value: String, pom: Dependabot::DependencyFile).returns(String) } def evaluated_value(value, pom) return value unless value.match?(PROPERTY_REGEX) - property_name = value.match(PROPERTY_REGEX) - .named_captures.fetch("property") - property_value = value_for_property(property_name, pom) + property_name = T.must(value.match(PROPERTY_REGEX)) + .named_captures.fetch("property") + property_value = value_for_property(T.must(property_name), pom) new_value = value.gsub(value.match(PROPERTY_REGEX).to_s, property_value) evaluated_value(new_value, pom) end + sig do + params(dependency_node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def property_source(dependency_node, pom) property_name = version_property_name(dependency_node) return unless property_name @@ -254,6 +295,7 @@ def property_source(dependency_node, pom) raise DependencyFileNotEvaluatable, msg end + sig { params(property_name: String, pom: Dependabot::DependencyFile).returns(String) } def value_for_property(property_name, pom) value = property_value_finder @@ -268,25 +310,35 @@ def value_for_property(property_name, pom) # Cached, since this can makes calls to the registry (to get property # values from parent POMs) + sig { returns(Dependabot::Maven::FileParser::PropertyValueFinder) } def property_value_finder - @property_value_finder ||= - PropertyValueFinder.new(dependency_files: dependency_files, credentials: credentials) + @property_value_finder ||= T.let( + PropertyValueFinder.new(dependency_files: dependency_files, credentials: credentials.map(&:to_s)), + T.nilable(Dependabot::Maven::FileParser::PropertyValueFinder) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def pomfiles - @pomfiles ||= + @pomfiles ||= T.let( dependency_files.select do |f| f.name.end_with?(".xml") && !f.name.end_with?("extensions.xml") - end + end, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def extensionfiles - @extensionfiles ||= - dependency_files.select { |f| f.name.end_with?("extensions.xml") } + @extensionfiles ||= T.let( + dependency_files.select { |f| f.name.end_with?("extensions.xml") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[String]) } def internal_dependency_names - @internal_dependency_names ||= + @internal_dependency_names ||= T.let( dependency_files.filter_map do |pom| doc = Nokogiri::XML(pom.content) group_id = doc.at_css("project > groupId") || @@ -296,9 +348,12 @@ def internal_dependency_names next unless group_id && artifact_id [group_id.content.strip, artifact_id.content.strip].join(":") - end + end, + T.nilable(T::Array[String]) + ) end + sig { override.void } def check_required_files raise "No pom.xml!" unless get_original_file("pom.xml") end diff --git a/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb b/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb index fad30682805..e707edc5b7a 100644 --- a/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb +++ b/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb @@ -1,6 +1,7 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" require "nokogiri" require "dependabot/dependency_file" @@ -11,15 +12,19 @@ module Dependabot module Maven class FileParser class PomFetcher + extend T::Sig + + sig { params(dependency_files: T::Array[DependencyFile]).void } def initialize(dependency_files:) @dependency_files = dependency_files - @poms = {} + @poms = T.let({}, T::Hash[String, DependencyFile]) end + sig { returns(T::Hash[String, DependencyFile]) } def internal_dependency_poms return @internal_dependency_poms if @internal_dependency_poms - @internal_dependency_poms = {} + @internal_dependency_poms = T.let({}, T.nilable(T::Hash[String, DependencyFile])) dependency_files.each do |pom| doc = Nokogiri::XML(pom.content) group_id = doc.at_css("project > groupId") || @@ -33,12 +38,20 @@ def internal_dependency_poms artifact_id.content.strip ].join(":") - @internal_dependency_poms[dependency_name] = pom + T.must(@internal_dependency_poms)[dependency_name] = pom end - @internal_dependency_poms + T.must(@internal_dependency_poms) end + sig do + params( + group_id: String, + artifact_id: String, + version: String, + urls_to_try: T::Array[String] + ).returns(T.nilable(DependencyFile)) # Fix: Added closing parenthesis + end def fetch_remote_parent_pom(group_id, artifact_id, version, urls_to_try) pom_id = "#{group_id}:#{artifact_id}:#{version}" return @poms[pom_id] if @poms.key?(pom_id) @@ -74,24 +87,33 @@ def fetch_remote_parent_pom(group_id, artifact_id, version, urls_to_try) private + sig { params(group_id: String, artifact_id: String, version: String, base_repo_url: String).returns(String) } def remote_pom_url(group_id, artifact_id, version, base_repo_url) "#{base_repo_url}/" \ "#{group_id.tr('.', '/')}/#{artifact_id}/#{version}/" \ "#{artifact_id}-#{version}.pom" end + sig do + params(group_id: String, artifact_id: String, version: String, snapshot_version: String, + base_repo_url: String).returns(String) + end def remote_pom_snapshot_url(group_id, artifact_id, version, snapshot_version, base_repo_url) "#{base_repo_url}/" \ "#{group_id.tr('.', '/')}/#{artifact_id}/#{version}/" \ "#{artifact_id}-#{snapshot_version}.pom" end + sig { params(group_id: String, artifact_id: String, version: String, base_repo_url: String).returns(String) } def remote_pom_snapshot_metadata_url(group_id, artifact_id, version, base_repo_url) "#{base_repo_url}/" \ "#{group_id.tr('.', '/')}/#{artifact_id}/#{version}/" \ "maven-metadata.xml" end + sig do + params(group_id: String, artifact_id: String, version: String, base_url: String).returns(T.nilable(String)) + end def fetch_snapshot_pom_url(group_id, artifact_id, version, base_url) url = remote_pom_snapshot_metadata_url(group_id, artifact_id, version, base_url) response = fetch(url) @@ -107,15 +129,18 @@ def fetch_snapshot_pom_url(group_id, artifact_id, version, base_url) remote_pom_snapshot_url(group_id, artifact_id, version, snapshot, base_url) end + sig { params(url: String).returns(Excon::Response) } def fetch(url) - @maven_responses ||= {} + @maven_responses ||= T.let({}, T.nilable(T::Hash[String, Excon::Response])) @maven_responses[url] ||= Dependabot::RegistryClient.get(url: url, options: { retry_limit: 1 }) end + sig { params(content: String).returns(T::Boolean) } def pom?(content) !Nokogiri::XML(content).at_css("project > artifactId").nil? end + sig { returns(T::Array[DependencyFile]) } attr_reader :dependency_files end end diff --git a/maven/lib/dependabot/maven/file_parser/property_value_finder.rb b/maven/lib/dependabot/maven/file_parser/property_value_finder.rb index 1928201c933..ad818ab1764 100644 --- a/maven/lib/dependabot/maven/file_parser/property_value_finder.rb +++ b/maven/lib/dependabot/maven/file_parser/property_value_finder.rb @@ -1,8 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "nokogiri" - +require "sorbet-runtime" require "dependabot/dependency_file" require "dependabot/maven/file_parser" require "dependabot/registry_client" @@ -14,17 +14,24 @@ module Dependabot module Maven class FileParser class PropertyValueFinder + extend T::Sig + require_relative "repositories_finder" require_relative "pom_fetcher" DOT_SEPARATOR_REGEX = %r{\.(?!\d+([.\/_\-]|$)+)} + sig { params(dependency_files: T::Array[DependencyFile], credentials: T::Array[String]).void } def initialize(dependency_files:, credentials: []) @dependency_files = dependency_files @credentials = credentials - @pom_fetcher = PomFetcher.new(dependency_files: dependency_files) + @pom_fetcher = T.let(PomFetcher.new(dependency_files: dependency_files), + Dependabot::Maven::FileParser::PomFetcher) end + sig do + params(property_name: String, callsite_pom: DependencyFile).returns(T.nilable(T::Hash[Symbol, T.untyped])) + end def property_details(property_name:, callsite_pom:) pom = callsite_pom doc = Nokogiri::XML(pom.content) @@ -71,8 +78,17 @@ def property_details(property_name:, callsite_pom:) private + sig { returns(T::Array[DependencyFile]) } attr_reader :dependency_files + sig do + params( + expression: String, + property_name: String, + callsite_pom: DependencyFile + ) + .returns(T.nilable(T::Hash[Symbol, String])) + end def extract_value_from_expression(expression:, property_name:, callsite_pom:) # and the expression is pointing to self then raise the error if expression.eql?("${#{property_name}}") @@ -83,14 +99,16 @@ def extract_value_from_expression(expression:, property_name:, callsite_pom:) end # and the expression is pointing to another tag, then get the value of that tag - property_details(property_name: expression.slice(2..-2), callsite_pom: callsite_pom) + property_details(property_name: T.must(expression.slice(2..-2)), callsite_pom: callsite_pom) end + sig { params(property_name: String).returns(String) } def sanitize_property_name(property_name) property_name.sub(/^pom\./, "").sub(/^project\./, "") end # rubocop:disable Metrics/PerceivedComplexity + sig { params(pom: DependencyFile).returns(T.nilable(DependencyFile)) } def parent_pom(pom) doc = Nokogiri::XML(pom.content) doc.remove_namespaces! @@ -111,6 +129,7 @@ def parent_pom(pom) end # rubocop:enable Metrics/PerceivedComplexity + sig { params(pom: DependencyFile).returns(T::Array[String]) } def parent_repository_urls(pom) repositories_finder.repository_urls( pom: pom, @@ -119,14 +138,17 @@ def parent_repository_urls(pom) ) end + sig { returns(RepositoriesFinder) } def repositories_finder - @repositories_finder ||= - RepositoriesFinder.new( + @repositories_finder ||= T.let( + Dependabot::Maven::FileParser::RepositoriesFinder.new( pom_fetcher: @pom_fetcher, dependency_files: dependency_files, credentials: @credentials, evaluate_properties: false - ) + ), + T.nilable(Dependabot::Maven::FileParser::RepositoriesFinder) + ) end end end diff --git a/maven/lib/dependabot/maven/file_updater/property_value_updater.rb b/maven/lib/dependabot/maven/file_updater/property_value_updater.rb index a902c438641..970a409f093 100644 --- a/maven/lib/dependabot/maven/file_updater/property_value_updater.rb +++ b/maven/lib/dependabot/maven/file_updater/property_value_updater.rb @@ -1,6 +1,7 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" require "nokogiri" require "dependabot/dependency_file" @@ -11,54 +12,72 @@ module Dependabot module Maven class FileUpdater class PropertyValueUpdater + extend T::Sig + + sig { params(dependency_files: T::Array[DependencyFile]).void } def initialize(dependency_files:) @dependency_files = dependency_files end + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity + sig do + params( + property_name: String, + callsite_pom: DependencyFile, + updated_value: String + ).returns(T::Array[DependencyFile]) + end def update_pomfiles_for_property_change(property_name:, callsite_pom:, updated_value:) declaration_details = property_value_finder.property_details( property_name: property_name, callsite_pom: callsite_pom ) - node = declaration_details.fetch(:node) - filename = declaration_details.fetch(:file) + node = declaration_details&.fetch(:node) + filename = declaration_details&.fetch(:file) pom_to_update = dependency_files.find { |f| f.name == filename } property_re = %r{<#{Regexp.quote(node.name)}> \s*#{Regexp.quote(node.content)}\s* }xm property_text = node.to_s - if pom_to_update.content&.match?(property_re) - updated_content = pom_to_update.content.sub( + if pom_to_update&.content&.match?(property_re) + updated_content = pom_to_update&.content&.sub( property_re, "<#{node.name}>#{updated_value}" ) - elsif pom_to_update.content.include? property_text + elsif pom_to_update&.content&.include? property_text node.content = updated_value - updated_content = pom_to_update.content.sub( + updated_content = pom_to_update&.content&.sub( property_text, node.to_s ) end updated_pomfiles = dependency_files.dup - updated_pomfiles[updated_pomfiles.index(pom_to_update)] = - update_file(file: pom_to_update, content: updated_content) + updated_pomfiles[T.must(updated_pomfiles.index(pom_to_update))] = + update_file(file: T.must(pom_to_update), content: T.must(updated_content)) updated_pomfiles end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize private + sig { returns T::Array[Dependabot::DependencyFile] } attr_reader :dependency_files + sig { returns Maven::FileParser::PropertyValueFinder } def property_value_finder - @property_value_finder ||= - Maven::FileParser::PropertyValueFinder - .new(dependency_files: dependency_files) + @property_value_finder ||= T.let( + Maven::FileParser::PropertyValueFinder.new(dependency_files: dependency_files), + T.nilable(Dependabot::Maven::FileParser::PropertyValueFinder) + ) end + sig { params(file: DependencyFile, content: String).returns(DependencyFile) } def update_file(file:, content:) updated_file = file.dup updated_file.content = content diff --git a/maven/lib/dependabot/maven/update_checker.rb b/maven/lib/dependabot/maven/update_checker.rb index ebef09ca70d..3898599975a 100644 --- a/maven/lib/dependabot/maven/update_checker.rb +++ b/maven/lib/dependabot/maven/update_checker.rb @@ -138,7 +138,7 @@ def property_updater def property_value_finder @property_value_finder ||= Maven::FileParser::PropertyValueFinder - .new(dependency_files: dependency_files, credentials: credentials) + .new(dependency_files: dependency_files, credentials: credentials.map(&:to_s)) end def version_comes_from_multi_dependency_property? diff --git a/npm_and_yarn/.rubocop.yml b/npm_and_yarn/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/npm_and_yarn/.rubocop.yml +++ b/npm_and_yarn/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/npm_and_yarn/Dockerfile b/npm_and_yarn/Dockerfile index eb3cde32fd2..d682fcf8c5b 100644 --- a/npm_and_yarn/Dockerfile +++ b/npm_and_yarn/Dockerfile @@ -4,7 +4,7 @@ FROM ghcr.io/dependabot/dependabot-updater-core ARG COREPACK_VERSION=0.24.0 # Check for updates at https://github.com/pnpm/pnpm/releases -ARG PNPM_VERSION=8.15.6 +ARG PNPM_VERSION=9.4.0 # Check for updates at https://github.com/yarnpkg/berry/releases ARG YARN_VERSION=4.1.1 diff --git a/npm_and_yarn/helpers/package-lock.json b/npm_and_yarn/helpers/package-lock.json index e8a941a861e..f13593ca0d6 100644 --- a/npm_and_yarn/helpers/package-lock.json +++ b/npm_and_yarn/helpers/package-lock.json @@ -9,8 +9,8 @@ "dependencies": { "@dependabot/yarn-lib": "^1.22.22", "@npmcli/arborist": "^7.5.3", - "@pnpm/dependency-path": "^4.0.0", - "@pnpm/lockfile-file": "^9.0.5", + "@pnpm/dependency-path": "^5.1.1", + "@pnpm/lockfile-file": "^9.1.1", "detect-indent": "^6.1.0", "nock": "^13.5.4", "npm": "6.14.18", @@ -21,10 +21,10 @@ "helper": "run.js" }, "devDependencies": { - "eslint": "^9.2.0", + "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", - "prettier": "^3.2.5" + "prettier": "^3.3.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -731,10 +731,47 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/@eslint/eslintrc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz", - "integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -761,9 +798,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -796,51 +833,23 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.2.0.tgz", - "integrity": "sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -854,16 +863,10 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, "node_modules/@humanwhocodes/retry": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.2.3.tgz", - "integrity": "sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true, "engines": { "node": ">=18.18" @@ -2397,11 +2400,11 @@ } }, "node_modules/@pnpm/core-loggers": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.0.tgz", - "integrity": "sha512-nf6DWO+75llaOxZ4Wb5xIzC86jb9PEeD8y7E4bbkLCJUvv/vRVgaPO3+Fo2GFTw5ZY7cip60rTF6dUzbP9dOVw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.2.tgz", + "integrity": "sha512-UUqWV0wUrMvlNWMe8Ch+XFRI3u3K35T2rS5jofhCsqtZa9UlsJAFW4jjVzOBWkV0uVbt6mdy7IMBHvJsYaU94w==", "dependencies": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "10.1.1" }, "engines": { "node": ">=18.12" @@ -2428,13 +2431,13 @@ } }, "node_modules/@pnpm/dependency-path": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-4.0.0.tgz", - "integrity": "sha512-d2tTvjnWJtqVjREPZa1h81i7wfQSeg7YkMc7BZAr8QJ4he5KlHY1Zmfa4LpyXVQJSV3trGfy/dmxhV2A5lo34g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-5.1.1.tgz", + "integrity": "sha512-HskPO2yVpvb8NHnfmByqdkyqSVRaSBpMUSBzfLwzYLRiexs3zo2+NjUvGErjnmVDG8KgY/iQZtw+y+06vMWy/w==", "dependencies": { "@pnpm/crypto.base32-hash": "3.0.0", - "@pnpm/types": "10.0.0", - "semver": "^7.6.0" + "@pnpm/types": "10.1.1", + "semver": "^7.6.2" }, "engines": { "node": ">=18.12" @@ -2458,14 +2461,14 @@ } }, "node_modules/@pnpm/fetch": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.0.tgz", - "integrity": "sha512-V9khLYMUmadH45A5zZnrt1nUsZ0NokWkw0QjjgSdiBCgRyQnf1SvFjVcj4sVWxK0ZaijZQnIhIcKvlV3/zB0Ig==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.2.tgz", + "integrity": "sha512-mh81jVdVzscYZcVyVRobv5mf/xPFFLIjnEqH6+4LrTHz0HqVRJd3Oknn7vRzXSx8k+xXUH44o3qroAq8KGJoSA==", "dependencies": { - "@pnpm/core-loggers": "10.0.0", + "@pnpm/core-loggers": "10.0.2", "@pnpm/fetching-types": "6.0.0", - "@pnpm/network.agent": "^1.0.1", - "@pnpm/types": "10.0.0", + "@pnpm/network.agent": "^2.0.0", + "@pnpm/types": "10.1.1", "@zkochan/retry": "^0.2.0", "node-fetch": "npm:@pnpm/node-fetch@1.0.0" }, @@ -2495,15 +2498,15 @@ } }, "node_modules/@pnpm/git-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.1.tgz", - "integrity": "sha512-B1FtKwEEUm8130XqmX7eqgMhqdBxJ5gPrWssOLnpIlp/rvmJFsfD2P//80OjORPNFWnpfqdfBF34c/+ZCzAxZg==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.3.tgz", + "integrity": "sha512-/4pxvDjtcTfnv2ElUr1TECRNmnOAx23eeZNKrSTkWOyI6I+J9f5M40M+jgTcVWS6l2L+I5gwemo3XKgCnX43Ag==", "dependencies": { - "@pnpm/fetch": "8.0.0", - "@pnpm/resolver-base": "12.0.0", + "@pnpm/fetch": "8.0.2", + "@pnpm/resolver-base": "12.0.2", "graceful-git": "^3.1.2", "hosted-git-info": "npm:@pnpm/hosted-git-info@1.0.0", - "semver": "^7.6.0" + "semver": "^7.6.2" }, "engines": { "node": ">=18.12" @@ -2586,26 +2589,26 @@ } }, "node_modules/@pnpm/lockfile-file": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.0.5.tgz", - "integrity": "sha512-QQFYohFy39FkAQbtDEtqVIzNu5XZhA9aonh/AM/vwvptNcfnajeBuNKfAJepdjWPg/xSBDuU96So29pkPMK8+Q==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.1.1.tgz", + "integrity": "sha512-Ybs/QCWbN38EyIdpYVviSJR9/gc6LZ1iZDJbVatPL8l4Z4J6ikWh4i32kLqJHutM8McqeFegnTrUO3f65UTajw==", "dependencies": { "@pnpm/constants": "8.0.0", - "@pnpm/dependency-path": "4.0.0", + "@pnpm/dependency-path": "5.1.1", "@pnpm/error": "6.0.1", - "@pnpm/git-resolver": "9.0.1", + "@pnpm/git-resolver": "9.0.3", "@pnpm/git-utils": "2.0.0", - "@pnpm/lockfile-types": "6.0.0", - "@pnpm/lockfile-utils": "10.1.1", - "@pnpm/merge-lockfile-changes": "6.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/lockfile-types": "7.1.1", + "@pnpm/lockfile-utils": "11.0.2", + "@pnpm/merge-lockfile-changes": "6.0.3", + "@pnpm/types": "10.1.1", "@pnpm/util.lex-comparator": "3.0.0", "@zkochan/rimraf": "^2.1.3", "comver-to-semver": "^1.0.0", "js-yaml": "npm:@zkochan/js-yaml@0.0.7", "normalize-path": "^3.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0", + "semver": "^7.6.2", "sort-keys": "^4.2.0", "strip-bom": "^4.0.0", "write-file-atomic": "^5.0.1" @@ -2668,11 +2671,11 @@ } }, "node_modules/@pnpm/lockfile-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-6.0.0.tgz", - "integrity": "sha512-a4/ULIPLZIIq8Qmi2HEoFgRTtEouGU5RNhuGDxnSmkxu1BjlNMNjLJeEI5jzMZCGOjBoML+AirY/XOO3bcEQ/w==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-7.1.1.tgz", + "integrity": "sha512-ODa3AiqOT/DbFOb+oRpfvB78pJOcrIaCQON30Y2Z/qS7Gs66trTMNl37KabnNTvAEpyvlVGw8rli215fFh1fSA==", "dependencies": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "10.1.1" }, "engines": { "node": ">=18.12" @@ -2682,15 +2685,15 @@ } }, "node_modules/@pnpm/lockfile-utils": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-10.1.1.tgz", - "integrity": "sha512-Zl5S1WW3Fk8SFjzjuV8jog7VYtPC+RMcsLpvmgFUDyMy/IRG1x2vQ7m3BY1SpmfRLf4XqxACRwKBlbjlRrVY4Q==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-11.0.2.tgz", + "integrity": "sha512-hbsLB52+/zK9ae4JPR8EnT5K6bB2eBaEx1Mei4IVxtvIRnj6XU7C95PXBX/4QmsRIkpr3PGaKg2GiQALE22WNg==", "dependencies": { - "@pnpm/dependency-path": "4.0.0", - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/dependency-path": "5.1.1", + "@pnpm/lockfile-types": "7.1.1", "@pnpm/pick-fetcher": "3.0.0", - "@pnpm/resolver-base": "12.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/resolver-base": "12.0.2", + "@pnpm/types": "10.1.1", "get-npm-tarball-url": "^2.1.0", "ramda": "npm:@pnpm/ramda@0.28.1" }, @@ -2715,14 +2718,15 @@ } }, "node_modules/@pnpm/merge-lockfile-changes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.0.tgz", - "integrity": "sha512-K9ARTZ+o/EZ10RPZY4dftlSnvPgJrVeOG0QwZLNTb9Z9q8D6EqSVwEh7CxDobGFe5FAj2lkDK6DY7EgPI4hhdw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.3.tgz", + "integrity": "sha512-XFd+c1PzNK5TkLAqaG90RyA0RFIpwhvFxprEDxMVnu9d5Gq8ws4TWz0vy5oTZuKTPODmN7i01xZzEV2/+0KrOw==", "dependencies": { - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/lockfile-types": "7.1.1", + "@pnpm/types": "10.1.1", "comver-to-semver": "^1.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0" + "semver": "^7.6.2" }, "engines": { "node": ">=18.12" @@ -2732,17 +2736,17 @@ } }, "node_modules/@pnpm/network.agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-1.0.1.tgz", - "integrity": "sha512-yRm8MzpZvst5IYF5IUgK7q5SvcncCUWOVBqpl527Pz6BafmDlcxAYyFy7lV4AiQr+VZ9VWudQsaHQeaYikyDGw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-2.0.0.tgz", + "integrity": "sha512-CqONDs5W6vaAdgQEHyFSr4vj25Pv8eVzwI+oUvId/FBHOcTCgHndLIJGON39JnyQS40+yT9kpEj21la3rcJK2w==", "dependencies": { - "@pnpm/network.config": "1.0.1", - "@pnpm/network.proxy-agent": "1.0.1", + "@pnpm/network.config": "2.0.0", + "@pnpm/network.proxy-agent": "2.0.0", "agentkeepalive": "4.2.1", "lru-cache": "7.10.1" }, "engines": { - "node": ">=12.22.0" + "node": ">=18.12" } }, "node_modules/@pnpm/network.agent/node_modules/lru-cache": { @@ -2754,54 +2758,29 @@ } }, "node_modules/@pnpm/network.config": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-1.0.1.tgz", - "integrity": "sha512-ZmTsSFxd4QT5+IZvwHtQjzSlkB7OXAty6MfSenRyHOvR1f8j3l1VDWVXJiNaiLrKeidiZH6ADfsMTr2N0CGDeA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-2.0.0.tgz", + "integrity": "sha512-DpTQTz4KBUgR0NNo/+/WXFlE4dy4+vgINhR9Eb+qo/Kb9RzGbhTN0ypv3sRYa6YG4UO5ft47rvEtHJ9i6VBwzA==", "dependencies": { "nerf-dart": "^1.0.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=18.12" } }, "node_modules/@pnpm/network.proxy-agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-1.0.1.tgz", - "integrity": "sha512-0q9Btpw43aTPzEJJmQY1TNBrwNlPINRae8EpO7VpqbmFflBRO6u6qady6XFfbi+wwPxpcpVOYr6rCDBzALXYHA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-2.0.0.tgz", + "integrity": "sha512-gCShibUggQS1vveAzr84PhDvwoChR4HrHHdvTB8CqXHQu12eoXO8R01awalZWERrHL3fDkUQcqLqCospm2O/QQ==", "dependencies": { - "@pnpm/error": "^4.0.0", + "@pnpm/error": "^6.0.0", "http-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.1", "lru-cache": "7.10.1", "socks-proxy-agent": "6.1.1" }, "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.proxy-agent/node_modules/@pnpm/constants": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@pnpm/constants/-/constants-6.2.0.tgz", - "integrity": "sha512-GlDVUkeTR2WK0oZAM+wtDY6RBMLw6b0Z/5qKgBbDszx4e+R7CHyfG7JofyypogRCfeWXeAXp2C2FkFTh+sNgIg==", - "engines": { - "node": ">=14.6" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/network.proxy-agent/node_modules/@pnpm/error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/error/-/error-4.0.1.tgz", - "integrity": "sha512-6UFakGqUDhnZVzYCfN+QaG1epxtBVS1M9mb9RzoBuvWxcimBYTT04fdYuyk1Nay8y/TvAVl3AVB/lCziWG0+2w==", - "dependencies": { - "@pnpm/constants": "6.2.0" - }, - "engines": { - "node": ">=14.6" - }, - "funding": { - "url": "https://opencollective.com/pnpm" + "node": ">=18.12" } }, "node_modules/@pnpm/network.proxy-agent/node_modules/lru-cache": { @@ -2824,11 +2803,11 @@ } }, "node_modules/@pnpm/resolver-base": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-12.0.0.tgz", - "integrity": "sha512-R5FmojIoHRIC8hZDyr6a9SM6TkpAQXQXgq5QrycUwknRvGjTnrOFD5JaTzMZohcfFg6TWdA3sp3B0w/mhj98Rg==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-12.0.2.tgz", + "integrity": "sha512-6Ged4cfUI+2RR6b/quphvuN8Tu+Sp0giMp9tqxqd8ls7P+A9qXGX6ATHUTl3jGfuOERYUWeYWrRrvxMmnYLy/g==", "dependencies": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "10.1.1" }, "engines": { "node": ">=18.12" @@ -2838,9 +2817,9 @@ } }, "node_modules/@pnpm/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-10.0.0.tgz", - "integrity": "sha512-P608MRTOExt5BkIN2hsrb/ycEchwaPW/x80ujJUAqxKZSXNVAOrlEu3KJ+2+jTCunyWmo/EcE01ZdwCw8jgVrQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-10.1.1.tgz", + "integrity": "sha512-xF8/Trk+ucZa2rUwEk1WgMtlfWUQN5bu6bGHCho+suN2pYrTy+vN+HgZ2SO1oa+6WoyuN5yllMMADOEXaHTOmA==", "engines": { "node": ">=18.12" }, @@ -3159,9 +3138,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3191,9 +3170,9 @@ } }, "node_modules/agent-base/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -3225,9 +3204,9 @@ } }, "node_modules/agentkeepalive/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -3647,9 +3626,9 @@ } }, "node_modules/bole": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.3.tgz", - "integrity": "sha512-4o8wk9dlpU0e69sXhIsPIaFfXgOvj6en2GgZkG8hadkqNEqYKcz9Y70ijg7Kjq9hz2prJkWXljca5OBJZ451xg==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.13.tgz", + "integrity": "sha512-JQ3xWh2nYsVUuJx7ZN4fzU3vHpzceWb7CC06LUXWwdY++Hzd7Wola7zN3Ud5XgmOVoH/6KzrdMmJokol/xtejw==", "peer": true, "dependencies": { "fast-safe-stringify": "^2.0.7", @@ -4558,18 +4537,18 @@ } }, "node_modules/eslint": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.2.0.tgz", - "integrity": "sha512-0n/I88vZpCOzO+PQpt0lbsqmn9AsnsJAQseIqhZFI8ibQT0U1AkEKRxA3EVMos0BoHSXDQvCXY25TUjB5tr8Og==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "9.2.0", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint/config-array": "^0.17.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.2.3", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", @@ -4578,8 +4557,8 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", - "esquery": "^1.4.2", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -4605,7 +4584,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-config-prettier": { @@ -4824,12 +4803,12 @@ } }, "node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -4865,9 +4844,9 @@ } }, "node_modules/esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -5630,9 +5609,9 @@ } }, "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -5677,9 +5656,9 @@ } }, "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -14083,9 +14062,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -14822,9 +14801,9 @@ } }, "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -16444,10 +16423,38 @@ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true }, + "@eslint/config-array": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@eslint/eslintrc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz", - "integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -16468,9 +16475,9 @@ "dev": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "requires": { "ms": "2.1.2" @@ -16494,38 +16501,16 @@ } }, "@eslint/js": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.2.0.tgz", - "integrity": "sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true }, - "@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true }, "@humanwhocodes/module-importer": { "version": "1.0.1", @@ -16533,16 +16518,10 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, - "@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, "@humanwhocodes/retry": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.2.3.tgz", - "integrity": "sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true }, "@isaacs/cliui": { @@ -17675,11 +17654,11 @@ "integrity": "sha512-yQosGUvYPpAjb1jOFcdbwekRjZRVxN6C0hHzfRCZrMKbxGjt/E0g0RcFlEDNVZ95tm4oMMcr7nEPa7H7LX3emw==" }, "@pnpm/core-loggers": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.0.tgz", - "integrity": "sha512-nf6DWO+75llaOxZ4Wb5xIzC86jb9PEeD8y7E4bbkLCJUvv/vRVgaPO3+Fo2GFTw5ZY7cip60rTF6dUzbP9dOVw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.2.tgz", + "integrity": "sha512-UUqWV0wUrMvlNWMe8Ch+XFRI3u3K35T2rS5jofhCsqtZa9UlsJAFW4jjVzOBWkV0uVbt6mdy7IMBHvJsYaU94w==", "requires": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "10.1.1" } }, "@pnpm/crypto.base32-hash": { @@ -17691,13 +17670,13 @@ } }, "@pnpm/dependency-path": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-4.0.0.tgz", - "integrity": "sha512-d2tTvjnWJtqVjREPZa1h81i7wfQSeg7YkMc7BZAr8QJ4he5KlHY1Zmfa4LpyXVQJSV3trGfy/dmxhV2A5lo34g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-5.1.1.tgz", + "integrity": "sha512-HskPO2yVpvb8NHnfmByqdkyqSVRaSBpMUSBzfLwzYLRiexs3zo2+NjUvGErjnmVDG8KgY/iQZtw+y+06vMWy/w==", "requires": { "@pnpm/crypto.base32-hash": "3.0.0", - "@pnpm/types": "10.0.0", - "semver": "^7.6.0" + "@pnpm/types": "10.1.1", + "semver": "^7.6.2" } }, "@pnpm/error": { @@ -17709,14 +17688,14 @@ } }, "@pnpm/fetch": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.0.tgz", - "integrity": "sha512-V9khLYMUmadH45A5zZnrt1nUsZ0NokWkw0QjjgSdiBCgRyQnf1SvFjVcj4sVWxK0ZaijZQnIhIcKvlV3/zB0Ig==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.2.tgz", + "integrity": "sha512-mh81jVdVzscYZcVyVRobv5mf/xPFFLIjnEqH6+4LrTHz0HqVRJd3Oknn7vRzXSx8k+xXUH44o3qroAq8KGJoSA==", "requires": { - "@pnpm/core-loggers": "10.0.0", + "@pnpm/core-loggers": "10.0.2", "@pnpm/fetching-types": "6.0.0", - "@pnpm/network.agent": "^1.0.1", - "@pnpm/types": "10.0.0", + "@pnpm/network.agent": "^2.0.0", + "@pnpm/types": "10.1.1", "@zkochan/retry": "^0.2.0", "node-fetch": "npm:@pnpm/node-fetch@1.0.0" } @@ -17731,15 +17710,15 @@ } }, "@pnpm/git-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.1.tgz", - "integrity": "sha512-B1FtKwEEUm8130XqmX7eqgMhqdBxJ5gPrWssOLnpIlp/rvmJFsfD2P//80OjORPNFWnpfqdfBF34c/+ZCzAxZg==", + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.3.tgz", + "integrity": "sha512-/4pxvDjtcTfnv2ElUr1TECRNmnOAx23eeZNKrSTkWOyI6I+J9f5M40M+jgTcVWS6l2L+I5gwemo3XKgCnX43Ag==", "requires": { - "@pnpm/fetch": "8.0.0", - "@pnpm/resolver-base": "12.0.0", + "@pnpm/fetch": "8.0.2", + "@pnpm/resolver-base": "12.0.2", "graceful-git": "^3.1.2", "hosted-git-info": "npm:@pnpm/hosted-git-info@1.0.0", - "semver": "^7.6.0" + "semver": "^7.6.2" }, "dependencies": { "hosted-git-info": { @@ -17799,26 +17778,26 @@ } }, "@pnpm/lockfile-file": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.0.5.tgz", - "integrity": "sha512-QQFYohFy39FkAQbtDEtqVIzNu5XZhA9aonh/AM/vwvptNcfnajeBuNKfAJepdjWPg/xSBDuU96So29pkPMK8+Q==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.1.1.tgz", + "integrity": "sha512-Ybs/QCWbN38EyIdpYVviSJR9/gc6LZ1iZDJbVatPL8l4Z4J6ikWh4i32kLqJHutM8McqeFegnTrUO3f65UTajw==", "requires": { "@pnpm/constants": "8.0.0", - "@pnpm/dependency-path": "4.0.0", + "@pnpm/dependency-path": "5.1.1", "@pnpm/error": "6.0.1", - "@pnpm/git-resolver": "9.0.1", + "@pnpm/git-resolver": "9.0.3", "@pnpm/git-utils": "2.0.0", - "@pnpm/lockfile-types": "6.0.0", - "@pnpm/lockfile-utils": "10.1.1", - "@pnpm/merge-lockfile-changes": "6.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/lockfile-types": "7.1.1", + "@pnpm/lockfile-utils": "11.0.2", + "@pnpm/merge-lockfile-changes": "6.0.3", + "@pnpm/types": "10.1.1", "@pnpm/util.lex-comparator": "3.0.0", "@zkochan/rimraf": "^2.1.3", "comver-to-semver": "^1.0.0", "js-yaml": "npm:@zkochan/js-yaml@0.0.7", "normalize-path": "^3.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0", + "semver": "^7.6.2", "sort-keys": "^4.2.0", "strip-bom": "^4.0.0", "write-file-atomic": "^5.0.1" @@ -17858,23 +17837,23 @@ } }, "@pnpm/lockfile-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-6.0.0.tgz", - "integrity": "sha512-a4/ULIPLZIIq8Qmi2HEoFgRTtEouGU5RNhuGDxnSmkxu1BjlNMNjLJeEI5jzMZCGOjBoML+AirY/XOO3bcEQ/w==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-7.1.1.tgz", + "integrity": "sha512-ODa3AiqOT/DbFOb+oRpfvB78pJOcrIaCQON30Y2Z/qS7Gs66trTMNl37KabnNTvAEpyvlVGw8rli215fFh1fSA==", "requires": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "10.1.1" } }, "@pnpm/lockfile-utils": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-10.1.1.tgz", - "integrity": "sha512-Zl5S1WW3Fk8SFjzjuV8jog7VYtPC+RMcsLpvmgFUDyMy/IRG1x2vQ7m3BY1SpmfRLf4XqxACRwKBlbjlRrVY4Q==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-11.0.2.tgz", + "integrity": "sha512-hbsLB52+/zK9ae4JPR8EnT5K6bB2eBaEx1Mei4IVxtvIRnj6XU7C95PXBX/4QmsRIkpr3PGaKg2GiQALE22WNg==", "requires": { - "@pnpm/dependency-path": "4.0.0", - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/dependency-path": "5.1.1", + "@pnpm/lockfile-types": "7.1.1", "@pnpm/pick-fetcher": "3.0.0", - "@pnpm/resolver-base": "12.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/resolver-base": "12.0.2", + "@pnpm/types": "10.1.1", "get-npm-tarball-url": "^2.1.0", "ramda": "npm:@pnpm/ramda@0.28.1" } @@ -17890,23 +17869,24 @@ } }, "@pnpm/merge-lockfile-changes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.0.tgz", - "integrity": "sha512-K9ARTZ+o/EZ10RPZY4dftlSnvPgJrVeOG0QwZLNTb9Z9q8D6EqSVwEh7CxDobGFe5FAj2lkDK6DY7EgPI4hhdw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.3.tgz", + "integrity": "sha512-XFd+c1PzNK5TkLAqaG90RyA0RFIpwhvFxprEDxMVnu9d5Gq8ws4TWz0vy5oTZuKTPODmN7i01xZzEV2/+0KrOw==", "requires": { - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/lockfile-types": "7.1.1", + "@pnpm/types": "10.1.1", "comver-to-semver": "^1.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0" + "semver": "^7.6.2" } }, "@pnpm/network.agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-1.0.1.tgz", - "integrity": "sha512-yRm8MzpZvst5IYF5IUgK7q5SvcncCUWOVBqpl527Pz6BafmDlcxAYyFy7lV4AiQr+VZ9VWudQsaHQeaYikyDGw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-2.0.0.tgz", + "integrity": "sha512-CqONDs5W6vaAdgQEHyFSr4vj25Pv8eVzwI+oUvId/FBHOcTCgHndLIJGON39JnyQS40+yT9kpEj21la3rcJK2w==", "requires": { - "@pnpm/network.config": "1.0.1", - "@pnpm/network.proxy-agent": "1.0.1", + "@pnpm/network.config": "2.0.0", + "@pnpm/network.proxy-agent": "2.0.0", "agentkeepalive": "4.2.1", "lru-cache": "7.10.1" }, @@ -17919,38 +17899,25 @@ } }, "@pnpm/network.config": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-1.0.1.tgz", - "integrity": "sha512-ZmTsSFxd4QT5+IZvwHtQjzSlkB7OXAty6MfSenRyHOvR1f8j3l1VDWVXJiNaiLrKeidiZH6ADfsMTr2N0CGDeA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-2.0.0.tgz", + "integrity": "sha512-DpTQTz4KBUgR0NNo/+/WXFlE4dy4+vgINhR9Eb+qo/Kb9RzGbhTN0ypv3sRYa6YG4UO5ft47rvEtHJ9i6VBwzA==", "requires": { "nerf-dart": "^1.0.0" } }, "@pnpm/network.proxy-agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-1.0.1.tgz", - "integrity": "sha512-0q9Btpw43aTPzEJJmQY1TNBrwNlPINRae8EpO7VpqbmFflBRO6u6qady6XFfbi+wwPxpcpVOYr6rCDBzALXYHA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-2.0.0.tgz", + "integrity": "sha512-gCShibUggQS1vveAzr84PhDvwoChR4HrHHdvTB8CqXHQu12eoXO8R01awalZWERrHL3fDkUQcqLqCospm2O/QQ==", "requires": { - "@pnpm/error": "^4.0.0", + "@pnpm/error": "^6.0.0", "http-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.1", "lru-cache": "7.10.1", "socks-proxy-agent": "6.1.1" }, "dependencies": { - "@pnpm/constants": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@pnpm/constants/-/constants-6.2.0.tgz", - "integrity": "sha512-GlDVUkeTR2WK0oZAM+wtDY6RBMLw6b0Z/5qKgBbDszx4e+R7CHyfG7JofyypogRCfeWXeAXp2C2FkFTh+sNgIg==" - }, - "@pnpm/error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/error/-/error-4.0.1.tgz", - "integrity": "sha512-6UFakGqUDhnZVzYCfN+QaG1epxtBVS1M9mb9RzoBuvWxcimBYTT04fdYuyk1Nay8y/TvAVl3AVB/lCziWG0+2w==", - "requires": { - "@pnpm/constants": "6.2.0" - } - }, "lru-cache": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", @@ -17964,17 +17931,17 @@ "integrity": "sha512-2eisylRAU/jeuxFEPnS1gjLZKJGbYc4QEtEW6MVUYjO4Xi+2ttkSm7825S0J5IPpUIvln8HYPCUS0eQWSfpOaQ==" }, "@pnpm/resolver-base": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-12.0.0.tgz", - "integrity": "sha512-R5FmojIoHRIC8hZDyr6a9SM6TkpAQXQXgq5QrycUwknRvGjTnrOFD5JaTzMZohcfFg6TWdA3sp3B0w/mhj98Rg==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-12.0.2.tgz", + "integrity": "sha512-6Ged4cfUI+2RR6b/quphvuN8Tu+Sp0giMp9tqxqd8ls7P+A9qXGX6ATHUTl3jGfuOERYUWeYWrRrvxMmnYLy/g==", "requires": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "10.1.1" } }, "@pnpm/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-10.0.0.tgz", - "integrity": "sha512-P608MRTOExt5BkIN2hsrb/ycEchwaPW/x80ujJUAqxKZSXNVAOrlEu3KJ+2+jTCunyWmo/EcE01ZdwCw8jgVrQ==" + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-10.1.1.tgz", + "integrity": "sha512-xF8/Trk+ucZa2rUwEk1WgMtlfWUQN5bu6bGHCho+suN2pYrTy+vN+HgZ2SO1oa+6WoyuN5yllMMADOEXaHTOmA==" }, "@pnpm/util.lex-comparator": { "version": "3.0.0", @@ -18235,9 +18202,9 @@ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==" }, "acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true }, "acorn-jsx": { @@ -18256,9 +18223,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -18281,9 +18248,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -18607,9 +18574,9 @@ } }, "bole": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.3.tgz", - "integrity": "sha512-4o8wk9dlpU0e69sXhIsPIaFfXgOvj6en2GgZkG8hadkqNEqYKcz9Y70ijg7Kjq9hz2prJkWXljca5OBJZ451xg==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.13.tgz", + "integrity": "sha512-JQ3xWh2nYsVUuJx7ZN4fzU3vHpzceWb7CC06LUXWwdY++Hzd7Wola7zN3Ud5XgmOVoH/6KzrdMmJokol/xtejw==", "peer": true, "requires": { "fast-safe-stringify": "^2.0.7", @@ -19302,18 +19269,18 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.2.0.tgz", - "integrity": "sha512-0n/I88vZpCOzO+PQpt0lbsqmn9AsnsJAQseIqhZFI8ibQT0U1AkEKRxA3EVMos0BoHSXDQvCXY25TUjB5tr8Og==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "9.2.0", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint/config-array": "^0.17.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.2.3", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", @@ -19322,8 +19289,8 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", - "esquery": "^1.4.2", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -19482,12 +19449,12 @@ "dev": true }, "espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "requires": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -19506,9 +19473,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -20091,9 +20058,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -20125,9 +20092,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -26371,9 +26338,9 @@ "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" }, "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true }, "pretty-format": { @@ -26905,9 +26872,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } diff --git a/npm_and_yarn/helpers/package.json b/npm_and_yarn/helpers/package.json index 3e978949edb..5b4a2687088 100644 --- a/npm_and_yarn/helpers/package.json +++ b/npm_and_yarn/helpers/package.json @@ -15,15 +15,15 @@ "detect-indent": "^6.1.0", "nock": "^13.5.4", "npm": "6.14.18", - "@pnpm/lockfile-file": "^9.0.5", - "@pnpm/dependency-path": "^4.0.0", + "@pnpm/lockfile-file": "^9.1.1", + "@pnpm/dependency-path": "^5.1.1", "semver": "^7.6.2", "patch-package": "^8.0.0" }, "devDependencies": { - "eslint": "^9.2.0", + "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", - "prettier": "^3.2.5" + "prettier": "^3.3.2" } } diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb index 09b0465c710..a7fd7daa4c6 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/errors" require "dependabot/logger" require "dependabot/npm_and_yarn/file_parser" @@ -15,9 +17,20 @@ module Dependabot module NpmAndYarn class FileUpdater < Dependabot::FileUpdaters::Base class NpmLockfileUpdater + extend T::Sig + require_relative "npmrc_builder" require_relative "package_json_updater" + sig do + params( + lockfile: Dependabot::DependencyFile, + dependencies: T::Array[Dependabot::Dependency], + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Credential] + ) + .void + end def initialize(lockfile:, dependencies:, dependency_files:, credentials:) @lockfile = lockfile @dependencies = dependencies @@ -25,6 +38,7 @@ def initialize(lockfile:, dependencies:, dependency_files:, credentials:) @credentials = credentials end + sig { returns(Dependabot::DependencyFile) } def updated_lockfile updated_file = lockfile.dup updated_file.content = updated_lockfile_content @@ -33,9 +47,16 @@ def updated_lockfile private + sig { returns(Dependabot::DependencyFile) } attr_reader :lockfile + + sig { returns(T::Array[Dependabot::Dependency]) } attr_reader :dependencies + + sig { returns(T::Array[Dependabot::DependencyFile]) } attr_reader :dependency_files + + sig { returns(T::Array[Credential]) } attr_reader :credentials UNREACHABLE_GIT = /fatal: repository '(?.*)' not found/ @@ -54,44 +75,54 @@ def updated_lockfile NPM8_MISSING_GIT_REF = /already exists and is not an empty directory/ NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/ + sig { returns(T.nilable(String)) } def updated_lockfile_content return lockfile.content if npmrc_disables_lockfile? return lockfile.content unless updatable_dependencies.any? - @updated_lockfile_content ||= + @updated_lockfile_content ||= T.let( SharedHelpers.in_a_temporary_directory do write_temporary_dependency_files updated_files = Dir.chdir(lockfile_directory) { run_current_npm_update } updated_lockfile_content = updated_files.fetch(lockfile_basename) post_process_npm_lockfile(updated_lockfile_content) - end + end, + T.nilable(String) + ) rescue SharedHelpers::HelperSubprocessFailed => e handle_npm_updater_error(e) end + sig { returns(T::Array[Dependabot::Dependency]) } def top_level_dependencies dependencies.select(&:top_level?) end + sig { returns(T::Array[Dependabot::Dependency]) } def sub_dependencies dependencies.reject(&:top_level?) end + sig { returns(T::Array[Dependabot::Dependency]) } def updatable_dependencies dependencies.reject do |dependency| dependency_up_to_date?(dependency) || top_level_dependency_update_not_required?(dependency) end end + sig { returns(T::Array[Dependabot::Dependency]) } def lockfile_dependencies - @lockfile_dependencies ||= + @lockfile_dependencies ||= T.let( NpmAndYarn::FileParser.new( dependency_files: [lockfile, *package_files], source: nil, credentials: credentials - ).parse + ).parse, + T.nilable(T::Array[Dependabot::Dependency]) + ) end + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def dependency_up_to_date?(dependency) existing_dep = lockfile_dependencies.find { |dep| dep.name == dependency.name } @@ -101,7 +132,7 @@ def dependency_up_to_date?(dependency) # (likely it is no longer required) return !dependency.top_level? if existing_dep.nil? - existing_dep&.version == dependency.version + existing_dep.version == dependency.version end # NOTE: Prevent changes to npm 6 lockfiles when the dependency has been @@ -109,16 +140,19 @@ def dependency_up_to_date?(dependency) # proj). npm 7 introduces workspace support so we explicitly want to # update the root lockfile and check if the dependency is in the # lockfile + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def top_level_dependency_update_not_required?(dependency) dependency.top_level? && !dependency_in_package_json?(dependency) && !dependency_in_lockfile?(dependency) end + sig { returns(T::Hash[String, String]) } def run_current_npm_update run_npm_updater(top_level_dependencies: top_level_dependencies, sub_dependencies: sub_dependencies) end + sig { returns(T::Hash[String, String]) } def run_previous_npm_update previous_top_level_dependencies = top_level_dependencies.map do |d| Dependabot::Dependency.new( @@ -126,7 +160,7 @@ def run_previous_npm_update package_manager: d.package_manager, version: d.previous_version, previous_version: d.previous_version, - requirements: d.previous_requirements, + requirements: T.must(d.previous_requirements), previous_requirements: d.previous_requirements ) end @@ -146,35 +180,47 @@ def run_previous_npm_update sub_dependencies: previous_sub_dependencies) end + sig do + params( + top_level_dependencies: T::Array[Dependabot::Dependency], + sub_dependencies: T::Array[Dependabot::Dependency] + ) + .returns(T::Hash[String, String]) + end def run_npm_updater(top_level_dependencies:, sub_dependencies:) SharedHelpers.with_git_configured(credentials: credentials) do - updated_files = {} + updated_files = T.let({}, T::Hash[String, String]) if top_level_dependencies.any? updated_files.merge!(run_npm_top_level_updater(top_level_dependencies: top_level_dependencies)) end if sub_dependencies.any? - updated_files.merge!(run_npm_subdependency_updater(sub_dependencies: sub_dependencies)) + updated_files.merge!(T.must(run_npm_subdependency_updater(sub_dependencies: sub_dependencies))) end updated_files end end + sig { params(top_level_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) } def run_npm_top_level_updater(top_level_dependencies:) if npm8? run_npm8_top_level_updater(top_level_dependencies: top_level_dependencies) else - SharedHelpers.run_helper_subprocess( - command: NativeHelpers.helper_path, - function: "npm6:update", - args: [ - Dir.pwd, - lockfile_basename, - top_level_dependencies.map(&:to_h) - ] + T.cast( + SharedHelpers.run_helper_subprocess( + command: NativeHelpers.helper_path, + function: "npm6:update", + args: [ + Dir.pwd, + lockfile_basename, + top_level_dependencies.map(&:to_h) + ] + ), + T::Hash[String, String] ) end end + sig { params(top_level_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) } def run_npm8_top_level_updater(top_level_dependencies:) dependencies_in_current_package_json = top_level_dependencies.any? do |dependency| dependency_in_package_json?(dependency) @@ -186,7 +232,7 @@ def run_npm8_top_level_updater(top_level_dependencies:) # lockfile. To overcome this, we save the content before the update, # and then re-run `npm install` after the update against the previous # content to remove that - previous_package_json = File.read(package_json.name) + previous_package_json = File.read(T.must(package_json).name) end # TODO: Update the npm 6 updater to use these args as we currently @@ -194,10 +240,10 @@ def run_npm8_top_level_updater(top_level_dependencies:) # the npm 7 rollout install_args = top_level_dependencies.map { |dependency| npm_install_args(dependency) } - run_npm_install_lockfile_only(*install_args) + run_npm_install_lockfile_only(install_args) unless dependencies_in_current_package_json - File.write(package_json.name, previous_package_json) + File.write(T.must(package_json).name, previous_package_json) run_npm_install_lockfile_only end @@ -205,24 +251,32 @@ def run_npm8_top_level_updater(top_level_dependencies:) { lockfile_basename => File.read(lockfile_basename) } end + sig do + params(sub_dependencies: T::Array[Dependabot::Dependency]).returns(T.nilable(T::Hash[String, String])) + end def run_npm_subdependency_updater(sub_dependencies:) if npm8? run_npm8_subdependency_updater(sub_dependencies: sub_dependencies) else - SharedHelpers.run_helper_subprocess( - command: NativeHelpers.helper_path, - function: "npm6:updateSubdependency", - args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)] + T.cast( + SharedHelpers.run_helper_subprocess( + command: NativeHelpers.helper_path, + function: "npm6:updateSubdependency", + args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)] + ), + T.nilable(T::Hash[String, String]) ) end end + sig { params(sub_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) } def run_npm8_subdependency_updater(sub_dependencies:) dependency_names = sub_dependencies.map(&:name) NativeHelpers.run_npm8_subdependency_update_command(dependency_names) { lockfile_basename => File.read(lockfile_basename) } end + sig { params(dependency: Dependabot::Dependency).returns(T.nilable(String)) } def updated_version_requirement_for_dependency(dependency) flattenend_manifest_dependencies[dependency.name] end @@ -231,13 +285,14 @@ def updated_version_requirement_for_dependency(dependency) # instead of fishing it out of the updated package json, we need to do # this because we don't store the same requirement in # Dependency#requirements for git dependencies - see PackageJsonUpdater + sig { returns(T::Hash[String, String]) } def flattenend_manifest_dependencies - return @flattenend_manifest_dependencies if defined?(@flattenend_manifest_dependencies) - - @flattenend_manifest_dependencies = + @flattenend_manifest_dependencies ||= T.let( NpmAndYarn::FileParser::DEPENDENCY_TYPES.inject({}) do |deps, type| deps.merge(parsed_package_json[type] || {}) - end + end, + T.nilable(T::Hash[String, String]) + ) end # Runs `npm install` with `--package-lock-only` flag to update the @@ -249,7 +304,8 @@ def flattenend_manifest_dependencies # to work around an issue in npm 6, we don't want that here # - `--ignore-scripts` disables prepare and prepack scripts which are # run when installing git dependencies - def run_npm_install_lockfile_only(*install_args) + sig { params(install_args: T::Array[String]).returns(String) } + def run_npm_install_lockfile_only(install_args = []) command = [ "install", *install_args, @@ -273,6 +329,7 @@ def run_npm_install_lockfile_only(*install_args) Helpers.run_npm_command(command, fingerprint: fingerprint) end + sig { params(dependency: Dependabot::Dependency).returns(String) } def npm_install_args(dependency) git_requirement = dependency.requirements.find { |req| req[:source] && req[:source][:type] == "git" } @@ -302,12 +359,14 @@ def npm_install_args(dependency) end end + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def dependency_in_package_json?(dependency) dependency.requirements.any? do |req| - req[:file] == package_json.name + req[:file] == T.must(package_json).name end end + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def dependency_in_lockfile?(dependency) lockfile_dependencies.any? do |dep| dep.name == dependency.name @@ -318,13 +377,14 @@ def dependency_in_lockfile?(dependency) # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity # rubocop:disable Metrics/MethodLength + sig { params(error: Exception).returns(T.noreturn) } def handle_npm_updater_error(error) error_message = error.message if error_message.match?(MISSING_PACKAGE) - package_name = error_message.match(MISSING_PACKAGE) - .named_captures["package_req"] - sanitized_name = sanitize_package_name(package_name) - sanitized_error = error_message.gsub(package_name, sanitized_name) + package_name = T.must(error_message.match(MISSING_PACKAGE)) + .named_captures["package_req"] + sanitized_name = sanitize_package_name(T.must(package_name)) + sanitized_error = error_message.gsub(T.must(package_name), sanitized_name) handle_missing_package(sanitized_name, sanitized_error) end @@ -370,26 +430,26 @@ def handle_npm_updater_error(error) end if error_message.match?(FORBIDDEN_PACKAGE) - package_name = error_message.match(FORBIDDEN_PACKAGE) - .named_captures["package_req"] - sanitized_name = sanitize_package_name(package_name) - sanitized_error = error_message.gsub(package_name, sanitized_name) + package_name = T.must(error_message.match(FORBIDDEN_PACKAGE)) + .named_captures["package_req"] + sanitized_name = sanitize_package_name(T.must(package_name)) + sanitized_error = error_message.gsub(T.must(package_name), sanitized_name) handle_missing_package(sanitized_name, sanitized_error) end # Some private registries return a 403 when the user is readonly if error_message.match?(FORBIDDEN_PACKAGE_403) - package_name = error_message.match(FORBIDDEN_PACKAGE_403) - .named_captures["package_req"] - sanitized_name = sanitize_package_name(package_name) - sanitized_error = error_message.gsub(package_name, sanitized_name) + package_name = T.must(error_message.match(FORBIDDEN_PACKAGE_403)) + .named_captures["package_req"] + sanitized_name = sanitize_package_name(T.must(package_name)) + sanitized_error = error_message.gsub(T.must(package_name), sanitized_name) handle_missing_package(sanitized_name, sanitized_error) end if (git_error = error_message.match(UNREACHABLE_GIT) || error_message.match(FORBIDDEN_GIT)) dependency_url = git_error.named_captures.fetch("url") - raise Dependabot::GitDependenciesNotReachable, dependency_url + raise Dependabot::GitDependenciesNotReachable, T.must(dependency_url) end # This error happens when the lockfile has been messed up and some @@ -421,7 +481,7 @@ def handle_npm_updater_error(error) end if error_message.include?("EBADENGINE") - msg = "Dependabot uses Node.js #{`node --version`} and NPM #{`npm --version`}. " \ + msg = "Dependabot uses Node.js #{`node --version`.strip} and NPM #{`npm --version`.strip}. " \ "Due to the engine-strict setting, the update will not succeed." raise Dependabot::DependencyFileNotResolvable, msg end @@ -433,6 +493,7 @@ def handle_npm_updater_error(error) # rubocop:enable Metrics/PerceivedComplexity # rubocop:enable Metrics/MethodLength + sig { params(error_message: String).returns(T.noreturn) } def raise_resolvability_error(error_message) dependency_names = dependencies.map(&:name).join(", ") msg = "Error whilst updating #{dependency_names} in " \ @@ -440,6 +501,7 @@ def raise_resolvability_error(error_message) raise Dependabot::DependencyFileNotResolvable, msg end + sig { params(error_message: String).returns(T.noreturn) } def raise_missing_lockfile_version_resolvability_error(error_message) modules_path = File.join(lockfile_directory, "node_modules") # NOTE: don't include the dependency names to prevent opening @@ -456,6 +518,7 @@ def raise_missing_lockfile_version_resolvability_error(error_message) raise Dependabot::DependencyFileNotResolvable, msg end + sig { params(package_name: String, error_message: String).void } def handle_missing_package(package_name, error_message) missing_dep = lockfile_dependencies.find { |dep| dep.name == package_name } @@ -474,10 +537,9 @@ def handle_missing_package(package_name, error_message) raise Dependabot::PrivateSourceAuthenticationFailure, reg end + sig { returns(T::Boolean) } def resolvable_before_update? - return @resolvable_before_update if defined?(@resolvable_before_update) - - @resolvable_before_update = + @resolvable_before_update ||= T.let( begin SharedHelpers.in_a_temporary_directory do write_temporary_dependency_files(update_package_json: false) @@ -487,18 +549,22 @@ def resolvable_before_update? true rescue SharedHelpers::HelperSubprocessFailed false - end + end, + T.nilable(T::Boolean) + ) end + sig { params(error_message: String).returns(T::Boolean) } def dependencies_in_error_message?(error_message) names = dependencies.map { |dep| dep.name.split("/").first } # Example format: No matching version found for # @dependabot/dummy-pkg-b@^1.3.0 names.any? do |name| - error_message.match?(%r{#{Regexp.quote(name)}[\/@]}) + error_message.match?(%r{#{Regexp.quote(T.must(name))}[\/@]}) end end + sig { params(update_package_json: T::Boolean).void } def write_temporary_dependency_files(update_package_json: true) write_lockfiles @@ -508,12 +574,13 @@ def write_temporary_dependency_files(update_package_json: true) path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) - updated_content = + updated_content = T.must( if update_package_json && top_level_dependencies.any? updated_package_json_content(file) else file.content end + ) package_json_preparer = package_json_preparer(updated_content) @@ -532,6 +599,7 @@ def write_temporary_dependency_files(update_package_json: true) end end + sig { void } def write_lockfiles excluded_lock = case lockfile.name @@ -549,8 +617,9 @@ def write_lockfiles # Takes a JSON string and detects if it is spaces or tabs and how many # levels deep it is indented. + sig { params(json: String).returns(String) } def detect_indentation(json) - indentation = json.scan(/^[[:blank:]]+/).min_by(&:length) + indentation = T.cast(json.scan(/^[[:blank:]]+/).min_by(&:length), T.nilable(String)) return "" if indentation.nil? # let npm set the default if we can't detect any indentation indentation_size = indentation.length @@ -559,6 +628,7 @@ def detect_indentation(json) indentation_type * indentation_size end + sig { params(content: String).returns(String) } def lock_git_deps(content) return content if git_dependencies_to_lock.empty? @@ -574,33 +644,35 @@ def lock_git_deps(content) JSON.pretty_generate(json, indent: indent) end + sig { returns(T::Hash[String, T.untyped]) } def git_dependencies_to_lock return {} unless package_locks.any? return @git_dependencies_to_lock if @git_dependencies_to_lock - @git_dependencies_to_lock = {} + @git_dependencies_to_lock = T.let({}, T.nilable(T::Hash[String, T.untyped])) dependency_names = dependencies.map(&:name) package_locks.each do |package_lock| - parsed_lockfile = JSON.parse(package_lock.content) + parsed_lockfile = JSON.parse(T.must(package_lock.content)) parsed_lockfile.fetch("dependencies", {}).each do |nm, details| next if dependency_names.include?(nm) next unless details["version"] next unless details["version"].start_with?("git") - @git_dependencies_to_lock[nm] = { + T.must(@git_dependencies_to_lock)[nm] = { version: details["version"], from: details["from"] } end end - @git_dependencies_to_lock + T.must(@git_dependencies_to_lock) end # When a package.json version requirement is set to `latest`, npm will # always try to update these dependencies when doing an `npm install`, # regardless of lockfile version. Prevent any unrelated updates by # changing the version requirement to `*` while updating the lockfile. + sig { params(content: String).returns(String) } def lock_deps_with_latest_reqs(content) json = JSON.parse(content) @@ -614,14 +686,17 @@ def lock_deps_with_latest_reqs(content) JSON.pretty_generate(json, indent: indent) end + sig { returns(T::Array[String]) } def git_ssh_requirements_to_swap - return @git_ssh_requirements_to_swap if @git_ssh_requirements_to_swap - - @git_ssh_requirements_to_swap = package_files.flat_map do |file| - package_json_preparer(file.content).swapped_ssh_requirements - end + @git_ssh_requirements_to_swap ||= T.let( + package_files.flat_map do |file| + package_json_preparer(T.must(file.content)).swapped_ssh_requirements + end, + T.nilable(T::Array[String]) + ) end + sig { params(updated_lockfile_content: String).returns(String) } def post_process_npm_lockfile(updated_lockfile_content) # Switch SSH requirements back for git dependencies updated_lockfile_content = replace_swapped_git_ssh_requirements(updated_lockfile_content) @@ -648,6 +723,13 @@ def post_process_npm_lockfile(updated_lockfile_content) replace_tarball_urls(updated_lockfile_content) end + sig do + params( + updated_lockfile_content: String, + parsed_updated_lockfile_content: T::Hash[String, T.untyped] + ) + .returns(String) + end def replace_project_name(updated_lockfile_content, parsed_updated_lockfile_content) current_name = parsed_updated_lockfile_content["name"] original_name = parsed_lockfile["name"] @@ -659,6 +741,13 @@ def replace_project_name(updated_lockfile_content, parsed_updated_lockfile_conte updated_lockfile_content end + sig do + params( + updated_lockfile_content: String, + parsed_updated_lockfile_content: T::Hash[String, T.untyped] + ) + .returns(String) + end def restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_content) return updated_lockfile_content unless npm8? @@ -684,6 +773,14 @@ def restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_cont updated_lockfile_content end + sig do + params( + current_name: String, + original_name: String, + updated_lockfile_content: String + ) + .returns(String) + end def replace_lockfile_name_attribute(current_name, original_name, updated_lockfile_content) updated_lockfile_content.sub( /"name":\s"#{current_name}"/, @@ -691,6 +788,14 @@ def replace_lockfile_name_attribute(current_name, original_name, updated_lockfil ) end + sig do + params( + current_name: String, + original_name: String, + updated_lockfile_content: String + ) + .returns(String) + end def replace_lockfile_packages_name_attribute(current_name, original_name, updated_lockfile_content) packages_key_line = '"": {' updated_lockfile_content.sub( @@ -699,6 +804,7 @@ def replace_lockfile_packages_name_attribute(current_name, original_name, update ) end + sig { params(current_name: String, updated_lockfile_content: String).returns(String) } def remove_lockfile_packages_name_attribute(current_name, updated_lockfile_content) packages_key_line = '"": {' updated_lockfile_content.gsub(/(#{packages_key_line})[\n\s]+"name":\s"#{current_name}",/, '\1') @@ -713,6 +819,13 @@ def remove_lockfile_packages_name_attribute(current_name, updated_lockfile_conte # `package.json` requirement for eslint at `^1.0.0`, in which case we # need to copy this from the manifest to the lockfile after the update # has finished. + sig do + params( + updated_lockfile_content: String, + parsed_updated_lockfile_content: T::Hash[String, T.untyped] + ) + .returns(String) + end def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated_lockfile_content) return updated_lockfile_content unless npm8? @@ -732,6 +845,7 @@ def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated updated_lockfile_content end + sig { params(updated_lockfile_content: String).returns(String) } def replace_swapped_git_ssh_requirements(updated_lockfile_content) git_ssh_requirements_to_swap.each do |req| new_r = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'git+https://\1/') @@ -742,6 +856,7 @@ def replace_swapped_git_ssh_requirements(updated_lockfile_content) updated_lockfile_content end + sig { params(updated_lockfile_content: String).returns(String) } def replace_locked_git_dependencies(updated_lockfile_content) # Switch from details back for git dependencies (they will have # changed because we locked them) @@ -766,6 +881,7 @@ def replace_locked_git_dependencies(updated_lockfile_content) updated_lockfile_content end + sig { params(updated_lockfile_content: String).returns(String) } def replace_tarball_urls(updated_lockfile_content) tarball_urls.each do |url| trimmed_url = url.gsub(/(\d+\.)*tgz$/, "") @@ -783,9 +899,10 @@ def replace_tarball_urls(updated_lockfile_content) updated_lockfile_content end + sig { returns(T::Array[String]) } def tarball_urls all_urls = [*package_locks, *shrinkwraps].flat_map do |file| - file.content.scan(/"resolved":\s+"(.*)\"/).flatten + T.must(file.content).scan(/"resolved":\s+"(.*)\"/).flatten end all_urls.uniq! { |url| url.gsub(/(\d+\.)*tgz$/, "") } @@ -800,6 +917,7 @@ def tarball_urls end end + sig { returns(String) } def npmrc_content NpmrcBuilder.new( credentials: credentials, @@ -807,73 +925,106 @@ def npmrc_content ).npmrc_content end + sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) } def updated_package_json_content(file) - @updated_package_json_content ||= {} - @updated_package_json_content[file.name] ||= + @updated_package_json_content ||= T.let( + {}, + T.nilable(T::Hash[String, T.nilable(String)]) + ) + @updated_package_json_content[file.name] ||= T.let( PackageJsonUpdater.new( package_json: file, dependencies: top_level_dependencies - ).updated_package_json.content + ).updated_package_json.content, + T.nilable(String) + ) end + sig { params(content: String).returns(Dependabot::NpmAndYarn::FileUpdater::PackageJsonPreparer) } def package_json_preparer(content) - @package_json_preparer ||= {} + @package_json_preparer ||= T.let( + {}, + T.nilable(T::Hash[String, Dependabot::NpmAndYarn::FileUpdater::PackageJsonPreparer]) + ) @package_json_preparer[content] ||= PackageJsonPreparer.new( package_json_content: content ) end + sig { returns(T::Boolean) } def npmrc_disables_lockfile? npmrc_content.match?(/^package-lock\s*=\s*false/) end + sig { returns(T::Boolean) } def npm8? - return @npm8 if defined?(@npm8) + return T.must(@npm8) if defined?(@npm8) - @npm8 = Dependabot::NpmAndYarn::Helpers.npm8?(lockfile) + @npm8 ||= T.let( + Dependabot::NpmAndYarn::Helpers.npm8?(lockfile), + T.nilable(T::Boolean) + ) end + sig { params(package_name: String).returns(String) } def sanitize_package_name(package_name) package_name.gsub("%2f", "/").gsub("%2F", "/") end + sig { returns(String) } def lockfile_directory Pathname.new(lockfile.name).dirname.to_s end + sig { returns(String) } def lockfile_basename Pathname.new(lockfile.name).basename.to_s end + sig { returns(T::Hash[String, T.untyped]) } def parsed_lockfile - @parsed_lockfile ||= JSON.parse(lockfile.content) + @parsed_lockfile ||= T.let( + JSON.parse(T.must(lockfile.content)), + T.nilable(T::Hash[String, T.untyped]) + ) end + sig { returns(T::Hash[String, T.untyped]) } def parsed_package_json return {} unless package_json - return @parsed_package_json if defined?(@parsed_package_json) - @parsed_package_json = JSON.parse(updated_package_json_content(package_json)) + @parsed_package_json ||= T.let( + JSON.parse(T.must(updated_package_json_content(T.must(package_json)))), + T.nilable(T::Hash[String, T.untyped]) + ) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def package_json package_name = lockfile.name.sub(lockfile_basename, "package.json") package_files.find { |f| f.name == package_name } end + sig { returns(T::Array[Dependabot::DependencyFile]) } def package_locks - @package_locks ||= + @package_locks ||= T.let( dependency_files - .select { |f| f.name.end_with?("package-lock.json") } + .select { |f| f.name.end_with?("package-lock.json") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def shrinkwraps - @shrinkwraps ||= + @shrinkwraps ||= T.let( dependency_files - .select { |f| f.name.end_with?("npm-shrinkwrap.json") } + .select { |f| f.name.end_with?("npm-shrinkwrap.json") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def package_files dependency_files.select { |f| f.name.end_with?("package.json") } end diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb index b328d55cf82..8bef8d1a108 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater.rb @@ -39,9 +39,10 @@ def updated_pnpm_lock_content(pnpm_lock) IRRESOLVABLE_PACKAGE = "ERR_PNPM_NO_MATCHING_VERSION" INVALID_REQUIREMENT = "ERR_PNPM_SPEC_NOT_SUPPORTED_BY_ANY_RESOLVER" - UNREACHABLE_GIT = %r{ERR_PNPM_FETCH_404[ [^:print:]]+GET (?https://codeload\.github\.com/[^/]+/[^/]+)/} + UNREACHABLE_GIT = %r{Command failed with exit code 128: git ls-remote (?.*github\.com/[^/]+/[^ ]+)} + UNREACHABLE_GIT_V8 = %r{ERR_PNPM_FETCH_404[ [^:print:]]+GET (?https://codeload\.github\.com/[^/]+/[^/]+)/} FORBIDDEN_PACKAGE = /ERR_PNPM_FETCH_403[ [^:print:]]+GET (?.*): Forbidden - 403/ - MISSING_PACKAGE = /ERR_PNPM_FETCH_404[ [^:print:]]+GET (?.*): Not Found - 404/ + MISSING_PACKAGE = /ERR_PNPM_FETCH_404[ [^:print:]]+GET (?.*): (?:Not Found)? - 404/ UNAUTHORIZED_PACKAGE = /ERR_PNPM_FETCH_401[ [^:print:]]+GET (?.*): Unauthorized - 401/ def run_pnpm_update(pnpm_lock:) @@ -95,7 +96,13 @@ def handle_pnpm_lock_updater_error(error, pnpm_lock) end if error_message.match?(UNREACHABLE_GIT) - url = error_message.match(UNREACHABLE_GIT).named_captures.fetch("url") + url = error_message.match(UNREACHABLE_GIT).named_captures.fetch("url").gsub("git+ssh://git@", "https://").delete_suffix(".git") + + raise Dependabot::GitDependenciesNotReachable, url + end + + if error_message.match?(UNREACHABLE_GIT_V8) + url = error_message.match(UNREACHABLE_GIT_V8).named_captures.fetch("url").gsub("codeload.", "") raise Dependabot::GitDependenciesNotReachable, url end @@ -122,6 +129,7 @@ def raise_package_access_error(dependency_url, pnpm_lock) package_name = RegistryParser.new(resolved_url: dependency_url, credentials: credentials).dependency_name missing_dep = lockfile_dependencies(pnpm_lock) .find { |dep| dep.name == package_name } + raise DependencyNotFound, package_name unless missing_dep reg = NpmAndYarn::UpdateChecker::RegistryFinder.new( dependency: missing_dep, diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb index 046568ee259..2d27e335c02 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker.rb @@ -106,7 +106,7 @@ def updated_requirements end def requirements_unlocked_or_can_be? - requirements_update_strategy != RequirementsUpdateStrategy::LockfileOnly + !requirements_update_strategy.lockfile_only? end def requirements_update_strategy diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb index 4e16787e326..7c68f879bb2 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/update_checker/requirements_updater.rb @@ -42,7 +42,7 @@ def initialize(requirements:, updated_source:, update_strategy:, end def updated_requirements - return requirements if update_strategy == RequirementsUpdateStrategy::LockfileOnly + return requirements if update_strategy.lockfile_only? requirements.map do |req| req = req.merge(source: updated_source) diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater_spec.rb index d3f16d8fd5c..6c4fb015be9 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/pnpm_lockfile_updater_spec.rb @@ -160,7 +160,43 @@ expect(error.dependency_urls) .to eq( [ - "https://codeload.github.com/Zelcord/electron-context-menu" + "https://github.com/Zelcord/electron-context-menu" + ] + ) + end + end + end + + context "with a private git dep we don't have access to in PNPM v8" do + let(:dependency_name) { "cross-fetch" } + let(:version) { "4.0.0" } + let(:previous_version) { "3.1.5" } + let(:requirements) do + [{ + file: "package.json", + requirement: "^4.0.0", + groups: ["dependencies"], + source: nil + }] + end + let(:previous_requirements) do + [{ + file: "package.json", + requirement: "^3.1.5", + groups: ["dependencies"], + source: nil + }] + end + + let(:project_name) { "pnpm/github_dependency_private_v8" } + + it "raises a helpful error" do + expect { updated_pnpm_lock_content } + .to raise_error(Dependabot::GitDependenciesNotReachable) do |error| + expect(error.dependency_urls) + .to eq( + [ + "https://github.com/Zelcord/electron-context-menu" ] ) end @@ -195,5 +231,34 @@ .to raise_error(Dependabot::PrivateSourceAuthenticationFailure) end end + + context "with a private registry with no configuration" do + let(:dependency_name) { "next" } + let(:version) { "14.2.4" } + let(:previous_version) { "13.2.4" } + let(:requirements) do + [{ + file: "package.json", + requirement: "^14.2.4", + groups: ["dependencies"], + source: nil + }] + end + let(:previous_requirements) do + [{ + file: "package.json", + requirement: "^13.2.4", + groups: ["dependencies"], + source: nil + }] + end + + let(:project_name) { "pnpm/private_registry_no_config" } + + it "raises a helpful error" do + expect { updated_pnpm_lock_content } + .to raise_error(Dependabot::DependencyNotFound) + end + end end end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater_spec.rb index 2cb418700a7..e853f820f81 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater_spec.rb @@ -3758,8 +3758,7 @@ f.name == "other_package/package.json" end - expect(lockfile.content).to include("/lodash@1.3.1:") - expect(lockfile.content).to include("/lodash").once + expect(lockfile.content).to include("lodash@1.3.1:\n resolution").once expect(package.content).to include('"lodash": "1.3.1"') expect(package1.content).to include('"lodash": "^1.3.1"') @@ -3792,8 +3791,8 @@ .to match_array(%w(pnpm-lock.yaml packages/package1/package.json)) lockfile = updated_files.find { |f| f.name == "pnpm-lock.yaml" } - expect(lockfile.content).to include("/chalk@0.4.0:") - expect(lockfile.content).to include("/chalk@0.4.0:").once + expect(lockfile.content).to include("chalk@0.4.0:") + expect(lockfile.content).to include("chalk@0.4.0:\n resolution").once end it "does not add the dependency to the top-level workspace" do @@ -3829,8 +3828,7 @@ root_lockfile = updated_files.find { |f| f.name == "pnpm-lock.yaml" } expect(updated_files.map(&:name)). to match_array(%w(pnpm-lock.yaml packages/package1/package.json)) - expect(root_lockfile.content).to include("/etag@1.8.1:") - expect(root_lockfile.content).to include("/etag@").once + expect(root_lockfile.content).to include("etag@1.8.1:\n resolution").once end it "updates the existing development declaration" do @@ -3854,8 +3852,7 @@ let(:previous_requirements) { [] } it "updates the version" do - expect(updated_pnpm_lock.content).to include("/acorn@5.7.3:") - expect(updated_pnpm_lock.content).to include("/acorn@").once + expect(updated_pnpm_lock.content).to include("acorn@5.7.3:\n resolution").once end end @@ -3878,8 +3875,7 @@ it "updates the resolution, as well as the declaration" do expect(updated_package_json.content).to include('"lodash": "3.10.1"') - expect(updated_pnpm_lock.content).to include("/lodash@3.10.1:") - expect(updated_pnpm_lock.content).to include("/lodash@").once + expect(updated_pnpm_lock.content).to include("lodash@3.10.1:\n resolution").once end end @@ -3902,8 +3898,7 @@ it "updates the resolution, as well as the declaration" do expect(updated_package_json.content).to include('"lodash": "3.10.1"') - expect(updated_pnpm_lock.content).to include("/lodash@3.10.1:") - expect(updated_pnpm_lock.content).to include("/lodash@").once + expect(updated_pnpm_lock.content).to include("lodash@3.10.1:\n resolution").once end end @@ -3933,8 +3928,7 @@ it "updates the manifest and lockfile" do expect(updated_files.map(&:name)).to match_array(%w(package.json pnpm-lock.yaml)) - expect(updated_pnpm_lock.content).to include("/node-adodb@5.0.3:") - expect(updated_pnpm_lock.content).to include("/node-adodb@").once + expect(updated_pnpm_lock.content).to include("node-adodb@5.0.3:\n resolution").once end end @@ -3950,8 +3944,7 @@ it "de-duplicates all entries to the same version" do expect(updated_files.map(&:name)).to contain_exactly("pnpm-lock.yaml") - expect(updated_pnpm_lock.content).to include("/js-yaml@3.14.1:") - expect(updated_pnpm_lock.content).to include("/js-yaml@").once + expect(updated_pnpm_lock.content).to include("js-yaml@3.14.1:\n resolution").once end end @@ -3974,8 +3967,8 @@ it "updates the lockfile" do expect(updated_files.map(&:name)).to eq(%w(pnpm-lock.yaml)) - expect(updated_pnpm_lock.content).to include("/typescript@2.1.4:") - expect(updated_pnpm_lock.content).to include("/typescript@2.9.1:") + expect(updated_pnpm_lock.content).to include("typescript@2.1.4:") + expect(updated_pnpm_lock.content).to include("typescript@2.9.1:") end end end diff --git a/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private/pnpm-lock.yaml b/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private/pnpm-lock.yaml index 8351e3bceed..08db1d3df60 100644 --- a/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private/pnpm-lock.yaml +++ b/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private/pnpm-lock.yaml @@ -1,28 +1,31 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - cross-fetch: - specifier: ^3.1.5 - version: 3.1.5 - electron-context-menu: - specifier: github:Zelcord/electron-context-menu - version: github.com/Zelcord/electron-context-menu/280c81398c02a063f46e3285a9708d8db1a7ce32 +importers: + + .: + dependencies: + cross-fetch: + specifier: ^3.1.5 + version: 3.1.5 + electron-context-menu: + specifier: github:Zelcord/electron-context-menu + version: https://codeload.github.com/Zelcord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32 packages: - /cross-fetch@3.1.5: + cross-fetch@3.1.5: resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} - dependencies: - node-fetch: 2.6.7 - transitivePeerDependencies: - - encoding - dev: false - /node-fetch@2.6.7: + electron-context-menu@https://codeload.github.com/Zelcord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32: + resolution: {tarball: https://codeload.github.com/Zelcord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32} + name: electron-context-menu + version: 3.5.0 + + node-fetch@2.6.7: resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} engines: {node: 4.x || >=6.0.0} peerDependencies: @@ -30,27 +33,35 @@ packages: peerDependenciesMeta: encoding: optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - /tr46@0.0.3: + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - /webidl-conversions@3.0.1: + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - /whatwg-url@5.0.0: + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + +snapshots: + + cross-fetch@3.1.5: + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + + electron-context-menu@https://codeload.github.com/Zelcord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32: {} + + node-fetch@2.6.7: + dependencies: + whatwg-url: 5.0.0 + + tr46@0.0.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - dev: false - - github.com/Zelcord/electron-context-menu/280c81398c02a063f46e3285a9708d8db1a7ce32: - resolution: {tarball: https://codeload.github.com/Zelcord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32} - name: electron-context-menu - version: 3.5.0 - dev: false diff --git a/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private_v8/package.json b/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private_v8/package.json new file mode 100644 index 00000000000..b6d9388ddf4 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private_v8/package.json @@ -0,0 +1,24 @@ +{ + "name": "Zelcord", + "version": "3.3.0", + "description": "Zelcord is a custom client designed to enhance your Discord experience while keeping everything lightweight.", + "main": "ts-out/main.js", + "engines": { + "node": ">=18.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Zelcord/Zelcord.git" + }, + "author": "smartfrigde", + "license": "OSL-3.0", + "bugs": { + "url": "https://github.com/Zelcord/Zelcord/issues" + }, + "homepage": "https://github.com/Zelcord/Zelcord#readme", + "dependencies": { + "cross-fetch": "^3.1.5", + "electron-context-menu": "github:Zelcord/electron-context-menu" + }, + "packageManager": "pnpm@8.15.6" +} diff --git a/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private_v8/pnpm-lock.yaml b/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private_v8/pnpm-lock.yaml new file mode 100644 index 00000000000..8351e3bceed --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/pnpm/github_dependency_private_v8/pnpm-lock.yaml @@ -0,0 +1,56 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + cross-fetch: + specifier: ^3.1.5 + version: 3.1.5 + electron-context-menu: + specifier: github:Zelcord/electron-context-menu + version: github.com/Zelcord/electron-context-menu/280c81398c02a063f46e3285a9708d8db1a7ce32 + +packages: + + /cross-fetch@3.1.5: + resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} + dependencies: + node-fetch: 2.6.7 + transitivePeerDependencies: + - encoding + dev: false + + /node-fetch@2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + github.com/Zelcord/electron-context-menu/280c81398c02a063f46e3285a9708d8db1a7ce32: + resolution: {tarball: https://codeload.github.com/Zelcord/electron-context-menu/tar.gz/280c81398c02a063f46e3285a9708d8db1a7ce32} + name: electron-context-menu + version: 3.5.0 + dev: false diff --git a/npm_and_yarn/spec/fixtures/projects/pnpm/private_registry_no_config/package.json b/npm_and_yarn/spec/fixtures/projects/pnpm/private_registry_no_config/package.json new file mode 100644 index 00000000000..0c507f9a0f7 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/pnpm/private_registry_no_config/package.json @@ -0,0 +1,9 @@ +{ + "name": "repo1", + "version": "1.0.0", + "description": "xxx", + "dependencies": { + "gsap": "npm:@gsap/business@^3.12.4", + "next": "^13.2.4" + } +} \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/pnpm/private_registry_no_config/pnpm-lock.yaml b/npm_and_yarn/spec/fixtures/projects/pnpm/private_registry_no_config/pnpm-lock.yaml new file mode 100644 index 00000000000..6b4de169214 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/pnpm/private_registry_no_config/pnpm-lock.yaml @@ -0,0 +1,11 @@ +lockfileVersion: "6.0" +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false +dependencies: + next: + specifier: ^13.2.4 + version: 13.5.6(@babel/core@7.23.9)(react-dom@18.2.0)(react@18.2.0)(sass@1.71.1) + gsap: + specifier: npm:@gsap/business@^3.12.4 + version: /@gsap/business@3.12.4 diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs new file mode 100644 index 00000000000..44db5bb03dd --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs @@ -0,0 +1,169 @@ +using System.Text; +using System.Xml.Linq; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Test; +using NuGetUpdater.Core.Test.Analyze; +using NuGetUpdater.Core.Test.Update; + +using Xunit; + +namespace NuGetUpdater.Cli.Test; + +using TestFile = (string Path, string Content); + +public partial class EntryPointTests +{ + public class Analyze : AnalyzeWorkerTestBase + { + [Fact] + public async Task FindsUpdatedPackageAndReturnsTheCorrectData() + { + var repositoryXml = XElement.Parse(""""""); + await RunAsync(path => + [ + "analyze", + "--repo-root", + path, + "--discovery-file-path", + Path.Join(path, "discovery.json"), + "--dependency-file-path", + Path.Join(path, "Some.Package.json"), + "--analysis-folder-path", + Path.Join(path, AnalyzeWorker.AnalysisDirectoryName), + "--verbose", + ], + packages: [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0", additionalMetadata: [repositoryXml]), + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.1", "net8.0", additionalMetadata: [repositoryXml]), + ], + dependencyName: "Some.Package", + initialFiles: + [ + ("discovery.json", """ + { + "Path": "", + "IsSuccess": true, + "Projects": [ + { + "FilePath": "project.csproj", + "Dependencies": [ + { + "Name": "Microsoft.NET.Sdk", + "Version": null, + "Type": "MSBuildSdk", + "EvaluationResult": null, + "TargetFrameworks": null, + "IsDevDependency": false, + "IsDirect": false, + "IsTransitive": false, + "IsOverride": false, + "IsUpdate": false + }, + { + "Name": "Some.Package", + "Version": "1.0.0", + "Type": "PackageReference", + "EvaluationResult": { + "ResultType": "Success", + "OriginalValue": "1.0.0", + "EvaluatedValue": "1.0.0", + "RootPropertyName": null, + "ErrorMessage": null + }, + "TargetFrameworks": [ + "net8.0" + ], + "IsDevDependency": false, + "IsDirect": true, + "IsTransitive": false, + "IsOverride": false, + "IsUpdate": false + } + ], + "IsSuccess": true, + "Properties": [ + { + "Name": "TargetFramework", + "Value": "net8.0", + "SourceFilePath": "project.csproj" + } + ], + "TargetFrameworks": [ + "net8.0" + ], + "ReferencedProjectPaths": [] + } + ], + "DirectoryPackagesProps": null, + "GlobalJson": null, + "DotNetToolsJson": null + } + """), + ("Some.Package.json", """ + { + "Name": "Some.Package", + "Version": "1.0.0", + "IsVulnerable": false, + "IgnoredVersions": [], + "Vulnerabilities": [] + } + """), + ("project.csproj", """ + + + net8.0 + + + + + + """), + ], + expectedResult: new() + { + UpdatedVersion = "1.0.1", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = + [ + new Dependency("Some.Package", "1.0.1", DependencyType.Unknown, TargetFrameworks: ["net8.0"], InfoUrl: "https://nuget.example.com/some.package") + ], + } + ); + } + + private static async Task RunAsync(Func getArgs, string dependencyName, TestFile[] initialFiles, ExpectedAnalysisResult expectedResult, MockNuGetPackage[]? packages = null) + { + var actualResult = await RunAnalyzerAsync(dependencyName, initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); + + try + { + await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, path); + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + } + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index d51d8178d9f..8ce89b75001 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -1,6 +1,7 @@ using System.Text; using NuGetUpdater.Core; +using NuGetUpdater.Core.Discover; using NuGetUpdater.Core.Test; using NuGetUpdater.Core.Test.Discover; using NuGetUpdater.Core.Test.Update; @@ -25,6 +26,8 @@ await RunAsync(path => path, "--workspace", "path/to/some directory with spaces", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [], initialFiles: @@ -42,7 +45,7 @@ await RunAsync(path => ], expectedResult: new() { - FilePath = "path/to/some directory with spaces", + Path = "path/to/some directory with spaces", Projects = [ new() { @@ -72,6 +75,8 @@ await RunAsync(path => path, "--workspace", "/", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [ @@ -129,7 +134,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -159,6 +164,8 @@ await RunAsync(path => path, "--workspace", "path/to", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [ @@ -193,7 +200,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = "path/to", + Path = "path/to", Projects = [ new() { @@ -224,6 +231,8 @@ await RunAsync(path => path, "--workspace", workspacePath, + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [ @@ -258,7 +267,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = workspacePath, + Path = workspacePath, Projects = [ new() { @@ -282,69 +291,72 @@ await RunAsync(path => public async Task WithDuplicateDependenciesOfDifferentTypes() { await RunAsync(path => - [ - "discover", - "--repo-root", - path, - "--workspace", - "path/to", - ], - new[] - { - ("path/to/my.csproj", """ - - - net8.0 - - - - - - - """), - ("path/Directory.Build.props", """ - - - - - - - - - """) - }, - expectedResult: new() - { - FilePath = "path/to", - Projects = [ - new() - { - FilePath = "my.csproj", - TargetFrameworks = ["net8.0"], - ReferencedProjectPaths = [], - ExpectedDependencyCount = 2, - Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), - // $(ManagePackageVersionsCentrally) evaluates false by default, we only get a PackageReference - new("System.Text.Json", "8.0.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"]) - ], - Properties = [ - new("TargetFramework", "net8.0", "path/to/my.csproj"), - ], - }, - new() - { - FilePath = "../Directory.Build.props", - ReferencedProjectPaths = [], - ExpectedDependencyCount = 2, - Dependencies = [ - new("System.Text.Json", "8.0.3", DependencyType.PackageReference, IsDirect: true), - new("System.Text.Json", "8.0.3", DependencyType.GlobalPackageReference, IsDirect: true) - ], - Properties = [], - } - ] - }); + [ + "discover", + "--repo-root", + path, + "--workspace", + "path/to", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) + ], + new[] + { + ("path/to/my.csproj", """ + + + net8.0 + + + + + + + """), + ("path/Directory.Build.props", """ + + + + + + + + + """) + }, + expectedResult: new() + { + Path = "path/to", + Projects = [ + new() + { + FilePath = "my.csproj", + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, + Dependencies = [ + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + // $(ManagePackageVersionsCentrally) evaluates false by default, we only get a PackageReference + new("System.Text.Json", "8.0.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"]) + ], + Properties = [ + new("TargetFramework", "net8.0", "path/to/my.csproj"), + ], + }, + new() + { + FilePath = "../Directory.Build.props", + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, + Dependencies = [ + new("System.Text.Json", "8.0.3", DependencyType.PackageReference, IsDirect: true), + new("System.Text.Json", "8.0.3", DependencyType.GlobalPackageReference, IsDirect: true) + ], + Properties = [], + } + ] + } + ); } private static async Task RunAsync( diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs index 0bc2f50d8df..280affb9801 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - using Xunit; namespace NuGetUpdater.Cli.Test; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs index 4f4a3ee2ce0..a9a5f6c9581 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -44,19 +44,19 @@ await Run(path => Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "my", "my.csproj", "{782E0C0A-10D3-444D-9640-263D03D2B20C}" EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution + GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.Build.0 = Debug|Any CPU {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.ActiveCfg = Release|Any CPU {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE - EndGlobalSection + EndGlobalSection EndGlobal """), ("path/to/my.csproj", """ @@ -82,7 +82,7 @@ await Run(path => """) - ], + ], expectedFiles: [ ("path/to/my.csproj", """ @@ -253,7 +253,7 @@ await Run(path => """), ("other-dir/Directory.Build.props", """ - + @@ -271,8 +271,7 @@ await Run(path => """), - ("some-dir/project1/project.csproj", - """ + ("some-dir/project1/project.csproj", """ Exe @@ -300,7 +299,7 @@ await Run(path => """), ("other-dir/Directory.Build.props", """ - + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs new file mode 100644 index 00000000000..1b227c9aa0d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs @@ -0,0 +1,37 @@ +using System.CommandLine; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Cli.Commands; + +internal static class AnalyzeCommand +{ + internal static readonly Option RepoRootOption = new("--repo-root") { IsRequired = true }; + internal static readonly Option DependencyFilePathOption = new("--dependency-file-path") { IsRequired = true }; + internal static readonly Option DiscoveryFilePathOption = new("--discovery-file-path") { IsRequired = true }; + internal static readonly Option AnalysisFolderOption = new("--analysis-folder-path") { IsRequired = true }; + internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); + + internal static Command GetCommand(Action setExitCode) + { + Command command = new("analyze", "Determines how to update a dependency based on the workspace discovery information.") + { + RepoRootOption, + DependencyFilePathOption, + DiscoveryFilePathOption, + AnalysisFolderOption, + VerboseOption + }; + + command.TreatUnmatchedTokensAsErrors = true; + + command.SetHandler(async (repoRoot, discoveryPath, dependencyPath, analysisDirectory, verbose) => + { + var worker = new AnalyzeWorker(new Logger(verbose)); + await worker.RunAsync(repoRoot.FullName, discoveryPath.FullName, dependencyPath.FullName, analysisDirectory.FullName); + }, RepoRootOption, DiscoveryFilePathOption, DependencyFilePathOption, AnalysisFolderOption, VerboseOption); + + return command; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index a08e2f589bb..6bd29c4bba6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -7,9 +7,9 @@ namespace NuGetUpdater.Cli.Commands; internal static class DiscoverCommand { - internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; + internal static readonly Option RepoRootOption = new("--repo-root") { IsRequired = true }; internal static readonly Option WorkspaceOption = new("--workspace") { IsRequired = true }; - internal static readonly Option OutputOption = new("--output", () => DiscoveryWorker.DiscoveryResultFileName) { IsRequired = false }; + internal static readonly Option OutputOption = new("--output") { IsRequired = true }; internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); internal static Command GetCommand(Action setExitCode) @@ -27,7 +27,7 @@ internal static Command GetCommand(Action setExitCode) command.SetHandler(async (repoRoot, workspace, outputPath, verbose) => { var worker = new DiscoveryWorker(new Logger(verbose)); - await worker.RunAsync(repoRoot.FullName, workspace, outputPath); + await worker.RunAsync(repoRoot.FullName, workspace, outputPath.FullName); }, RepoRootOption, WorkspaceOption, OutputOption, VerboseOption); return command; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs index 772fb9bf683..ff0235509b4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs @@ -15,6 +15,7 @@ internal static async Task Main(string[] args) { FrameworkCheckCommand.GetCommand(setExitCode), DiscoverCommand.GetCommand(setExitCode), + AnalyzeCommand.GetCommand(setExitCode), UpdateCommand.GetCommand(setExitCode), }; command.TreatUnmatchedTokensAsErrors = true; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs new file mode 100644 index 00000000000..bace499ff65 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; +using System.Text.Json; + +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Discover; +using NuGetUpdater.Core.Test.Update; +using NuGetUpdater.Core.Test.Utilities; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +using TestFile = (string Path, string Content); + +public class AnalyzeWorkerTestBase +{ + protected static async Task TestAnalyzeAsync( + WorkspaceDiscoveryResult discovery, + DependencyInfo dependencyInfo, + ExpectedAnalysisResult expectedResult, + MockNuGetPackage[]? packages = null) + { + var relativeDependencyPath = $"./dependabot/dependency/{dependencyInfo.Name}.json"; + + TestFile[] files = [ + (DiscoveryWorker.DiscoveryResultFileName, JsonSerializer.Serialize(discovery, AnalyzeWorker.SerializerOptions)), + (relativeDependencyPath, JsonSerializer.Serialize(dependencyInfo, AnalyzeWorker.SerializerOptions)), + ]; + + var actualResult = await RunAnalyzerAsync(dependencyInfo.Name, files, async directoryPath => + { + await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, directoryPath); + + var discoveryPath = Path.GetFullPath(DiscoveryWorker.DiscoveryResultFileName, directoryPath); + var dependencyPath = Path.GetFullPath(relativeDependencyPath, directoryPath); + var analysisPath = Path.GetFullPath(AnalyzeWorker.AnalysisDirectoryName, directoryPath); + + var worker = new AnalyzeWorker(new Logger(verbose: true)); + await worker.RunAsync(directoryPath, discoveryPath, dependencyPath, analysisPath); + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + + protected static void ValidateAnalysisResult(ExpectedAnalysisResult expectedResult, AnalysisResult actualResult) + { + Assert.NotNull(actualResult); + Assert.Equal(expectedResult.UpdatedVersion, actualResult.UpdatedVersion); + Assert.Equal(expectedResult.CanUpdate, actualResult.CanUpdate); + Assert.Equal(expectedResult.VersionComesFromMultiDependencyProperty, actualResult.VersionComesFromMultiDependencyProperty); + ValidateDependencies(expectedResult.UpdatedDependencies, actualResult.UpdatedDependencies); + Assert.Equal(expectedResult.ExpectedUpdatedDependenciesCount ?? expectedResult.UpdatedDependencies.Length, actualResult.UpdatedDependencies.Length); + + return; + + void ValidateDependencies(ImmutableArray expectedDependencies, ImmutableArray actualDependencies) + { + if (expectedDependencies.IsDefault) + { + return; + } + + foreach (var expectedDependency in expectedDependencies) + { + var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); + Assert.Equal(expectedDependency.Name, actualDependency.Name); + Assert.Equal(expectedDependency.Version, actualDependency.Version); + Assert.Equal(expectedDependency.Type, actualDependency.Type); + AssertEx.Equal(expectedDependency.TargetFrameworks, actualDependency.TargetFrameworks); + Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect); + Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive); + Assert.Equal(expectedDependency.InfoUrl, actualDependency.InfoUrl); + } + } + } + + protected static async Task RunAnalyzerAsync(string dependencyName, TestFile[] files, Func action) + { + // write initial files + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + // run discovery + await action(temporaryDirectory.DirectoryPath); + + // gather results + var resultPath = Path.Join(temporaryDirectory.DirectoryPath, AnalyzeWorker.AnalysisDirectoryName, $"{dependencyName}.json"); + var resultJson = await File.ReadAllTextAsync(resultPath); + return JsonSerializer.Deserialize(resultJson, DiscoveryWorker.SerializerOptions)!; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs new file mode 100644 index 00000000000..7fab8c46aca --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -0,0 +1,304 @@ +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase +{ + [Fact] + public async Task FindsUpdatedVersion() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0"), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.1.0", "net8.0"), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.2.0", "net8.0"), // `IgnoredVersions` should prevent this from being selected + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "1.0.0", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "1.0.0", + IgnoredVersions = [Requirement.Parse("> 1.1.0")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "1.1.0", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Some.Package", "1.1.0", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FindsUpdatedPeerDependencies() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "4.0.1", DependencyType.PackageReference), + new("Some.Transitive.Dependency", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Some.Package", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Some.Transitive.Dependency", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task DeterminesMultiPropertyVersion() + { + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(SomePackageVersion)", "4.0.1", "SomePackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Transitive.Dependency", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Transitive.Dependency", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [ + new("Some.Package", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Some.Transitive.Dependency", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FailsToUpdateMultiPropertyVersion() + { + // Package.A and Package.B happen to share some versions but would fail to update in sync with each other. + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(TestPackageVersion)", "4.5.0", "TestPackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Package.A", "4.5.0", "net8.0"), // initial package versions match, purely by accident + MockNuGetPackage.CreateSimplePackage("Package.A", "4.9.2", "net8.0"), // subsequent versions do not match + MockNuGetPackage.CreateSimplePackage("Package.A", "4.9.3", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Package.B", "4.5.0", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Package.B", "4.5.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Package.B", "4.5.2", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Package.A", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Package.B", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Package.A", + Version = "4.5.0", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.5.0", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [], + } + ); + } + + + [Fact] + public async Task ReturnsUpToDate_ForMissingVersionProperty() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Transitive.Dependency", "$(MissingPackageVersion)", DependencyType.PackageReference, EvaluationResult: new EvaluationResult(EvaluationResultType.PropertyNotFound, "$(MissingPackageVersion)", "$(MissingPackageVersion)", "$(MissingPackageVersion)", ErrorMessage: null)), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "$(MissingPackageVersion)", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "$(MissingPackageVersion)", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } + + [Fact] + public async Task ReturnsUpToDate_ForMissingDependency() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Transitive.Dependency", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.0.1", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs new file mode 100644 index 00000000000..8ad02a925b1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs @@ -0,0 +1,145 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class CompatibilityCheckerTests +{ + [Fact] + public void PerformCheck_CompatiblePackage_IsCompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_IncompatiblePackage_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithPackageFrameworks_IsChecked() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithoutPackageFrameworks_IsCompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_WithoutPackageFrameworks_IsIncompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_WithoutProjectFrameworks_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = []; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs new file mode 100644 index 00000000000..d6302fc70cf --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs @@ -0,0 +1,8 @@ +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Core.Test.Analyze; + +public record ExpectedAnalysisResult : AnalysisResult +{ + public int? ExpectedUpdatedDependenciesCount { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs new file mode 100644 index 00000000000..f2150ae11f0 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs @@ -0,0 +1,69 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class RequirementTests +{ + // Supported OPs (=, !=, >, <, >=, <=, ~>) + [Theory] + [InlineData("1.0.0", "1.0.0", true)] + [InlineData("1.0.0-alpha", "1.0.0", false)] + [InlineData("1.0.0", "= 1.0.0", true)] + [InlineData("1.0.0-alpha", "= 1.0.0", false)] + [InlineData("1.0.0", "!=1.0.1", true)] + [InlineData("1.0.0", "!= 1.0.0", false)] + [InlineData("1.0.1", "> 1.0.0", true)] + [InlineData("1.0.0-alpha", "> 1.0.0", false)] + [InlineData("1.0.0", "< 1.0.1", true)] + [InlineData("1.0.0", "<1.0.0-alpha", false)] + [InlineData("1.0.0", ">= 1.0.0", true)] + [InlineData("1.0.1", ">= 1.0.0", true)] + [InlineData("1.0.0-alpha", ">= 1.0.0", false)] + [InlineData("1.0.0", "<= 1.0.0", true)] + [InlineData("1.0.0-alpha", "<= 1.0.0", true)] + [InlineData("1.0.1", "<= 1.0.0", false)] + [InlineData("1.0.1", "~>1.0.0", true)] + [InlineData("1.1.0", "~> 1.0.0", false)] + [InlineData("1.1", "~> 1.0", true)] + [InlineData("2.0", "~> 1.0", false)] + [InlineData("1", "~> 1", true)] + [InlineData("2", "~> 1", false)] + public void IsSatisfiedBy(string versionString, string requirementString, bool expected) + { + var version = NuGetVersion.Parse(versionString); + var requirement = Requirement.Parse(requirementString); + + var actual = requirement.IsSatisfiedBy(version); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("> = 1.0.0")] // Invalid format + [InlineData("<>= 1.0.0")] // Invalid Operator + [InlineData(">")] // Missing version + public void Parse_ThrowsForInvalid(string requirementString) + { + Assert.Throws(() => Requirement.Parse(requirementString)); + } + + [Theory] + [InlineData("1.0.0-alpha", "1.1.0.0")] + [InlineData("1.0.0.0", "1.0.1.0")] + [InlineData("1.0.0", "1.1.0.0")] + [InlineData("1.0", "2.0.0.0")] + [InlineData("1", "2.0.0.0")] + public void Bump(string versionString, string expectedString) + { + var version = NuGetVersion.Parse(versionString); + var expected = Version.Parse(expectedString); + + var actual = Requirement.Bump(version); + + Assert.Equal(expected, actual); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs new file mode 100644 index 00000000000..1460e910f6e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs @@ -0,0 +1,78 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class SecurityVulnerabilityExtensionsTests +{ + [Fact] + public void VersionInSafeVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionInVulnerableVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } + + [Fact] + public void VersionNotInVulnerableVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionNotInSafeVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs new file mode 100644 index 00000000000..a1617961929 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs @@ -0,0 +1,193 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class VersionFinderTests +{ + [Fact] + public void VersionFilter_VersionInIgnoredVersions_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInIgnoredVersions_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionInVulnerabilities_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInVulnerabilities_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionLessThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionHigherThanCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_PreviewVersionDifferentThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1-beta"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_PreviewVersionSameAsCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_WildcardPreviewVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "*-*", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 0f811bf28ac..4e482315ad1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -7,7 +7,6 @@ using NuGetUpdater.Core.Test.Utilities; using Xunit; -using Xunit.Sdk; namespace NuGetUpdater.Core.Test.Discover; @@ -35,7 +34,7 @@ protected static async Task TestDiscoveryAsync( protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult) { Assert.NotNull(actualResult); - Assert.Equal(expectedResult.FilePath.NormalizePathToUnix(), actualResult.FilePath.NormalizePathToUnix()); + Assert.Equal(expectedResult.Path.NormalizePathToUnix(), actualResult.Path.NormalizePathToUnix()); ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps); ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson); ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs index b2dafa70e61..e7804d091b8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs @@ -36,7 +36,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", @@ -80,7 +80,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs index fafb99d7ec2..35c252d2943 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs @@ -26,7 +26,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", @@ -60,7 +60,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs index e6adca58bee..cebdae1a624 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs @@ -34,7 +34,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs index 7922a61a531..c4739edf353 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs @@ -54,7 +54,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "dependabot", + Path = "dependabot", Projects = [ new() diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs index 07e31184a86..6833e4ff5ad 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -37,7 +37,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -108,7 +108,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 2, Projects = [ new() @@ -189,7 +189,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -299,7 +299,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 5, Projects = [ new() @@ -365,7 +365,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -386,6 +386,59 @@ await TestDiscoveryAsync( ); } + [Fact] + public async Task TargetFrameworkCanBeResolvedFromImplicitlyImportedFile() + { + await TestDiscoveryAsync( + packages: [], + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + $(SomeTfm) + + + + + + """), + ("Directory.Build.props", """ + + + net8.0 + + + """) + ], + expectedResult: new() + { + Path = "", + Projects = [ + new() + { + FilePath = "Directory.Build.props", + Dependencies = [], + }, + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 2, + Dependencies = [ + new("Package.A", "1.2.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + ], + Properties = [ + new("SomeTfm", "net8.0", "Directory.Build.props"), + new("TargetFramework", "$(SomeTfm)", "myproj.csproj"), + ], + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + } + ] + } + ); + } + [Fact] public async Task NoDependenciesReturnedIfNoTargetFrameworkCanBeResolved() @@ -407,12 +460,52 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [] } ); } + [Fact] + public async Task PropertyWithWildcardVersionIsRetained() + { + await TestDiscoveryAsync( + packages: [], + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + net8.0 + + + + + + """) + ], + expectedResult: new() + { + Path = "", + Projects = [ + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 2, + Dependencies = [ + new("Some.Package", "1.*", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + ], + Properties = [ + new("TargetFramework", "net8.0", "myproj.csproj"), + ], + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + } + ] + } + ); + } + [Fact] public async Task DiscoverReportsTransitivePackageVersionsWithFourPartsForMultipleTargetFrameworks() { @@ -438,7 +531,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -493,7 +586,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "test", + Path = "test", Projects = [ new() { @@ -569,7 +662,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "solutions", + Path = "solutions", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index bc1c8139056..f35cf224561 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -33,7 +33,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "src", + Path = "src", Projects = [ new() { @@ -92,7 +92,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "src", + Path = "src", Projects = [ new() { @@ -151,7 +151,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "src", + Path = "src", ExpectedProjectCount = 2, Projects = [ new() @@ -276,7 +276,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 2, Projects = [ new() diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index 4fd2094d7b2..e5d65ad09dd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -4,9 +4,9 @@ namespace NuGetUpdater.Core.Test.Discover; -public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult +public record ExpectedWorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public int? ExpectedProjectCount { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs index d5dce346346..f1dc2cae175 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs @@ -71,12 +71,18 @@ public void WriteToDirectory(string localPackageSourcePath) /// Creates a mock NuGet package with a single assembly in the appropriate `lib/` directory. The assembly will /// be empty. /// - public static MockNuGetPackage CreateSimplePackage(string id, string version, string targetFramework, (string? TargetFramework, (string Id, string Version)[] Packages)[]? dependencyGroups = null) + public static MockNuGetPackage CreateSimplePackage( + string id, + string version, + string targetFramework, + (string? TargetFramework, (string Id, string Version)[] Packages)[]? dependencyGroups = null, + XElement[]? additionalMetadata = null + ) { return new( id, version, - AdditionalMetadata: null, + AdditionalMetadata: additionalMetadata, DependencyGroups: dependencyGroups, Files: [ diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs index 9397fb8a999..d03c978fdd5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs @@ -246,12 +246,13 @@ public static async Task MockNuGetPackagesInDirectory(MockNuGetPackage[]? packag } // ensure only the test feed is used + string relativeLocalFeedPath = Path.GetRelativePath(temporaryDirectory, localFeedPath); await File.WriteAllTextAsync(Path.Join(temporaryDirectory, "NuGet.Config"), $""" - + """ diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index 74c79c1c013..10c3c7a2c61 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -383,7 +383,7 @@ [new Dependency("Some.Package", "4.5.11", DependencyType.Unknown)] } [Fact] - public async Task AllPackageDependenciesCanBeFoundWithNuGetConfig() + public async Task LocalPackageSourcesAreHonored() { var nugetPackagesDirectory = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); var nugetHttpCacheDirectory = Environment.GetEnvironmentVariable("NUGET_HTTP_CACHE_PATH"); @@ -399,20 +399,21 @@ public async Task AllPackageDependenciesCanBeFoundWithNuGetConfig() Environment.SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", tempNuGetHttpCacheDirectory); // create two local package sources with different packages available in each - string localSource1 = Path.Combine(temp.DirectoryPath, "localSource1"); + string localSource1 = Path.Combine(temp.DirectoryPath, "local", "source1"); Directory.CreateDirectory(localSource1); - string localSource2 = Path.Combine(temp.DirectoryPath, "localSource2"); + string localSource2 = Path.Combine(temp.DirectoryPath, "local", "source2"); Directory.CreateDirectory(localSource2); - // `Package.A` will only live in `localSource1` and will have a dependency on `Package.B` which is only - // available in `localSource2` + // `Package.A` will only live in `local\source1` and uses Windows-style directory separators and will have + // a dependency on `Package.B` which is only available in `local/source2` and uses Unix-style directory + // separators. MockNuGetPackage.CreateSimplePackage("Package.A", "1.0.0", "net8.0", [(null, [("Package.B", "2.0.0")])]).WriteToDirectory(localSource1); MockNuGetPackage.CreateSimplePackage("Package.B", "2.0.0", "net8.0").WriteToDirectory(localSource2); await File.WriteAllTextAsync(Path.Join(temp.DirectoryPath, "NuGet.Config"), """ - - + + """); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs new file mode 100644 index 00000000000..5c62a0944ab --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public record AnalysisResult +{ + public required string UpdatedVersion { get; init; } + public bool CanUpdate { get; init; } + public bool VersionComesFromMultiDependencyProperty { get; init; } + public required ImmutableArray UpdatedDependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs new file mode 100644 index 00000000000..f5e60227c0e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -0,0 +1,441 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core.Discover; + +namespace NuGetUpdater.Core.Analyze; + +using MultiDependency = (string PropertyName, ImmutableArray TargetFrameworks, ImmutableHashSet DependencyNames); + +public partial class AnalyzeWorker +{ + public const string AnalysisDirectoryName = "./.dependabot/analysis"; + + private readonly Logger _logger; + + internal static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new RequirementConverter() }, + }; + + public AnalyzeWorker(Logger logger) + { + _logger = logger; + } + + public async Task RunAsync(string repoRoot, string discoveryPath, string dependencyPath, string analysisDirectory) + { + var discovery = await DeserializeJsonFileAsync(discoveryPath, nameof(WorkspaceDiscoveryResult)); + var dependencyInfo = await DeserializeJsonFileAsync(dependencyPath, nameof(DependencyInfo)); + var startingDirectory = PathHelper.JoinPath(repoRoot, discovery.Path); + + _logger.Log($"Starting analysis of {dependencyInfo.Name}..."); + + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) + .ToImmutableArray(); + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + var propertyBasedDependencies = discovery.Projects.SelectMany(p + => p.Dependencies.Where(d => !d.IsTransitive && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + + bool usesMultiDependencyProperty = false; + NuGetVersion? updatedVersion = null; + ImmutableArray updatedDependencies = []; + + bool isUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency); + if (isUpdateNecessary) + { + var nugetContext = new NuGetContext(startingDirectory); + if (!Directory.Exists(nugetContext.TempPackageDirectory)) + { + Directory.CreateDirectory(nugetContext.TempPackageDirectory); + } + + _logger.Log($" Determining multi-dependency property."); + var multiDependencies = DetermineMultiDependencyDetails( + discovery, + dependencyInfo.Name, + propertyBasedDependencies); + + usesMultiDependencyProperty = multiDependencies.Any(md => md.DependencyNames.Count > 1); + var dependenciesToUpdate = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.DependencyNames) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + : [dependencyInfo.Name]; + var applicableTargetFrameworks = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.TargetFrameworks) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(NuGetFramework.Parse) + .ToImmutableArray() + : projectFrameworks; + + _logger.Log($" Finding updated version."); + updatedVersion = await FindUpdatedVersionAsync( + startingDirectory, + dependencyInfo, + dependenciesToUpdate, + applicableTargetFrameworks, + nugetContext, + _logger, + CancellationToken.None); + + _logger.Log($" Finding updated peer dependencies."); + updatedDependencies = updatedVersion is not null + ? await FindUpdatedDependenciesAsync( + repoRoot, + discovery, + dependenciesToUpdate, + updatedVersion, + nugetContext, + _logger, + CancellationToken.None) + : []; + + //TODO: At this point we should add the peer dependencies to a queue where + // we will analyze them one by one to see if they themselves are part of a + // multi-dependency property. Basically looping this if-body until we have + // emptied the queue and have a complete list of updated dependencies. We + // should track the dependenciesToUpdate as they have already been analyzed. + } + + var result = new AnalysisResult + { + UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version, + CanUpdate = updatedVersion is not null, + VersionComesFromMultiDependencyProperty = usesMultiDependencyProperty, + UpdatedDependencies = updatedDependencies, + }; + + await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result, _logger); + + _logger.Log($"Analysis complete."); + } + + private static bool IsUpdateNecessary(DependencyInfo dependencyInfo, ImmutableArray projectsWithDependency) + { + if (projectsWithDependency.Length == 0) + { + return false; + } + + // We will even attempt to update transitive dependencies if the dependency is vulnerable. + if (dependencyInfo.IsVulnerable) + { + return true; + } + + // Since the dependency is not vulnerable, we only need to update if it is not transitive. + return projectsWithDependency.Any(p => + p.Dependencies.Any(d => + d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && + !d.IsTransitive)); + } + + internal static async Task DeserializeJsonFileAsync(string path, string fileType) + { + var json = File.Exists(path) + ? await File.ReadAllTextAsync(path) + : throw new FileNotFoundException($"{fileType} file not found.", path); + + return JsonSerializer.Deserialize(json, SerializerOptions) + ?? throw new InvalidOperationException($"{fileType} file is empty."); + } + + internal static async Task FindUpdatedVersionAsync( + string startingDirectory, + DependencyInfo dependencyInfo, + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + dependencyInfo, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + dependencyInfo.Version, + versionResult, + projectFrameworks, + findLowestVersion: dependencyInfo.IsVulnerable, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetVersion version, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + packageIds.First(), + version, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + version.ToNormalizedString(), + versionResult, + projectFrameworks, + findLowestVersion, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + ImmutableArray projectFrameworks, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versions = versionResult.GetVersions(); + var orderedVersions = findLowestVersion + ? versions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. + : versions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. + + return await FindFirstCompatibleVersion( + packageIds, + versionString, + versionResult, + orderedVersions, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindFirstCompatibleVersion( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + IEnumerable orderedVersions, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + if (NuGetVersion.TryParse(versionString, out var currentVersion)) + { + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + currentVersion, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + + if (!isCompatible) + { + // If the current package is incompatible, then don't check for compatibility. + return orderedVersions.First(); + } + } + + foreach (var version in orderedVersions) + { + var existsForAll = await VersionFinder.DoVersionsExistAsync(packageIds, version, nugetContext, logger, cancellationToken); + if (!existsForAll) + { + continue; + } + + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + version, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + + if (isCompatible) + { + return version; + } + } + + // Could not find a compatible version + return null; + } + + internal static async Task AreAllPackagesCompatibleAsync( + ImmutableHashSet packageIds, + NuGetVersion currentVersion, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + var isCompatible = await CompatibilityChecker.CheckAsync( + new(packageId, currentVersion), + projectFrameworks, + nugetContext, + logger, + cancellationToken); + if (!isCompatible) + { + return false; + } + } + + return true; + } + + internal static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + Dependency package, + Logger logger) + { + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + [package], + logger); + result.Add(framework, [.. dependencies]); + } + return result.ToImmutable(); + } + + internal static async Task> FindUpdatedDependenciesAsync( + string repoRoot, + WorkspaceDiscoveryResult discovery, + ImmutableHashSet packageIds, + NuGetVersion updatedVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => packageIds.Contains(d.Name))) + .ToImmutableArray(); + if (projectsWithDependency.Length == 0) + { + return []; + } + + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + + // When updating peer dependencies, we only need to consider top-level dependencies. + var projectDependencyNames = projectsWithDependency + .SelectMany(p => p.Dependencies) + .Where(d => !d.IsTransitive) + .Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + // Determine updated peer dependencies + var workspacePath = PathHelper.JoinPath(repoRoot, discovery.Path); + // We need any project path so the dependency finder can locate the nuget.config + var projectPath = Path.Combine(workspacePath, projectsWithDependency.First().FilePath); + + // Create distinct list of dependencies taking the highest version of each + var dependencyResult = await DependencyFinder.GetDependenciesAsync( + workspacePath, + projectPath, + projectFrameworks, + packageIds, + updatedVersion, + nugetContext, + logger, + cancellationToken); + + // Filter dependencies by whether any project references them + var dependencies = dependencyResult.GetDependencies() + .Where(d => projectDependencyNames.Contains(d.Name)) + .ToImmutableArray(); + + return dependencies; + } + + internal static ImmutableArray DetermineMultiDependencyDetails( + WorkspaceDiscoveryResult discovery, + string packageId, + ImmutableArray propertyBasedDependencies) + { + var packageDeclarationsUsingProperty = discovery.Projects + .SelectMany(p => + p.Dependencies.Where(d => !d.IsTransitive && + d.Name.Equals(packageId, StringComparison.OrdinalIgnoreCase) && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + + return packageDeclarationsUsingProperty + .Select(d => d.EvaluationResult!.RootPropertyName!) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(property => + { + // Find all dependencies that use the same property + var packages = propertyBasedDependencies + .Where(d => property.Equals(d.EvaluationResult?.RootPropertyName, StringComparison.OrdinalIgnoreCase)); + + // Combine all their target frameworks + var tfms = packages.SelectMany(d => d.TargetFrameworks ?? []) + .Distinct() + .ToImmutableArray(); + + var packageIds = packages.Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + return (property, tfms, packageIds); + }).ToImmutableArray(); + } + + internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result, Logger logger) + { + if (!Directory.Exists(analysisDirectory)) + { + Directory.CreateDirectory(analysisDirectory); + } + + var resultPath = Path.Combine(analysisDirectory, $"{dependencyName}.json"); + + logger.Log($" Writing analysis result to [{resultPath}]."); + + var resultJson = JsonSerializer.Serialize(result, SerializerOptions); + await File.WriteAllTextAsync(path: resultPath, resultJson); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs new file mode 100644 index 00000000000..29aa2fa88ba --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -0,0 +1,177 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +using NuGetUpdater.Core.FrameworkChecker; + +namespace NuGetUpdater.Core.Analyze; + +using PackageInfo = (bool IsDevDependency, ImmutableArray Frameworks); +using PackageReaders = (IAsyncPackageCoreReader CoreReader, IAsyncPackageContentReader ContentReader); + +internal static class CompatibilityChecker +{ + public static async Task CheckAsync( + PackageIdentity package, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync( + package, + nugetContext, + cancellationToken); + + return PerformCheck(package, projectFrameworks, isDevDependency, packageFrameworks, logger); + } + + internal static bool PerformCheck( + PackageIdentity package, + ImmutableArray projectFrameworks, + bool isDevDependency, + ImmutableArray packageFrameworks, + Logger logger) + { + // development dependencies are packages such as analyzers which need to be compatible with the compiler not the + // project itself, but some packages that report themselves as development dependencies still contain target + // framework dependencies and should be checked for compatibility through the regular means + if (isDevDependency && packageFrameworks.Length == 0) + { + return true; + } + + if (packageFrameworks.Length == 0 || projectFrameworks.Length == 0) + { + return false; + } + + var compatibilityService = new FrameworkCompatibilityService(); + var compatibleFrameworks = compatibilityService.GetCompatibleFrameworks(packageFrameworks); + + var incompatibleFrameworks = projectFrameworks.Where(f => !compatibleFrameworks.Contains(f)).ToArray(); + if (incompatibleFrameworks.Length > 0) + { + logger.Log($"The package {package} is not compatible. Incompatible project frameworks: {string.Join(", ", incompatibleFrameworks.Select(f => f.GetShortFolderName()))}"); + return false; + } + + return true; + } + + internal static async Task GetPackageInfoAsync( + PackageIdentity package, + NuGetContext nugetContext, + CancellationToken cancellationToken) + { + var tempPackagePath = GetTempPackagePath(package, nugetContext); + var readers = File.Exists(tempPackagePath) + ? ReadPackage(tempPackagePath) + : await DownloadPackageAsync(package, nugetContext, cancellationToken); + + var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken); + var reader = new NuspecReader(nuspecStream); + + var isDevDependency = reader.GetDevelopmentDependency(); + + var tfms = reader.GetDependencyGroups() + .Select(d => d.TargetFramework) + .ToImmutableArray(); + if (tfms.Length == 0) + { + // If the nuspec doesn't have any dependency groups, + // try to get the TargetFramework from files in the lib folder. + var libItems = (await readers.ContentReader.GetLibItemsAsync(cancellationToken)).ToList(); + if (libItems.Count == 0) + { + // If there is no lib folder in this package, then assume it is a dev dependency. + isDevDependency = true; + } + + tfms = libItems.Select(item => item.TargetFramework) + .Distinct() + .ToImmutableArray(); + } + + // The interfaces we given are not disposable but the underlying type can be. + // This will ensure we dispose of any resources that need to be cleaned up. + (readers.CoreReader as IDisposable)?.Dispose(); + (readers.ContentReader as IDisposable)?.Dispose(); + + return (isDevDependency, tfms); + } + + internal static PackageReaders ReadPackage(string tempPackagePath) + { + var stream = new FileStream( + tempPackagePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 4096); + PackageArchiveReader archiveReader = new(stream); + return (archiveReader, archiveReader); + } + + internal static async Task DownloadPackageAsync( + PackageIdentity package, + NuGetContext context, + CancellationToken cancellationToken) + { + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(package.Id).ToHashSet(); + var sources = packageSources.Count == 0 + ? context.PackageSources + : context.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}"); + } + + var exists = await feed.DoesPackageExistAsync( + package.Id, + package.Version, + context.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + + if (!exists) + { + continue; + } + + var downloader = await feed.GetPackageDownloaderAsync( + package, + context.SourceCacheContext, + context.Logger, + cancellationToken); + + var tempPackagePath = GetTempPackagePath(package, context); + var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken); + if (!isDownloaded) + { + throw new Exception($"Failed to download package [{package.Id}/{package.Version}] from [${source.SourceUri}]"); + } + + return (downloader.CoreReader, downloader.ContentReader); + } + + throw new Exception($"Package [{package.Id}/{package.Version}] does not exist in any of the configured sources."); + } + + internal static string GetTempPackagePath(PackageIdentity package, NuGetContext context) + => Path.Combine(context.TempPackageDirectory, package.Id + "." + package.Version + ".nupkg"); +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs new file mode 100644 index 00000000000..87fb53778f3 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static class DependencyFinder +{ + public static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + ImmutableHashSet packageIds, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionString = version.ToNormalizedString(); + var packages = packageIds + .Select(id => new Dependency(id, versionString, DependencyType.Unknown)) + .ToImmutableArray(); + + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + packages, + logger); + var updatedDependencies = new List(); + foreach (var dependency in dependencies) + { + var infoUrl = await nugetContext.GetPackageInfoUrlAsync(dependency.Name, dependency.Version!, cancellationToken); + var updatedDependency = dependency with { IsTransitive = false, InfoUrl = infoUrl }; + updatedDependencies.Add(updatedDependency); + } + + result.Add(framework, updatedDependencies.ToImmutableArray()); + } + + return result.ToImmutable(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs new file mode 100644 index 00000000000..e04b96e17f1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record DependencyInfo +{ + public required string Name { get; init; } + public required string Version { get; init; } + public required bool IsVulnerable { get; init; } + public ImmutableArray IgnoredVersions { get; init; } + public ImmutableArray Vulnerabilities { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs new file mode 100644 index 00000000000..8089fd5e78f --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core; + +internal static class Extensions +{ + public static ImmutableArray GetDependencies(this ImmutableDictionary> dependenciesByTfm) + { + Dictionary dependencies = []; + foreach (var (_framework, dependenciesForTfm) in dependenciesByTfm) + { + foreach (var dependency in dependenciesForTfm) + { + if (dependencies.TryGetValue(dependency.Name, out Dependency? value)) + { + if (NuGetVersion.Parse(value.Version!) < NuGetVersion.Parse(dependency.Version!)) + { + dependencies[dependency.Name] = dependency with + { + TargetFrameworks = [.. value.TargetFrameworks ?? [], .. dependency.TargetFrameworks ?? []] + }; + } + } + else + { + dependencies.Add(dependency.Name, dependency); + } + } + } + + return [.. dependencies.Values]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs new file mode 100644 index 00000000000..0d87b417203 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Text; + +using NuGet.CommandLine; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal record NuGetContext : IDisposable +{ + public SourceCacheContext SourceCacheContext { get; } + public PackageDownloadContext PackageDownloadContext { get; } + public string CurrentDirectory { get; } + public ISettings Settings { get; } + public IMachineWideSettings MachineWideSettings { get; } + public ImmutableArray PackageSources { get; } + public ILogger Logger { get; } + public string TempPackageDirectory { get; } + + public NuGetContext(string? currentDirectory = null, ILogger? logger = null) + { + SourceCacheContext = new SourceCacheContext(); + PackageDownloadContext = new PackageDownloadContext(SourceCacheContext); + CurrentDirectory = currentDirectory ?? Environment.CurrentDirectory; + MachineWideSettings = new CommandLineMachineWideSettings(); + Settings = NuGet.Configuration.Settings.LoadDefaultSettings( + CurrentDirectory, + configFileName: null, + MachineWideSettings); + var sourceProvider = new PackageSourceProvider(Settings); + PackageSources = sourceProvider.LoadPackageSources() + .Where(p => p.IsEnabled) + .ToImmutableArray(); + Logger = logger ?? NullLogger.Instance; + TempPackageDirectory = Path.Combine(Path.GetTempPath(), ".dependabot", "packages"); + } + + public void Dispose() + { + SourceCacheContext.Dispose(); + } + + private readonly Dictionary _packageInfoUrlCache = new(); + + public async Task GetPackageInfoUrlAsync(string packageId, string packageVersion, CancellationToken cancellationToken) + { + var packageIdentity = new PackageIdentity(packageId, NuGetVersion.Parse(packageVersion)); + if (_packageInfoUrlCache.TryGetValue(packageIdentity, out var cachedUrl)) + { + return cachedUrl; + } + + var infoUrl = await FindPackageInfoUrlAsync(packageIdentity, cancellationToken); + _packageInfoUrlCache[packageIdentity] = infoUrl; + + return infoUrl; + } + + private async Task FindPackageInfoUrlAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken) + { + var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(Settings); + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageIdentity.Id).ToHashSet(); + var sources = packageSources.Count == 0 + ? PackageSources + : PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + var message = new StringBuilder(); + message.AppendLine($"finding info url for {packageIdentity}, using package sources: {string.Join(", ", sources.Select(s => s.Name))}"); + + foreach (var source in sources) + { + message.AppendLine($" checking {source.Name}"); + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(cancellationToken); + if (feed is null) + { + message.AppendLine($" feed for {source.Name} was null"); + continue; + } + + var existsInFeed = await feed.Exists( + packageIdentity, + includeUnlisted: false, + SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (!existsInFeed) + { + message.AppendLine($" package {packageIdentity} does not exist in {source.Name}"); + continue; + } + + var downloadResource = await sourceRepository.GetResourceAsync(cancellationToken); + using var downloadResult = await downloadResource.GetDownloadResourceResultAsync(packageIdentity, PackageDownloadContext, globalPackagesFolder, Logger, cancellationToken); + if (downloadResult.Status == DownloadResourceResultStatus.Available) + { + var repositoryMetadata = downloadResult.PackageReader.NuspecReader.GetRepositoryMetadata(); + message.AppendLine($" repometadata: type=[{repositoryMetadata.Type}], url=[{repositoryMetadata.Url}], branch=[{repositoryMetadata.Branch}], commit=[{repositoryMetadata.Commit}]"); + if (!string.IsNullOrEmpty(repositoryMetadata.Url)) + { + return repositoryMetadata.Url; + } + } + else + { + message.AppendLine($" download result status: {downloadResult.Status}"); + } + + var metadataResource = await sourceRepository.GetResourceAsync(cancellationToken); + var metadata = await metadataResource.GetMetadataAsync(packageIdentity, SourceCacheContext, Logger, cancellationToken); + var url = metadata.ProjectUrl ?? metadata.LicenseUrl; + if (url is not null) + { + return url.ToString(); + } + } + + return null; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs new file mode 100644 index 00000000000..63695e787c3 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs @@ -0,0 +1,105 @@ +using System.Collections.Immutable; + +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +/// +/// A Requirement is a set of one or more version restrictions. It supports a +/// few (=, !=, >, <, >=, <=, ~>) different restriction operators. +/// +/// +/// See Gem::Version for a description on how versions and requirements work +/// together in RubyGems. +/// +public class Requirement +{ + private static readonly ImmutableDictionary> Operators = new Dictionary>() + { + ["="] = (v, r) => v == r, + ["!="] = (v, r) => v != r, + [">"] = (v, r) => v > r, + ["<"] = (v, r) => v < r, + [">="] = (v, r) => v >= r, + ["<="] = (v, r) => v <= r, + ["~>"] = (v, r) => v >= r && v.Version < Bump(r), + }.ToImmutableDictionary(); + + public static Requirement Parse(string requirement) + { + var splitIndex = requirement.LastIndexOfAny(['=', '>', '<']); + + // Throw if the requirement is all operator and no version. + if (splitIndex == requirement.Length - 1) + { + throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement)); + } + + string[] parts = splitIndex == -1 + ? [requirement.Trim()] + : [requirement[..(splitIndex + 1)].Trim(), requirement[(splitIndex + 1)..].Trim()]; + + var op = parts.Length == 1 ? "=" : parts[0]; + var version = NuGetVersion.Parse(parts[^1]); + + return new Requirement(op, version); + } + + public string Operator { get; } + public NuGetVersion Version { get; } + + public Requirement(string op, NuGetVersion version) + { + if (!Operators.ContainsKey(op)) + { + throw new ArgumentException("Invalid operator", nameof(op)); + } + + Operator = op; + Version = version; + } + + public override string ToString() + { + return $"{Operator} {Version}"; + } + + public bool IsSatisfiedBy(NuGetVersion version) + { + return Operators[Operator](version, Version); + } + + private static readonly Dictionary BumpMap = []; + /// + /// Return a new version object where the next to the last revision + /// number is one greater (e.g., 5.3.1 => 5.4). + /// + /// + /// This logic intended to be similar to RubyGems Gem::Version#bump + /// + public static Version Bump(NuGetVersion version) + { + if (BumpMap.TryGetValue(version.OriginalVersion!, out var bumpedVersion)) + { + return bumpedVersion; + } + + var versionParts = version.OriginalVersion! // Get the original string this version was created from + .Split('-')[0] // Get the version part without pre-release + .Split('.') // Split into Major.Minor.Patch.Revision if present + .Select(int.Parse) + .ToArray(); + + if (versionParts.Length > 1) + { + versionParts = versionParts[..^1]; // Remove the last part + } + + versionParts[^1]++; // Increment the new last part + + bumpedVersion = NuGetVersion.Parse(string.Join('.', versionParts)).Version; + BumpMap[version.OriginalVersion!] = bumpedVersion; + + return bumpedVersion; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs new file mode 100644 index 00000000000..361643f67fa --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGetUpdater.Core.Analyze; + +public class RequirementConverter : JsonConverter +{ + public override Requirement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Requirement.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, Requirement value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs new file mode 100644 index 00000000000..e2edb0541c1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record SecurityVulnerability +{ + public required string DependencyName { get; init; } + public required string PackageManager { get; init; } + public required ImmutableArray VulnerableVersions { get; init; } + public required ImmutableArray SafeVersions { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs new file mode 100644 index 00000000000..3123e5ac55d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs @@ -0,0 +1,36 @@ +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +public static class SecurityVulnerabilityExtensions +{ + // This logic taken from Dependabot security_advisory.rb + public static bool IsVulnerable(this SecurityVulnerability vulnerability, NuGetVersion version) + { + var inSafeRange = vulnerability.SafeVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inSafeRange) + { + // If version is known safe for this advisory, it's not vulnerable + return false; + } + + var inVulnerableRange = vulnerability.VulnerableVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inVulnerableRange) + { + // If in the vulnerable range and not known safe, it's vulnerable + return true; + } + + if (vulnerability.VulnerableVersions.Length > 0) + { + // If a vulnerable range present but not met, it's not vulnerable + return false; + } + + // Finally, if no vulnerable range provided, but a safe range provided, + // and this versions isn't included (checked earlier), it's vulnerable + return vulnerability.SafeVersions.Length > 0; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs new file mode 100644 index 00000000000..2ad8a4fb07b --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -0,0 +1,179 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static class VersionFinder +{ + public static Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionFilter = CreateVersionFilter(currentVersion); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static Task GetVersionsAsync( + DependencyInfo dependencyInfo, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var packageId = dependencyInfo.Name; + var versionRange = VersionRange.Parse(dependencyInfo.Version); + var currentVersion = versionRange.MinVersion!; + var versionFilter = CreateVersionFilter(dependencyInfo, versionRange); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static async Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + Func versionFilter, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = currentVersion.IsPrerelease; + VersionResult result = new(currentVersion); + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + packageId, + includePrerelease, + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (!existsInFeed) + { + continue; + } + + var feedVersions = (await feed.GetVersions( + packageId, + includePrerelease, + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + CancellationToken.None)).ToHashSet(); + + if (feedVersions.Contains(currentVersion)) + { + result.AddCurrentVersionSource(source); + } + + result.AddRange(source, feedVersions.Where(versionFilter)); + } + + return result; + } + + internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, VersionRange versionRange) + { + // If we are floating to the absolute latest version, we should not filter pre-release versions at all. + var currentVersion = versionRange.Float?.FloatBehavior != NuGetVersionFloatBehavior.AbsoluteLatest + ? versionRange.MinVersion + : null; + + return version => (currentVersion is null || version > currentVersion) + && versionRange.Satisfies(version) + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) + && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) + && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + } + + internal static Func CreateVersionFilter(NuGetVersion currentVersion) + { + return version => version > currentVersion + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version); + } + + public static async Task DoVersionsExistAsync( + IEnumerable packageIds, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + if (!await DoesVersionExistAsync(packageId, version, nugetContext, logger, cancellationToken)) + { + return false; + } + } + + return true; + } + + public static async Task DoesVersionExistAsync( + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = version.IsPrerelease; + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + new PackageIdentity(packageId, version), + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (existsInFeed) + { + return true; + } + } + + return false; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs new file mode 100644 index 00000000000..2c7d304ec28 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; + +using NuGet.Configuration; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal class VersionResult +{ + private readonly Dictionary> _versions = []; + private readonly List _currentVersionSources = []; + + public NuGetVersion CurrentVersion { get; } + + public VersionResult(NuGetVersion currentVersion) + { + CurrentVersion = currentVersion; + } + + public void AddCurrentVersionSource(PackageSource source) + { + _currentVersionSources.Add(source); + } + + public void AddRange(PackageSource source, IEnumerable versions) + { + foreach (var version in versions) + { + if (_versions.ContainsKey(version)) + { + _versions[version].Add(source); + } + else + { + _versions.Add(version, [source]); + } + } + } + + public ImmutableArray GetPackageSources(NuGetVersion version) + { + if (version == CurrentVersion) + { + return [.. _currentVersionSources]; + } + + return [.. _versions[version]]; + } + + public ImmutableArray GetVersions() + { + return [.. _versions.Keys]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs index 9fcbf8072c4..aff773c6db4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs @@ -14,7 +14,8 @@ public sealed record Dependency( bool IsDirect = false, bool IsTransitive = false, bool IsOverride = false, - bool IsUpdate = false) : IEquatable + bool IsUpdate = false, + string? InfoUrl = null) : IEquatable { public bool Equals(Dependency? other) { @@ -37,7 +38,8 @@ public bool Equals(Dependency? other) IsDirect == other.IsDirect && IsTransitive == other.IsTransitive && IsOverride == other.IsOverride && - IsUpdate == other.IsUpdate; + IsUpdate == other.IsUpdate && + InfoUrl == other.InfoUrl; } public override int GetHashCode() @@ -53,6 +55,7 @@ public override int GetHashCode() hash.Add(IsTransitive); hash.Add(IsOverride); hash.Add(IsUpdate); + hash.Add(InfoUrl); return hash.ToHashCode(); } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 32177af1cab..521b70f293b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -39,6 +39,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out workspacePath = workspacePath[1..]; } + string initialWorkspacePath = workspacePath; workspacePath = Path.Combine(repoRootPath, workspacePath); DotNetToolsJsonDiscoveryResult? dotNetToolsJsonDiscovery = null; @@ -75,7 +76,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out var result = new WorkspaceDiscoveryResult { - FilePath = repoRootPath != workspacePath ? Path.GetRelativePath(repoRootPath, workspacePath) : string.Empty, + Path = initialWorkspacePath, DotNetToolsJson = dotNetToolsJsonDiscovery, GlobalJson = globalJsonDiscovery, DirectoryPackagesProps = directoryPackagesPropsDiscovery, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index cae5259bb4c..9f38552b529 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -2,9 +2,9 @@ namespace NuGetUpdater.Core.Discover; -public sealed record WorkspaceDiscoveryResult : IDiscoveryResult +public sealed record WorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs index e49f4c9faf3..7457bd20346 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs @@ -1,5 +1,3 @@ -using System.Linq; - using NuGet.Frameworks; namespace NuGetUpdater.Core.FrameworkChecker; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs index e3e6642e159..349df7b7e6e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using NuGetGallery.Frameworks; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs index 9ded2bf4f2b..429fa041c07 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using static NuGet.Frameworks.FrameworkConstants; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs index 81fde97a042..892d506ef8f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs @@ -1,10 +1,5 @@ extern alias CoreV2; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; using CoreV2::NuGet.Runtime; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs index 79c57646861..29bdf75dbe2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs @@ -1,10 +1,6 @@ extern alias CoreV2; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 5b8fc4c89d9..3ac4c8c769b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -78,8 +78,10 @@ private static async Task DoesDependencyRequireUpdateAsync( tfm, topLevelDependencies, logger); - foreach (var (packageName, packageVersion, _, _, _, _, _, _, _, _) in dependencies) + foreach (var dependency in dependencies) { + var packageName = dependency.Name; + var packageVersion = dependency.Version; if (packageVersion is null) { continue; @@ -263,8 +265,10 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin var packagesAndVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (_, dependencies) in tfmsAndDependencies) { - foreach (var (packageName, packageVersion, _, _, _, _, _, _, _, _) in dependencies) + foreach (var dependency in dependencies) { + var packageName = dependency.Name; + var packageVersion = dependency.Version; if (packagesAndVersions.TryGetValue(packageName, out var existingVersion) && existingVersion != packageVersion) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs index 8468cdb8c56..5d7cce01a8c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs index 5b9af19eda4..39423fb04c7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs @@ -1,5 +1,3 @@ -using System; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs index 41e8051a030..7dc0d277fca 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; - namespace NuGetUpdater.Core.Utilities; public static class HashSetExtensions diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs index ec139486e19..ec1f10aef84 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs index a658bbdfe6c..f4f8eddf3f8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace NuGetUpdater.Core; public sealed class Logger diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 84366d1d183..f952aaefc3d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -231,11 +231,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl ? evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')') : evaluationResult.EvaluatedValue; - // We don't know the version for range requirements or wildcard - // requirements, so return "" for these. - yield return packageVersion.Contains(',') || packageVersion.Contains('*') - ? new Dependency(name, string.Empty, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate) - : new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate); + yield return new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate); } } @@ -483,11 +479,13 @@ internal static async Task CreateTempProjectAsync( // if the source is relative to the original location, copy it to the temp directory if (PathHelper.IsSubdirectoryOf(nugetConfigDir!, localSource.Source)) { - string sourceRelativePath = Path.GetRelativePath(nugetConfigDir!, localSource.Source); + // normalize the directory separators and copy the contents + string localSourcePath = localSource.Source.Replace("\\", "/"); + string sourceRelativePath = Path.GetRelativePath(nugetConfigDir!, localSourcePath); string destPath = Path.Join(tempDir.FullName, sourceRelativePath); - if (Directory.Exists(localSource.Source)) + if (Directory.Exists(localSourcePath)) { - PathHelper.CopyDirectory(localSource.Source, destPath); + PathHelper.CopyDirectory(localSourcePath, destPath); } } } @@ -542,6 +540,7 @@ await File.WriteAllTextAsync( true + false diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs index e6ee282cfb9..3810795c3ad 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - namespace NuGetUpdater.Core; internal static class PathHelper diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs index c7b5da87580..7fd5172e18a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs @@ -1,8 +1,5 @@ -using System; using System.Diagnostics; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace NuGetUpdater.Core; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs index 1a38affa7d7..a747bf54a57 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core; diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb new file mode 100644 index 00000000000..99b08cffb30 --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/dependency" +require "dependabot/nuget/analysis/dependency_analysis" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "json" +require "sorbet-runtime" + +module Dependabot + module Nuget + class AnalysisJsonReader + extend T::Sig + + sig { returns(String) } + def self.temp_directory + File.join(NativeDiscoveryJsonReader.temp_directory, "analysis") + end + + sig { params(dependency_name: String).returns(String) } + def self.analysis_file_path(dependency_name:) + File.join(temp_directory, "#{dependency_name}.json") + end + + sig { params(dependency_name: String).returns(T.nilable(DependencyFile)) } + def self.analysis_json(dependency_name:) + file_path = analysis_file_path(dependency_name: dependency_name) + return unless File.exist?(file_path) + + DependencyFile.new( + name: Pathname.new(file_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(file_path) + ) + end + + sig { params(analysis_json: DependencyFile).void } + def initialize(analysis_json:) + @analysis_json = analysis_json + end + + sig { returns(DependencyAnalysis) } + def dependency_analysis + @dependency_analysis ||= T.let(begin + raise Dependabot::DependencyFileNotParseable, analysis_json.path unless analysis_json.content + + Dependabot.logger.info("#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}") + + parsed_json = T.let(JSON.parse(T.must(analysis_json.content)), T::Hash[String, T.untyped]) + DependencyAnalysis.from_json(parsed_json) + end, T.nilable(DependencyAnalysis)) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, analysis_json.path + end + + private + + sig { returns(DependencyFile) } + attr_reader :analysis_json + end + end +end diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb new file mode 100644 index 00000000000..abb13d946c6 --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/version" +require "sorbet-runtime" + +module Dependabot + module Nuget + class DependencyAnalysis + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(DependencyAnalysis) } + def self.from_json(json) + updated_version = T.let(json.fetch("UpdatedVersion"), String) + can_update = T.let(json.fetch("CanUpdate"), T::Boolean) + version_comes_from_multi_dependency_property = T.let(json.fetch("VersionComesFromMultiDependencyProperty"), + T::Boolean) + updated_dependencies = T.let(json.fetch("UpdatedDependencies"), + T::Array[T::Hash[String, T.untyped]]).map do |dep| + NativeDependencyDetails.from_json(dep) + end + + DependencyAnalysis.new( + updated_version: updated_version, + can_update: can_update, + version_comes_from_multi_dependency_property: version_comes_from_multi_dependency_property, + updated_dependencies: updated_dependencies + ) + end + + sig do + params(updated_version: String, + can_update: T::Boolean, + version_comes_from_multi_dependency_property: T::Boolean, + updated_dependencies: T::Array[NativeDependencyDetails]).void + end + def initialize(updated_version:, can_update:, version_comes_from_multi_dependency_property:, + updated_dependencies:) + @updated_version = updated_version + @can_update = can_update + @version_comes_from_multi_dependency_property = version_comes_from_multi_dependency_property + @updated_dependencies = updated_dependencies + end + + sig { returns(String) } + attr_reader :updated_version + + sig { returns(T::Boolean) } + attr_reader :can_update + + sig { returns(T::Boolean) } + attr_reader :version_comes_from_multi_dependency_property + + sig { returns(T::Array[NativeDependencyDetails]) } + attr_reader :updated_dependencies + + sig { returns(Dependabot::Nuget::Version) } + def numeric_updated_version + @numeric_updated_version ||= T.let(Version.new(updated_version), T.nilable(Dependabot::Nuget::Version)) + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/file_fetcher.rb b/nuget/lib/dependabot/nuget/file_fetcher.rb index 289a0514b0c..2715ee5c23f 100644 --- a/nuget/lib/dependabot/nuget/file_fetcher.rb +++ b/nuget/lib/dependabot/nuget/file_fetcher.rb @@ -34,12 +34,13 @@ def self.required_files_message end sig do - params( - source: Dependabot::Source, - credentials: T::Array[Credential], - repo_contents_path: T.nilable(String), - options: T::Hash[String, String] - ).void + override + .params( + source: Dependabot::Source, + credentials: T::Array[Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[String, String] + ).void end def initialize(source:, credentials:, repo_contents_path: nil, options: {}) super(source: source, credentials: credentials, repo_contents_path: repo_contents_path, options: options) diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index e602f458bc3..6b02c39e7ba 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/discovery/discovery_json_reader" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" require "dependabot/nuget/native_helpers" require "sorbet-runtime" @@ -18,39 +18,46 @@ class FileParser < Dependabot::FileParsers::Base require "dependabot/file_parsers/base/dependency_set" require_relative "cache_manager" + sig { returns(T::Hash[String, T::Array[Dependabot::Dependency]]) } + def self.file_dependency_cache + T.let(CacheManager.cache("file_parser.parse"), T::Hash[String, T::Array[Dependabot::Dependency]]) + end + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse return [] unless repo_contents_path - cache = T.let(CacheManager.cache("file_parser.parse"), T::Hash[String, T::Array[Dependabot::Dependency]]) - # key the cache on the dependency files, excluding the content - key = dependency_files.map { |d| d.to_h.except("content") }.to_s - cache[key] ||= begin + key = NativeDiscoveryJsonReader.create_cache_key(dependency_files) + workspace_path = source&.directory || "/" + self.class.file_dependency_cache[key] ||= begin # run discovery for the repo + discovery_json_path = NativeDiscoveryJsonReader.create_discovery_file_path_from_dependency_files( + dependency_files + ) NativeHelpers.run_nuget_discover_tool(repo_root: T.must(repo_contents_path), - workspace_path: source&.directory || "/", - output_path: DiscoveryJsonReader.discovery_file_path, + workspace_path: workspace_path, + output_path: discovery_json_path, credentials: credentials) - discovered_dependencies.dependencies - end - T.must(cache[key]) - end + discovery_json = NativeDiscoveryJsonReader.discovery_json_from_path(discovery_json_path) + return [] unless discovery_json - private + Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") + discovery_json_reader = NativeDiscoveryJsonReader.new( + discovery_json: discovery_json + ) - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def discovered_dependencies - discovery_json = DiscoveryJsonReader.discovery_json - return DependencySet.new unless discovery_json - - Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") + # cache discovery results + NativeDiscoveryJsonReader.set_discovery_from_dependency_files(dependency_files: dependency_files, + discovery: discovery_json_reader) + discovery_json_reader.dependency_set.dependencies + end - DiscoveryJsonReader.new( - discovery_json: discovery_json - ).dependency_set + T.must(self.class.file_dependency_cache[key]) end + private + sig { returns(T::Array[Dependabot::DependencyFile]) } def proj_files projfile = /\.proj$/ diff --git a/nuget/lib/dependabot/nuget/file_updater.rb b/nuget/lib/dependabot/nuget/file_updater.rb index b9a40f0e844..10b5d758cea 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/discovery/dependency_details" -require "dependabot/nuget/discovery/discovery_json_reader" -require "dependabot/nuget/discovery/workspace_discovery" +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/native_helpers" require "dependabot/shared_helpers" require "sorbet-runtime" @@ -31,7 +31,7 @@ def self.updated_files_regex sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files - base_dir = T.must(dependency_files.first).directory + base_dir = "/" SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do dependencies.each do |dependency| try_update_projects(dependency) || try_update_json(dependency) @@ -111,7 +111,7 @@ def call_nuget_updater_tool(dependency, proj_path) # Ideally we should find a way to not run this code in prod # (or a better way to track calls made to NativeHelpers) @update_tooling_calls ||= T.let({}, T.nilable(T::Hash[String, Integer])) - key = proj_path + dependency.name + key = "#{proj_path.delete_prefix(T.must(repo_contents_path))}+#{dependency.name}" @update_tooling_calls[key] = if @update_tooling_calls[key] T.must(@update_tooling_calls[key]) + 1 @@ -126,18 +126,10 @@ def testonly_update_tooling_calls @update_tooling_calls end - sig { returns(T.nilable(WorkspaceDiscovery)) } + sig { returns(T.nilable(NativeWorkspaceDiscovery)) } def workspace - @workspace ||= T.let(begin - discovery_json = DiscoveryJsonReader.discovery_json - if discovery_json - workspace = DiscoveryJsonReader.new( - discovery_json: discovery_json - ).workspace_discovery - end - - workspace - end, T.nilable(WorkspaceDiscovery)) + discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files) + discovery_json_reader.workspace_discovery end sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[String]) } @@ -145,17 +137,20 @@ 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[DependencyDetails]) } + sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[NativeDependencyDetails]) } def project_dependencies(project_file) - workspace&.projects&.find { |p| p.file_path == project_file.name }&.dependencies || [] + workspace&.projects&.find do |p| + full_project_file_path = File.join(project_file.directory, project_file.name) + p.file_path == full_project_file_path + end&.dependencies || [] end - sig { returns(T::Array[DependencyDetails]) } + sig { returns(T::Array[NativeDependencyDetails]) } def global_json_dependencies workspace&.global_json&.dependencies || [] end - sig { returns(T::Array[DependencyDetails]) } + sig { returns(T::Array[NativeDependencyDetails]) } def dotnet_tools_json_dependencies workspace&.dotnet_tools_json&.dependencies || [] end @@ -164,13 +159,15 @@ def dotnet_tools_json_dependencies sig { params(dependency_file: Dependabot::DependencyFile, updated_content: String).returns(String) } def normalize_content(dependency_file, updated_content) # Fix up line endings - if dependency_file.content&.include?("\r\n") && updated_content.match?(/(? "application/json" } - ) - return unless response.status == 200 + source_url = dependency_source_url + return Source.from_url(source_url) if source_url - # Extract the query url e.g. https://nuget.pkg.github.com/ORG/query - search_base = extract_search_url(response.body) - return unless search_base - - response = Dependabot::RegistryClient.get( - url: search_base + "?q=#{dependency.name.downcase}&prerelease=true&semVerLevel=2.0.0", - headers: { **auth_header, "Accept" => "application/json" } - ) - return unless response.status == 200 - - # Find a projectUrl or licenseUrl that look like a source URL - extract_source_repo(response.body) - rescue JSON::ParserError - # Ignored, this is expected for some registries that don't handle these request. - end - - sig { params(body: String).returns(T.nilable(String)) } - def extract_search_url(body) - JSON.parse(body) - .fetch("resources", []) - .find { |r| r.fetch("@type") == "SearchQueryService" } - &.fetch("@id") - end - - sig { params(body: String).returns(T.nilable(Dependabot::Source)) } - def extract_source_repo(body) - JSON.parse(body).fetch("data", []).each do |search_result| - next unless search_result["id"].casecmp(dependency.name).zero? - - if search_result.key?("projectUrl") - source = Source.from_url(search_result.fetch("projectUrl")) - return source if source - end - if search_result.key?("licenseUrl") - source = Source.from_url(search_result.fetch("licenseUrl")) - return source if source - end - end - # failed to find a source URL nil end - sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(Dependabot::Source)) } - def look_up_source_in_nuspec(nuspec) - potential_source_urls = [ - nuspec.at_css("package > metadata > repository") - &.attribute("url")&.value, - nuspec.at_css("package > metadata > repository > url")&.content, - nuspec.at_css("package > metadata > projectUrl")&.content, - nuspec.at_css("package > metadata > licenseUrl")&.content - ].compact - - source_url = potential_source_urls.find { |url| Source.from_url(url) } - source_url ||= source_from_anywhere_in_nuspec(nuspec) - - Source.from_url(source_url) - end - - sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(String)) } - def source_from_anywhere_in_nuspec(nuspec) - github_urls = [] - nuspec.to_s.force_encoding(Encoding::UTF_8) - .scan(Source::SOURCE_REGEX) do - github_urls << Regexp.last_match.to_s - end - - github_urls.find do |url| - repo = T.must(Source.from_url(url)).repo - repo.downcase.end_with?(dependency.name.downcase) - end - end - - sig { returns(T.nilable(Nokogiri::XML::Document)) } - def dependency_nuspec_file - return @dependency_nuspec_file unless @dependency_nuspec_file.nil? - - return if dependency_nuspec_url.nil? - - response = Dependabot::RegistryClient.get( - url: T.must(dependency_nuspec_url), - headers: auth_header - ) - - @dependency_nuspec_file = Nokogiri::XML(response.body) - end - - sig { returns(T.nilable(String)) } - def dependency_nuspec_url - source = dependency.requirements - .find { |r| r.fetch(:source) }&.fetch(:source) - - source.fetch(:nuspec_url) if source&.key?(:nuspec_url) - end - sig { returns(T.nilable(String)) } def dependency_source_url source = dependency.requirements @@ -164,32 +32,6 @@ def dependency_source_url source.fetch("source_url") end - - # rubocop:disable Metrics/PerceivedComplexity - sig { returns(T::Hash[String, String]) } - def auth_header - source = dependency.requirements - .find { |r| r.fetch(:source) }&.fetch(:source) - url = source&.fetch(:url, nil) || source&.fetch("url") - - token = credentials - .select { |cred| cred["type"] == "nuget_feed" } - .find { |cred| cred["url"] == url } - &.fetch("token", nil) - - 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 - # rubocop:enable Metrics/PerceivedComplexity 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 new file mode 100644 index 00000000000..94cdecf0dab --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb @@ -0,0 +1,102 @@ +# 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 new file mode 100644 index 00000000000..5e4fe8e25a9 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb @@ -0,0 +1,129 @@ +# 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,Metrics/CyclomaticComplexity,Metrics/AbcSize + 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_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) + 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_directory_packages_props_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_directory_packages_props_discovery.rb new file mode 100644 index 00000000000..8e66873225e --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_directory_packages_props_discovery.rb @@ -0,0 +1,44 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_dependency_details" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeDirectoryPackagesPropsDiscovery < NativeDependencyFileDiscovery + extend T::Sig + + sig do + override.params(json: T.nilable(T::Hash[String, T.untyped]), + directory: String).returns(T.nilable(NativeDirectoryPackagesPropsDiscovery)) + end + def self.from_json(json, directory) + return nil if json.nil? + + file_path = File.join(directory, 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| + NativeDependencyDetails.from_json(dep) + end + + NativeDirectoryPackagesPropsDiscovery.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[NativeDependencyDetails]).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/native_discovery/native_discovery_json_reader.rb b/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb new file mode 100644 index 00000000000..26f53fbaff9 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb @@ -0,0 +1,174 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/dependency" +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.discovery_result_cache + T.let(CacheManager.cache("discovery_json_cache"), T::Hash[String, NativeDiscoveryJsonReader]) + end + + sig { returns(T::Hash[String, String]) } + def self.discovery_path_cache + T.let(CacheManager.cache("discovery_path_cache"), T::Hash[String, String]) + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(NativeDiscoveryJsonReader) + end + def self.get_discovery_from_dependency_files(dependency_files) + key = create_cache_key(dependency_files) + discovery_json = discovery_result_cache[key] + raise "No discovery result for specified dependency files: #{key}" unless discovery_json + + discovery_json + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile], + discovery: NativeDiscoveryJsonReader + ).void + end + def self.set_discovery_from_dependency_files(dependency_files:, discovery:) + key = create_cache_key(dependency_files) + discovery_result_cache[key] = discovery + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(String) + end + def self.get_discovery_file_path_from_dependency_files(dependency_files) + key = create_cache_key(dependency_files) + discovery_path = discovery_path_cache[key] + raise "No discovery path found for specified dependency files: #{key}" unless discovery_path + + discovery_path + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(String) + end + def self.create_discovery_file_path_from_dependency_files(dependency_files) + discovery_key = create_cache_key(dependency_files) + if discovery_path_cache[discovery_key] + raise "Discovery file path already exists for the given dependency files: #{discovery_key}" + end + + discovery_counter_cache = T.let(CacheManager.cache("discovery_counter_cache"), T::Hash[String, Integer]) + counter_key = "counter" + current_counter = discovery_counter_cache[counter_key] || 0 + current_counter += 1 + discovery_counter_cache[counter_key] = current_counter + incremeted_discovery_file_path = File.join(temp_directory, "discovery.#{current_counter}.json") + discovery_path_cache[discovery_key] = incremeted_discovery_file_path + incremeted_discovery_file_path + end + + # this is a test-only method + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).void + end + def self.clear_discovery_file_path_from_cache(dependency_files) + key = create_cache_key(dependency_files) + discovery_file_path = discovery_path_cache[key] + File.delete(discovery_file_path) if discovery_file_path && File.exist?(discovery_file_path) + discovery_path_cache.delete(key) + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(String) + end + def self.create_cache_key(dependency_files) + dependency_files.map { |d| d.to_h.except("content") }.to_s + end + + sig { returns(String) } + def self.temp_directory + File.join(Dir.tmpdir, ".dependabot") + end + + sig do + params( + discovery_json_path: String + ).returns(T.nilable(DependencyFile)) + end + def self.discovery_json_from_path(discovery_json_path) + return unless File.exist?(discovery_json_path) + + DependencyFile.new( + name: Pathname.new(discovery_json_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(discovery_json_path) + ) + end + + sig { returns(T.nilable(NativeWorkspaceDiscovery)) } + attr_reader :workspace_discovery + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + attr_reader :dependency_set + + sig { params(discovery_json: DependencyFile).void } + def initialize(discovery_json:) + @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) + end + + private + + 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.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 + dependency_set += T.must(workspace_result.global_json).dependency_set if workspace_result.global_json + + dependency_set + 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 new file mode 100644 index 00000000000..37e13d67ad5 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb @@ -0,0 +1,63 @@ +# 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 new file mode 100644 index 00000000000..fc92becc047 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb @@ -0,0 +1,82 @@ +# 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 + + 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 + + NativeProjectDiscovery.new(file_path: file_path, + properties: properties, + target_frameworks: target_frameworks, + referenced_project_paths: referenced_project_paths, + dependencies: dependencies) + end + + 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]).void + end + def initialize(file_path:, properties:, target_frameworks:, referenced_project_paths:, dependencies:) + super(file_path: file_path, dependencies: dependencies) + @properties = properties + @target_frameworks = target_frameworks + @referenced_project_paths = referenced_project_paths + 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 { 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 new file mode 100644 index 00000000000..aa29f5c48ea --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb @@ -0,0 +1,43 @@ +# 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 new file mode 100644 index 00000000000..b16ec1ee447 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb @@ -0,0 +1,68 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_dependency_file_discovery" +require "dependabot/nuget/native_discovery/native_directory_packages_props_discovery" +require "dependabot/nuget/native_discovery/native_project_discovery" +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) + 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 + directory_packages_props = NativeDirectoryPackagesPropsDiscovery + .from_json(T.let(json.fetch("DirectoryPackagesProps"), + T.nilable(T::Hash[String, T.untyped])), path) + 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, + directory_packages_props: directory_packages_props, + global_json: global_json, + dotnet_tools_json: dotnet_tools_json) + end + + sig do + params(path: String, + projects: T::Array[NativeProjectDiscovery], + directory_packages_props: T.nilable(NativeDirectoryPackagesPropsDiscovery), + global_json: T.nilable(NativeDependencyFileDiscovery), + dotnet_tools_json: T.nilable(NativeDependencyFileDiscovery)).void + end + def initialize(path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) + @path = path + @projects = projects + @directory_packages_props = directory_packages_props + @global_json = global_json + @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(NativeDirectoryPackagesPropsDiscovery)) } + attr_reader :directory_packages_props + + 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_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index 4047feb226d..f75ac88db27 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -110,6 +110,65 @@ def self.run_nuget_discover_tool(repo_root:, workspace_path:, output_path:, cred end end + sig do + params(repo_root: String, discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String).returns([String, String]) + end + def self.get_nuget_analyze_tool_command(repo_root:, discovery_file_path:, dependency_file_path:, + analysis_folder_path:) + exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") + command_parts = [ + exe_path, + "analyze", + "--repo-root", + repo_root, + "--discovery-file-path", + discovery_file_path, + "--dependency-file-path", + dependency_file_path, + "--analysis-folder-path", + analysis_folder_path, + "--verbose" + ].compact + + command = Shellwords.join(command_parts) + + fingerprint = [ + exe_path, + "analyze", + "--discovery-file-path", + "", + "--dependency-file-path", + "", + "--analysis-folder-path", + "", + "--verbose" + ].compact.join(" ") + + [command, fingerprint] + end + + sig do + params( + repo_root: String, discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String, credentials: T::Array[Dependabot::Credential] + ).void + end + def self.run_nuget_analyze_tool(repo_root:, discovery_file_path:, dependency_file_path:, + analysis_folder_path:, credentials:) + (command, fingerprint) = get_nuget_analyze_tool_command(repo_root: repo_root, + discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path) + + puts "running NuGet analyze:\n" + command + + NuGetConfigCredentialHelpers.patch_nuget_config_for_action(credentials) do + output = SharedHelpers.run_shell_command(command, allow_unsafe_shell_command: true, fingerprint: fingerprint) + puts output + end + end + sig do params(repo_root: String, proj_path: String, dependency: Dependency, is_transitive: T::Boolean).returns([String, String]) diff --git a/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb b/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb new file mode 100644 index 00000000000..ac8264e9c50 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb @@ -0,0 +1,105 @@ +# 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 new file mode 100644 index 00000000000..de3e07aabcf --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb @@ -0,0 +1,200 @@ +# 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 + File.join(NativeDiscoveryJsonReader.temp_directory, "dependency", "#{dependency.name}.json") + end + + sig { returns(AnalysisJsonReader) } + def request_analysis + discovery_file_path = NativeDiscoveryJsonReader.get_discovery_file_path_from_dependency_files(dependency_files) + 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 + + File.write(dependency_file_path, dependency_info) + end + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def discovered_dependencies + discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files) + discovery_json_reader.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_config_credential_helpers.rb b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb index c1ac11f2a02..ad8eee731b2 100644 --- a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb +++ b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb @@ -69,12 +69,13 @@ def self.patch_nuget_config_for_action(credentials, &_block) begin yield rescue StandardError => e - Dependabot.logger.error( + log_message = <<~LOG_MESSAGE Block argument of NuGetConfigCredentialHelpers::patch_nuget_config_for_action causes an exception #{e}: #{e.message} LOG_MESSAGE - ) + Dependabot.logger.error(log_message) + puts log_message ensure restore_user_nuget_config end diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index 28b3a26eeb2..2e7dde12866 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -16,10 +16,19 @@ class UpdateChecker < Dependabot::UpdateCheckers::Base 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? @@ -32,6 +41,8 @@ def latest_version 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 @@ -39,10 +50,12 @@ 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) end - sig { override.returns(T.nilable(Dependabot::Version)) } + sig { override.returns(T.nilable(Dependabot::Nuget::Version)) } def lowest_resolvable_security_fix_version return nil if version_comes_from_multi_dependency_property? @@ -51,12 +64,16 @@ def lowest_resolvable_security_fix_version 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? + RequirementsUpdater.new( requirements: dependency.requirements, latest_version: preferred_resolvable_version_details&.fetch(:version, nil)&.to_s, @@ -66,6 +83,8 @@ def updated_requirements 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? @@ -89,6 +108,26 @@ def requirements_unlocked_or_can_be? 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) + ) + 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 @@ -101,6 +140,10 @@ def preferred_resolvable_version_details 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? + 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? @@ -109,6 +152,10 @@ def latest_version_resolvable_with_full_unlock? 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}." diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index c8b965d0ca8..276d4178f9e 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -14,6 +14,17 @@ 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 + + let(:dependency_files) { [csproj_file] + additional_files } + let(:additional_files) { [] } + let(:csproj_file) do + Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) + end + let(:csproj_body) { fixture("csproj", "basic.csproj") } + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:directory) { "/" } let(:source) do Dependabot::Source.new( provider: "github", @@ -21,71 +32,111 @@ directory: directory ) end - let(:directory) { "/" } - let(:parser) do - described_class.new(dependency_files: files, - source: source, - repo_contents_path: repo_contents_path) - end - let(:repo_contents_path) { write_tmp_repo(files) } - let(:csproj_body) { fixture("csproj", "basic.csproj") } - let(:csproj_file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:additional_files) { [] } let(:files) { [csproj_file] + additional_files } it_behaves_like "a dependency file parser" - describe "parse" do - subject(:top_level_dependencies) { dependencies.select(&:top_level?) } + def run_parser_test(&_block) + # caching is explicitly required for these tests + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" - let(:dependencies) { parser.parse } + # don't allow a previous test to pollute the file parser cache + Dependabot::Nuget::FileParser.file_dependency_cache.clear - context "with a single project file" do - before do - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1", "1.1.1"]) - stub_search_results_with_versions_v3("microsoft.aspnetcore.app", []) - stub_search_results_with_versions_v3("microsoft.net.test.sdk", []) - stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", ["1.1.0"]) - stub_search_results_with_versions_v3("system.collections.specialized", ["4.3.0"]) - end + # create the parser... + parser = Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path) - its(:length) { is_expected.to eq(5) } - - describe "the Microsoft.Extensions.DependencyModel dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } + # ...and invoke the actual test + yield parser + ensure + Dependabot::Nuget::NativeDiscoveryJsonReader.clear_discovery_file_path_from_cache(dependency_files) + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" + end - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) + def intercept_native_tools(discovery_content_hash:) + return unless stub_native_tools + + # don't allow `FileParser#parse` to call into the native tool; just fake it + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_discover_tool) + .and_wrap_original do |_original_method, *args, &_block| + discovery_json_path = args[0][:output_path] + FileUtils.mkdir_p(File.dirname(discovery_json_path)) + if report_stub_debug_information + puts "stubbing call to `run_nuget_discover_tool` with args #{args}; writing prefabricated discovery " \ + "response to discovery.json to #{discovery_json_path}" end + discovery_json_content = discovery_content_hash.to_json + File.write(discovery_json_path, discovery_json_content) + end + end + + describe "parse" do + context "with a single project file" do + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "System.Collections.Specialized", + Version: "4.3.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFrameworks", + Value: "net462", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net462"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - describe "the System.Collections.Specialized dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "System.Collections.Specialized" } } + it "is returns the expected set of dependencies" do + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(2) - it "has the right details" do + dependency = dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.Collections.Specialized") - expect(dependency.version).to eq("4.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "4.3.0", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) + expect(dependency.version).to eq("1.1.1") + expect(dependency.requirements).to eq([{ + requirement: "1.1.1", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }]) end end end @@ -100,50 +151,80 @@ end before do - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1", "1.1.1"]) - stub_search_results_with_versions_v3("microsoft.aspnetcore.app", []) - stub_search_results_with_versions_v3("microsoft.net.test.sdk", []) - stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", ["1.1.0"]) - stub_search_results_with_versions_v3("system.collections.specialized", ["4.3.0"]) - stub_search_results_with_versions_v3("serilog", ["2.3.0"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFrameworks", + Value: "net462", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net462"], + ReferencedProjectPaths: [] + }, { + FilePath: "my.vbproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFrameworks", + Value: "net462", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net462"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(6) } - - describe "the Microsoft.Extensions.DependencyModel dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } - - it "has the right details" do + it "reports the correct dependency information" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } expect(dependency).to be_a(Dependabot::Dependency) expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel") expect(dependency.version).to eq("1.0.1") expect(dependency.requirements).to eq( [{ requirement: "1.1.1", - file: "my.csproj", + file: "/my.csproj", groups: ["dependencies"], source: nil }, { requirement: "1.0.1", - file: "my.vbproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "the Serilog dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Serilog") - expect(dependency.version).to eq("2.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "2.3.0", - file: "my.vbproj", + file: "/my.vbproj", groups: ["dependencies"], source: nil }] @@ -172,24 +253,52 @@ XML end - its(:length) { is_expected.to eq(9) } - - describe "the Microsoft.CodeDom.Providers.DotNetCompilerPlatform dependency" do - subject(:dependency) do - dependencies.find do |d| - d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" - end - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.CodeDom.Providers.DotNetCompilerPlatform", + Version: "1.0.0", + Type: "PackagesConfig", + EvaluationResult: nil, + TargetFrameworks: ["netstandard2.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard2.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard2.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end - it "has the right details" do + it "reports the correct dependencies" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.CodeDom.Providers.DotNetCompilerPlatform") expect(dependency.version).to eq("1.0.0") expect(dependency.requirements).to eq( [{ requirement: "1.0.0", - file: "packages.config", + file: "/packages.config", groups: ["dependencies"], source: nil }] @@ -197,26 +306,7 @@ end end - describe "the Microsoft.Net.Compilers dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Net.Compilers" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.Net.Compilers") - expect(dependency.version).to eq("1.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.1", - file: "packages.config", - groups: ["devDependencies"], - source: nil - }] - ) - end - end - - context "when the dependency is nested" do + context "when it is nested" do let(:directory) { "/dir" } let(:packages_config) do Dependabot::DependencyFile.new( @@ -228,49 +318,58 @@ Dependabot::DependencyFile.new(name: "dir/my.csproj", content: csproj_body) end - its(:length) { is_expected.to eq(9) } - - describe "the Microsoft.CodeDom.Providers.DotNetCompilerPlatform dependency" do - subject(:dependency) do - dependencies.find do |d| - d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" - end - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "dir", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.CodeDom.Providers.DotNetCompilerPlatform", + Version: "1.0.0", + Type: "PackagesConfig", + EvaluationResult: nil, + TargetFrameworks: ["netstandard2.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard2.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard2.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end - it "has the right details" do + it "reports the correct results" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.CodeDom.Providers.DotNetCompilerPlatform") expect(dependency.version).to eq("1.0.0") expect(dependency.requirements).to eq( [{ requirement: "1.0.0", - file: "packages.config", + file: "/dir/packages.config", groups: ["dependencies"], source: nil }] ) end end - - describe "the Microsoft.Net.Compilers dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Net.Compilers" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.Net.Compilers") - expect(dependency.version).to eq("1.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.1", - file: "packages.config", - groups: ["devDependencies"], - source: nil - }] - ) - end - end end end @@ -283,19 +382,45 @@ ) end - its(:length) { is_expected.to eq(6) } - - describe "the Microsoft.Build.Traversal dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Build.Traversal" } } + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [], # not relevant for this test + DirectoryPackagesProps: nil, + GlobalJson: { + FilePath: "global.json", + IsSuccess: true, + Dependencies: [{ + Name: "Microsoft.Build.Traversal", + Version: "1.0.45", + Type: "MSBuildSdk", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + }, + DotNetToolsJson: nil + } + ) + end - it "has the right details" do + it "reports the expected results" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.Build.Traversal" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Build.Traversal") expect(dependency.version).to eq("1.0.45") expect(dependency.requirements).to eq( [{ requirement: "1.0.45", - file: "global.json", + file: "/global.json", groups: ["dependencies"], source: nil }] @@ -313,19 +438,45 @@ ) end - its(:length) { is_expected.to eq(7) } - - describe "the dotnetsay dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "dotnetsay" } } + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [], # not relevant for this test + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: { + FilePath: ".config/dotnet-tools.json", + IsSuccess: true, + Dependencies: [{ + Name: "dotnetsay", + Version: "1.0.0", + Type: "DotNetTool", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + } + ) + end - it "has the right details" do + it "has the right details" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "dotnetsay" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("dotnetsay") expect(dependency.version).to eq("1.0.0") expect(dependency.requirements).to eq( [{ requirement: "1.0.0", - file: ".config/dotnet-tools.json", + file: "/.config/dotnet-tools.json", groups: ["dependencies"], source: nil }] @@ -355,31 +506,82 @@ end before do - stub_search_results_with_versions_v3("serilog", ["2.3.0"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "commonprops.props", + Dependencies: [{ + Name: "Serilog", + Version: "2.3.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [], + TargetFrameworks: [], + ReferencedProjectPaths: [] + }, { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Serilog", + Version: "2.3.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["netstandard1.6"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard1.6", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard1.6"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(1) } - describe "the Serilog dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Serilog") - expect(dependency.version).to eq("2.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "2.3.0", - file: "commonprops.props", - groups: ["dependencies"], - source: nil - }, { - requirement: "2.3.0", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Serilog" } + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("Serilog") + expect(dependency.version).to eq("2.3.0") + expect(dependency.requirements).to eq( + [{ + requirement: "2.3.0", + file: "/commonprops.props", + groups: ["dependencies"], + source: nil + }, { + requirement: "2.3.0", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }] + ) + end end end end @@ -405,35 +607,81 @@ end before do - stub_search_results_with_versions_v3("microsoft.sourcelink.github", ["1.0.0-beta2-19367-01"]) - stub_search_results_with_versions_v3("system.lycos", ["3.23.3"]) - stub_search_results_with_versions_v3("system.askjeeves", ["2.2.2"]) - stub_search_results_with_versions_v3("system.google", ["0.1.0-beta.3"]) - stub_search_results_with_versions_v3("system.webcrawler", ["1.1.1"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["netstandard1.6"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: true, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard1.6", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard1.6"], + ReferencedProjectPaths: [] + }, { + FilePath: "packages.props", + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: true, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [], + TargetFrameworks: [], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(5) } - describe "the System.WebCrawler dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "System.WebCrawler" } } - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.WebCrawler") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }, { - requirement: "1.1.1", - file: "packages.props", - groups: ["dependencies"], - source: nil - }] - ) + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "System.WebCrawler" } + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.version).to eq("1.1.1") + expect(dependency.requirements).to eq( + [{ + requirement: "1.1.1", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }, { + requirement: "1.1.1", + file: "/packages.props", + groups: ["dependencies"], + source: nil + }] + ) + end end end end @@ -464,40 +712,87 @@ end before do - stub_search_results_with_versions_v3("system.lycos", ["3.23.3"]) - stub_search_results_with_versions_v3("system.askjeeves", ["2.2.2"]) - stub_search_results_with_versions_v3("system.google", ["0.1.0-beta.3"]) - stub_search_results_with_versions_v3("system.webcrawler", ["1.1.1"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["netstandard1.6"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard1.6", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard1.6"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: { + FilePath: "Directory.Packages.props", + IsSuccess: true, + IsTransitivePinningEnabled: false, + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageVersion", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + }, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(4) } - describe "the System.WebCrawler dependency" do subject(:dependency) { dependencies.find { |d| d.name == "System.WebCrawler" } } it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.WebCrawler") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }, { - requirement: "1.1.1", - file: "Directory.Packages.props", - groups: ["dependencies"], - source: nil - }] - ) + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "System.WebCrawler" } + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.version).to eq("1.1.1") + expect(dependency.requirements).to eq( + [{ + requirement: "1.1.1", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }, { + requirement: "1.1.1", + file: "/Directory.Packages.props", + groups: ["dependencies"], + source: nil + }] + ) + end end end end context "with only directory.packages.props file" do - let(:files) { [packages_file] } + let(:dependency_files) { [packages_file] } let(:packages_file) do Dependabot::DependencyFile.new( name: "directory.packages.props", @@ -505,8 +800,12 @@ ) end - it do - expect { dependencies }.to raise_error(Dependabot::DependencyFileNotFound) + it "fails in the initializer" do + expect do + run_parser_test do |parser| + _dependencies = parser.parse + end + end.to raise_error(Dependabot::DependencyFileNotFound) end end @@ -531,96 +830,28 @@ before do allow(Dependabot.logger).to receive(:info) - stub_search_results_with_versions_v3("some.package", ["1.2.3"]) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/some.package/1.2.3/some.package.nuspec") - .to_return( - status: 200, - body: - <<~XML - - - Some.Package - 1.2.3 - - - - - - - XML - ) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end it "reports the relevant information" do - expect(dependencies.length).to eq(1) # this line is really just to force evaluation so we can see the infos - expect(Dependabot.logger).to have_received(:info).with( - <<~INFO - Discovery JSON content: { - "FilePath": "", - "IsSuccess": true, - "Projects": [ - { - "FilePath": "my.csproj", - "Dependencies": [ - { - "Name": "Microsoft.NET.Sdk", - "Version": null, - "Type": "MSBuildSdk", - "EvaluationResult": null, - "TargetFrameworks": null, - "IsDevDependency": false, - "IsDirect": false, - "IsTransitive": false, - "IsOverride": false, - "IsUpdate": false - }, - { - "Name": "Some.Package", - "Version": "1.2.3", - "Type": "PackageReference", - "EvaluationResult": { - "ResultType": "Success", - "OriginalValue": "$(SomePackageVersion)", - "EvaluatedValue": "1.2.3", - "RootPropertyName": "SomePackageVersion", - "ErrorMessage": null - }, - "TargetFrameworks": [ - "net8.0" - ], - "IsDevDependency": false, - "IsDirect": true, - "IsTransitive": false, - "IsOverride": false, - "IsUpdate": false - } - ], - "IsSuccess": true, - "Properties": [ - { - "Name": "SomePackageVersion", - "Value": "1.2.3", - "SourceFilePath": "my.csproj" - }, - { - "Name": "TargetFramework", - "Value": "net8.0", - "SourceFilePath": "my.csproj" - } - ], - "TargetFrameworks": [ - "net8.0" - ], - "ReferencedProjectPaths": [] - } - ], - "DirectoryPackagesProps": null, - "GlobalJson": null, - "DotNetToolsJson": null - } - INFO - .chomp - ) + run_parser_test do |parser| + _dependencies = parser.parse # the result doesn't matter, but it forces discovery to run + expect(Dependabot.logger).to have_received(:info).with( + <<~INFO + Discovery JSON content: {"Path":"","IsSuccess":true,"Projects":[],"DirectoryPackagesProps":null,"GlobalJson":null,"DotNetToolsJson":null} + INFO + .chomp + ) + end end end @@ -645,35 +876,71 @@ before do allow(Dependabot.logger).to receive(:warn) - stub_search_results_with_versions_v3("package.a", ["1.2.3"]) - stub_search_results_with_versions_v3("package.b", ["4.5.6"]) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/package.a/1.2.3/package.a.nuspec") - .to_return( - status: 200, - body: - <<~XML - - - Package.A - 1.2.3 - - - - - - - XML - ) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Package.A", + Version: "1.2.3", + Type: "PackageReference", + EvaluationResult: { + ResultType: "Success", + OriginalValue: "1.2.3", + EvaluatedValue: "1.2.3", + RootPropertyName: nil, + ErrorMessage: nil + }, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.B", + Version: "$(ThisPropertyCannotBeResolved)", + Type: "PackageReference", + EvaluationResult: { + ResultType: "PropertyNotFound", + OriginalValue: "$(ThisPropertyCannotBeResolved)", + EvaluatedValue: "$(ThisPropertyCannotBeResolved)", + RootPropertyName: "ThisPropertyCannotBeResolved", + ErrorMessage: "Property 'ThisPropertyCannotBeResolved' was not found." + }, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(1) } - - describe "the Package.A dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Package.A" } } - - it "has the right details" do + it "has the right details" do + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(1) + dependency = dependencies[0] expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Package.A") expect(dependency.version).to eq("1.2.3") expect(Dependabot.logger).to have_received(:warn).with( "Dependency 'Package.B' excluded due to unparsable version: $(ThisPropertyCannotBeResolved)" @@ -682,34 +949,7 @@ end end - context "with a property that can't be evaluated" do - let(:csproj_file) do - Dependabot::DependencyFile.new( - name: "my.csproj", - content: - <<~XML - - - $(SomeCommonTfmThatCannotBeResolved) - - - - - - XML - ) - end - - before do - allow(Dependabot.logger).to receive(:warn) - end - - it "does not return the `.csproj` with an unresolvable TFM" do - expect(dependencies.length).to eq(0) - end - end - - context "when packages are referenced in implicitly included `.targets` file" do + context "when packages referenced in implicitly included `.targets` file are reported" do let(:additional_files) { [directory_build_targets] } let(:csproj_file) do Dependabot::DependencyFile.new( @@ -742,19 +982,83 @@ end before do - stub_search_results_with_versions_v3("package.a", ["1.2.3"]) - stub_search_results_with_versions_v3("package.b", ["4.5.6"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "Directory.Build.targets", + Dependencies: [{ + Name: "Package.B", + Version: "4.5.6", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [], + TargetFrameworks: [], + ReferencedProjectPaths: [] + }, { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Package.A", + Version: "1.2.3", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.B", + Version: "4.5.6", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end it "returns the correct dependency set" do - expect(dependencies.length).to eq(2) - expect(dependencies.map(&:name)).to match_array(%w(Package.A Package.B)) - expect(dependencies.map(&:version)).to match_array(%w(1.2.3 4.5.6)) + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(2) + expect(dependencies.map(&:name)).to match_array(%w(Package.A Package.B)) + expect(dependencies.map(&:version)).to match_array(%w(1.2.3 4.5.6)) + end end end - context "when the project element can be resolved from implicitly imported file" do - let(:additional_files) { [directory_build_props] } + context "when non-concrete version numbers are reported" do let(:csproj_file) do Dependabot::DependencyFile.new( name: "my.csproj", @@ -762,7 +1066,7 @@ <<~XML - $(SomeTfm) + net8.0 @@ -771,28 +1075,97 @@ XML ) end - let(:directory_build_props) do - Dependabot::DependencyFile.new( - name: "Directory.Build.props", - content: - <<~XML - - - net8.0 - - - XML - ) - end before do - stub_search_results_with_versions_v3("package.a", ["1.2.3"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Package.A", + Version: nil, # not reported without version + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.B", + Version: "", # not reported with empty version + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.C", + Version: "[1.0,2.0)", # not reported with range + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.D", + Version: "1.*", # not reported with wildcard + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.E", + Version: "1.2.3", # regular version _is_ reported + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end it "returns the correct dependency set" do - expect(dependencies.length).to eq(1) - expect(dependencies[0].name).to eq("Package.A") - expect(dependencies[0].version).to eq("1.2.3") + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(1) + expect(dependencies[0].name).to eq("Package.E") + end end end end diff --git a/nuget/spec/dependabot/nuget/file_updater_spec.rb b/nuget/spec/dependabot/nuget/file_updater_spec.rb index 0d8904e4128..6c0bf3d5dbc 100644 --- a/nuget/spec/dependabot/nuget/file_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/file_updater_spec.rb @@ -16,16 +16,21 @@ config.include(NuGetSearchStubs) end - let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } - let(:previous_requirements) do - [{ file: "dirs.proj", requirement: "1.0.0", groups: [], source: nil }] - end - let(:requirements) do - [{ file: "dirs.proj", requirement: "1.1.1", groups: [], source: nil }] + 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 + + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) end - let(:dependency_previous_version) { "1.0.0" } - let(:dependency_version) { "1.1.1" } - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } + let(:dependencies) { [dependency] } + let(:project_name) { "file_updater_dirsproj" } + let(:directory) { "/" } + # project_dependency files comes back with directory files first, we need the closest project at the top + let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } let(:dependency) do Dependabot::Dependency.new( name: dependency_name, @@ -36,23 +41,41 @@ package_manager: "nuget" ) end - # project_dependency files comes back with directory files first, we need the closest project at the top - let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } - let(:directory) { "/" } - let(:project_name) { "file_updater_dirsproj" } - let(:dependencies) { [dependency] } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) + let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } + let(:dependency_version) { "1.1.1" } + let(:dependency_previous_version) { "1.0.0" } + let(:requirements) do + [{ file: "dirs.proj", requirement: "1.1.1", groups: [], source: nil }] + end + let(:previous_requirements) do + [{ file: "dirs.proj", requirement: "1.0.0", groups: [], source: nil }] end - let(:file_updater_instance) do + let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } + + 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 run_update_test(&_block) + # caching is explicitly required for these tests + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" + + # don't allow a previous test to pollute the file parser cache + Dependabot::Nuget::FileParser.file_dependency_cache.clear + + # calling `#parse` is necessary to force `discover` which is stubbed below Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, source: source, repo_contents_path: repo_contents_path).parse - described_class.new( + + # create the file updater... + updater = described_class.new( dependency_files: dependency_files, dependencies: dependencies, credentials: [{ @@ -61,31 +84,82 @@ }], repo_contents_path: repo_contents_path ) - 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")) + # ...and invoke the actual test + yield updater + ensure + Dependabot::Nuget::NativeDiscoveryJsonReader.clear_discovery_file_path_from_cache(dependency_files) + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" end - it_behaves_like "a dependency file updater" + def intercept_native_tools(discovery_content_hash:) + return unless stub_native_tools + + # don't allow `FileParser#parse` to call into the native tool; just fake it + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_discover_tool) + .and_wrap_original do |_original_method, *args, &_block| + discovery_json_path = args[0][:output_path] + FileUtils.mkdir_p(File.dirname(discovery_json_path)) + if report_stub_debug_information + puts "stubbing call to `run_nuget_discover_tool` with args #{args}; writing prefabricated discovery " \ + "response to discovery.json to #{discovery_json_path}" + end + discovery_json_content = discovery_content_hash.to_json + File.write(discovery_json_path, discovery_json_content) + end + end describe "#updated_dependency_files" do - subject(:updated_files) { file_updater_instance.updated_dependency_files } + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "Proj1/Proj1/Proj1.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net461"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net461", + SourceFilePath: "Proj1/Proj1/Proj1.csproj" + }], + TargetFrameworks: ["net461"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end context "with a dirs.proj" do it "does not repeatedly update the same project" do - puts dependency_files.map(&:name) - expect(updated_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj") + run_update_test do |updater| + expect(updater.updated_dependency_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj") - expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( - { - "#{repo_contents_path}/Proj1/Proj1/Proj1.csprojMicrosoft.Extensions.DependencyModel" => 1 - } - ) + expect(updater.send(:testonly_update_tooling_calls)).to eq( + { + "/Proj1/Proj1/Proj1.csproj+Microsoft.Extensions.DependencyModel" => 1 + } + ) + end end context "when the file has only deleted lines" do @@ -98,30 +172,94 @@ end it "does not update the project" do - expect(updated_files.map(&:name)).to be_empty + run_update_test do |updater| + expect(updater.updated_dependency_files.map(&:name)).to be_empty + end end end end end describe "#updated_dependency_files_with_wildcard" do - subject(:updated_files) { file_updater_instance.updated_dependency_files } - let(:project_name) { "file_updater_dirsproj_wildcards" } let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } let(:dependency_version) { "1.1.1" } let(:dependency_previous_version) { "1.0.0" } - it "updates the wildcard project" do - expect(updated_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj", "Proj2/Proj2.csproj") - - expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( - { - "#{repo_contents_path}/Proj1/Proj1/Proj1.csprojMicrosoft.Extensions.DependencyModel" => 1, - "#{repo_contents_path}/Proj2/Proj2.csprojMicrosoft.Extensions.DependencyModel" => 1 + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "Proj1/Proj1/Proj1.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net461"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net461", + SourceFilePath: "Proj1/Proj1/Proj1.csproj" + }], + TargetFrameworks: ["net461"], + ReferencedProjectPaths: [] + }, { + FilePath: "Proj2/Proj2.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net461"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net461", + SourceFilePath: "Proj2/Proj2.csproj" + }], + TargetFrameworks: ["net461"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil } ) end + + it "updates the wildcard project" do + run_update_test do |updater| + expect(updater.updated_dependency_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj", + "Proj2/Proj2.csproj") + + expect(updater.send(:testonly_update_tooling_calls)).to eq( + { + "/Proj1/Proj1/Proj1.csproj+Microsoft.Extensions.DependencyModel" => 1, + "/Proj2/Proj2.csproj+Microsoft.Extensions.DependencyModel" => 1 + } + ) + end + end end end diff --git a/nuget/spec/dependabot/nuget/metadata_finder_spec.rb b/nuget/spec/dependabot/nuget/metadata_finder_spec.rb index 75e3194e9c6..a99298e4399 100644 --- a/nuget/spec/dependabot/nuget/metadata_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/metadata_finder_spec.rb @@ -26,212 +26,76 @@ Dependabot::Dependency.new( name: dependency_name, version: dependency_version, - requirements: [{ - file: "my.csproj", - requirement: dependency_version, - groups: ["dependencies"], - source: source - }], + requirements: requirements, package_manager: "nuget" ) end - it_behaves_like "a dependency metadata finder" - - describe "#source_url" do - subject(:source_url) { finder.source_url } + let(:requirements) do + [{ + file: "my.csproj", + requirement: dependency_version, + groups: ["dependencies"], + source: source + }] + end - let(:nuget_url) do - "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/2.1.0/" \ - "microsoft.extensions.dependencymodel.nuspec" - end - let(:nuget_response) do - fixture( - "nuspecs", - "Microsoft.Extensions.DependencyModel.nuspec" - ) - end + it_behaves_like "a dependency metadata finder" - before do - stub_request(:get, nuget_url).to_return(status: 200, body: nuget_response) - stub_request(:get, "https://example.com/status").to_return( - status: 200, - body: "Not GHES", - headers: {} - ) - end + describe "#dependency_source_url" do + subject(:look_up_source) { finder.send(:dependency_source_url) } - context "with a source" do + context "with a source object with symbol keys" do let(:source) do { - type: "nuget_repo", - url: "https://www.myget.org/F/exceptionless/api/v3/index.json", - source_url: nil, - nuspec_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/2.1.0/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package", + type: "nuget_repo" } end - let(:nuget_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/2.1.0/" \ - "microsoft.extensions.dependencymodel.nuspec" - end - - it { is_expected.to eq("https://github.com/dotnet/core-setup") } - - it "caches the call to nuget" do - 2.times { source_url } - expect(WebMock).to have_requested(:get, nuget_url).once - end - - context "when nuget repo has a source_url only" do - let(:source) do - { - type: "nuget_repo", - url: "https://www.myget.org/F/exceptionless/api/v3/index.json", - source_url: "https://github.com/my/repo", - nuspec_url: nil - } - end - - it { is_expected.to eq("https://github.com/my/repo") } - end - - context "when the nuget repo has neither a source_url nor a nuspec_url" do - let(:source) do - { - type: "nuget_repo", - url: "https://www.myget.org/F/exceptionless/api/v3/index.json", - source_url: nil, - nuspec_url: nil - } - end - - it { is_expected.to be_nil } - end - - context "with details in the credentials (but no token)" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json" - }] - end - - it { is_expected.to eq("https://github.com/dotnet/core-setup") } - end - - context "when the registry requires authentication" do - before do - stub_request(:get, nuget_url).to_return(status: 404) - stub_request(:get, nuget_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_response) - end - - it { is_expected.to be_nil } - - context "with details in the credentials" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - "token" => "my:passw0rd" - }] - end - - it { is_expected.to eq("https://github.com/dotnet/core-setup") } - end - end - - context "when the registry doesn't support .nuspec routes" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback begins by getting the search URL from the index - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: fixture("nuspecs", "index.json")) - # next query for the package at the search URL returned - stub_request(:get, "https://azuresearch-usnc.nuget.org/query?prerelease=true&q=microsoft.extensions.dependencymodel&semVerLevel=2.0.0") - .to_return(status: 200, body: fixture("nuspecs", "microsoft.extensions.dependencymodel-results.json")) - end + it { is_expected.to eq("https://nuget.example.com/some.package") } + end - # data was extracted from the projectUrl in the search results - it { is_expected.to eq "https://github.com/dotnet/core-setup" } + context "with a source object with string keys" do + let(:source) do + { + "source_url" => "https://nuget.example.com/some.package", + "type" => "nuget_repo" + } end - context "when the index returns XML" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback tries to get the index, but gets a 200 with XML - # This might be due to artifactory not supporting index? - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: 'world') - end + it { is_expected.to eq("https://nuget.example.com/some.package") } + end - # no exceptions - it { is_expected.to be_nil } - end + context "with a nil source object" do + let(:source) { nil } - context "when the search results do not contain a projectUrl" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback begins by getting the search URL from the index - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: fixture("nuspecs", "index.json")) - # the search results have a blank projectUrl field AND missing the licenseUrl field entirely - stub_request(:get, "https://azuresearch-usnc.nuget.org/query?prerelease=true&q=microsoft.extensions.dependencymodel&semVerLevel=2.0.0") - .to_return(status: 200, body: '{"data":[{"id":"Microsoft.Extensions.DependencyModel","projectUrl":""}]}') - end + it { is_expected.to be_nil } + end - # no exceptions - it { is_expected.to be_nil } + context "with multiple requirements" do + let(:requirements) do + [{ + file: "project.csproj", + requirement: dependency_version, + groups: ["dependencies"], + source: nil + }, { + file: "my.csproj", + requirement: dependency_version, + groups: ["dependencies"], + source: source + }] end - context "when the source url fails to get the index.json" do - before do - # registry is in a bad state - stub_request(:get, nuget_url).to_return(status: 500) - # it falls back to get search URL from the index, but it fails too - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 500, body: "internal server error") - end - - it { is_expected.to be_nil } + let(:source) do + { + source_url: "https://nuget.example.com/some.package", + type: "nuget_repo" + } end - context "when it fails to get the search results" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback begins by getting the search URL from the index - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: fixture("nuspecs", "index.json")) - # oops, we're a little overloaded - stub_request(:get, "https://azuresearch-usnc.nuget.org/query?prerelease=true&q=microsoft.extensions.dependencymodel&semVerLevel=2.0.0") - .to_return(status: 503, body: "") - end - - it { is_expected.to be_nil } - end + it { is_expected.to eq("https://nuget.example.com/some.package") } end end end diff --git a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb b/nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb similarity index 68% rename from nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb rename to nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb index 2273dbdae17..573c886d5cd 100644 --- a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb @@ -2,14 +2,13 @@ # frozen_string_literal: true require "spec_helper" -require "dependabot/nuget/update_checker/requirements_updater" +require "dependabot/nuget/native_update_checker/native_requirements_updater" -RSpec.describe Dependabot::Nuget::UpdateChecker::RequirementsUpdater do +RSpec.describe Dependabot::Nuget::NativeUpdateChecker::NativeRequirementsUpdater do let(:updater) do described_class.new( requirements: requirements, - latest_version: latest_version, - source_details: source_details + dependency_details: dependency_details ) end @@ -23,20 +22,25 @@ } end let(:csproj_req_string) { "23.3-jre" } - let(:latest_version) { version_class.new("23.6-jre") } - let(:source_details) do - { - source_url: nil, - repo_url: "https://api.nuget.org/v3/index.json", - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + 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({ + Name: "unused", + Version: latest_version, + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: info_url + }.to_json)) end - let(:version_class) { Dependabot::Nuget::Version } - - describe "#updated_requirements" do + describe "#updated_requirements.version" do subject { updater.updated_requirements.first } specify { expect(updater.updated_requirements.count).to eq(1) } @@ -48,7 +52,7 @@ end context "when there is a latest version" do - let(:latest_version) { version_class.new("23.6-jre") } + let(:latest_version) { "23.6-jre" } context "when no requirement was previously specified" do let(:csproj_req_string) { nil } @@ -69,9 +73,7 @@ end context "when a suffixed requirement was previously specified" do - let(:latest_version) do - version_class.new("3.0.0-beta4.20210.2+38fe3493") - end + let(:latest_version) { "3.0.0-beta4.20210.2+38fe3493" } let(:csproj_req_string) { "3.0.0-beta4.20207.4+07df2f07" } its([:requirement]) do @@ -124,11 +126,7 @@ groups: ["dependencies"], source: { type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package" } }, { file: "another/my.csproj", @@ -136,11 +134,7 @@ groups: ["dependencies"], source: { type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package" } }) end @@ -155,11 +149,7 @@ groups: ["dependencies"], source: { type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package" } }, { file: "another/my.csproj", diff --git a/nuget/spec/dependabot/nuget/nuget_client_spec.rb b/nuget/spec/dependabot/nuget/nuget_client_spec.rb deleted file mode 100644 index 1c9256904ac..00000000000 --- a/nuget/spec/dependabot/nuget/nuget_client_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/nuget/nuget_client" - -RSpec.describe Dependabot::Nuget::NugetClient do - describe "#get_package_versions" do - subject(:package_versions) do - described_class.get_package_versions(dependency_name, repository_details) - end - - let(:dependency_name) { "Some.Dependency" } - - context "when retrieving the package versions from local" do - let(:repository_details) do - nuget_dir = File.join(File.dirname(__FILE__), "..", "..", "fixtures", "nuget_responses", "local_repo") - base_url = File.expand_path(nuget_dir) - - { - base_url: base_url, - repository_type: "local" - } - end - - it "expects to crawl the directory" do - expect(package_versions).to eq(Set["1.0.0", "1.1.0"]) - end - end - - context "when the package versions _might_ have the `listed` flag" do - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, # nuget.org provides this flag and it should be honored - version: "0.1.0" - } - }, - { - catalogEntry: { - listed: false, # if this is ever false, the package should not be included - version: "0.1.1" - } - }, - { - catalogEntry: { - # e.g., github doesn't have the `listed` flag, but should still be returned - version: "0.1.2" - } - } - ] - ] - }.to_json - ) - end - - let(:repository_details) do - { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "#{dependency_name.downcase}/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=#{dependency_name.downcase}&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["0.1.0", "0.1.2"]) - end - end - - context "when the versions can be retrieved from v2 apis" do - before do - stub_request(:get, "https://www.nuget.org/api/v2/FindPackagesById()?id=%27Some.Dependency%27") - .to_return( - status: 200, - body: - <<~XML - - - - - Some.Dependency - - - 1.0.0.0 - - - - - Some.Dependency - - 1.1.0.0 - - - - - Some.Dependency.But.The.Wrong.One - - 1.2.0.0 - - - - - XML - ) - end - - let(:repository_details) do - { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: "https://www.nuget.org/api/v2/FindPackagesById()?id='#{dependency_name}'", - auth_header: {}, - repository_type: "v2" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["1.0.0.0", "1.1.0.0"]) - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb deleted file mode 100644 index 9a923e3eccc..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ /dev/null @@ -1,260 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/compatibility_checker" -require "dependabot/nuget/update_checker/repository_finder" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::CompatibilityChecker do - subject(:checker) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency_urls: dependency_urls, - dependency: dependency - ) - end - - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency_urls) do - Dependabot::Nuget::RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: [] - ).dependency_urls - end - - let(:credentials) do - [{ - "type" => "nuget_feed", - "url" => "https://api.nuget.org/v3/index.json", - "token" => "my:passw0rd" - }] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_name) { "Microsoft.AppCenter.Crashes" } - let(:dependency_version) { "5.0.2" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "5.0.2", groups: ["dependencies"], source: nil }] - end - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) do - <<~XML - - - uap10.0.16299 - - - - - - XML - end - - describe "#compatible?" do - subject(:compatible) { checker.compatible?(version) } - - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/microsoft.appcenter.crashes/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, - version: "5.0.2" - } - }, - { - catalogEntry: { - listed: true, - version: "5.0.3" - } - } - ] - ] - }.to_json - ) - end - - context "when the `.nuspec` reports itself as a development dependency, but still has regular dependencies" do - let(:csproj_body) do - <<~XML - - - net6.0 - - - - - - XML - end - - before do - nuspec502 = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - true - - - - - - - XML - nuspec503 = nuspec502.gsub("5.0.2", "5.0.3") - nuspec601 = nuspec502.gsub("5.0.2", "6.0.1").gsub("net6.0", "net8.0") - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec502 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec503 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/6.0.1/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec601 - ) - end - - context "with a targetFramework compatible version" do - let(:version) { "5.0.3" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "with a targetFramework non-compatible version" do - let(:version) { "6.0.1" } - - it "returns the correct data" do - expect(compatible).to be_falsey - end - end - end - - context "when the `.nuspec` has groups without a `targetFramework` attribute" do - let(:version) { "5.0.3" } - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - end - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "when the `.nupkg` zip object contains an empty `lib/` entry" do - def create_nupkg_with_lib_contents(package_name, nuspec_contents, lib_subdirectories) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{package_name}.nuspec") - zio.write(nuspec_contents) - - zio.put_next_entry("lib/") # some zip files have an empty directory object entry - - lib_subdirectories.each do |lib| - zio.put_next_entry("lib/#{lib}/_._") - zio.write("fake contents") - end - end - content.rewind - content.sysread - end - - let(:csproj_body) do - <<~XML - - - net481 - - - - - - XML - end - - before do - nuspec_xml = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - - - - - - - XML - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec_xml - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.5.0.2.nupkg") - .to_return( - status: 200, - body: create_nupkg_with_lib_contents(dependency_name, nuspec_xml, ["net45"]) - ) - end - - context "when checking the `.nupkg` contents" do - let(:version) { "5.0.2" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb deleted file mode 100644 index 2abed2229cd..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/dependency_finder" - -RSpec.describe Dependabot::Nuget::UpdateChecker::DependencyFinder do - subject(:finder) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - ignored_versions: [], - credentials: credentials, - repo_contents_path: "test/repo" - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - # Can get transitive dependencies - describe "#transitive_dependencies", :vcr do - subject(:transitive_dependencies) { finder.transitive_dependencies } - - its(:length) { is_expected.to eq(34) } - end - - context "when the api.nuget.org is not hit due to absence in the NuGet.Config" do - subject(:transitive_dependencies) { finder.transitive_dependencies } - - let(:dependency_version) { "42.42.42" } - let(:nuget_config_body) { fixture("configs", "example.com_nuget.config") } - let(:nuget_config) { Dependabot::DependencyFile.new(name: "NuGet.Config", content: nuget_config_body) } - let(:dependency_files) { [csproj, nuget_config] } - - def create_nupkg(nuspec_name, nuspec_fixture_path) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(fixture("nuspecs", nuspec_fixture_path)) - end - content.rewind - content.sysread - end - - before(:context) do - disallowed_urls = %w( - https://api.nuget.org/v3/index.json - https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.nuspec - https://api.nuget.org/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.nuspec - ) - - disallowed_urls.each do |url| - stub_request(:get, url) - .to_raise(StandardError.new("Not allowed to query `#{url}`")) - end - - stub_request(:get, "https://nuget.example.com/v3/index.json") - .to_return(status: 200, body: fixture("nuget_responses", "example_index.json")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.42.42.42.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.Extensions.DependencyModel", - "Microsoft.Extensions.DependencyModel_42.42.42_faked.nuspec")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.43.43.43.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.NETCore.Platforms", - "Microsoft.NETCore.Platforms_43.43.43_faked.nuspec")) - end - - # this test doesn't really care about the dependency count, we just need to ensure that `api.nuget.org` wasn't hit - its(:length) do - is_expected.to eq(1) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb deleted file mode 100644 index e99e71d0880..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nupkg_fetcher" -require "dependabot/nuget/update_checker/repository_finder" - -RSpec.describe Dependabot::Nuget::NupkgFetcher do - describe "#fetch_nupkg_url_from_repository" do - subject(:nupkg_url) do - described_class.fetch_nupkg_url_from_repository(repository_details, package_name, package_version) - end - - let(:dependency) { Dependabot::Dependency.new(name: package_name, requirements: [], package_manager: "nuget") } - let(:package_name) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:credentials) { [] } - let(:config_files) { [nuget_config] } - let(:nuget_config) do - Dependabot::DependencyFile.new( - name: "NuGet.config", - content: nuget_config_content - ) - end - let(:nuget_config_content) do - <<~XML - - - - - - - - XML - end - let(:repository_finder) do - Dependabot::Nuget::RepositoryFinder.new(dependency: dependency, credentials: credentials, - config_files: config_files) - end - let(:repository_details) { repository_finder.dependency_urls.first } - - context "with a nuget feed url" do - let(:feed_url) { "https://api.nuget.org/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "nuget.index.json") - ) - end - - it { is_expected.to eq("https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with an azure feed url" do - let(:feed_url) { "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "dotnet-public.index.json") - ) - end - - it { is_expected.to eq("https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/45bacae2-5efb-47c8-91e5-8ec20c22b4f8/nuget/v3/flat2/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a github feed url" do - let(:feed_url) { "https://nuget.pkg.github.com/some-namespace/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it { is_expected.to eq("https://nuget.pkg.github.com/some-namespace/download/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a v2 feed url" do - let(:feed_url) { "https://www.nuget.org/api/v2" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: - +<<~XML - - - - Packages - - - - XML - ) - stub_request(:get, "https://www.nuget.org/api/v2/Packages(Id='Newtonsoft.Json',Version='13.0.1')") - .to_return( - status: 200, - body: - <<~XML - - - - - XML - ) - end - - it { is_expected.to eq("https://www.nuget.org/api/v2/Download/Newtonsoft.Json/13.0.1") } - end - - context "when the v3 feed doesn't specify `PackageBaseAddress`" do - let(:feed_url) { "https://nuget.example.com/v3-without-package-base/index.json" } - - before do - # initial `index.json` response; only provides `SearchQueryService` and not `PackageBaseAddress` - stub_request(:get, feed_url) - .to_return( - status: 200, - body: { - version: "3.0.0", - resources: [ - { - "@id" => "https://nuget.example.com/query", - "@type" => "SearchQueryService" - } - ] - }.to_json - ) - # SearchQueryService - stub_request(:get, "https://nuget.example.com/query?q=newtonsoft.json&prerelease=true&semVerLevel=2.0.0") - .to_return( - status: 200, - body: { - totalHits: 2, - data: [ - # this is a false match - { - registration: "not-used", - version: "42.42.42", - versions: [ - { - version: "1.0.0", - "@id" => "not-used" - }, - { - version: "42.42.42", - "@id" => "not-used" - } - ], - id: "Newtonsoft.Json.False.Match" - }, - # this is the real one - { - registration: "not-used", - version: "13.0.1", - versions: [ - { - version: "12.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/12.0.1.json" - }, - { - version: "13.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json" - } - ], - id: "Newtonsoft.Json" - } - ] - }.to_json - ) - # registration content - stub_request(:get, "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json") - .to_return( - status: 200, - body: { - listed: true, - packageContent: "https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg", - registration: "not-used", - "@id" => "not-used" - }.to_json - ) - end - - it { is_expected.to eq("https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg") } - end - end - - describe "#fetch_nupkg_buffer" do - subject(:nupkg_buffer) do - described_class.fetch_nupkg_buffer(dependency_urls, package_id, package_version) - end - - let(:package_id) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:repository_details) { Dependabot::Nuget::RepositoryFinder.get_default_repository_details(package_id) } - let(:dependency_urls) { [repository_details] } - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") - .to_return( - status: 301, - headers: { - "Location" => "https://api.nuget.org/redirect-on-301" - }, - body: "redirecting on 301" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-301") - .to_return( - status: 302, - headers: { - "Location" => "https://api.nuget.org/redirect-on-302" - }, - body: "redirecting on 302" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-302") - .to_return( - status: 303, - headers: { - "Location" => "https://api.nuget.org/redirect-on-303" - }, - body: "redirecting on 303" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-303") - .to_return( - status: 307, - headers: { - "Location" => "https://api.nuget.org/redirect-on-307" - }, - body: "redirecting on 307" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-307") - .to_return( - status: 308, - headers: { - "Location" => "https://api.nuget.org/redirect-on-308" - }, - body: "redirecting on 308" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-308") - .to_return( - status: 200, - body: "the final contents" - ) - end - - it "fetches the nupkg after multiple redirects" do - expect(nupkg_buffer.to_s).to eq("the final contents") - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb deleted file mode 100644 index 8752837f118..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nuspec_fetcher" - -RSpec.describe Dependabot::Nuget::NuspecFetcher do - describe "#feed_supports_nuspec_download?" do - context "when checking with a azure feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://pkgs.dev.azure.com/dependabot/dependabot-test/_packaging/dependabot-feed/nuget/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with a azure feed url (no project)" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://pkgs.dev.azure.com/dependabot/_packaging/dependabot-feed/nuget/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with a visual studio feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://dynamicscrm.pkgs.visualstudio.com/_packaging/CRM.Engineering/nuget/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with the nuget.org feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://api.nuget.org/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with github feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://nuget.pkg.github.com/some_namespace/index.json" } - - it { is_expected.to be_falsy } - end - end - - describe "remove_invalid_characters" do - context "when a utf-16 bom is present" do - subject(:result) { described_class.remove_invalid_characters(response_body) } - - let(:response_body) { "\xFE\xFF" } - - it { is_expected.to eq("") } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb deleted file mode 100644 index aec08e163c6..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ /dev/null @@ -1,963 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/repository_finder" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::RepositoryFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - subject(:finder) do - described_class.new( - dependency: dependency, - credentials: credentials, - config_files: [config_file].compact - ) - end - - let(:config_file) { nil } - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:dependency) do - Dependabot::Dependency.new( - name: "Microsoft.Extensions.DependencyModel", - version: "1.1.1", - requirements: [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }], - package_manager: "nuget" - ) - end - - describe "local path in NuGet.Config" do - subject(:known_repositories) { finder.known_repositories } - - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content, - directory: "some/directory" - ) - end - - it "finds all local paths" do - urls = known_repositories.map { |r| r[:url] } - expected = [ - "/some/directory/SomePath", - "/some/directory/RelativePath", - "/AbsolutePath", - "https://nuget.example.com/index.json" - ] - expect(urls).to match_array(expected) - end - end - - describe "environment variables in NuGet.Config" do - subject(:known_repositories) { finder.known_repositories } - - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - context "when expanded" do - before do - allow(Dependabot.logger).to receive(:warn) - ENV["FEED_URL"] = "https://nuget.example.com/index.json" - ENV["THIS_VARIBLE_EXISTS"] = "replacement-text" - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - - after do - ENV.delete("THIS_VARIBLE_EXISTS") - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - - it "contains the expected values and warns on unavailable" do - repo = known_repositories[0] - expect(repo[:url]).to eq("https://nuget.example.com/index.json") - expect(repo[:token]).to eq("user:(head)replacement-text(mid)%THIS_VARIABLE_DOES_NOT%(tail)") - expect(Dependabot.logger).to have_received(:warn).with( - <<~WARN - The variable '%THIS_VARIABLE_DOES_NOT%' could not be expanded in NuGet.Config - WARN - ) - end - end - end - - describe "dependency_urls" do - subject(:dependency_urls) { finder.dependency_urls } - - it "gets the right URL without making any requests" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - - context "with a URL passed as a credential" do - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - - context "when the PackageBaseAddress is not returned" do - let(:custom_repo_url) { "http://localhost:8082/artifactory/api/nuget/v3/nuget-local" } - - before do - stub_request(:get, custom_repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: nil, - registration_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/registration/microsoft.extensions.dependencymodel/index.json", - repository_url: custom_repo_url, - search_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "when URLs need to be escaped" do - let(:custom_repo_url) { "https://www.myget.org/F/exceptionless/api with spaces/v3/index.json" } - - before do - stub_request(:get, "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json") - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "when a request returns a 404 response" do - before { stub_request(:get, custom_repo_url).to_return(status: 404) } - - # TODO: Might want to raise here instead? - it { is_expected.to eq([]) } - end - - context "when a request returns a 403 response" do - before { stub_request(:get, custom_repo_url).to_return(status: 403) } - - it "raises a useful error" do - error_class = Dependabot::PrivateSourceAuthenticationFailure - expect { finder.dependency_urls } - .to raise_error do |error| - expect(error).to be_a(error_class) - expect(error.source).to eq(custom_repo_url) - end - end - end - - context "without a token" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: nil) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - end - - context "with a URL included in the nuget.config" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", config_file_fixture_name) - ) - end - let(:config_file_fixture_name) { "nuget.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url).to_return(status: 404) - stub_request(:get, repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - # skipped - # it "gets the right URLs" do - # expect(dependency_urls).to match_array( - # [{ - # repository_url: "https://api.nuget.org/v3/index.json", - # versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - # "microsoft.extensions.dependencymodel/index.json", - # search_url: "https://azuresearch-usnc.nuget.org/query" \ - # "?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: {}, - # repository_type: "v3" - # }, { - # repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "index.json", - # versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "flatcontainer/microsoft.extensions." \ - # "dependencymodel/index.json", - # search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "query?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - # repository_type: "v3" - # }] - # ) - # end - - context "when including the default repository" do - let(:config_file_fixture_name) { "include_default_disable_ext_sources.config" } - - it "with disable external source" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }, { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when the spec overrides the default package sources" do - let(:config_file_fixture_name) { "override_def_source_with_same_key.config" } - let(:config_file_fixture_name) { "override_def_source_with_same_key_default.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "when the default api key of default registry is provided without clear" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - - it "when the default api key of default registry is provided with clear" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when not including the default repository" do - let(:config_file_fixture_name) { "excludes_default.config" } - - it "still includes the default repository (as it wasn't cleared)" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - - context "when spec clears default repo info" do - let(:config_file_fixture_name) { "clears_default.config" } - - it "still excludes the default repository" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - end - - context "when the spec has disabled package sources" do - let(:config_file_fixture_name) { "disabled_sources.config" } - - it "when only including the enabled package sources" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - end - - context "when the spec has disabled default package sources" do - let(:config_file_fixture_name) { "disabled_default_sources.config" } - - it "only includes the enable package sources" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - end - end - - context "when the spec has a numeric key" do - let(:config_file_fixture_name) { "numeric_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when only providing versioned `SearchQueryService`` entries" do - let(:config_file_fixture_name) { "versioned_search.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when including repositories in the `trustedSigners` section" do - let(:config_file_fixture_name) { "with_trustedSigners.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, - { - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "with GitHub packages url" do - let(:config_file_fixture_name) { "github.nuget.config" } - - before do - repo_url = "https://nuget.pkg.github.com/some-namespace/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://nuget.pkg.github.com/some-namespace/download", - registration_url: "https://nuget.pkg.github.com/some-namespace/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.pkg.github.com/some-namespace/index.json", - versions_url: "https://nuget.pkg.github.com/some-namespace/download/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.pkg.github.com/some-namespace/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "when the repo URL has a non-ascii key" do - let(:config_file_fixture_name) { "non_ascii_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when the repo URL uses the v2 API alongside the v3 API" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }) - end - end - - context "when the repo URL has no base url in v2 API response" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_no_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2/", - repository_url: "https://www.nuget.org/api/v2/", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice/api/v2", - repository_url: "https://www.myget.org/F/azure-appservice/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice-staging/api/v2", - repository_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - repository_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - versions_url: - "https://www.myget.org/F/fusemandistfeed/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - repository_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - versions_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }) - end - end - - context "when matching `packageSourceMapping` entries are honored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # `source1` and `source3` should never be queried - stub_index_json("https://nuget.example.com/source2/index.json") - end - - it "matches on the best pattern" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when non-matching `packageSourceMapping` entries are ignored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # all sources will need to be queried - stub_index_json("https://nuget.example.com/source1/index.json") - stub_index_json("https://nuget.example.com/source2/index.json") - stub_index_json("https://nuget.example.com/source3/index.json") - end - - it "returns all sources" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://nuget.example.com/source1/PackageBaseAddress", - registration_url: "https://nuget.example.com/source1/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source1/index.json", - versions_url: "https://nuget.example.com/source1/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source1/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source3/PackageBaseAddress", - registration_url: "https://nuget.example.com/source3/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source3/index.json", - versions_url: "https://nuget.example.com/source3/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source3/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb deleted file mode 100644 index 36d1e103b13..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::TfmFinder do - subject(:frameworks) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.frameworks(dependency) - end - - let(:project_name) { "tfm_finder" } - let(:dependency_files) { nuget_project_dependency_files(project_name, directory: "/").reverse } - let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - describe "#frameworks" do - context "when checking for a transitive dependency" do - let(:dependency_requirements) { [] } - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - its(:length) { is_expected.to eq(2) } - end - - context "when checking for a top-level dependency" do - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "2.3.0", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Serilog" } - let(:dependency_version) { "2.3.0" } - - its(:length) { is_expected.to eq(1) } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb deleted file mode 100644 index f89471b084d..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb +++ /dev/null @@ -1,720 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/version_finder" -require "dependabot/nuget/update_checker/tfm_comparer" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::UpdateChecker::VersionFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - - let(:finder) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: raise_on_ignored, - security_advisories: security_advisories, - repo_contents_path: repo_contents_path - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:ignored_versions) { [] } - let(:raise_on_ignored) { false } - let(:security_advisories) { [] } - - let(:nuget_versions_url) do - "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:nuget_search_url) do - "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:version_class) { Dependabot::Nuget::Version } - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - let(:nuspec) do - fixture("nuspecs", "#{dependency_name}.#{dependency_version}.nuspec") - end - - let(:nuspec_url) do - "https://api.nuget.org/v3-flatcontainer/#{dependency_name.downcase}/#{dependency_version}/#{dependency_name.downcase}.nuspec" - end - - let(:version_instance) do - version_class.new(dependency_version) - end - - let(:expected_version_instance) do - version_class.new(expected_version) - end - - before do - stub_request(:get, nuget_versions_url) - .to_return(status: 200, body: nuget_versions) - stub_request(:get, nuget_search_url) - .to_return(status: 200, body: nuget_search_results) - end - - describe "#latest_version_details" do - subject(:latest_version_details) { finder.latest_version_details } - - let(:expected_version) { "2.1.0" } - let(:current_compatible) { true } - let(:expected_compatible) { true } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(current_compatible) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(expected_compatible) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - - context "when the returned versions is prefixed with a zero-width char" do - let(:nuget_search_results) do - fixture("nuget_responses", "search_results_zero_width.json") - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when the user wants a pre-release" do - let(:dependency_version) { "2.2.0-preview1-26216-03" } - let(:expected_version) { "2.2.0-preview2-26406-04" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - - context "when dealing with a previous version" do - let(:dependency_version) { "2.1.0-preview1-26216-03" } - let(:expected_version) { "2.1.0" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - end - end - - context "when the user wants a pre-release with wildcard" do - let(:dependency_version) { "*-*" } - let(:current_compatible) { false } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "*-*", groups: ["dependencies"], source: nil }] - end - - its([:version]) do - is_expected.to eq(version_class.new("2.2.0-preview2-26406-04")) - end - end - - context "when the user is using an unfound property" do - let(:dependency_version) { "$PackageVersion_LibGit2SharpNativeBinaries" } - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "when raise_on_ignored is enabled and later versions are allowed" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - - context "when the user is on the latest version" do - let(:dependency_version) { "2.1.0" } - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - end - - context "when the current version isn't known" do - let(:dependency_version) { nil } - let(:current_compatible) { false } - let(:expected_version) { nil } - let(:expected_compatible) { false } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - end - - context "when the dependency is a git dependency" do - let(:dependency_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - end - - context "when the user is ignoring all later versions" do - let(:ignored_versions) { ["> 1.1.1"] } - - its([:version]) { is_expected.to eq(version_class.new("1.1.1")) } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "raises an error" do - expect { latest_version_details }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when the user is ignoring the latest version" do - let(:ignored_versions) { ["[2.a,3.0.0)"] } - let(:expected_version) { "1.1.2" } - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when a version range is specified using Ruby syntax" do - let(:ignored_versions) { [">= 2.a"] } - let(:expected_version) { "1.1.2" } - - its([:version]) { is_expected.to eq(version_class.new("1.1.2")) } - end - - context "when the user has ignored all versions" do - let(:ignored_versions) { ["[0,)"] } - - it "returns nil" do - expect(latest_version_details).to be_nil - end - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "raises an error" do - expect { latest_version_details }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when an open version range is specified using Ruby syntax" do - let(:ignored_versions) { ["> 0"] } - - it "returns nil" do - expect(latest_version_details).to be_nil - end - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "raises an error" do - expect { latest_version_details }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when the user is ignoring all versions but a very specific one" do - let(:ignored_versions) { ["< 1.1.1, > 1.1.1"] } - let(:expected_version) { "1.1.1" } - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "with a custom repo in a nuget.config file" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "nuget.config") - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - # skipped - # its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "when the repo uses the v2 API" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "with_v2_endpoints.config") - ) - end - - let(:custom_v3_nuget_versions_url) do - "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/" \ - "#{dependency_name}/index.json" - end - - let(:expected_version) { "4.8.1" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_v3_nuget_versions_url) - .to_return(status: 404) - - custom_v2_nuget_versions_url = - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'#{dependency_name}'" - stub_request(:get, custom_v2_nuget_versions_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_versions.xml") - ) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - end - - context "with a package that returns paginated api results when using the v2 nuget api", :vcr do - let(:dependency_files) { project_dependency_files("paginated_package_v2_api") } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "4.7.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "FakeItEasy" } - let(:dependency_version) { "4.7.1" } - let(:expected_version) { "7.3.0" } - - it "returns the expected version" do - expect(latest_version_details[:version]).to eq(expected_version_instance) - end - end - - context "with a custom repo in the credentials", :vcr do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "when the URL does not return PackageBaseAddress" do - let(:custom_repo_url) { "http://www.myget.org/artifactory/api/nuget/v3/dependabot-nuget-local" } - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(admin password)) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - end - - context "with a version range specified" do - let(:dependency_files) { project_dependency_files("version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0, 3.0.0)", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with an open upper version range specified" do - let(:dependency_files) { project_dependency_files("open_upper_version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0-alpha,", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with a package that is implicitly referenced", :vcr do - let(:dependency_files) { project_dependency_files("implicit_reference") } - let(:dependency_requirements) do - [{ file: "implicitReference.csproj", requirement: "1.1.2-beta1.22511.2", groups: ["dependencies"], - source: nil }] - end - let(:dependency_name) { "NuGet.Protocol" } - let(:dependency_version) { "6.3.0" } - - it "returns the expected version" do - skip "This test was commented out and does not work at the moment" - expect(latest_version_details[:version]).to eq(version_class.new("6.5.0")) - end - end - - context "when the package can't be meaninfully sorted by just version" do - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - reported_versions = [ - "2.6.1", - "2.7.1", - "3.4.0", - "3.14.0", - "4.0.1" - ] - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/nunit/index.json") - .to_return( - status: 200, - body: { - items: [ - items: reported_versions.map { |v| { catalogEntry: { listed: true, version: v } } } - ] - }.to_json - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/3.14.0/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.3.14.0_faked.nuspec")) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/4.0.1/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.4.0.1_faked.nuspec")) - end - - let(:csproj_body) do - <<~XML - - - netcoreapp3.1 - - - - - - XML - end - let(:expected_version) { version_class.new("3.14.0") } - let(:dependency_version) { "3.14.0" } - let(:dependency) do - Dependabot::Dependency.new( - name: "nunit", - version: dependency_version, - requirements: [{ file: "my.csproj", requirement: "3.14.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - - it "returns the expected version" do - expect(latest_version_details[:version]).to eq(version_class.new("3.14.0")) - end - end - - context "when `packageSourceMapping`s are specified" do - let(:csproj_body) do - <<~XML - - - net8.0 - - - - - - XML - end - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: - <<~XML - - - - - - - - - - - - - - - - - - XML - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:dependency) do - Dependabot::Dependency.new( - name: "Some.Package", - version: "1.0.0", - requirements: [{ file: "my.csproj", requirement: "1.0.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - let(:expected_version) { version_class.new("1.1.0") } - - def create_nupkg(nuspec_name, nuspec_content) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(nuspec_content) - end - content.rewind - content.sysread - end - - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - - # stub source 1 - stub_index_json("https://nuget.example.com/source1/index.json") - stub_request(:get, "https://nuget.example.com/source1/RegistrationsBaseUrl/some.package/index.json") - .to_return( - status: 200, - body: { - count: 1, - items: [ - { - count: 2, - items: [ - { - catalogEntry: { - id: "Some.Package", - version: "1.0.0" # this is what's currently installed - } - }, - { - catalogEntry: { - id: "Some.Package", - version: "1.1.0" # this is what we'd like to upgrade to - } - } - ] - } - ] - }.to_json - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.0.0/some.package.1.0.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.1.0/some.package.1.1.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - # none of the `source2` URLs should be called - end - - it "returns the expected version honoring the package source mapping" do - expect(latest_version_details[:version]).to eq(version_class.new("1.1.0")) - end - end - end - - describe "#lowest_security_fix_version_details" do - subject(:lowest_security_fix_version_details) do - finder.lowest_security_fix_version_details - end - - let(:dependency_version) { "1.1.1" } - let(:security_advisories) do - [ - Dependabot::SecurityAdvisory.new( - dependency_name: "rails", - package_manager: "nuget", - vulnerable_versions: ["< 2.0.0"] - ) - ] - end - - let(:expected_version) { "2.0.0" } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(true) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(true) - end - - its([:version]) { is_expected.to eq(version_class.new("2.0.0")) } - - context "when the user is ignoring the lowest version" do - let(:ignored_versions) { ["<= 2.0.0"] } - let(:expected_version) { "2.0.3" } - - its([:version]) { is_expected.to eq(version_class.new("2.0.3")) } - end - end - - describe "#versions" do - subject(:versions) { finder.versions } - - it "includes the correct versions" do - expect(versions.count).to eq(21) - expect(versions.first).to eq( - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.0.0-rc2-002702/" \ - "microsoft.extensions.dependencymodel.nuspec", - repo_url: "https://api.nuget.org/v3/index.json", - source_url: nil, - version: Dependabot::Nuget::Version.new("1.0.0-rc2-002702") - ) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index 8b99530899e..e7301b1ed08 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -4,9 +4,14 @@ require "spec_helper" 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/file_parser" require "dependabot/nuget/update_checker" +require "dependabot/nuget/requirement" require "dependabot/nuget/version" require_common_spec "update_checkers/shared_examples_for_update_checkers" + RSpec.describe Dependabot::Nuget::UpdateChecker do let(:version_class) { Dependabot::Nuget::Version } let(:security_advisories) { [] } @@ -37,119 +42,409 @@ package_manager: "nuget" ) end - let(:checker) do - described_class.new( + + 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 + + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end + + before do + Dependabot::Experiments.register(:nuget_native_analysis, true) + end + + it_behaves_like "an update checker" + + def run_analyze_test(&_block) + # caching is explicitly required for these tests + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" + + # don't allow a previous test to pollute the file parser cache + Dependabot::Nuget::FileParser.file_dependency_cache.clear + + # calling `#parse` is necessary to force `discover` which is stubbed below + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse + + # create the checker... + checker = described_class.new( dependency: dependency, dependency_files: dependency_files, credentials: credentials, + repo_contents_path: repo_contents_path, ignored_versions: ignored_versions, security_advisories: security_advisories ) - end - - it_behaves_like "an update checker" - def nuspec_url(name, version) - "https://api.nuget.org/v3-flatcontainer/#{name.downcase}/#{version}/#{name.downcase}.nuspec" + # ...and invoke the actual test + yield checker + ensure + Dependabot::Nuget::NativeDiscoveryJsonReader.clear_discovery_file_path_from_cache(dependency_files) + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" end def registration_index_url(name) "https://api.nuget.org/v3/registration5-gz-semver2/#{name.downcase}/index.json" end - describe "up_to_date?" do - subject(:up_to_date?) { checker.up_to_date? } - - context "with a property dependency" do - context "when a dependency's property couldn't be found" do - let(:dependency_name) { "Nuke.Common" } - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] + def intercept_native_tools(discovery_content_hash:, dependency_name:, analysis_content_hash:) + return unless stub_native_tools + + # don't allow `FileParser#parse` to call into the native tool; just fake it + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_discover_tool) + .and_wrap_original do |_original_method, *args, &_block| + discovery_json_path = args[0][:output_path] + FileUtils.mkdir_p(File.dirname(discovery_json_path)) + if report_stub_debug_information + puts "stubbing call to `run_nuget_discover_tool` with args #{args}; writing prefabricated discovery " \ + "response to discovery.json to #{discovery_json_path}" end - let(:dependency_version) { "$(NukeVersion)" } - - it { is_expected.to be(true) } + discovery_json_content = discovery_content_hash.to_json + File.write(discovery_json_path, discovery_json_content) end - end - - context "with a transient dependency" do - context "with no vulnerability" do - let(:dependency_name) { "Nuke.Common" } - let(:dependency_requirements) { [] } - let(:dependency_version) { "2.0.0" } - it { is_expected.to be(true) } + # prevent calling the analysis tool + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_analyze_tool) + .and_wrap_original do |_original_method, *args, &_block| + # write prefabricated analysis json file + analysis_json_path = Dependabot::Nuget::AnalysisJsonReader.analysis_file_path(dependency_name: dependency_name) + if report_stub_debug_information + puts "stubbing call to `run_nuget_analyze_tool` with args #{args}; writing prefabricated analysis response " \ + "to #{analysis_json_path}" + end + analysis_json_content = analysis_content_hash.to_json + FileUtils.mkdir_p(File.dirname(analysis_json_path)) + File.write(analysis_json_path, analysis_json_content) end - end end - describe "#latest_version" do - subject { checker.latest_version } + describe "up_to_date?" do + context "with a property dependency whose property couldn't be found" do + let(:dependency_name) { "Nuke.Common" } + let(:dependency_requirements) do + [{ + requirement: "$(NukeVersion)", + file: "my.csproj", + groups: ["dependencies"], + source: nil, + metadata: { property_name: "NukeVersion" } + }] + end + let(:dependency_version) { "$(NukeVersion)" } - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:latest_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [], # dependency not found + IsSuccess: true, + Properties: [ + { + Name: "TargetFrameworks", + Value: "netstandard1.6;net462", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net462", "netstandard1.6"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "$(NukeVersion)", + CanUpdate: false, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end - expect(checker.latest_version).to eq("1.2.3") + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.up_to_date?).to be(true) + end + end end - context "when the package could not be found on any source" do + context "with a dependency that is not reported" do + let(:dependency_name) { "Nuke.Common" } + let(:dependency_requirements) { [] } + let(:dependency_version) { "2.0.0" } + before do - stub_request(:get, registration_index_url("microsoft.extensions.dependencymodel")) - .to_return(status: 404) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [], # dependency not found + IsSuccess: true, + Properties: [ + { + Name: "TargetFrameworks", + Value: "netstandard1.6;net462", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net462", "netstandard1.6"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "2.0.0", + CanUpdate: false, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [] + } + ) end - it "reports the current version" do - expect(checker.latest_version).to eq("1.1.1") + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.up_to_date?).to be(true) + end end end - end - describe "#lowest_security_fix_version" do - subject { checker.lowest_security_fix_version } - - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:lowest_security_fix_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) - - expect(checker.lowest_security_fix_version).to eq("1.2.3") + context "with a dependency that can be updated" do + let(:dependency_name) { "Nuke.Common" } + let(:dependency_requirements) { [] } + let(:dependency_version) { "2.0.0" } + + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Nuke.Common", + Version: "2.0.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net462", "netstandard1.6"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFrameworks", + Value: "netstandard1.6;net462", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net462", "netstandard1.6"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "2.0.1", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Nuke.Common", + Version: "2.0.1", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net462", "netstandard1.6"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end + + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.up_to_date?).to be(false) + end + end end end describe "#latest_resolvable_version" do - subject(:latest_resolvable_version) { checker.latest_resolvable_version } + context "when a partial unlock cannot be performed" do + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "1.1.2", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.2", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end - it { is_expected.to be_nil } + it "reports `nil`" do + run_analyze_test do |checker| + expect(checker.latest_resolvable_version).to be_nil + end + end + end end describe "#latest_resolvable_version_with_no_unlock" do - subject { checker.latest_resolvable_version_with_no_unlock } + context "when a full unlock cannot be performed" do + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "1.1.2", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.2", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end - it { is_expected.to be_nil } + it "returns `nil`" do + run_analyze_test do |checker| + expect(checker.latest_resolvable_version_with_no_unlock).to be_nil + end + end + end end - describe "#can_update?(requirements_to_unlock: :all)" do - subject(:can_update) { checker.can_update?(requirements_to_unlock: :all) } - - context "with a property dependency" do + describe "#requirements_unlocked_or_can_be?" do + context "when there is a newer package available" do let(:dependency_requirements) do [{ requirement: "0.1.434", @@ -162,101 +457,75 @@ def registration_index_url(name) let(:dependency_name) { "Nuke.Common" } let(:dependency_version) { "0.1.434" } - context "when a property is used for multiple dependencies" do - let(:csproj_body) do - fixture("csproj", "property_version.csproj") - end - - context "when all dependencies can update to the latest version" do - before do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive_messages(all_property_based_dependencies: [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ], latest_version: "0.9.0", property_updater: dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - end - - it { is_expected.to be(true) } - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Nuke.Common", + Version: "0.1.434", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "6.3.0", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Nuke.Common", + Version: "6.3.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end - context "when all dependencies cannot update to the latest version" do - before do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive_messages(all_property_based_dependencies: [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ], latest_version: "0.9.0", property_updater: dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(false) - end - - it { is_expected.to be(false) } + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.requirements_unlocked_or_can_be?).to be(true) end end end end - describe "#updated_requirements" do - subject(:updated_requirements) { checker.updated_requirements } - - let(:target_version) { "2.1.0" } - - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:latest_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] - ) - end - - context "with a security vulnerability" do + describe "#lowest_security_fix_version" do + context "when an appropriate version is returned" do let(:target_version) { "2.0.0" } let(:vulnerable_versions) { ["< 2.0.0"] } let(:security_advisories) do @@ -269,120 +538,142 @@ def registration_index_url(name) ] end - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:lowest_security_fix_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "2.0.0", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "2.0.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] ) end - context "when the security vulnerability excludes all compatible packages" do - subject(:updated_requirement_version) { updated_requirements[0].fetch(:requirement) } - - let(:target_version) { "1.1.1" } - let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad - - before do - # only vulnerable versions are returned - stub_request(:get, registration_index_url(dependency_name)) - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - version: "1.1.1" # the currently installed version, but it's vulnerable - } - }, - { - catalogEntry: { - version: "3.0.0" # newer version, but it's still vulnerable - } - } - ] - ] - }.to_json - ) - end - - it "reports the currently installed version" do - expect(updated_requirement_version).to eq(target_version) + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.lowest_security_fix_version).to eq(target_version) end end end - end - describe "#requirements_unlocked_or_can_be?" do - subject(:requirements_unlocked_or_can_be) do - checker.requirements_unlocked_or_can_be? - end + context "when the security vulnerability excludes all compatible packages" do + let(:target_version) { "1.1.1" } + let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: dependency_name, + package_manager: "nuget", + vulnerable_versions: vulnerable_versions + ) + ] + end - context "with a property dependency" do - let(:dependency_requirements) do - [{ - requirement: "0.1.434", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "1.1.1", + CanUpdate: false, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [] + } + ) end - let(:dependency_name) { "Nuke.Common" } - let(:dependency_version) { "0.1.434" } - it { is_expected.to be(true) } - - context "when a dependency's property couldn't be found" do - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.lowest_security_fix_version).to eq(target_version) end - let(:dependency_version) { "$(NukeVersion)" } - - it { is_expected.to be(false) } end end end describe "#updated_dependencies(requirements_to_unlock: :all)" do - subject(:updated_dependencies) do - checker.updated_dependencies(requirements_to_unlock: :all) - end - - context "with a property dependency" do + context "when all dependencies can update to the latest version" do let(:dependency_requirements) do [{ requirement: "0.1.434", @@ -395,38 +686,135 @@ def registration_index_url(name) let(:dependency_name) { "Nuke.Common" } let(:dependency_version) { "0.1.434" } - context "when a property is used for multiple dependencies" do - let(:csproj_body) do - fixture("csproj", "property_version.csproj") - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Nuke.CodeGeneration", + Version: "0.1.434", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Nuke.Common", + Version: "0.1.434", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "6.3.0", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Nuke.CodeGeneration", + Version: "6.3.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: "https://nuget.example.com/nuke.codegeneration" + }, { + Name: "Nuke.Common", + Version: "6.3.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: "https://nuget.example.com/nuke.common" + }] + } + ) + end - context "when all dependencies can update to the latest version" do - before do - allow(checker).to receive_messages(latest_version: "0.9.0", all_property_based_dependencies: [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ]) - end - - it "delegates to PropertyUpdater" do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - expect(dummy_property_updater).to receive(:updated_dependencies).and_return([dependency]) - - updated_dependencies - end + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.updated_dependencies(requirements_to_unlock: :all)).to eq([ + Dependabot::Dependency.new( + name: "Nuke.CodeGeneration", + version: "6.3.0", + previous_version: "0.1.434", + requirements: [{ + requirement: "6.3.0", + file: "/my.csproj", + groups: ["dependencies"], + source: { + type: "nuget_repo", + source_url: "https://nuget.example.com/nuke.codegeneration" + } + }], + previous_requirements: [{ + requirement: "0.1.434", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }], + package_manager: "nuget" + ), + Dependabot::Dependency.new( + name: "Nuke.Common", + version: "6.3.0", + previous_version: "0.1.434", + requirements: [{ + requirement: "6.3.0", + file: "/my.csproj", + groups: ["dependencies"], + source: { + type: "nuget_repo", + source_url: "https://nuget.example.com/nuke.common" + } + }], + previous_requirements: [{ + requirement: "0.1.434", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }], + package_manager: "nuget" + ) + ]) end end end diff --git a/nuget/spec/fixtures/csproj/property_version.csproj b/nuget/spec/fixtures/csproj/property_version.csproj index 62557f88433..4d8198bc238 100644 --- a/nuget/spec/fixtures/csproj/property_version.csproj +++ b/nuget/spec/fixtures/csproj/property_version.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net5.0 false False diff --git a/nuget/spec/spec_helper.rb b/nuget/spec/spec_helper.rb index a23c3fa17f4..a06f2f997b8 100644 --- a/nuget/spec/spec_helper.rb +++ b/nuget/spec/spec_helper.rb @@ -1,6 +1,9 @@ # 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" diff --git a/pub/.rubocop.yml b/pub/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/pub/.rubocop.yml +++ b/pub/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/pub/lib/dependabot/pub/file_fetcher.rb b/pub/lib/dependabot/pub/file_fetcher.rb index 6345ca002b7..62e81d7ed2d 100644 --- a/pub/lib/dependabot/pub/file_fetcher.rb +++ b/pub/lib/dependabot/pub/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "sorbet-runtime" @@ -13,10 +13,12 @@ class FileFetcher < Dependabot::FileFetchers::Base extend T::Sig extend T::Helpers + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) filenames.include?("pubspec.yaml") end + sig { override.returns(String) } def self.required_files_message "Repo must contain a pubspec.yaml." end @@ -38,14 +40,16 @@ def fetch_files private + sig { returns(DependencyFile) } def pubspec_yaml - @pubspec_yaml ||= fetch_file_from_host("pubspec.yaml") + @pubspec_yaml ||= T.let(fetch_file_from_host("pubspec.yaml"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(DependencyFile)) } def pubspec_lock return @pubspec_lock if defined?(@pubspec_lock) - @pubspec_lock = fetch_file_if_present("pubspec.lock") + @pubspec_lock = T.let(fetch_file_if_present("pubspec.lock"), T.nilable(Dependabot::DependencyFile)) end end end diff --git a/pub/lib/dependabot/pub/requirement.rb b/pub/lib/dependabot/pub/requirement.rb index ac0a89669d2..be6ca5485b3 100644 --- a/pub/lib/dependabot/pub/requirement.rb +++ b/pub/lib/dependabot/pub/requirement.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true # For details on pub version constraints see: @@ -20,11 +20,16 @@ class Requirement < Dependabot::Requirement quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|") version_pattern = Pub::Version::VERSION_PATTERN - PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze + PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze, String) PATTERN = /\A#{PATTERN_RAW}\z/ # Use Pub::Version rather than Gem::Version to ensure that # pre-release versions aren't transformed. + sig do + params( + obj: T.any(String, Gem::Version, Pub::Version) + ).returns(T::Array[T.any(String, Pub::Version)]) + end def self.parse(obj) return ["=", Pub::Version.new(obj.to_s)] if obj.is_a?(Gem::Version) @@ -43,9 +48,10 @@ def self.parse(obj) # contains a single element. sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) } def self.requirements_array(requirement_string) - [new(requirement_string)] + [new(T.must(requirement_string))] end + sig { params(requirements: T.any(String, T::Array[String]), raw_constraint: T.nilable(String)).void } def initialize(*requirements, raw_constraint: nil) requirements = requirements.flatten.flat_map do |req_string| req_string.split(",").map(&:strip).map do |r| @@ -57,6 +63,7 @@ def initialize(*requirements, raw_constraint: nil) @raw_constraint = raw_constraint end + sig { returns(String) } def to_s if @raw_constraint.nil? as_list.join " " @@ -67,6 +74,7 @@ def to_s private + sig { params(req_string: String).returns(T.any(String, T::Array[T.nilable(String)])) } def convert_dart_constraint_to_ruby_constraint(req_string) if req_string.empty? || req_string == "any" then ">= 0" elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string) @@ -77,18 +85,21 @@ def convert_dart_constraint_to_ruby_constraint(req_string) end end + sig { params(req_string: String).returns(String) } def convert_tilde_req(req_string) version = req_string.gsub(/^~/, "") parts = version.split(".") "~> #{parts.join('.')}" end + sig { params(req_string: String).returns(T::Array[T.nilable(String)]) } def convert_range_req(req_string) req_string.scan( /((?:>|<|=|<=|>=)\s*#{Pub::Version::VERSION_PATTERN})\s*/o - ).map { |x| x[0].strip } + ).map { |x| x[0]&.strip } end + sig { params(req_string: String).returns(String) } def ruby_range(req_string) parts = req_string.split(".") @@ -103,6 +114,7 @@ def ruby_range(req_string) "~> #{parts.join('.')}" end + sig { params(req_string: String).returns(T::Array[String]) } def convert_caret_req(req_string) # Copied from Cargo::Requirement which allows less than 3 components # so we could be more strict in the parsing here. @@ -112,7 +124,7 @@ def convert_caret_req(req_string) first_non_zero_index = first_non_zero ? parts.index(first_non_zero) : parts.count - 1 upper_bound = parts.map.with_index do |part, i| - if i < first_non_zero_index then part + if i < T.must(first_non_zero_index) then part elsif i == first_non_zero_index then (part.to_i + 1).to_s else 0 diff --git a/pub/spec/dependabot/pub/file_updater_spec.rb b/pub/spec/dependabot/pub/file_updater_spec.rb index 6db90b99b37..eac37910f23 100644 --- a/pub/spec/dependabot/pub/file_updater_spec.rb +++ b/pub/spec/dependabot/pub/file_updater_spec.rb @@ -11,11 +11,13 @@ RSpec.describe Dependabot::Pub::FileUpdater do let(:project) { "can_update" } + let(:dev_null) { WEBrick::Log.new("/dev/null", 7) } + let(:server) { WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) } let(:dependency_files) do files = project_dependency_files(project) files.each do |file| # Simulate that the lockfile was from localhost: - file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{@server[:Port]}") + file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{server[:Port]}") end files end @@ -31,7 +33,7 @@ "password" => "token" }], options: { - pub_hosted_url: "http://localhost:#{@server[:Port]}" + pub_hosted_url: "http://localhost:#{server[:Port]}" } ) end @@ -41,33 +43,25 @@ after do sample_files.each do |f| package = File.basename(f, ".json") - @server.unmount "/api/packages/#{package}" + server.unmount "/api/packages/#{package}" end + server.shutdown end before do + # Because we do the networking in dependency_services we have to run an + # actual web server. + Thread.new do + server.start + end sample_files.each do |f| package = File.basename(f, ".json") - @server.mount_proc "/api/packages/#{package}" do |_req, res| + server.mount_proc "/api/packages/#{package}" do |_req, res| res.body = File.read(File.join("..", "..", "..", f)) end end end - after(:all) do - @server.shutdown - end - - before(:all) do - # Because we do the networking in dependency_services we have to run an - # actual web server. - dev_null = WEBrick::Log.new("/dev/null", 7) - @server = WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) - Thread.new do - @server.start - end - end - it_behaves_like "a dependency file updater" def manifest(files) diff --git a/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb b/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb index e3d73fc328f..1e7b7fedfdf 100644 --- a/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb +++ b/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb @@ -8,29 +8,26 @@ require "webrick" RSpec.describe "Helpers" do - before(:all) do + let(:dev_null) { WEBrick::Log.new("/dev/null", 7) } + let(:inferred_result) do + Dependabot::Pub::Helpers.run_infer_sdk_versions \ + File.join("spec", "fixtures", "projects", project), url: "http://localhost:#{server[:Port]}/flutter_releases.json" + end + let(:server) { WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) } + + before do # Because we do the networking in infer_sdk_versions we have to run an # actual web server. - dev_null = WEBrick::Log.new("/dev/null", 7) - @server = WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) Thread.new do - @server.start + server.start end - end - - after(:all) do - @server.shutdown - end - - before do - @server.mount_proc "/flutter_releases.json" do |_req, res| + server.mount_proc "/flutter_releases.json" do |_req, res| res.body = File.read(File.join(__dir__, "..", "..", "fixtures", "flutter_releases.json")) end end - let(:inferred_result) do - Dependabot::Pub::Helpers.run_infer_sdk_versions \ - File.join("spec", "fixtures", "projects", project), url: "http://localhost:#{@server[:Port]}/flutter_releases.json" + after do + server.shutdown end describe "Will resolve to latest beta if needed" do diff --git a/pub/spec/dependabot/pub/update_checker_spec.rb b/pub/spec/dependabot/pub/update_checker_spec.rb index f176f1461d1..6bede9e6c7f 100644 --- a/pub/spec/dependabot/pub/update_checker_spec.rb +++ b/pub/spec/dependabot/pub/update_checker_spec.rb @@ -18,11 +18,13 @@ let(:can_update) { checker.can_update?(requirements_to_unlock: requirements_to_unlock) } let(:directory) { nil } let(:project) { "can_update" } + let(:dev_null) { WEBrick::Log.new("/dev/null", 7) } + let(:server) { WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) } let(:dependency_files) do files = project_dependency_files(project) files.each do |file| # Simulate that the lockfile was from localhost: - file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{@server[:Port]}") + file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{server[:Port]}") if defined?(git_dir) file.content.gsub!("$GIT_DIR", git_dir) file.content.gsub!("$REF", dependency_version) @@ -58,8 +60,8 @@ }], ignored_versions: ignored_versions, options: { - pub_hosted_url: "http://localhost:#{@server[:Port]}", - flutter_releases_url: "http://localhost:#{@server[:Port]}/flutter_releases.json" + pub_hosted_url: "http://localhost:#{server[:Port]}", + flutter_releases_url: "http://localhost:#{server[:Port]}/flutter_releases.json" }, raise_on_ignored: raise_on_ignored, security_advisories: security_advisories, @@ -72,36 +74,28 @@ after do sample_files.each do |f| package = File.basename(f, ".json") - @server.unmount "/api/packages/#{package}" + server.unmount "/api/packages/#{package}" end + server.shutdown end before do + # Because we do the networking in dependency_services we have to run an + # actual web server. + Thread.new do + server.start + end sample_files.each do |f| package = File.basename(f, ".json") - @server.mount_proc "/api/packages/#{package}" do |_req, res| + server.mount_proc "/api/packages/#{package}" do |_req, res| res.body = File.read(File.join("..", "..", "..", f)) end end - @server.mount_proc "/flutter_releases.json" do |_req, res| + server.mount_proc "/flutter_releases.json" do |_req, res| res.body = File.read(File.join(__dir__, "..", "..", "fixtures", "flutter_releases.json")) end end - after(:all) do - @server.shutdown - end - - before(:all) do - # Because we do the networking in dependency_services we have to run an - # actual web server. - dev_null = WEBrick::Log.new("/dev/null", 7) - @server = WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) - Thread.new do - @server.start - end - end - it_behaves_like "an update checker" context "when given an outdated dependency, not requiring unlock" do @@ -527,7 +521,7 @@ WebMock.allow_net_connect! # To find the vulnerable versions we do a package listing before invoking the helper. # Stub this out here: - stub_request(:get, "http://localhost:#{@server[:Port]}/api/packages/#{dependency.name}").to_return( + stub_request(:get, "http://localhost:#{server[:Port]}/api/packages/#{dependency.name}").to_return( status: 200, body: fixture("pub_dev_responses/simple/#{dependency.name}.json"), headers: {} @@ -601,7 +595,7 @@ WebMock.allow_net_connect! # To find the vulnerable versions we do a package listing before invoking the helper. # Stub this out here: - stub_request(:get, "http://localhost:#{@server[:Port]}/api/packages/#{dependency.name}").to_return( + stub_request(:get, "http://localhost:#{server[:Port]}/api/packages/#{dependency.name}").to_return( status: 200, body: fixture("pub_dev_responses/simple/#{dependency.name}.json"), headers: {} diff --git a/python/helpers/requirements.txt b/python/helpers/requirements.txt index ef35b13a34a..4aad2ff2eb0 100644 --- a/python/helpers/requirements.txt +++ b/python/helpers/requirements.txt @@ -4,7 +4,7 @@ flake8==7.1.0 hashin==1.0.1 pipenv==2023.12.1 plette==2.1.0 -poetry==1.8.2 +poetry==1.8.3 # TODO: Replace 3p package `toml` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10. toml==0.10.2 diff --git a/python/lib/dependabot/python/authed_url_builder.rb b/python/lib/dependabot/python/authed_url_builder.rb index 37c10eb560d..e2eb95ed7f1 100644 --- a/python/lib/dependabot/python/authed_url_builder.rb +++ b/python/lib/dependabot/python/authed_url_builder.rb @@ -7,6 +7,7 @@ class AuthedUrlBuilder def self.authed_url(credential:) token = credential.fetch("token", nil) url = credential.fetch("index-url", nil) + return "" unless url return url unless token basic_auth_details = diff --git a/python/lib/dependabot/python/file_fetcher.rb b/python/lib/dependabot/python/file_fetcher.rb index 910c95f43cb..e878452fc1a 100644 --- a/python/lib/dependabot/python/file_fetcher.rb +++ b/python/lib/dependabot/python/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: false +# typed: true # frozen_string_literal: true require "toml-rb" @@ -139,7 +139,7 @@ def python_version_file # Check the top-level for a .python-version file, too reverse_path = Pathname.new(directory[0]).relative_path_from(directory) - @python_version_file ||= + @python_version_file = fetch_support_file(File.join(reverse_path, ".python-version")) &.tap { |f| f.name = ".python-version" } end diff --git a/python/lib/dependabot/python/file_updater.rb b/python/lib/dependabot/python/file_updater.rb index c7d4a7c88bc..cc02c3160b4 100644 --- a/python/lib/dependabot/python/file_updater.rb +++ b/python/lib/dependabot/python/file_updater.rb @@ -1,19 +1,23 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "toml-rb" require "dependabot/file_updaters" require "dependabot/file_updaters/base" require "dependabot/shared_helpers" +require "sorbet-runtime" module Dependabot module Python class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + require_relative "file_updater/pipfile_file_updater" require_relative "file_updater/pip_compile_file_updater" require_relative "file_updater/poetry_file_updater" require_relative "file_updater/requirement_file_updater" + sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ /^Pipfile$/, @@ -27,6 +31,7 @@ def self.updated_files_regex ] end + sig { override.returns(T::Array[DependencyFile]) } def updated_dependency_files updated_files = case resolver_type @@ -48,6 +53,8 @@ def updated_dependency_files private # rubocop:disable Metrics/PerceivedComplexity + + sig { returns(Symbol) } def resolver_type reqs = dependencies.flat_map(&:requirements) changed_reqs = reqs.zip(dependencies.flat_map(&:previous_requirements)) @@ -76,6 +83,7 @@ def resolver_type end # rubocop:enable Metrics/PerceivedComplexity + sig { returns(Symbol) } def subdependency_resolver return :pipfile if pipfile_lock return :poetry if poetry_lock @@ -84,6 +92,7 @@ def subdependency_resolver raise "Claimed to be a sub-dependency, but no lockfile exists!" end + sig { returns(T::Array[DependencyFile]) } def updated_pipfile_based_files PipfileFileUpdater.new( dependencies: dependencies, @@ -93,6 +102,7 @@ def updated_pipfile_based_files ).updated_dependency_files end + sig { returns(T::Array[DependencyFile]) } def updated_poetry_based_files PoetryFileUpdater.new( dependencies: dependencies, @@ -101,6 +111,7 @@ def updated_poetry_based_files ).updated_dependency_files end + sig { returns(T::Array[DependencyFile]) } def updated_pip_compile_based_files PipCompileFileUpdater.new( dependencies: dependencies, @@ -110,6 +121,7 @@ def updated_pip_compile_based_files ).updated_dependency_files end + sig { returns(T::Array[DependencyFile]) } def updated_requirement_based_files RequirementFileUpdater.new( dependencies: dependencies, @@ -119,6 +131,7 @@ def updated_requirement_based_files ).updated_dependency_files end + sig { returns(T::Array[String]) } def pip_compile_index_urls if credentials.any?(&:replaces_base?) credentials.select(&:replaces_base?).map { |cred| AuthedUrlBuilder.authed_url(credential: cred) } @@ -130,6 +143,7 @@ def pip_compile_index_urls end end + sig { override.void } def check_required_files filenames = dependency_files.map(&:name) return if filenames.any? { |name| name.end_with?(".txt", ".in") } @@ -141,31 +155,39 @@ def check_required_files raise "Missing required files!" end + sig { returns(T::Boolean) } def poetry_based? return false unless pyproject - !TomlRB.parse(pyproject.content).dig("tool", "poetry").nil? + !TomlRB.parse(pyproject&.content).dig("tool", "poetry").nil? end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pipfile - @pipfile ||= get_original_file("Pipfile") + @pipfile ||= T.let(get_original_file("Pipfile"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pipfile_lock - @pipfile_lock ||= get_original_file("Pipfile.lock") + @pipfile_lock ||= T.let(get_original_file("Pipfile.lock"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pyproject - @pyproject ||= get_original_file("pyproject.toml") + @pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def poetry_lock - @poetry_lock ||= get_original_file("poetry.lock") + @poetry_lock ||= T.let(get_original_file("poetry.lock"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Array[DependencyFile]) } def pip_compile_files - @pip_compile_files ||= - dependency_files.select { |f| f.name.end_with?(".in") } + @pip_compile_files ||= T.let( + dependency_files.select { |f| f.name.end_with?(".in") }, + T.nilable(T::Array[DependencyFile]) + ) end end end diff --git a/python/lib/dependabot/python/pip_compile_file_matcher.rb b/python/lib/dependabot/python/pip_compile_file_matcher.rb index 11e14beb837..7ac2bbb65dc 100644 --- a/python/lib/dependabot/python/pip_compile_file_matcher.rb +++ b/python/lib/dependabot/python/pip_compile_file_matcher.rb @@ -1,29 +1,35 @@ -# typed: true +# typed: strict # frozen_string_literal: true module Dependabot module Python class PipCompileFileMatcher + extend T::Sig + + sig { params(requirements_in_files: T::Array[Dependabot::Python::Requirement]).void } def initialize(requirements_in_files) @requirements_in_files = requirements_in_files end + sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) } def lockfile_for_pip_compile_file?(file) return false unless requirements_in_files.any? name = file.name return false unless name.end_with?(".txt") - return true if file.content.match?(output_file_regex(name)) + return true if file.content&.match?(output_file_regex(name)) basename = name.gsub(/\.txt$/, "") - requirements_in_files.any? { |f| f.name == basename + ".in" } + requirements_in_files.any? { |f| f.instance_variable_get(:@name) == basename + ".in" } end private + sig { returns(T::Array[Dependabot::Python::Requirement]) } attr_reader :requirements_in_files + sig { params(filename: T.any(String, Symbol)).returns(String) } def output_file_regex(filename) "--output-file[=\s]+#{Regexp.escape(filename)}(?:\s|$)" end diff --git a/python/lib/dependabot/python/requirement.rb b/python/lib/dependabot/python/requirement.rb index 69cb742ea6d..6c9a4a84c36 100644 --- a/python/lib/dependabot/python/requirement.rb +++ b/python/lib/dependabot/python/requirement.rb @@ -20,6 +20,11 @@ class Requirement < Dependabot::Requirement "===" => ->(v, r) { v.to_s == r.to_s } ) + # Override the lower bound logic for bump versions strategy. + BUMP_VERSIONS_OPS = OPS.merge( + ">=" => ->(v, r) { v.to_s == r.to_s } + ) + quoted = OPS.keys.sort_by(&:length).reverse .map { |k| Regexp.quote(k) }.join("|") version_pattern = Python::Version::VERSION_PATTERN @@ -78,10 +83,10 @@ def initialize(*requirements) super(requirements) end - def satisfied_by?(version) + def satisfied_by?(version, ops = OPS) version = Python::Version.new(version.to_s) - requirements.all? { |op, rv| (OPS[op] || OPS["="]).call(version, rv) } + requirements.all? { |op, rv| (ops[op] || ops["="]).call(version, rv) } end def exact? diff --git a/python/lib/dependabot/python/update_checker.rb b/python/lib/dependabot/python/update_checker.rb index 983fa1d3c50..0d1f1b43922 100644 --- a/python/lib/dependabot/python/update_checker.rb +++ b/python/lib/dependabot/python/update_checker.rb @@ -81,7 +81,7 @@ def updated_requirements end def requirements_unlocked_or_can_be? - requirements_update_strategy != RequirementsUpdateStrategy::LockfileOnly + !requirements_update_strategy.lockfile_only? end def requirements_update_strategy diff --git a/python/lib/dependabot/python/update_checker/requirements_updater.rb b/python/lib/dependabot/python/update_checker/requirements_updater.rb index 444c4119848..26012363852 100644 --- a/python/lib/dependabot/python/update_checker/requirements_updater.rb +++ b/python/lib/dependabot/python/update_checker/requirements_updater.rb @@ -34,7 +34,7 @@ def initialize(requirements:, update_strategy:, has_lockfile:, end def updated_requirements - return requirements if update_strategy == RequirementsUpdateStrategy::LockfileOnly + return requirements if update_strategy.lockfile_only? requirements.map do |req| case req[:file] @@ -278,14 +278,14 @@ def update_requirements_range(requirement_strings) requirement_strings.map { |r| requirement_class.new(r) } updated_requirement_strings = ruby_requirements.flat_map do |r| - next r.to_s if r.satisfied_by?(latest_resolvable_version) + next r.to_s if r.satisfied_by?(latest_resolvable_version, Requirement::BUMP_VERSIONS_OPS) case op = r.requirements.first.first when "<" - "<" + update_greatest_version(r.requirements.first.last, latest_resolvable_version) - when "<=" - "<=" + latest_resolvable_version.to_s - when "!=", ">", ">=" + "#{op}#{update_greatest_version(r.requirements.first.last, latest_resolvable_version)}" + when "<=", ">=" + "#{op}#{latest_resolvable_version}" + when "!=", ">" raise UnfixableRequirement else raise "Unexpected op for unsatisfied requirement: #{op}" diff --git a/python/spec/dependabot/python/authed_url_builder_spec.rb b/python/spec/dependabot/python/authed_url_builder_spec.rb index a3ee9d7633d..a84526da7c1 100644 --- a/python/spec/dependabot/python/authed_url_builder_spec.rb +++ b/python/spec/dependabot/python/authed_url_builder_spec.rb @@ -9,6 +9,20 @@ describe ".authed_url" do subject(:authed_url) { described_class.authed_url(credential: credential) } + context "without index-url" do + let(:credential) do + Dependabot::Credential.new({ + "type" => "python_index", + "replaces-base" => true + }) + end + + it "returns empty string" do + expect(authed_url) + .to eq("") + end + end + context "without a token" do let(:credential) do Dependabot::Credential.new({ diff --git a/python/spec/dependabot/python/requirement_spec.rb b/python/spec/dependabot/python/requirement_spec.rb index 4ac72b51d35..d59e3d04be4 100644 --- a/python/spec/dependabot/python/requirement_spec.rb +++ b/python/spec/dependabot/python/requirement_spec.rb @@ -67,7 +67,7 @@ its(:to_s) { is_expected.to eq(Gem::Requirement.new("~> 1.2.0").to_s) } end - context "when dealing with one digits" do + context "when dealing with one digit" do let(:requirement_string) { "~1" } its(:to_s) { is_expected.to eq(Gem::Requirement.new("~> 1.0").to_s) } @@ -185,7 +185,7 @@ it { is_expected.to eq([Gem::Requirement.new("1.2.1")]) } end - context "with a illformed parentheses" do + context "with illformed parentheses" do let(:requirement_string) { "(== 1.2).1" } it "raises a helpful error" do @@ -319,4 +319,50 @@ end end end + + describe "#satisfied_by? with BUMP_VERSIONS_OPS" do + subject(:requirement_satisfied_by) { requirement.satisfied_by?(version, described_class::BUMP_VERSIONS_OPS) } + + let(:requirement_string) { ">=1.0.0" } + + context "with a Python::Version" do + let(:version) { version_class.new(version_string) } + + context "when dealing with the exact version" do + let(:version_string) { "1.0.0" } + + it { is_expected.to be(true) } + end + + context "when dealing with a different version" do + let(:version_string) { "2.0.0" } + + it { is_expected.to be(false) } + end + + context "when dealing with the latest resolvable version" do + let(:version_string) { "1.0.0" } + + it { is_expected.to be(true) } + + context "when the requirement includes a local version" do + let(:requirement_string) { ">=1.0.0+gc.1" } + + it { is_expected.to be(false) } + end + + context "when the version includes a local version" do + let(:version_string) { "1.0.0+gc.1" } + + it { is_expected.to be(false) } + end + end + + context "when dealing with an out-of-range version" do + let(:version_string) { "0.9.0" } + + it { is_expected.to be(false) } + end + end + end end diff --git a/python/spec/dependabot/python/update_checker/requirements_updater_spec.rb b/python/spec/dependabot/python/update_checker/requirements_updater_spec.rb index 52f5b312af8..8511f9004d3 100644 --- a/python/spec/dependabot/python/update_checker/requirements_updater_spec.rb +++ b/python/spec/dependabot/python/update_checker/requirements_updater_spec.rb @@ -121,6 +121,24 @@ end end + context "when a > req was specified" do + let(:requirement_txt_req_string) { "> 1.3.0" } + + it { is_expected.to eq(requirement_txt_req) } + + context "when dealing with exactly the version being updated to" do + let(:requirement_txt_req_string) { "> 1.5.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + + context "when dealing with the version lower than the lower bound" do + let(:requirement_txt_req_string) { "> 1.6.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + end + context "when a range requirement was specified" do let(:requirement_txt_req_string) { ">=1.3.0" } @@ -129,7 +147,7 @@ context "when requirement version is too high" do let(:requirement_txt_req_string) { ">=2.0.0" } - its([:requirement]) { is_expected.to eq(:unfixable) } + its([:requirement]) { is_expected.to eq(">=1.5.0") } end context "when requirement had a local version" do @@ -146,13 +164,13 @@ context "when needing an update" do let(:requirement_txt_req_string) { ">=1.3.0, <1.5" } - its([:requirement]) { is_expected.to eq(">=1.3.0,<1.6") } + its([:requirement]) { is_expected.to eq(">=1.5.0,<1.6") } context "when requirement version has more digits than the new version" do let(:requirement_txt_req_string) { "<=1.9.2,>=1.9" } let(:latest_resolvable_version) { "1.10" } - its([:requirement]) { is_expected.to eq(">=1.9,<=1.10") } + its([:requirement]) { is_expected.to eq("<=1.10,>=1.10") } end end end @@ -268,7 +286,7 @@ context "when the requirement version is too high" do let(:setup_py_req_string) { ">=2.0.0" } - its([:requirement]) { is_expected.to eq(:unfixable) } + its([:requirement]) { is_expected.to eq(">=1.5.0") } end context "with an upper bound" do @@ -279,7 +297,7 @@ context "when needing an update" do let(:setup_py_req_string) { ">=1.3.0, <1.5" } - its([:requirement]) { is_expected.to eq(">=1.3.0,<1.6") } + its([:requirement]) { is_expected.to eq(">=1.5.0,<1.6") } end end end @@ -370,7 +388,7 @@ context "when the requirement version is too high" do let(:setup_cfg_req_string) { ">=2.0.0" } - its([:requirement]) { is_expected.to eq(:unfixable) } + its([:requirement]) { is_expected.to eq(">=1.5.0") } end context "with an upper bound" do @@ -381,7 +399,7 @@ context "when needing an update" do let(:setup_cfg_req_string) { ">=1.3.0, <1.5" } - its([:requirement]) { is_expected.to eq(">=1.3.0,<1.6") } + its([:requirement]) { is_expected.to eq(">=1.5.0,<1.6") } end end end @@ -496,6 +514,36 @@ it { is_expected.to eq(pyproject_req) } end + context "when a != req was specified" do + let(:pyproject_req_string) { "!= 1.3.0" } + + it { is_expected.to eq(pyproject_req) } + + context "when dealing with exactly the version being updated to" do + let(:pyproject_req_string) { "!=1.5.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + end + + context "when a > req was specified" do + let(:pyproject_req_string) { "> 1.3.0" } + + it { is_expected.to eq(pyproject_req) } + + context "when dealing with exactly the version being updated to" do + let(:pyproject_req_string) { "> 1.5.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + + context "when dealing with the version lower than the lower bound" do + let(:pyproject_req_string) { "> 1.6.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + end + context "when a range requirement was specified" do let(:pyproject_req_string) { ">=1.3.0" } @@ -504,7 +552,7 @@ context "when the requirement version is too high" do let(:pyproject_req_string) { ">=2.0.0" } - its([:requirement]) { is_expected.to eq(:unfixable) } + its([:requirement]) { is_expected.to eq(">=1.5.0") } end context "when the requirement had a local version" do @@ -521,7 +569,7 @@ context "when needing an update" do let(:pyproject_req_string) { ">=1.3.0, <1.5" } - its([:requirement]) { is_expected.to eq(">=1.3.0,<1.6") } + its([:requirement]) { is_expected.to eq(">=1.5.0,<1.6") } end end end @@ -636,6 +684,36 @@ it { is_expected.to eq(pyproject_req) } end + context "when a != req was specified" do + let(:pyproject_req_string) { "!= 1.3.0" } + + it { is_expected.to eq(pyproject_req) } + + context "when dealing with exactly the version being updated to" do + let(:pyproject_req_string) { "!=1.5.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + end + + context "when a > req was specified" do + let(:pyproject_req_string) { "> 1.3.0" } + + it { is_expected.to eq(pyproject_req) } + + context "when dealing with exactly the version being updated to" do + let(:pyproject_req_string) { "> 1.5.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + + context "when dealing with the version lower than the lower bound" do + let(:pyproject_req_string) { "> 1.6.0" } + + its([:requirement]) { is_expected.to eq(:unfixable) } + end + end + context "when a range requirement was specified" do let(:pyproject_req_string) { ">=1.3.0" } @@ -650,7 +728,7 @@ context "when the requirement is too high" do let(:pyproject_req_string) { ">=2.0.0" } - its([:requirement]) { is_expected.to eq(:unfixable) } + its([:requirement]) { is_expected.to eq(">=1.5.0") } end context "with an upper bound" do @@ -661,7 +739,7 @@ context "when needing an update" do let(:pyproject_req_string) { ">=1.3.0, <1.5" } - its([:requirement]) { is_expected.to eq(">=1.3.0,<1.6") } + its([:requirement]) { is_expected.to eq(">=1.5.0,<1.6") } end end end diff --git a/silent/.rubocop.yml b/silent/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/silent/.rubocop.yml +++ b/silent/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/swift/.rubocop.yml b/swift/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/swift/.rubocop.yml +++ b/swift/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/terraform/.rubocop.yml b/terraform/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/terraform/.rubocop.yml +++ b/terraform/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/terraform/Dockerfile b/terraform/Dockerfile index 15623217884..474e0291842 100644 --- a/terraform/Dockerfile +++ b/terraform/Dockerfile @@ -2,13 +2,13 @@ FROM ghcr.io/dependabot/dependabot-updater-core ARG TARGETARCH # See https://github.com/hashicorp/terraform/releases or https://releases.hashicorp.com/terraform/ -ARG TERRAFORM_VERSION=1.8.0 +ARG TERRAFORM_VERSION=1.9.1 # curl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS" | grep "terraform_${TERRAFORM_VERSION}_linux_amd64.zip" -ARG TERRAFORM_AMD64_CHECKSUM=dcc4670379a22213e72faa6cb709b3391e7e54967e40288ecf591e2b83cfd39e +ARG TERRAFORM_AMD64_CHECKSUM=c3e1dade1c81fdc5e293529e480709f047c0113ea9feb8d9f35002df09ec6a34 # curl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS" | grep "terraform_${TERRAFORM_VERSION}_linux_arm64.zip" -ARG TERRAFORM_ARM64_CHECKSUM=47cbde7184ce260160ff0355065d454ffa5628a2259ba325736dbcf740351193 +ARG TERRAFORM_ARM64_CHECKSUM=f1426fccbf2500202b37993ef6b92e1fc60d114dd32c79bfadbc843929b2c7e2 RUN cd /tmp \ && curl -o terraform-${TARGETARCH}.tar.gz https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${TARGETARCH}.zip \ diff --git a/updater/.rubocop.yml b/updater/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/updater/.rubocop.yml +++ b/updater/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index f128e7171e8..d0cb5831ef8 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -1,20 +1,20 @@ PATH remote: ../bundler specs: - dependabot-bundler (0.262.0) - dependabot-common (= 0.262.0) + dependabot-bundler (0.264.0) + dependabot-common (= 0.264.0) parallel (~> 1.24) PATH remote: ../cargo specs: - dependabot-cargo (0.262.0) - dependabot-common (= 0.262.0) + dependabot-cargo (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../common specs: - dependabot-common (0.262.0) + dependabot-common (0.264.0) aws-sdk-codecommit (~> 1.28) aws-sdk-ecr (~> 1.5) bundler (>= 1.16, < 3.0.0) @@ -37,107 +37,107 @@ PATH PATH remote: ../composer specs: - dependabot-composer (0.262.0) - dependabot-common (= 0.262.0) + dependabot-composer (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../devcontainers specs: - dependabot-devcontainers (0.262.0) - dependabot-common (= 0.262.0) + dependabot-devcontainers (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../docker specs: - dependabot-docker (0.262.0) - dependabot-common (= 0.262.0) + dependabot-docker (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../elm specs: - dependabot-elm (0.262.0) - dependabot-common (= 0.262.0) + dependabot-elm (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../git_submodules specs: - dependabot-git_submodules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-git_submodules (0.264.0) + dependabot-common (= 0.264.0) parseconfig (~> 1.0, < 1.1.0) PATH remote: ../github_actions specs: - dependabot-github_actions (0.262.0) - dependabot-common (= 0.262.0) + dependabot-github_actions (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../go_modules specs: - dependabot-go_modules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-go_modules (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../gradle specs: - dependabot-gradle (0.262.0) - dependabot-common (= 0.262.0) - dependabot-maven (= 0.262.0) + dependabot-gradle (0.264.0) + dependabot-common (= 0.264.0) + dependabot-maven (= 0.264.0) PATH remote: ../hex specs: - dependabot-hex (0.262.0) - dependabot-common (= 0.262.0) + dependabot-hex (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../maven specs: - dependabot-maven (0.262.0) - dependabot-common (= 0.262.0) + dependabot-maven (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../npm_and_yarn specs: - dependabot-npm_and_yarn (0.262.0) - dependabot-common (= 0.262.0) + dependabot-npm_and_yarn (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../nuget specs: - dependabot-nuget (0.262.0) - dependabot-common (= 0.262.0) + dependabot-nuget (0.264.0) + dependabot-common (= 0.264.0) rubyzip (>= 2.3.2, < 3.0) PATH remote: ../pub specs: - dependabot-pub (0.262.0) - dependabot-common (= 0.262.0) + dependabot-pub (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../python specs: - dependabot-python (0.262.0) - dependabot-common (= 0.262.0) + dependabot-python (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../silent specs: - dependabot-silent (0.262.0) - dependabot-common (= 0.262.0) + dependabot-silent (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../swift specs: - dependabot-swift (0.262.0) - dependabot-common (= 0.262.0) + dependabot-swift (0.264.0) + dependabot-common (= 0.264.0) PATH remote: ../terraform specs: - dependabot-terraform (0.262.0) - dependabot-common (= 0.262.0) + dependabot-terraform (0.264.0) + dependabot-common (= 0.264.0) GEM remote: https://rubygems.org/ @@ -168,6 +168,7 @@ GEM crack (1.0.0) bigdecimal rexml + csv (3.3.0) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) @@ -208,7 +209,8 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) http-form_data (2.3.0) - httparty (0.21.0) + httparty (0.22.0) + csv mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) io-console (0.7.2) @@ -224,9 +226,10 @@ GEM mime-types (3.4.1) mime-types-data (~> 3.2015) mime-types-data (3.2022.0105) - mini_mime (1.1.2) + mini_mime (1.1.5) mini_portile2 (2.8.6) - multi_xml (0.6.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) netrc (0.11.0) nokogiri (1.16.5) mini_portile2 (~> 2.8.2) @@ -359,7 +362,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sorbet-runtime (0.5.11415) + sorbet-runtime (0.5.11444) stackprof (0.2.25) stringio (3.1.0) terminal-table (3.0.2) @@ -433,4 +436,4 @@ DEPENDENCIES webrick (>= 1.7) BUNDLED WITH - 2.5.11 + 2.5.14 diff --git a/updater/lib/dependabot/service.rb b/updater/lib/dependabot/service.rb index 1237cb01084..5e69e3d27a8 100644 --- a/updater/lib/dependabot/service.rb +++ b/updater/lib/dependabot/service.rb @@ -128,7 +128,8 @@ def capture_exception(error:, job: nil, dependency: nil, dependency_group: nil, ErrorAttributes::PACKAGE_MANAGER => job&.package_manager, ErrorAttributes::JOB_ID => job&.id, ErrorAttributes::DEPENDENCIES => dependency&.name || job&.dependencies, - ErrorAttributes::DEPENDENCY_GROUPS => dependency_group&.name || job&.dependency_groups + ErrorAttributes::DEPENDENCY_GROUPS => dependency_group&.name || job&.dependency_groups, + ErrorAttributes::SECURITY_UPDATE => job&.security_updates_only? }.compact record_update_job_unknown_error(error_type: "unknown_error", error_details: error_details) end diff --git a/updater/spec/dependabot/file_fetcher_command_spec.rb b/updater/spec/dependabot/file_fetcher_command_spec.rb index 2d855cef072..e97647bfde0 100644 --- a/updater/spec/dependabot/file_fetcher_command_spec.rb +++ b/updater/spec/dependabot/file_fetcher_command_spec.rb @@ -163,7 +163,8 @@ Dependabot::ErrorAttributes::CLASS => "StandardError", Dependabot::ErrorAttributes::PACKAGE_MANAGER => "bundler", Dependabot::ErrorAttributes::JOB_ID => "123123", - Dependabot::ErrorAttributes::DEPENDENCY_GROUPS => [] + Dependabot::ErrorAttributes::DEPENDENCY_GROUPS => [], + Dependabot::ErrorAttributes::SECURITY_UPDATE => false } ) expect(api_client).to receive(:mark_job_as_processed) diff --git a/updater/spec/dependabot/service_spec.rb b/updater/spec/dependabot/service_spec.rb index 2352dd82123..a0f9572d71b 100644 --- a/updater/spec/dependabot/service_spec.rb +++ b/updater/spec/dependabot/service_spec.rb @@ -371,6 +371,25 @@ ) end + it "extracts information from a security job if provided" do + job = OpenStruct.new(id: 1234, package_manager: "npm_and_yarn", repo_private?: false, repo_owner: "foo", + security_updates_only?: true) + service.capture_exception(error: error, job: job) + + expect(mock_client) + .to have_received(:record_update_job_unknown_error) + .with( + error_type: "unknown_error", + error_details: hash_including( + Dependabot::ErrorAttributes::CLASS => "Dependabot::DependabotError", + Dependabot::ErrorAttributes::MESSAGE => "Something went wrong", + Dependabot::ErrorAttributes::JOB_ID => job.id, + Dependabot::ErrorAttributes::PACKAGE_MANAGER => job.package_manager, + Dependabot::ErrorAttributes::SECURITY_UPDATE => true + ) + ) + end + it "extracts information from a dependency_group if provided" do dependency_group = OpenStruct.new(name: "all-the-things") allow(dependency_group).to receive(:is_a?).with(Dependabot::DependencyGroup).and_return(true)