diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1ae8105879..c5ee8530ff 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,10 +19,10 @@ // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", "features": { - "ghcr.io/devcontainers/features/docker-from-docker": "latest", + "ghcr.io/devcontainers/features/docker-outside-of-docker": "latest", "ghcr.io/devcontainers/features/github-cli": "latest", "ghcr.io/devcontainers/features/node": "lts", - "ghcr.io/devcontainers/features/golang": "latest", + "ghcr.io/devcontainers/features/go": "latest", "ghcr.io/devcontainers/features/ruby": "3.1.4", "ghcr.io/devcontainers/features/rust": "latest", "ghcr.io/devcontainers/features/dotnet": "latest", diff --git a/.github/workflows/images-branch.yml b/.github/workflows/images-branch.yml index f3223130cc..ebd5e74dfb 100644 --- a/.github/workflows/images-branch.yml +++ b/.github/workflows/images-branch.yml @@ -73,7 +73,7 @@ jobs: contents: read packages: write env: - TAG: ${{ github.sha }} + DEPENDABOT_UPDATER_VERSION: ${{ github.sha }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -81,7 +81,7 @@ jobs: submodules: recursive - name: Prepare tag - run: echo "TAG=${{ github.sha }}" >> $GITHUB_ENV + run: echo "DEPENDABOT_UPDATER_VERSION=${{ github.sha }}" >> $GITHUB_ENV if: github.event_name == 'pull_request' - name: Prepare tag (forks) @@ -90,7 +90,7 @@ jobs: git fetch origin main git merge origin/main --ff-only || exit 1 git submodule update --init --recursive - echo "TAG=$(git rev-parse HEAD)" >> $GITHUB_ENV + echo "DEPENDABOT_UPDATER_VERSION=$(git rev-parse HEAD)" >> $GITHUB_ENV if: github.event_name == 'workflow_dispatch' - name: Log in to GHCR @@ -102,12 +102,12 @@ jobs: - name: Push branch image run: | - docker tag "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}" "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}:$TAG" - docker push "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}:$TAG" + docker tag "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}" "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}:$DEPENDABOT_UPDATER_VERSION" + docker push "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}:$DEPENDABOT_UPDATER_VERSION" - name: Set summary run: | - echo "updater uploaded with tag \`$TAG\`" >> $GITHUB_STEP_SUMMARY + echo "updater uploaded with tag \`$DEPENDABOT_UPDATER_VERSION\`" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}:$TAG" >> $GITHUB_STEP_SUMMARY + echo "ghcr.io/dependabot/dependabot-updater-${{ matrix.suite.ecosystem }}:$DEPENDABOT_UPDATER_VERSION" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/.rubocop.yml b/.rubocop.yml index c01be5379e..af8a138672 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -335,6 +335,12 @@ Style/SelectByRegexp: Sorbet/TrueSigil: Exclude: - "**/spec/**/*" +Sorbet/StrictSigil: + Exclude: + - "**/spec/**/*" +Sorbet/StrongSigil: + Exclude: + - "**/spec/**/*" # TODO these were temporarily disabled during the Ruby 2.7 -> 3.1 upgrade # in order to keep the upgrade diff small, they will be enabled/fixed in diff --git a/Dockerfile.updater-core b/Dockerfile.updater-core index 2ba9f75882..b643af9f70 100644 --- a/Dockerfile.updater-core +++ b/Dockerfile.updater-core @@ -1,4 +1,6 @@ -FROM ubuntu:22.04 +FROM docker.io/library/ubuntu:22.04 + +ARG TARGETARCH LABEL org.opencontainers.image.source="https://github.com/dependabot/dependabot-core" @@ -52,7 +54,7 @@ COPY --chown=dependabot:dependabot LICENSE $DEPENDABOT_HOME # Install Ruby from official Docker image # When bumping Ruby minor, need to also add the previous version to `bundler/helpers/v{1,2}/monkey_patches/definition_ruby_version_patch.rb` -COPY --from=ruby:3.1.4-bookworm --chown=dependabot:dependabot /usr/local /usr/local +COPY --from=docker.io/library/ruby:3.1.4-bookworm --chown=dependabot:dependabot /usr/local /usr/local # We had to explicitly bump this as the bundled version `0.2.2` in ubuntu 22.04 has a bug. # Once Ubuntu base image pulls in a new enough yaml version, we may not need to @@ -72,7 +74,7 @@ ENV DEPENDABOT=true ENV GIT_LFS_SKIP_SMUDGE=1 # Place a git shim ahead of git on the path to rewrite git arguments to use HTTPS. -ARG SHIM="https://github.com/dependabot/git-shim/releases/download/v1.4.0/git-v1.4.0-linux-amd64.tar.gz" +ARG SHIM="https://github.com/dependabot/git-shim/releases/download/v1.4.0/git-v1.4.0-linux-${TARGETARCH}.tar.gz" RUN curl -sL $SHIM -o git-shim.tar.gz && mkdir -p ~/bin && tar -xvf git-shim.tar.gz -C ~/bin && rm git-shim.tar.gz COPY --chown=dependabot:dependabot omnibus omnibus @@ -126,6 +128,8 @@ RUN gem install bundler -v $BUNDLER_V2_VERSION --no-document && \ ENV PATH="$DEPENDABOT_HOME/bin:$PATH" ENV DEPENDABOT_NATIVE_HELPERS_PATH="/opt" +ENV DEPENDABOT_UPDATER_VERSION=${DEPENDABOT_UPDATER_VERSION:-development} + USER root CMD ["bin/run"] diff --git a/Gemfile.lock b/Gemfile.lock index a38f22e836..aabb4a08ac 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,19 +1,19 @@ PATH remote: bundler specs: - dependabot-bundler (0.242.1) - dependabot-common (= 0.242.1) + dependabot-bundler (0.244.0) + dependabot-common (= 0.244.0) PATH remote: cargo specs: - dependabot-cargo (0.242.1) - dependabot-common (= 0.242.1) + dependabot-cargo (0.244.0) + dependabot-common (= 0.244.0) PATH remote: common specs: - dependabot-common (0.242.1) + dependabot-common (0.244.0) aws-sdk-codecommit (~> 1.28) aws-sdk-ecr (~> 1.5) bundler (>= 1.16, < 3.0.0) @@ -35,107 +35,107 @@ PATH PATH remote: composer specs: - dependabot-composer (0.242.1) - dependabot-common (= 0.242.1) + dependabot-composer (0.244.0) + dependabot-common (= 0.244.0) PATH remote: devcontainers specs: - dependabot-devcontainers (0.242.1) - dependabot-common (= 0.242.1) + dependabot-devcontainers (0.244.0) + dependabot-common (= 0.244.0) PATH remote: docker specs: - dependabot-docker (0.242.1) - dependabot-common (= 0.242.1) + dependabot-docker (0.244.0) + dependabot-common (= 0.244.0) PATH remote: elm specs: - dependabot-elm (0.242.1) - dependabot-common (= 0.242.1) + dependabot-elm (0.244.0) + dependabot-common (= 0.244.0) PATH remote: git_submodules specs: - dependabot-git_submodules (0.242.1) - dependabot-common (= 0.242.1) + dependabot-git_submodules (0.244.0) + dependabot-common (= 0.244.0) parseconfig (~> 1.0, < 1.1.0) PATH remote: github_actions specs: - dependabot-github_actions (0.242.1) - dependabot-common (= 0.242.1) + dependabot-github_actions (0.244.0) + dependabot-common (= 0.244.0) PATH remote: go_modules specs: - dependabot-go_modules (0.242.1) - dependabot-common (= 0.242.1) + dependabot-go_modules (0.244.0) + dependabot-common (= 0.244.0) PATH remote: gradle specs: - dependabot-gradle (0.242.1) - dependabot-common (= 0.242.1) - dependabot-maven (= 0.242.1) + dependabot-gradle (0.244.0) + dependabot-common (= 0.244.0) + dependabot-maven (= 0.244.0) PATH remote: hex specs: - dependabot-hex (0.242.1) - dependabot-common (= 0.242.1) + dependabot-hex (0.244.0) + dependabot-common (= 0.244.0) PATH remote: maven specs: - dependabot-maven (0.242.1) - dependabot-common (= 0.242.1) + dependabot-maven (0.244.0) + dependabot-common (= 0.244.0) PATH remote: npm_and_yarn specs: - dependabot-npm_and_yarn (0.242.1) - dependabot-common (= 0.242.1) + dependabot-npm_and_yarn (0.244.0) + dependabot-common (= 0.244.0) PATH remote: nuget specs: - dependabot-nuget (0.242.1) - dependabot-common (= 0.242.1) + dependabot-nuget (0.244.0) + dependabot-common (= 0.244.0) rubyzip (>= 2.3.2, < 3.0) PATH remote: pub specs: - dependabot-pub (0.242.1) - dependabot-common (= 0.242.1) + dependabot-pub (0.244.0) + dependabot-common (= 0.244.0) PATH remote: python specs: - dependabot-python (0.242.1) - dependabot-common (= 0.242.1) + dependabot-python (0.244.0) + dependabot-common (= 0.244.0) PATH remote: silent specs: - dependabot-silent (0.242.1) - dependabot-common (= 0.242.1) + dependabot-silent (0.244.0) + dependabot-common (= 0.244.0) PATH remote: swift specs: - dependabot-swift (0.242.1) - dependabot-common (= 0.242.1) + dependabot-swift (0.244.0) + dependabot-common (= 0.244.0) PATH remote: terraform specs: - dependabot-terraform (0.242.1) - dependabot-common (= 0.242.1) + dependabot-terraform (0.244.0) + dependabot-common (= 0.244.0) GEM remote: https://rubygems.org/ diff --git a/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb b/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb index de9bf03b6b..35857a9509 100644 --- a/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb +++ b/bundler/helpers/v2/monkey_patches/definition_ruby_version_patch.rb @@ -6,7 +6,14 @@ module BundlerDefinitionRubyVersionPatch def ruby_version super || begin - Bundler::RubyVersion.from_string(File.read(".ruby-version", chomp: true)) + file_content = Bundler.read_file(".ruby-version") + ruby_version = + if /^ruby(-|\s+)([^\s#]+)/ =~ file_content + ::Regexp.last_match(2) + else + file_content.strip + end + Bundler::RubyVersion.new(ruby_version, nil, nil, nil) if ruby_version rescue SystemCallError # .ruby-version doesn't exist, fallback to the Ruby Dependabot runs end diff --git a/bundler/helpers/v2/spec/ruby_version_spec.rb b/bundler/helpers/v2/spec/ruby_version_spec.rb new file mode 100644 index 0000000000..bacf092f03 --- /dev/null +++ b/bundler/helpers/v2/spec/ruby_version_spec.rb @@ -0,0 +1,40 @@ +# typed: false +# frozen_string_literal: true + +require "native_spec_helper" +require "shared_contexts" + +RSpec.describe BundlerDefinitionRubyVersionPatch do + include_context "in a temporary bundler directory" + include_context "stub rubygems compact index" + + let(:project_name) { "ruby_version_implied" } + before do + @ui = Bundler.ui + Bundler.ui = Bundler::UI::Silent.new + end + after { Bundler.ui = @ui } + + it "updates to the most recent version" do + in_tmp_folder do + File.delete(".ruby-version") + definition = Bundler::Definition.build("Gemfile", "Gemfile.lock", gems: ["statesman"]) + definition.resolve_remotely! + specs = definition.resolve["statesman"] + expect(specs.size).to eq(1) + spec = specs.first + expect(spec.version).to eq("7.2.0") + end + end + + it "doesn't update to a version that is not compatible with the Ruby version implied by .ruby-version" do + in_tmp_folder do + definition = Bundler::Definition.build("Gemfile", "Gemfile.lock", gems: ["statesman"]) + definition.resolve_remotely! + specs = definition.resolve["statesman"] + expect(specs.size).to eq(1) + spec = specs.first + expect(spec.version).to eq("2.0.1") + end + end +end diff --git a/bundler/lib/dependabot/bundler/file_fetcher.rb b/bundler/lib/dependabot/bundler/file_fetcher.rb index 8c1a4f8a91..b2daa0b68f 100644 --- a/bundler/lib/dependabot/bundler/file_fetcher.rb +++ b/bundler/lib/dependabot/bundler/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: false +# typed: true # frozen_string_literal: true require "sorbet-runtime" @@ -105,7 +105,7 @@ def ruby_version_file end def path_gemspecs - gemspec_files = [] + gemspec_files = T.let([], T::Array[Dependabot::DependencyFile]) unfetchable_gems = [] path_gemspec_paths.each do |path| @@ -152,6 +152,7 @@ def require_relative_files(files) .tap { |req_files| req_files.each { |f| f.support_file = true } } end + sig { params(dir_path: T.any(String, Pathname)).returns(T::Array[DependencyFile]) } def fetch_gemspecs_from_directory(dir_path) repo_contents(dir: dir_path, fetch_submodules: true) .select { |f| f.name.end_with?(".gemspec", ".specification") } diff --git a/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/.ruby-version b/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/.ruby-version new file mode 100644 index 0000000000..8dbb0f26ba --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/.ruby-version @@ -0,0 +1 @@ +2.1.10 diff --git a/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/Gemfile b/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/Gemfile new file mode 100644 index 0000000000..3a258a2ac6 --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/Gemfile @@ -0,0 +1,4 @@ +source "https://rubygems.org" + +gem "business" +gem "statesman" diff --git a/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/Gemfile.lock b/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/Gemfile.lock new file mode 100644 index 0000000000..5bc9e7117a --- /dev/null +++ b/bundler/spec/fixtures/projects/bundler2/ruby_version_implied/Gemfile.lock @@ -0,0 +1,15 @@ +GEM + remote: https://rubygems.org/ + specs: + business (1.12.0) + statesman (2.0.1) + +PLATFORMS + ruby + +DEPENDENCIES + business + statesman + +BUNDLED WITH + 2.5.3 diff --git a/cargo/Dockerfile b/cargo/Dockerfile index 62f43c0599..8cb768d4eb 100644 --- a/cargo/Dockerfile +++ b/cargo/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:1.75.0-bookworm as rust +FROM docker.io/library/rust:1.75.0-bookworm as rust FROM ghcr.io/dependabot/dependabot-updater-core diff --git a/common/lib/dependabot.rb b/common/lib/dependabot.rb index 29fad7bbc9..3cb3b68ed0 100644 --- a/common/lib/dependabot.rb +++ b/common/lib/dependabot.rb @@ -2,5 +2,5 @@ # frozen_string_literal: true module Dependabot - VERSION = "0.242.1" + VERSION = "0.244.0" end diff --git a/common/lib/dependabot/clients/azure.rb b/common/lib/dependabot/clients/azure.rb index f00813ff86..14ea75b861 100644 --- a/common/lib/dependabot/clients/azure.rb +++ b/common/lib/dependabot/clients/azure.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "dependabot/shared_helpers" @@ -7,6 +7,7 @@ module Dependabot module Clients + # rubocop:disable Metrics/ClassLength class Azure extend T::Sig @@ -24,12 +25,16 @@ class Forbidden < StandardError; end class TagsCreationForbidden < StandardError; end - RETRYABLE_ERRORS = [InternalServerError, BadGateway, ServiceNotAvailable].freeze + RETRYABLE_ERRORS = T.let( + [InternalServerError, BadGateway, ServiceNotAvailable].freeze, + T::Array[T.class_of(StandardError)] + ) ####################### # Constructor methods # ####################### + sig { params(source: Dependabot::Source, credentials: T::Array[Dependabot::Credential]).returns(Azure) } def self.for_source(source:, credentials:) credential = credentials @@ -43,15 +48,24 @@ def self.for_source(source:, credentials:) # Client # ########## + sig do + params( + source: Dependabot::Source, + credentials: T.nilable(Dependabot::Credential), + max_retries: T.nilable(Integer) + ) + .void + end def initialize(source, credentials, max_retries: 3) @source = source @credentials = credentials - @auth_header = auth_header_for(credentials&.fetch("token", nil)) - @max_retries = max_retries || 3 + @auth_header = T.let(auth_header_for(credentials&.fetch("token", nil)), T::Hash[String, String]) + @max_retries = T.let(max_retries || 3, Integer) end + sig { params(_repo: T.nilable(String), branch: String).returns(String) } def fetch_commit(_repo, branch) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/stats/branches?name=" + branch) @@ -61,18 +75,26 @@ def fetch_commit(_repo, branch) JSON.parse(response.body).fetch("commit").fetch("commitId") end + sig { params(_repo: String).returns(String) } def fetch_default_branch(_repo) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo) JSON.parse(response.body).fetch("defaultBranch").gsub("refs/heads/", "") end + sig do + params( + commit: T.nilable(String), + path: T.nilable(String) + ) + .returns(T::Array[T::Hash[String, T.untyped]]) + end def fetch_repo_contents(commit = nil, path = nil) tree = fetch_repo_contents_treeroot(commit, path) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/trees/" + tree + "?recursive=false") @@ -80,18 +102,19 @@ def fetch_repo_contents(commit = nil, path = nil) JSON.parse(response.body).fetch("treeEntries") end + sig { params(commit: T.nilable(String), path: T.nilable(String)).returns(String) } def fetch_repo_contents_treeroot(commit = nil, path = nil) actual_path = path actual_path = "/" if path.to_s.empty? - tree_url = source.api_endpoint + + tree_url = T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + - "/items?path=" + actual_path + "/items?path=" + T.must(actual_path) unless commit.to_s.empty? tree_url += "&versionDescriptor.versionType=commit" \ - "&versionDescriptor.version=" + commit + "&versionDescriptor.version=" + T.must(commit) end tree_response = get(tree_url) @@ -99,8 +122,9 @@ def fetch_repo_contents_treeroot(commit = nil, path = nil) JSON.parse(tree_response.body).fetch("objectId") end + sig { params(commit: String, path: String).returns(String) } def fetch_file_contents(commit, path) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/items?path=" + path + @@ -110,21 +134,23 @@ def fetch_file_contents(commit, path) response.body end + sig { params(branch_name: T.nilable(String)).returns(T::Array[T::Hash[String, T.untyped]]) } def commits(branch_name = nil) - commits_url = source.api_endpoint + + commits_url = T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/commits" - commits_url += "?searchCriteria.itemVersion.version=" + branch_name unless branch_name.to_s.empty? + commits_url += "?searchCriteria.itemVersion.version=" + T.must(branch_name) unless branch_name.to_s.empty? response = get(commits_url) JSON.parse(response.body).fetch("value") end + sig { params(branch_name: String).returns(T.nilable(T::Hash[String, T.untyped])) } def branch(branch_name) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/refs?filter=heads/" + branch_name) @@ -132,8 +158,9 @@ def branch(branch_name) JSON.parse(response.body).fetch("value").first end + sig { params(source_branch: String, target_branch: String).returns(T::Array[T::Hash[String, T.untyped]]) } def pull_requests(source_branch, target_branch) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pullrequests?searchCriteria.status=all" \ @@ -143,6 +170,16 @@ def pull_requests(source_branch, target_branch) JSON.parse(response.body).fetch("value") end + sig do + params( + branch_name: String, + base_commit: String, + commit_message: String, + files: T::Array[Dependabot::DependencyFile], + author_details: T.nilable(T::Hash[String, String]) + ) + .returns(T.untyped) + end def create_commit(branch_name, base_commit, commit_message, files, author_details) content = { @@ -158,7 +195,7 @@ def create_commit(branch_name, base_commit, commit_message, files, changeType: "edit", item: { path: file.path }, newContent: { - content: Base64.encode64(file.content), + content: Base64.encode64(T.must(file.content)), contentType: "base64encoded" } } @@ -167,12 +204,25 @@ def create_commit(branch_name, base_commit, commit_message, files, ] } - post(source.api_endpoint + source.organization + "/" + source.project + + post(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pushes?api-version=5.0", content.to_json) end # rubocop:disable Metrics/ParameterLists + sig do + params( + pr_name: String, + source_branch: String, + target_branch: String, + pr_description: String, + labels: T::Array[String], + reviewers: T.nilable(T::Array[String]), + assignees: T.nilable(T::Array[String]), + work_item: T.nilable(Integer) + ) + .returns(T.untyped) + end def create_pull_request(pr_name, source_branch, target_branch, pr_description, labels, reviewers = nil, assignees = nil, work_item = nil) @@ -187,12 +237,25 @@ def create_pull_request(pr_name, source_branch, target_branch, workItemRefs: [{ id: work_item }] } - post(source.api_endpoint + + post(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pullrequests?api-version=5.0", content.to_json) end + sig do + params( + pull_request_id: Integer, + auto_complete_set_by: String, + merge_commit_message: String, + delete_source_branch: T::Boolean, + squash_merge: T::Boolean, + merge_strategy: String, + trans_work_items: T::Boolean, + ignore_config_ids: T::Array[String] + ) + .returns(T.untyped) + end def autocomplete_pull_request(pull_request_id, auto_complete_set_by, merge_commit_message, delete_source_branch = true, squash_merge = true, merge_strategy = "squash", trans_work_items = true, ignore_config_ids = []) @@ -211,7 +274,7 @@ def autocomplete_pull_request(pull_request_id, auto_complete_set_by, merge_commi } } - response = patch(source.api_endpoint + + response = patch(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/pullrequests/" + pull_request_id.to_s + "?api-version=5.1", content.to_json) @@ -219,14 +282,16 @@ def autocomplete_pull_request(pull_request_id, auto_complete_set_by, merge_commi JSON.parse(response.body) end + sig { params(pull_request_id: String).returns(T::Hash[String, T.untyped]) } def pull_request(pull_request_id) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/pullrequests/" + pull_request_id) JSON.parse(response.body) end + sig { params(branch_name: String, old_commit: String, new_commit: String).returns(T::Hash[String, T.untyped]) } def update_ref(branch_name, old_commit, new_commit) content = [ { @@ -236,7 +301,7 @@ def update_ref(branch_name, old_commit, new_commit) } ] - response = post(source.api_endpoint + source.organization + "/" + source.project + + response = post(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/refs?api-version=5.0", content.to_json) @@ -244,8 +309,15 @@ def update_ref(branch_name, old_commit, new_commit) end # rubocop:enable Metrics/ParameterLists + sig do + params( + previous_tag: T.nilable(String), new_tag: T.nilable(String), + type: String + ) + .returns(T::Array[T::Hash[String, T.untyped]]) + end def compare(previous_tag, new_tag, type) - response = get(source.api_endpoint + + response = get(T.must(source.api_endpoint) + source.organization + "/" + source.project + "/_apis/git/repositories/" + source.unscoped_repo + "/commits?searchCriteria.itemVersion.versionType=#{type}" \ @@ -311,7 +383,7 @@ def post(url, json) # rubocop:disable Metrics/PerceivedComplexity raise Unauthorized if response&.status == 401 if response&.status == 403 - raise TagsCreationForbidden if tags_creation_forbidden?(response) + raise TagsCreationForbidden if tags_creation_forbidden?(T.must(response)) raise Forbidden end @@ -354,7 +426,8 @@ def patch(url, json) private - def retry_connection_failures + sig { params(blk: T.proc.void).void } + def retry_connection_failures(&blk) # rubocop:disable Lint/UnusedMethodArgument retry_attempt = 0 begin @@ -365,6 +438,7 @@ def retry_connection_failures end end + sig { params(token: T.nilable(String)).returns(T::Hash[String, String]) } def auth_header_for(token) return {} unless token @@ -379,6 +453,7 @@ def auth_header_for(token) end end + sig { params(response: Excon::Response).returns(T::Boolean) } def tags_creation_forbidden?(response) return false if response.body.empty? @@ -386,6 +461,13 @@ def tags_creation_forbidden?(response) message&.include?("TF401289") end + sig do + params( + reviewers: T.nilable(T::Array[String]), + assignees: T.nilable(T::Array[String]) + ) + .returns(T::Array[T::Hash[Symbol, T.untyped]]) + end def pr_reviewers(reviewers, assignees) return [] unless reviewers || assignees @@ -393,9 +475,15 @@ def pr_reviewers(reviewers, assignees) pr_reviewers + (assignees&.map { |r_id| { id: r_id, isRequired: false } } || []) end + sig { returns(T::Hash[String, String]) } attr_reader :auth_header + + sig { returns(T.nilable(Dependabot::Credential)) } attr_reader :credentials + + sig { returns(Dependabot::Source) } attr_reader :source end + # rubocop:enable Metrics/ClassLength end end diff --git a/common/lib/dependabot/credential.rb b/common/lib/dependabot/credential.rb index 2fabfc7038..4398acb5dd 100644 --- a/common/lib/dependabot/credential.rb +++ b/common/lib/dependabot/credential.rb @@ -2,13 +2,14 @@ # frozen_string_literal: true require "sorbet-runtime" +require "forwardable" module Dependabot class Credential extend T::Sig extend Forwardable - def_delegators :@credential, :fetch, :keys, :[]=, :delete + def_delegators :@credential, :fetch, :keys, :[]=, :delete, :slice, :values, :entries sig { params(credential: T::Hash[String, T.any(T::Boolean, String)]).void } def initialize(credential) @@ -26,5 +27,15 @@ def replaces_base? def [](key) @credential[key] end + + sig { params(other: Credential).returns(Credential) } + def merge(other) + Credential.new(@credential.merge(other.to_h)) + end + + sig { returns(T::Hash[String, String]) } + def to_h + @credential + end end end diff --git a/common/lib/dependabot/dependency_group.rb b/common/lib/dependabot/dependency_group.rb index c89c752c08..008e98fc2a 100644 --- a/common/lib/dependabot/dependency_group.rb +++ b/common/lib/dependabot/dependency_group.rb @@ -22,15 +22,21 @@ class DependencyGroup sig { returns(T::Array[Dependabot::Dependency]) } attr_reader :dependencies + sig { returns(String) } + attr_reader :applies_to + sig do params( name: String, - rules: T::Hash[String, T.untyped] + rules: T::Hash[String, T.untyped], + applies_to: T.nilable(String) ) .void end - def initialize(name:, rules:) + def initialize(name:, rules:, applies_to: "version-updates") @name = name + # For backwards compatibility, if no applies_to is provided, default to "version-updates" + @applies_to = T.let(applies_to || "version-updates", String) @rules = rules @dependencies = T.let([], T::Array[Dependabot::Dependency]) end diff --git a/common/lib/dependabot/file_parsers/base.rb b/common/lib/dependabot/file_parsers/base.rb index a8960f0c19..3251027ec5 100644 --- a/common/lib/dependabot/file_parsers/base.rb +++ b/common/lib/dependabot/file_parsers/base.rb @@ -50,7 +50,7 @@ def initialize(dependency_files:, source:, repo_contents_path: nil, check_required_files end - sig { abstract.returns(Dependabot::DependencyFile) } + sig { abstract.returns(T::Array[Dependabot::Dependency]) } def parse; end private diff --git a/common/lib/dependabot/file_updaters/base.rb b/common/lib/dependabot/file_updaters/base.rb index a7c59c1240..b984cdfcb9 100644 --- a/common/lib/dependabot/file_updaters/base.rb +++ b/common/lib/dependabot/file_updaters/base.rb @@ -26,7 +26,7 @@ class Base sig { returns(T::Hash[Symbol, T.untyped]) } attr_reader :options - sig { overridable.returns(String) } + sig { overridable.returns(T::Array[Regexp]) } def self.updated_files_regex raise NotImplementedError end diff --git a/common/lib/dependabot/metadata_finders/base.rb b/common/lib/dependabot/metadata_finders/base.rb index 6867ddf8b1..f2311ff954 100644 --- a/common/lib/dependabot/metadata_finders/base.rb +++ b/common/lib/dependabot/metadata_finders/base.rb @@ -176,7 +176,7 @@ def source @source = T.let(look_up_source, T.nilable(Dependabot::Source)) end - sig { overridable.returns(Dependabot::Source) } + sig { overridable.returns(T.nilable(Dependabot::Source)) } def look_up_source raise NotImplementedError end diff --git a/common/lib/dependabot/metadata_finders/base/changelog_finder.rb b/common/lib/dependabot/metadata_finders/base/changelog_finder.rb index 9277ce967f..9b9d2ff418 100644 --- a/common/lib/dependabot/metadata_finders/base/changelog_finder.rb +++ b/common/lib/dependabot/metadata_finders/base/changelog_finder.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "excon" @@ -9,9 +9,11 @@ require "dependabot/clients/bitbucket_with_retries" require "dependabot/shared_helpers" require "dependabot/metadata_finders/base" + module Dependabot module MetadataFinders class Base + # rubocop:disable Metrics/ClassLength class ChangelogFinder extend T::Sig @@ -19,24 +21,49 @@ class ChangelogFinder require_relative "commits_finder" # Earlier entries are preferred - CHANGELOG_NAMES = %w( - changelog news changes history release whatsnew releases - ).freeze + CHANGELOG_NAMES = T.let( + %w(changelog news changes history release whatsnew releases).freeze, + T::Array[String] + ) + + sig { returns(T.nilable(Dependabot::Source)) } + attr_reader :source + + sig { returns(Dependabot::Dependency) } + attr_reader :dependency + + sig { returns(T::Array[Dependabot::Credential]) } + attr_reader :credentials - attr_reader :source, :dependency, :credentials, :suggested_changelog_url + sig { returns(T.nilable(String)) } + attr_reader :suggested_changelog_url + sig do + params( + source: T.nilable(Dependabot::Source), + dependency: Dependabot::Dependency, + credentials: T::Array[Dependabot::Credential], + suggested_changelog_url: T.nilable(String) + ) + .void + end def initialize(source:, dependency:, credentials:, suggested_changelog_url: nil) @source = source @dependency = dependency @credentials = credentials @suggested_changelog_url = suggested_changelog_url + + @new_version = T.let(nil, T.nilable(String)) + @changelog_from_suggested_url = T.let(nil, T.untyped) end + sig { returns(T.nilable(String)) } def changelog_url changelog&.html_url end + sig { returns(T.nilable(String)) } def changelog_text return unless full_changelog_text @@ -46,19 +73,25 @@ def changelog_text ).pruned_text end + sig { returns(T.nilable(String)) } def upgrade_guide_url upgrade_guide&.html_url end + sig { returns(T.nilable(String)) } def upgrade_guide_text return unless upgrade_guide - @upgrade_guide_text ||= fetch_file_text(upgrade_guide) + @upgrade_guide_text ||= T.let( + fetch_file_text(upgrade_guide), + T.nilable(String) + ) end private # rubocop:disable Metrics/PerceivedComplexity + sig { returns(T.untyped) } def changelog return unless changelog_from_suggested_url || source return if git_source? && !ref_changed? @@ -66,13 +99,13 @@ def changelog # If there is a changelog, and it includes the new version, return it if new_version && default_branch_changelog && - fetch_file_text(default_branch_changelog)&.include?(new_version) + fetch_file_text(default_branch_changelog)&.include?(T.must(new_version)) return default_branch_changelog end # Otherwise, look for a changelog at the tag for this version if new_version && relevant_tag_changelog && - fetch_file_text(relevant_tag_changelog)&.include?(new_version) + fetch_file_text(relevant_tag_changelog)&.include?(T.must(new_version)) return relevant_tag_changelog end @@ -81,8 +114,9 @@ def changelog end # rubocop:enable Metrics/PerceivedComplexity + sig { returns(T.nilable(Sawyer::Resource)) } def changelog_from_suggested_url - return @changelog_from_suggested_url if defined?(@changelog_from_suggested_url) + return @changelog_from_suggested_url unless @changelog_from_suggested_url.nil? return unless suggested_changelog_url # TODO: Support other providers @@ -90,29 +124,40 @@ def changelog_from_suggested_url return unless suggested_source&.provider == "github" opts = { path: suggested_source&.directory, ref: suggested_source&.branch }.compact - suggested_source_client = github_client_for_source(suggested_source) - tmp_files = suggested_source_client.contents(suggested_source&.repo, opts) + suggested_source_client = github_client_for_source(T.must(suggested_source)) + tmp_files = T.unsafe(suggested_source_client).contents(suggested_source&.repo, opts) - filename = suggested_changelog_url.split("/").last.split("#").first + filename = T.must(T.must(suggested_changelog_url).split("/").last).split("#").first @changelog_from_suggested_url = tmp_files.find { |f| f.name == filename } rescue Octokit::NotFound, Octokit::UnavailableForLegalReasons @changelog_from_suggested_url = nil end + sig { returns(T.nilable(T.any(OpenStruct, Sawyer::Resource))) } def default_branch_changelog return unless source - @default_branch_changelog ||= changelog_from_ref(nil) + @default_branch_changelog ||= + T.let( + changelog_from_ref(nil), + T.nilable(T.any(OpenStruct, Sawyer::Resource)) + ) end + sig { returns(T.nilable(T.any(OpenStruct, Sawyer::Resource))) } def relevant_tag_changelog return unless source return unless tag_for_new_version - @relevant_tag_changelog ||= changelog_from_ref(tag_for_new_version) + @relevant_tag_changelog ||= + T.let( + changelog_from_ref(tag_for_new_version), + T.nilable(T.any(OpenStruct, Sawyer::Resource)) + ) end + sig { params(ref: T.nilable(String)).returns(T.nilable(T.any(OpenStruct, Sawyer::Resource))) } def changelog_from_ref(ref) files = dependency_file_list(ref) @@ -125,6 +170,7 @@ def changelog_from_ref(ref) end # rubocop:disable Metrics/PerceivedComplexity + sig { params(files: T::Array[T.untyped]).returns(T.untyped) } def select_best_changelog(files) CHANGELOG_NAMES.each do |name| candidates = files.select { |f| f.name =~ /#{name}/i } @@ -150,15 +196,20 @@ def select_best_changelog(files) end # rubocop:enable Metrics/PerceivedComplexity + sig { returns(T.nilable(String)) } def tag_for_new_version @tag_for_new_version ||= - CommitsFinder.new( - dependency: dependency, - source: source, - credentials: credentials - ).new_tag + T.let( + CommitsFinder.new( + dependency: dependency, + source: source, + credentials: credentials + ).new_tag, + T.nilable(String) + ) end + sig { returns(T.nilable(String)) } def full_changelog_text return unless changelog @@ -167,7 +218,7 @@ def full_changelog_text sig { params(file: T.untyped).returns(T.nilable(String)) } def fetch_file_text(file) - @file_text ||= {} + @file_text ||= T.let({}, T.nilable(T::Hash[String, T.untyped])) unless @file_text.key?(file.download_url) file_source = T.must(Source.from_url(file.html_url)) @@ -187,12 +238,14 @@ def fetch_file_text(file) @file_text[file.download_url].rstrip end + sig { params(file_source: Dependabot::Source, file: T.untyped).returns(String) } def fetch_github_file(file_source, file) # Hitting the download URL directly causes encoding problems - raw_content = github_client_for_source(file_source).get(file.url).content + raw_content = T.unsafe(github_client_for_source(file_source)).get(file.url).content Base64.decode64(raw_content).force_encoding("UTF-8").encode end + sig { params(file: T.untyped).returns(String) } def fetch_gitlab_file(file) Excon.get( file.download_url, @@ -201,16 +254,19 @@ def fetch_gitlab_file(file) ).body.force_encoding("UTF-8").encode end + sig { params(file: T.untyped).returns(String) } def fetch_bitbucket_file(file) - bitbucket_client.get(file.download_url).body - .force_encoding("UTF-8").encode + T.unsafe(bitbucket_client).get(file.download_url).body + .force_encoding("UTF-8").encode end + sig { params(file: T.untyped).returns(String) } def fetch_azure_file(file) azure_client.get(file.download_url).body .force_encoding("UTF-8").encode end + sig { returns(T.untyped) } def upgrade_guide return unless source @@ -225,49 +281,55 @@ def upgrade_guide .max_by(&:size) end + sig { params(ref: T.nilable(String)).returns(T.untyped) } def dependency_file_list(ref = nil) - @dependency_file_list ||= {} + @dependency_file_list ||= T.let({}, T.nilable(T::Hash[T.nilable(String), T.untyped])) @dependency_file_list[ref] ||= fetch_dependency_file_list(ref) end + sig { params(ref: T.nilable(String)).returns(T::Array[T.untyped,]) } def fetch_dependency_file_list(ref) - case source.provider + case T.must(source).provider when "github" then fetch_github_file_list(ref) when "bitbucket" then fetch_bitbucket_file_list when "gitlab" then fetch_gitlab_file_list when "azure" then fetch_azure_file_list when "codecommit" then [] # TODO: Fetch Files from Codecommit - else raise "Unexpected repo provider '#{source.provider}'" + else raise "Unexpected repo provider '#{T.must(source).provider}'" end end + # rubocop:disable Metrics/AbcSize + sig { params(ref: T.nilable(String)).returns(T::Array[T.untyped]) } def fetch_github_file_list(ref) files = [] - if source.directory - opts = { path: source.directory, ref: ref }.compact - tmp_files = github_client.contents(source.repo, opts) + if T.must(source).directory + opts = { path: T.must(source).directory, ref: ref }.compact + tmp_files = T.unsafe(github_client).contents(T.must(source).repo, opts) files += tmp_files if tmp_files.is_a?(Array) end opts = { ref: ref }.compact - files += github_client.contents(source.repo, opts) + files += T.unsafe(github_client).contents(T.must(source).repo, opts) files.uniq.each do |f| next unless f.type == "dir" && f.name.match?(/docs?/o) opts = { path: f.path, ref: ref }.compact - files += github_client.contents(source.repo, opts) + files += T.unsafe(github_client).contents(T.must(source).repo, opts) end files rescue Octokit::NotFound, Octokit::UnavailableForLegalReasons [] end + # rubocop:enable Metrics/AbcSize + sig { returns(T.untyped) } def fetch_bitbucket_file_list branch = default_bitbucket_branch - bitbucket_client.fetch_repo_contents(source.repo).map do |file| + T.unsafe(bitbucket_client).fetch_repo_contents(T.must(source).repo).map do |file| type = case file.fetch("type") when "commit_file" then "file" when "commit_directory" then "dir" @@ -277,8 +339,8 @@ def fetch_bitbucket_file_list name: file.fetch("path").split("/").last, type: type, size: file.fetch("size", 100), - html_url: "#{source.url}/src/#{branch}/#{file['path']}", - download_url: "#{source.url}/raw/#{branch}/#{file['path']}" + html_url: "#{T.must(source).url}/src/#{branch}/#{file['path']}", + download_url: "#{T.must(source).url}/raw/#{branch}/#{file['path']}" ) end rescue Dependabot::Clients::Bitbucket::NotFound, @@ -287,9 +349,10 @@ def fetch_bitbucket_file_list [] end + sig { returns(T.untyped) } def fetch_gitlab_file_list branch = default_gitlab_branch - gitlab_client.repo_tree(source.repo).map do |file| + T.unsafe(gitlab_client).repo_tree(T.must(source).repo).map do |file| type = case file.type when "blob" then "file" when "tree" then "dir" @@ -299,14 +362,15 @@ def fetch_gitlab_file_list name: file.name, type: type, size: 100, # GitLab doesn't return file size - html_url: "#{source.url}/blob/#{branch}/#{file.path}", - download_url: "#{source.url}/raw/#{branch}/#{file.path}" + html_url: "#{T.must(source).url}/blob/#{branch}/#{file.path}", + download_url: "#{T.must(source).url}/raw/#{branch}/#{file.path}" ) end rescue Gitlab::Error::NotFound [] end + sig { returns(T.untyped) } def fetch_azure_file_list azure_client.fetch_repo_contents.map do |entry| type = case entry.fetch("gitObjectType") @@ -320,7 +384,7 @@ def fetch_azure_file_list type: type, size: entry.fetch("size"), path: entry.fetch("relativePath"), - html_url: "#{source.url}?path=/#{entry.fetch('relativePath')}", + html_url: "#{T.must(source).url}?path=/#{entry.fetch('relativePath')}", download_url: entry.fetch("url") ) end @@ -330,20 +394,23 @@ def fetch_azure_file_list [] end + sig { returns(T.nilable(String)) } def new_version - return @new_version if defined?(@new_version) + return @new_version unless @new_version.nil? new_version = git_source? && new_ref ? new_ref : dependency.version @new_version = new_version&.gsub(/^v/, "") end + sig { returns(T.nilable(String)) } def previous_ref - previous_refs = dependency.previous_requirements.filter_map do |r| + previous_refs = dependency.previous_requirements&.filter_map do |r| r.dig(:source, "ref") || r.dig(:source, :ref) - end.uniq - previous_refs.first if previous_refs.count == 1 + end&.uniq + previous_refs&.first if previous_refs&.count == 1 end + sig { returns(T.nilable(String)) } def new_ref new_refs = dependency.requirements.filter_map do |r| r.dig(:source, "ref") || r.dig(:source, :ref) @@ -351,12 +418,14 @@ def new_ref new_refs.first if new_refs.count == 1 end + sig { returns(T::Boolean) } def ref_changed? # We could go from multiple previous refs (nil) to a single new ref previous_ref != new_ref end # TODO: Refactor me so that Composer doesn't need to be special cased + sig { returns(T::Boolean) } def git_source? # Special case Composer, which uses git as a source but handles tags # internally @@ -369,51 +438,77 @@ def git_source? sources.all? { |s| s[:type] == "git" || s["type"] == "git" } end + sig { returns(T::Boolean) } def major_version_upgrade? return false unless dependency.version&.match?(/^\d/) return false unless dependency.previous_version&.match?(/^\d/) - dependency.version.split(".").first.to_i - - dependency.previous_version.split(".").first.to_i >= 1 + T.must(dependency.version).split(".").first.to_i - + T.must(dependency.previous_version).split(".").first.to_i >= 1 end + sig { returns(Dependabot::Clients::GitlabWithRetries) } def gitlab_client - @gitlab_client ||= Dependabot::Clients::GitlabWithRetries - .for_gitlab_dot_com(credentials: credentials) + @gitlab_client ||= + T.let( + Dependabot::Clients::GitlabWithRetries.for_gitlab_dot_com(credentials: credentials), + T.nilable(Dependabot::Clients::GitlabWithRetries) + ) end + sig { returns(Dependabot::Clients::GithubWithRetries) } def github_client - @github_client ||= Dependabot::Clients::GithubWithRetries - .for_source(source: source, credentials: credentials) + @github_client ||= + T.let( + Dependabot::Clients::GithubWithRetries.for_source(source: source, credentials: credentials), + T.nilable(Dependabot::Clients::GithubWithRetries) + ) end + sig { returns(Dependabot::Clients::Azure) } def azure_client - @azure_client ||= Dependabot::Clients::Azure - .for_source(source: source, credentials: credentials) + @azure_client ||= + T.let( + Dependabot::Clients::Azure.for_source(source: T.must(source), credentials: credentials), + T.nilable(Dependabot::Clients::Azure) + ) end + sig { params(client_source: Dependabot::Source).returns(Dependabot::Clients::GithubWithRetries) } def github_client_for_source(client_source) return github_client if client_source == source - Dependabot::Clients::GithubWithRetries - .for_source(source: client_source, credentials: credentials) + Dependabot::Clients::GithubWithRetries.for_source(source: client_source, credentials: credentials) end + sig { returns(Dependabot::Clients::BitbucketWithRetries) } def bitbucket_client - @bitbucket_client ||= Dependabot::Clients::BitbucketWithRetries - .for_bitbucket_dot_org(credentials: credentials) + @bitbucket_client ||= + T.let( + Dependabot::Clients::BitbucketWithRetries.for_bitbucket_dot_org(credentials: credentials), + T.nilable(Dependabot::Clients::BitbucketWithRetries) + ) end + sig { returns(String) } def default_bitbucket_branch @default_bitbucket_branch ||= - bitbucket_client.fetch_default_branch(source.repo) + T.let( + T.unsafe(bitbucket_client).fetch_default_branch(T.must(source).repo), + T.nilable(String) + ) end + sig { returns(String) } def default_gitlab_branch @default_gitlab_branch ||= - gitlab_client.fetch_default_branch(source.repo) + T.let( + gitlab_client.fetch_default_branch(T.must(source).repo), + T.nilable(String) + ) end end + # rubocop:enable Metrics/ClassLength end end end diff --git a/common/lib/dependabot/metadata_finders/base/commits_finder.rb b/common/lib/dependabot/metadata_finders/base/commits_finder.rb index 97b2c59d6d..1b66fd37c2 100644 --- a/common/lib/dependabot/metadata_finders/base/commits_finder.rb +++ b/common/lib/dependabot/metadata_finders/base/commits_finder.rb @@ -382,7 +382,7 @@ def github_client def azure_client @azure_client ||= T.let( - Dependabot::Clients::Azure.for_source(source: source, credentials: credentials), + Dependabot::Clients::Azure.for_source(source: T.must(source), credentials: credentials), T.nilable(Dependabot::Clients::Azure) ) end diff --git a/common/lib/dependabot/pull_request_creator.rb b/common/lib/dependabot/pull_request_creator.rb index f24bcc8ec5..94b6c27c05 100644 --- a/common/lib/dependabot/pull_request_creator.rb +++ b/common/lib/dependabot/pull_request_creator.rb @@ -295,7 +295,7 @@ def gitlab_creator approvers: reviewers, assignees: assignees, milestone: milestone, - target_project_id: provider_metadata&.fetch(:target_project_id) + target_project_id: provider_metadata&.fetch(:target_project_id, nil) ) end diff --git a/common/lib/dependabot/pull_request_creator/azure.rb b/common/lib/dependabot/pull_request_creator/azure.rb index bbf2a52783..07b49c564b 100644 --- a/common/lib/dependabot/pull_request_creator/azure.rb +++ b/common/lib/dependabot/pull_request_creator/azure.rb @@ -56,7 +56,7 @@ def azure_client_for_source def branch_exists? azure_client_for_source.branch(branch_name) - rescue ::Azure::Error::NotFound + rescue ::Dependabot::Clients::Azure::NotFound false end diff --git a/common/lib/dependabot/shared_helpers.rb b/common/lib/dependabot/shared_helpers.rb index 02139fcebf..fb4cd64c59 100644 --- a/common/lib/dependabot/shared_helpers.rb +++ b/common/lib/dependabot/shared_helpers.rb @@ -405,7 +405,6 @@ def self.run_shell_command(command, stderr_to_stdout: true) start = Time.now cmd = allow_unsafe_shell_command ? command : escape_command(command) - if stderr_to_stdout stdout, process = Open3.capture2e(env || {}, cmd) else @@ -425,12 +424,31 @@ def self.run_shell_command(command, process_exit_value: process.to_s } + check_out_of_disk_memory_error(stderr, error_context) + raise SharedHelpers::HelperSubprocessFailed.new( message: stderr_to_stdout ? stdout : "#{stderr}\n#{stdout}", error_context: error_context ) end + sig { params(stderr: T.nilable(String), error_context: T::Hash[Symbol, String]).void } + def self.check_out_of_disk_memory_error(stderr, error_context) + if stderr&.include?("No space left on device") || stderr&.include?("Out of diskspace") + raise HelperSubprocessFailed.new( + message: "No space left on device", + error_class: "Dependabot::OutOfDisk", + error_context: error_context + ) + elsif stderr&.include?("MemoryError") + raise HelperSubprocessFailed.new( + message: "MemoryError", + error_class: "Dependabot::OutOfMemory", + error_context: error_context + ) + end + end + sig { params(command: String, stdin_data: String, env: T.nilable(T::Hash[String, String])).returns(String) } def self.helper_subprocess_bash_command(command:, stdin_data:, env:) escaped_stdin_data = stdin_data.gsub("\"", "\\\"") diff --git a/common/lib/dependabot/workspace/git.rb b/common/lib/dependabot/workspace/git.rb index f58a55484b..bd9411bf83 100644 --- a/common/lib/dependabot/workspace/git.rb +++ b/common/lib/dependabot/workspace/git.rb @@ -26,7 +26,7 @@ def initialize(path) sig { returns(T::Boolean) } def changed? - changes.any? || !changed_files.empty? + changes.any? || !changed_files(ignored_mode: "no").empty? end sig { override.returns(String) } @@ -53,7 +53,7 @@ def reset! .returns(T.nilable(T::Array[Dependabot::Workspace::ChangeAttempt])) end def store_change(memo = nil) - return nil if changed_files.empty? + return nil if changed_files(ignored_mode: "no").empty? debug("store_change - before: #{current_commit}") sha, diff = commit(memo) diff --git a/common/spec/dependabot/clients/azure_spec.rb b/common/spec/dependabot/clients/azure_spec.rb index da24bd1ee2..c5b6e0ba16 100644 --- a/common/spec/dependabot/clients/azure_spec.rb +++ b/common/spec/dependabot/clients/azure_spec.rb @@ -14,7 +14,7 @@ it "Using #{credential['token_type']} token in credentials" do client = described_class.for_source( source: source, - credentials: credential["credentials"] + credentials: [credential["credentials"]] ) response = JSON.parse(client.get(base_url).body) expect(response["result"]).to eq("Success") @@ -25,12 +25,12 @@ let(:username) { "username" } let(:password) { "password" } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "dev.azure.com", "username" => username, "password" => password - }] + })] end let(:branch) { "master" } let(:base_url) { "https://dev.azure.com/org/gocardless" } @@ -378,37 +378,37 @@ basic_non_encoded_token_data = { "token_type" => "basic non encoded", - "credentials" => [ + "credentials" => Dependabot::Credential.new( { "type" => "git_source", "host" => "dev.azure.com", "token" => token } - ], + ), "headers" => { "Authorization" => "Basic #{encoded_token}" } } basic_encoded_token_data = { "token_type" => "basic encoded", - "credentials" => [ + "credentials" => Dependabot::Credential.new( { "type" => "git_source", "host" => "dev.azure.com", "token" => encoded_token.to_s } - ], + ), "headers" => { "Authorization" => "Basic #{encoded_token}" } } bearer_token_data = { "token_type" => "bearer", - "credentials" => [ + "credentials" => Dependabot::Credential.new( { "type" => "git_source", "host" => "dev.azure.com", "token" => bearer_token } - ], + ), "headers" => { "Authorization" => "Bearer #{bearer_token}" } } diff --git a/common/spec/dependabot/metadata_finders/base/changelog_finder_spec.rb b/common/spec/dependabot/metadata_finders/base/changelog_finder_spec.rb index 38453db920..f67af07d02 100644 --- a/common/spec/dependabot/metadata_finders/base/changelog_finder_spec.rb +++ b/common/spec/dependabot/metadata_finders/base/changelog_finder_spec.rb @@ -4,6 +4,7 @@ require "octokit" require "gitlab" require "spec_helper" +require "dependabot/credential" require "dependabot/dependency" require "dependabot/source" require "dependabot/metadata_finders/base/changelog_finder" @@ -588,17 +589,20 @@ context "with credentials" do let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "git_source", - "host" => "dev.azure.com", - "username" => "greysteil", - "password" => "secret_token" - }] + [ + Dependabot::Credential.new({ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }), + Dependabot::Credential.new({ + "type" => "git_source", + "host" => "dev.azure.com", + "username" => "greysteil", + "password" => "secret_token" + }) + ] end it "uses the credentials" do diff --git a/common/spec/dependabot/pull_request_creator/azure_spec.rb b/common/spec/dependabot/pull_request_creator/azure_spec.rb index d2ade7b7f3..2ca6ce7926 100644 --- a/common/spec/dependabot/pull_request_creator/azure_spec.rb +++ b/common/spec/dependabot/pull_request_creator/azure_spec.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "spec_helper" +require "dependabot/credential" require "dependabot/dependency" require "dependabot/dependency_file" require "dependabot/pull_request_creator/azure" @@ -31,12 +32,12 @@ let(:branch_name) { "dependabot/bundler/business-1.5.0" } let(:base_commit) { "basecommitsha" } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "dev.azure.com", "username" => "x-access-token", "password" => "token" - }] + })] end let(:files) { [gemfile, gemfile_lock] } let(:commit_message) { "Commit msg" } diff --git a/common/spec/dependabot/pull_request_updater/azure_spec.rb b/common/spec/dependabot/pull_request_updater/azure_spec.rb index 8d081aadf4..2c09c739b1 100644 --- a/common/spec/dependabot/pull_request_updater/azure_spec.rb +++ b/common/spec/dependabot/pull_request_updater/azure_spec.rb @@ -2,6 +2,7 @@ # frozen_string_literal: true require "spec_helper" +require "dependabot/credential" require "dependabot/dependency_file" require "dependabot/pull_request_updater/azure" @@ -36,12 +37,12 @@ let(:temp_branch) { source_branch + "-temp" } let(:path) { "files/are/here" } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "dev.azure.com", "username" => "x-access-token", "password" => "token" - }] + })] end let(:gemfile) do diff --git a/common/spec/dependabot/shared_helpers_spec.rb b/common/spec/dependabot/shared_helpers_spec.rb index 0c522cd6eb..0f2b03aedd 100644 --- a/common/spec/dependabot/shared_helpers_spec.rb +++ b/common/spec/dependabot/shared_helpers_spec.rb @@ -268,6 +268,26 @@ def existing_tmp_folders .to raise_error(Dependabot::SharedHelpers::HelperSubprocessFailed) end end + + context "when the subprocess exits with out of disk error" do + let(:command) { File.join(spec_root, "helpers/test/error_bash disk") } + it "raises a HelperSubprocessFailed out of disk error" do + expect { run_shell_command } + .to raise_error(Dependabot::SharedHelpers::HelperSubprocessFailed) do |error| + expect(error.message).to include("No space left on device") + end + end + + context "when the subprocess exits with out of memory error" do + let(:command) { File.join(spec_root, "helpers/test/error_bash memory") } + it "raises a HelperSubprocessFailed out of memory error" do + expect { run_shell_command } + .to raise_error(Dependabot::SharedHelpers::HelperSubprocessFailed) do |error| + expect(error.message).to include("MemoryError") + end + end + end + end end describe ".escape_command" do diff --git a/common/spec/dependabot/workspace/git_spec.rb b/common/spec/dependabot/workspace/git_spec.rb index edac3bf4f2..3473f01b6b 100644 --- a/common/spec/dependabot/workspace/git_spec.rb +++ b/common/spec/dependabot/workspace/git_spec.rb @@ -210,8 +210,22 @@ end end + context "when there are changes to ignored files" do + # See: common/spec/fixtures/projects/simple/.gitignore + + it "returns nil and doesn't add any changes to the workspace" do + workspace.change { `echo "ignore me" >> ignored-file.txt` } + workspace.store_change("modify ignored file") + workspace.change { `mkdir -p ignored-dir && echo "ignore me" >> ignored-dir/file.txt` } + workspace.store_change("modify file in ignored directory") + + expect(workspace).not_to be_changed + expect(workspace.changes).to be_empty + end + end + context "when there are changes to store" do - it "captures the stores the changes correctly" do + it "stores the changes correctly" do workspace.change("timecop") do `echo 'gem "timecop", "~> 0.9.6", group: :test' >> Gemfile` end diff --git a/common/spec/helpers/test/error_bash b/common/spec/helpers/test/error_bash index 6320c2e1cd..076b5b9baf 100755 --- a/common/spec/helpers/test/error_bash +++ b/common/spec/helpers/test/error_bash @@ -2,4 +2,10 @@ set -e +if [ "$1" = "disk" ]; then + echo "No space left on device" >&2 +elif [ "$1" = "memory" ]; then + echo "MemoryError" >&2 +fi + exit 1 diff --git a/devcontainers/.rubocop.yml b/devcontainers/.rubocop.yml index fc2019d46a..b8168698e8 100644 --- a/devcontainers/.rubocop.yml +++ b/devcontainers/.rubocop.yml @@ -1 +1,4 @@ inherit_from: ../.rubocop.yml + +Sorbet/TrueSigil: + Enabled: true diff --git a/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb b/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb index 8b419744a8..2ea57409d2 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_fetcher.rb @@ -1,6 +1,7 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" require "dependabot/file_fetchers" require "dependabot/file_fetchers/base" require "dependabot/devcontainers/utils" @@ -8,16 +9,21 @@ module Dependabot module Devcontainers class FileFetcher < Dependabot::FileFetchers::Base + extend T::Sig + + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) # There's several other places a devcontainer.json can be checked into # See: https://containers.dev/implementors/spec/#devcontainerjson filenames.any? { |f| f.end_with?("devcontainer.json") } end + sig { override.returns(String) } def self.required_files_message "Repo must contain a dev container configuration file." end + sig { override.returns(T::Array[Dependabot::DependencyFile]) } def fetch_files fetched_files = [] fetched_files += root_files @@ -34,16 +40,19 @@ def fetch_files private + sig { returns(T::Array[Dependabot::DependencyFile]) } def root_files fetch_config_and_lockfile_from(".") end + sig { returns(T::Array[Dependabot::DependencyFile]) } def scoped_files return [] unless devcontainer_directory fetch_config_and_lockfile_from(".devcontainer") end + sig { returns(T::Array[Dependabot::DependencyFile]) } def custom_directory_files return [] unless devcontainer_directory @@ -52,16 +61,21 @@ def custom_directory_files end end + sig { returns(T::Array[T.untyped]) } def custom_directories repo_contents(dir: ".devcontainer").select { |f| f.type == "dir" && f.name != ".devcontainer" } end + sig { returns(T.untyped) } def devcontainer_directory - return @devcontainer_directory if defined?(@devcontainer_directory) - - @devcontainer_directory = repo_contents.find { |f| f.type == "dir" && f.name == ".devcontainer" } + @devcontainer_directory ||= + T.let( + repo_contents.find { |f| f.type == "dir" && f.name == ".devcontainer" }, + T.untyped + ) end + sig { params(directory: String).returns(T::Array[Dependabot::DependencyFile]) } def fetch_config_and_lockfile_from(directory) files = [] diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser.rb index 9d2fcc7204..6bd5d64b4b 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_parser.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_parser.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/file_parsers" require "dependabot/file_parsers/base" require "dependabot/devcontainers/version" @@ -9,8 +11,11 @@ module Dependabot module Devcontainers class FileParser < Dependabot::FileParsers::Base + extend T::Sig + require "dependabot/file_parsers/base/dependency_set" + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse dependency_set = DependencySet.new @@ -25,12 +30,14 @@ def parse private + sig { override.void } def check_required_files return if config_dependency_files.any? raise "No dev container configuration!" end + sig { params(config_dependency_file: Dependabot::DependencyFile).returns(T::Array[Dependabot::Dependency]) } def parse_features(config_dependency_file) FeatureDependencyParser.new( config_dependency_file: config_dependency_file, @@ -39,10 +46,14 @@ def parse_features(config_dependency_file) ).parse end + sig { returns(T::Array[Dependabot::DependencyFile]) } def config_dependency_files - @config_dependency_files ||= dependency_files.select do |f| - f.name.end_with?("devcontainer.json") - end + @config_dependency_files ||= T.let( + dependency_files.select do |f| + f.name.end_with?("devcontainer.json") + end, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end end end diff --git a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb index 46dc04074d..a64eabbae5 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_parser/feature_dependency_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "dependabot/devcontainers/requirement" @@ -6,18 +6,30 @@ require "dependabot/shared_helpers" require "dependabot/dependency" require "json" +require "sorbet-runtime" require "uri" module Dependabot module Devcontainers class FileParser < Dependabot::FileParsers::Base class FeatureDependencyParser + extend T::Sig + + sig do + params( + config_dependency_file: Dependabot::DependencyFile, + repo_contents_path: T.nilable(String), + credentials: T::Array[Dependabot::Credential] + ) + .void + end def initialize(config_dependency_file:, repo_contents_path:, credentials:) @config_dependency_file = config_dependency_file @repo_contents_path = repo_contents_path @credentials = credentials end + sig { returns(T::Array[Dependabot::Dependency]) } def parse SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do SharedHelpers.with_git_configured(credentials: credentials) do @@ -28,19 +40,23 @@ def parse private + sig { returns(String) } def base_dir File.dirname(config_dependency_file.path) end + sig { returns(String) } def config_name File.basename(config_dependency_file.path) end + sig { returns(T.nilable(String)) } def config_contents config_dependency_file.content end # https://github.com/devcontainers/cli/blob/9444540283b236298c28f397dea879e7ec222ca1/src/spec-node/devContainersSpecCLI.ts#L1072 + sig { returns(T::Hash[String, T.untyped]) } def evaluate_with_cli raise "config_name must be a string" unless config_name.is_a?(String) && !config_name.empty? @@ -55,6 +71,7 @@ def evaluate_with_cli JSON.parse(json) end + sig { params(json: T::Hash[String, T.untyped]).returns(T::Array[Dependabot::Dependency]) } def parse_cli_json(json) dependencies = [] @@ -90,7 +107,14 @@ def parse_cli_json(json) dependencies end - attr_reader :config_dependency_file, :repo_contents_path, :credentials + sig { returns(Dependabot::DependencyFile) } + attr_reader :config_dependency_file + + sig { returns(T.nilable(String)) } + attr_reader :repo_contents_path + + sig { returns(T::Array[Dependabot::Credential]) } + attr_reader :credentials end end end diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater.rb index 30fb765cdf..e792014aec 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/file_updaters" require "dependabot/file_updaters/base" require "dependabot/devcontainers/file_updater/config_updater" @@ -8,6 +10,9 @@ module Dependabot module Devcontainers class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + + sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ /^\.?devcontainer\.json$/, @@ -15,6 +20,7 @@ def self.updated_files_regex ] end + sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files updated_files = [] @@ -24,7 +30,7 @@ def updated_dependency_files config_contents, lockfile_contents = update(manifest, requirement) - updated_files << updated_file(file: manifest, content: config_contents) if file_changed?(manifest) + updated_files << updated_file(file: manifest, content: T.must(config_contents)) if file_changed?(manifest) lockfile = lockfile_for(manifest) @@ -36,23 +42,30 @@ def updated_dependency_files private + sig { returns(Dependabot::Dependency) } def dependency # TODO: Handle one dependency at a time - dependencies.first + T.must(dependencies.first) end + sig { override.void } def check_required_files return if dependency_files.any? raise "No dev container configuration!" end + sig { returns(T::Array[Dependabot::DependencyFile]) } def manifests - @manifests ||= dependency_files.select do |f| - f.name.end_with?("devcontainer.json") - end + @manifests ||= T.let( + dependency_files.select do |f| + f.name.end_with?("devcontainer.json") + end, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { params(manifest: Dependabot::DependencyFile).returns(T.nilable(Dependabot::DependencyFile)) } def lockfile_for(manifest) lockfile_name = lockfile_name_for(manifest) @@ -61,6 +74,7 @@ def lockfile_for(manifest) end end + sig { params(manifest: Dependabot::DependencyFile).returns(String) } def lockfile_name_for(manifest) basename = File.basename(manifest.name) lockfile_name = Utils.expected_lockfile_name(basename) @@ -68,13 +82,20 @@ def lockfile_name_for(manifest) manifest.name.delete_suffix(basename).concat(lockfile_name) end + sig do + params( + manifest: Dependabot::DependencyFile, + requirement: T::Hash[Symbol, T.untyped] + ) + .returns(T::Array[String]) + end def update(manifest, requirement) ConfigUpdater.new( feature: dependency.name, requirement: requirement[:requirement], - version: dependency.version, + version: T.must(dependency.version), manifest: manifest, - repo_contents_path: repo_contents_path, + repo_contents_path: T.must(repo_contents_path), credentials: credentials ).update end diff --git a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb index f24f5e7d8b..da2e5a10be 100644 --- a/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb +++ b/devcontainers/lib/dependabot/devcontainers/file_updater/config_updater.rb @@ -1,15 +1,31 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/file_updaters/base" require "dependabot/shared_helpers" require "dependabot/logger" require "dependabot/devcontainers/utils" +require "dependabot/devcontainers/version" module Dependabot module Devcontainers class FileUpdater < Dependabot::FileUpdaters::Base class ConfigUpdater + extend T::Sig + + sig do + params( + feature: String, + requirement: T.any(String, Dependabot::Devcontainers::Version), + version: String, + manifest: Dependabot::DependencyFile, + repo_contents_path: String, + credentials: T::Array[Dependabot::Credential] + ) + .void + end def initialize(feature:, requirement:, version:, manifest:, repo_contents_path:, credentials:) @feature = feature @requirement = requirement @@ -19,6 +35,7 @@ def initialize(feature:, requirement:, version:, manifest:, repo_contents_path:, @credentials = credentials end + sig { returns(T::Array[String]) } def update SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do SharedHelpers.with_git_configured(credentials: credentials) do @@ -34,18 +51,28 @@ def update private + sig { returns(String) } def base_dir File.dirname(manifest.path) end + sig { returns(String) } def manifest_name File.basename(manifest.path) end + sig { returns(String) } def lockfile_name Utils.expected_lockfile_name(manifest_name) end + sig do + params( + target_requirement: T.any(String, Dependabot::Devcontainers::Version), + target_version: String + ) + .void + end def update_manifests(target_requirement:, target_version:) # First force target version to upgrade lockfile. run_devcontainer_upgrade(target_version) @@ -55,10 +82,12 @@ def update_manifests(target_requirement:, target_version:) force_target_requirement(lockfile_name, from: target_version, to: target_requirement) end + sig { params(file_name: String, from: String, to: T.any(String, Dependabot::Devcontainers::Version)).void } def force_target_requirement(file_name, from:, to:) File.write(file_name, File.read(file_name).gsub("#{feature}:#{from}", "#{feature}:#{to}")) end + sig { params(target_version: String).void } def run_devcontainer_upgrade(target_version) cmd = "devcontainer upgrade " \ "--workspace-folder . " \ @@ -71,7 +100,23 @@ def run_devcontainer_upgrade(target_version) SharedHelpers.run_shell_command(cmd, stderr_to_stdout: false) end - attr_reader :feature, :requirement, :version, :manifest, :repo_contents_path, :credentials + sig { returns(String) } + attr_reader :feature + + sig { returns(T.any(String, Dependabot::Devcontainers::Version)) } + attr_reader :requirement + + sig { returns(String) } + attr_reader :version + + sig { returns(Dependabot::DependencyFile) } + attr_reader :manifest + + sig { returns(String) } + attr_reader :repo_contents_path + + sig { returns(T::Array[Dependabot::Credential]) } + attr_reader :credentials end end end diff --git a/devcontainers/lib/dependabot/devcontainers/metadata_finder.rb b/devcontainers/lib/dependabot/devcontainers/metadata_finder.rb index 2cc2b4b21d..ec7097483b 100644 --- a/devcontainers/lib/dependabot/devcontainers/metadata_finder.rb +++ b/devcontainers/lib/dependabot/devcontainers/metadata_finder.rb @@ -1,14 +1,19 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/metadata_finders" require "dependabot/metadata_finders/base" module Dependabot module Devcontainers class MetadataFinder < Dependabot::MetadataFinders::Base + extend T::Sig + private + sig { override.returns(T.nilable(Dependabot::Source)) } def look_up_source # TODO: Make upstream changes to dev container CLI to point to docs. # Specifically, 'devcontainers features info' can be augmented to expose documentationUrl diff --git a/devcontainers/lib/dependabot/devcontainers/requirement.rb b/devcontainers/lib/dependabot/devcontainers/requirement.rb index 9983d788cd..dbceef44a5 100644 --- a/devcontainers/lib/dependabot/devcontainers/requirement.rb +++ b/devcontainers/lib/dependabot/devcontainers/requirement.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "sorbet-runtime" @@ -14,17 +14,18 @@ class Requirement < Dependabot::Requirement # For consistency with other languages, we define a requirements array. # Devcontainers don't have an `OR` separator for requirements, so it # always contains a single element. - sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) } + sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Dependabot::Requirement]) } def self.requirements_array(requirement_string) [new(requirement_string)] end # Patches Gem::Requirement to make it accept requirement strings like # "~> 4.2.5, >= 4.2.5.1" without first needing to split them. + sig { params(requirements: T.nilable(String)).void } def initialize(*requirements) requirements = requirements.flatten.flat_map do |req_string| - req_string.split(",").map(&:strip) - end + req_string&.split(",")&.map(&:strip) + end.compact super(requirements) end diff --git a/devcontainers/lib/dependabot/devcontainers/update_checker.rb b/devcontainers/lib/dependabot/devcontainers/update_checker.rb index 08a1da621e..fa70e70ed4 100644 --- a/devcontainers/lib/dependabot/devcontainers/update_checker.rb +++ b/devcontainers/lib/dependabot/devcontainers/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/devcontainers/version" @@ -10,14 +12,19 @@ module Dependabot module Devcontainers class UpdateChecker < Dependabot::UpdateCheckers::Base + extend T::Sig + + sig { returns(T.nilable(Gem::Version)) } def latest_version - @latest_version ||= fetch_latest_version + @latest_version ||= T.let(fetch_latest_version, T.nilable(Gem::Version)) end + sig { returns(T.nilable(Gem::Version)) } def latest_resolvable_version latest_version # TODO end + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements dependency.requirements.map do |requirement| required_version = version_class.new(requirement[:requirement]) @@ -32,41 +39,61 @@ def updated_requirements end end + sig { override.returns(T.nilable(Dependabot::Version)) } def latest_resolvable_version_with_no_unlock raise NotImplementedError end private + sig { returns(T::Array[Dependabot::Devcontainers::Version]) } def viable_candidates - @viable_candidates ||= fetch_viable_candidates + @viable_candidates ||= T.let( + fetch_viable_candidates, + T.nilable(T::Array[Dependabot::Devcontainers::Version]) + ) end + sig { returns(T::Array[Dependabot::Devcontainers::Version]) } def fetch_viable_candidates candidates = comparable_versions_from_registry candidates = filter_ignored(candidates) candidates.sort end + sig { returns(Dependabot::Devcontainers::Version) } def fetch_latest_version return current_version unless viable_candidates.any? - viable_candidates.last + T.must(viable_candidates.last) end + sig do + params( + versions: T::Array[Dependabot::Devcontainers::Version], + required_version: Dependabot::Devcontainers::Version + ) + .returns(T::Array[Dependabot::Devcontainers::Version]) + end def remove_precision_changes(versions, required_version) versions.select do |version| version.same_precision?(required_version) end end + sig do + params( + versions: T::Array[Dependabot::Devcontainers::Version] + ) + .returns(T::Array[Dependabot::Devcontainers::Version]) + end def filter_ignored(versions) filtered = versions.reject do |version| ignore_requirements.any? { |r| version.satisfies?(r) } end - if @raise_on_ignored && + if raise_on_ignored && filter_lower_versions(filtered).empty? && filter_lower_versions(versions).any? raise AllVersionsIgnored @@ -75,16 +102,19 @@ def filter_ignored(versions) filtered end + sig { returns(T::Array[Dependabot::Devcontainers::Version]) } def comparable_versions_from_registry tags_from_registry.filter_map do |tag| version_class.correct?(tag) && version_class.new(tag) end end + sig { returns(T::Array[String]) } def tags_from_registry - @tags_from_registry ||= fetch_tags_from_registry + @tags_from_registry ||= T.let(fetch_tags_from_registry, T.nilable(T::Array[String])) end + sig { returns(T::Array[String]) } def fetch_tags_from_registry cmd = "devcontainer features info tags #{dependency.name} --output-format json" @@ -95,16 +125,19 @@ def fetch_tags_from_registry JSON.parse(output).fetch("publishedTags") end + sig { params(versions: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) } def filter_lower_versions(versions) versions.select do |version| version > current_version end end + sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? false # TODO end + sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock raise NotImplementedError end diff --git a/devcontainers/lib/dependabot/devcontainers/utils.rb b/devcontainers/lib/dependabot/devcontainers/utils.rb index d0b5e6a213..713c21117c 100644 --- a/devcontainers/lib/dependabot/devcontainers/utils.rb +++ b/devcontainers/lib/dependabot/devcontainers/utils.rb @@ -1,17 +1,24 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module Devcontainers module Utils + extend T::Sig + + sig { params(directory: String).returns(String) } def self.expected_config_basename(directory) root_directory?(directory) ? ".devcontainer.json" : "devcontainer.json" end + sig { params(directory: String).returns(T::Boolean) } def self.root_directory?(directory) Pathname.new(directory).cleanpath.to_path == Pathname.new(".").cleanpath.to_path end + sig { params(config_file_name: String).returns(String) } def self.expected_lockfile_name(config_file_name) if config_file_name.start_with?(".") ".devcontainer-lock.json" diff --git a/devcontainers/lib/dependabot/devcontainers/version.rb b/devcontainers/lib/dependabot/devcontainers/version.rb index 7077ef5bc4..158a864c6f 100644 --- a/devcontainers/lib/dependabot/devcontainers/version.rb +++ b/devcontainers/lib/dependabot/devcontainers/version.rb @@ -1,20 +1,27 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/version" require "dependabot/utils" module Dependabot module Devcontainers class Version < Dependabot::Version + extend T::Sig + + sig { params(other: Dependabot::Devcontainers::Version).returns(T::Boolean) } def same_precision?(other) precision == other.precision end + sig { params(requirement: Dependabot::Requirement).returns(T::Boolean) } def satisfies?(requirement) requirement.satisfied_by?(self) end + sig { params(other: BasicObject).returns(T.nilable(Integer)) } def <=>(other) if self == other precision <=> other.precision @@ -25,6 +32,7 @@ def <=>(other) protected + sig { returns(Integer) } def precision segments.size end diff --git a/docker/README.md b/docker/README.md index 79df0f6211..af783c168c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -16,3 +16,30 @@ Docker support for [`dependabot-core`][core-repo]. ``` [core-repo]: https://github.com/dependabot/dependabot-core + +### Supported tag schemas + +Dependabot supports updates for Docker tags that use semver versioning, dates, and build numbers. +The Docker tag class is located at: +https://github.com/dependabot/dependabot-core/blob/main/docker/lib/dependabot/docker/tag.rb + +#### Semver + +Dependabot will attempt to parse a semver version from a tag and will only update it to a tag with a matching prefix and suffix. + +As an example, `base-12.5.1` and `base-12.5.1-golden` would be parsed as `-` and `--` respectively. + +That means for `base-12.5.1` only another `-` tag would be a viable update, and for `base-12.5.1-golden`, only another `--` tag would be viable. The exception to this is if the suffix is a SHA, in which case it does not get compared and only the `` parts are considered in finding a viable tag. + +#### Dates + +Dependabot will parse dates in the `yyyy-mm`, `yyyy-mm-dd` formats (or with `.` instead of `-`) and update tags to the latest date. + +As an example, `2024-01` will get updated to `2024-02` and `2024.01.29` will get updated to `2024.03.15`. + +#### Build numbers + +Dependabot will recognize build numbers and will update to the highest build number available. + +As an example, `21-ea-32`, `22-ea-7`, and `22-ea-jdk-nanoserver-1809` are mapped to `-ea-`, `-ea-`, and `-ea-jdk-nanoserver-` respectively. +That means only "22-ea-7" will be considered as a viable update candidate for `21-ea-32`, since it's the only one that respects that format. diff --git a/docker/lib/dependabot/docker/file_updater.rb b/docker/lib/dependabot/docker/file_updater.rb index 0aaa455e1b..c442c69f41 100644 --- a/docker/lib/dependabot/docker/file_updater.rb +++ b/docker/lib/dependabot/docker/file_updater.rb @@ -66,12 +66,7 @@ def updated_dockerfile_content(file) updated_content = file.content old_sources.zip(new_sources).each do |old_source, new_source| - updated_content = - if specified_with_digest?(old_source) - update_digest_and_tag(updated_content, old_source, new_source) - else - update_tag(updated_content, old_source, new_source) - end + updated_content = update_digest_and_tag(updated_content, old_source, new_source) end raise "Expected content to change!" if updated_content == file.content @@ -86,35 +81,38 @@ def update_digest_and_tag(previous_content, old_source, new_source) old_tag = old_source[:tag] new_tag = new_source[:tag] - old_declaration_regex = /^#{FROM_REGEX}\s+.*@sha256:#{old_digest}/ - - previous_content.gsub(old_declaration_regex) do |old_dec| - old_dec - .gsub("@sha256:#{old_digest}", "@sha256:#{new_digest}") - .gsub(":#{old_tag}", ":#{new_tag}") - end - end - - def update_tag(previous_content, old_source, new_source) - old_tag = old_source[:tag] - new_tag = new_source[:tag] - old_declaration = if private_registry_url(old_source) then "#{private_registry_url(old_source)}/" else "" end - old_declaration += "#{dependency.name}:#{old_tag}" + old_declaration += dependency.name + old_declaration += + if specified_with_tag?(old_source) then ":#{old_tag}" + else + "" + end + old_declaration += + if specified_with_digest?(old_source) then "@sha256:#{old_digest}" + else + "" + end escaped_declaration = Regexp.escape(old_declaration) old_declaration_regex = %r{^#{FROM_REGEX}\s+(docker\.io/)?#{escaped_declaration}(?=\s|$)} previous_content.gsub(old_declaration_regex) do |old_dec| - old_dec.gsub(":#{old_tag}", ":#{new_tag}") + old_dec + .gsub("@sha256:#{old_digest}", "@sha256:#{new_digest}") + .gsub(":#{old_tag}", ":#{new_tag}") end end + def specified_with_tag?(source) + source[:tag] + end + def specified_with_digest?(source) source[:digest] end diff --git a/docker/lib/dependabot/docker/update_checker.rb b/docker/lib/dependabot/docker/update_checker.rb index b03f080158..f2675f27b0 100644 --- a/docker/lib/dependabot/docker/update_checker.rb +++ b/docker/lib/dependabot/docker/update_checker.rb @@ -108,7 +108,7 @@ def latest_tag_from(version) # NOTE: It's important that this *always* returns a tag (even if # it's the existing one) as it is what we later check the digest of. def fetch_latest_tag(version_tag) - return Tag.new(latest_digest) if version_tag.digest? + return Tag.new(latest_digest) if version_tag.digest? && latest_digest return version_tag unless version_tag.comparable? # Prune out any downgrade tags before checking for pre-releases diff --git a/docker/lib/dependabot/docker/utils/credentials_finder.rb b/docker/lib/dependabot/docker/utils/credentials_finder.rb index 26b38eaa8b..e945739faf 100644 --- a/docker/lib/dependabot/docker/utils/credentials_finder.rb +++ b/docker/lib/dependabot/docker/utils/credentials_finder.rb @@ -4,12 +4,15 @@ require "aws-sdk-ecr" require "base64" +require "dependabot/credential" require "dependabot/errors" module Dependabot module Docker module Utils class CredentialsFinder + extend T::Sig + AWS_ECR_URL = /dkr\.ecr\.(?[^.]+)\.amazonaws\.com/ DEFAULT_DOCKER_HUB_REGISTRY = "registry.hub.docker.com" @@ -17,6 +20,7 @@ def initialize(credentials) @credentials = credentials end + sig { params(registry_hostname: String).returns(T.nilable(Dependabot::Credential)) } def credentials_for_registry(registry_hostname) registry_details = credentials @@ -42,8 +46,10 @@ def using_dockerhub?(registry) private + sig { returns(T::Array[Dependabot::Credential]) } attr_reader :credentials + sig { params(registry_details: Dependabot::Credential).returns(Dependabot::Credential) } def build_aws_credentials(registry_details) # If credentials have been generated from AWS we can just return them return registry_details if registry_details["username"] == "AWS" @@ -75,7 +81,7 @@ def build_aws_credentials(registry_details) ecr_client.get_authorization_token.authorization_data.first.authorization_token username, password = Base64.decode64(@authorization_tokens[registry_hostname]).split(":") - registry_details.merge("username" => username, "password" => password) + registry_details.merge(Dependabot::Credential.new({ "username" => username, "password" => password })) rescue Aws::Errors::MissingCredentialsError, Aws::ECR::Errors::UnrecognizedClientException, Aws::ECR::Errors::InvalidSignatureException diff --git a/docker/spec/dependabot/docker/file_updater_spec.rb b/docker/spec/dependabot/docker/file_updater_spec.rb index aa12fa2817..3229f3299f 100644 --- a/docker/spec/dependabot/docker/file_updater_spec.rb +++ b/docker/spec/dependabot/docker/file_updater_spec.rb @@ -211,6 +211,77 @@ end end + context "when multiple identical named dependencies with same tag, but different variants with digests" do + let(:dockerfile_body) do + fixture("docker", "dockerfiles", "multi_stage_different_variants_with_digests") + end + let(:dependency) do + Dependabot::Dependency.new( + name: "python", + version: "3.10.6", + previous_version: "3.10.5", + requirements: [{ + requirement: nil, + groups: [], + file: "Dockerfile", + source: { + tag: "3.10.6", + digest: "8d1f943ceaaf3b3ce05df5c0926e7958836b048b70" \ + "0176bf9c56d8f37ac13fca" + } + }, { + requirement: nil, + groups: [], + file: "Dockerfile", + source: { + tag: "3.10.6-slim", + digest: "c8ef926b002a8371fff6b4f40142dcc6d6f7e217f7" \ + "afce2c2d1ed2e6c28e2b7c" + } + }], + previous_requirements: [ + { + requirement: nil, + groups: [], + file: "Dockerfile", + source: { + tag: "3.10.5", + digest: "bdf0079de4094afdb26b94d9f89b716499436282c9" \ + "72461d945a87899c015c23" + } + }, + { + requirement: nil, + groups: [], + file: "Dockerfile", + source: { + tag: "3.10.5-slim", + digest: "bdf0079de4094afdb26b94d9f89b716499436282c9" \ + "72461d945a87899c015c23" + } + } + ], + package_manager: "docker" + ) + end + + describe "the updated Dockerfile" do + subject(:updated_dockerfile) do + updated_files.find { |f| f.name == "Dockerfile" } + end + + its(:content) do + is_expected.to include "FROM python:3.10.6@sha256:8d1f943ceaaf3b3ce05df5c0926e7958836b048b" \ + "700176bf9c56d8f37ac13fca AS base\n" + end + its(:content) do + is_expected.to include "FROM python:3.10.6-slim@sha256:c8ef926b002a8371fff6b4f40142dcc6d6f" \ + "7e217f7afce2c2d1ed2e6c28e2b7c AS production\n" + end + its(:content) { is_expected.to include "ENV PIP_NO_CACHE_DIR=off \\\n" } + end + end + context "when the dependency has a namespace" do let(:dockerfile_body) { fixture("docker", "dockerfiles", "namespace") } let(:dependency) do @@ -340,7 +411,7 @@ groups: [], file: "Dockerfile", source: { - tag: "17.10", + # corresponds to the tag "17.10" digest: "3ea1ca1aa8483a38081750953ad75046e6cc9f6b86" \ "ca97eba880ebf600d68608" } @@ -350,7 +421,7 @@ groups: [], file: "Dockerfile", source: { - tag: "12.04.5", + # corresponds to the tag "12.04.5" digest: "18305429afa14ea462f810146ba44d4363ae76e4c8" \ "dfc38288cf73aa07485005" } @@ -374,6 +445,35 @@ fixture("docker", "dockerfiles", "digest_and_tag") end + let(:dependency) do + Dependabot::Dependency.new( + name: "ubuntu", + version: "17.10", + previous_version: "12.04.5", + requirements: [{ + requirement: nil, + groups: [], + file: "Dockerfile", + source: { + tag: "17.10", + digest: "3ea1ca1aa8483a38081750953ad75046e6cc9f6b86" \ + "ca97eba880ebf600d68608" + } + }], + previous_requirements: [{ + requirement: nil, + groups: [], + file: "Dockerfile", + source: { + tag: "12.04.5", + digest: "18305429afa14ea462f810146ba44d4363ae76e4c8" \ + "dfc38288cf73aa07485005" + } + }], + package_manager: "docker" + ) + end + its(:content) do is_expected.to include "FROM ubuntu:17.10@sha256:3ea1ca1aa" end diff --git a/docker/spec/dependabot/docker/update_checker_spec.rb b/docker/spec/dependabot/docker/update_checker_spec.rb index 194ba27d84..41a04b0734 100644 --- a/docker/spec/dependabot/docker/update_checker_spec.rb +++ b/docker/spec/dependabot/docker/update_checker_spec.rb @@ -24,12 +24,12 @@ let(:ignored_versions) { [] } let(:raise_on_ignored) { false } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }] + })] end let(:dependency) do @@ -1107,17 +1107,17 @@ def stub_tag_with_no_digest(tag) context "with authentication credentials" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }, { + }), Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "registry-host.io:5000", "username" => "grey", "password" => "pa55word" - }] + })] end before do @@ -1130,15 +1130,15 @@ def stub_tag_with_no_digest(tag) context "that don't have a username or password" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }, { + }), Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "registry-host.io:5000" - }] + })] end it { is_expected.to eq("17.10") } diff --git a/docker/spec/dependabot/docker/utils/credentials_finder_spec.rb b/docker/spec/dependabot/docker/utils/credentials_finder_spec.rb index d88eab770e..1161b405ba 100644 --- a/docker/spec/dependabot/docker/utils/credentials_finder_spec.rb +++ b/docker/spec/dependabot/docker/utils/credentials_finder_spec.rb @@ -10,12 +10,12 @@ RSpec.describe Dependabot::Docker::Utils::CredentialsFinder do subject(:finder) { described_class.new(credentials) } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com", "username" => "grey", "password" => "pa55word" - }] + })] end describe "#credentials_for_registry" do @@ -30,12 +30,12 @@ context "with a non-AWS registry" do let(:registry) { "my.registry.com" } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "my.registry.com", "username" => "grey", "password" => "pa55word" - }] + })] end it { is_expected.to eq(credentials.first) } @@ -46,12 +46,12 @@ context "with 'AWS' as the username" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com", "username" => "AWS", "password" => "pa55word" - }] + })] end it { is_expected.to eq(credentials.first) } @@ -59,10 +59,10 @@ context "without a username or password" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com" - }] + })] end context "and a valid AWS response (via proxying)" do @@ -75,7 +75,7 @@ end it "returns details without credentials" do - expect(found_credentials).to eq( + expect(found_credentials.to_h).to eq( "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com" ) @@ -85,12 +85,12 @@ context "with as AKID as the username" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com", "username" => "AKIAIHYCC4QXL4X2OTCQ", "password" => "pa55word" - }] + })] end context "and an invalid secret key as the password" do @@ -145,7 +145,7 @@ end it "returns an updated set of credentials" do - expect(found_credentials).to eq( + expect(found_credentials.to_h).to eq( "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com", "username" => "AWS", @@ -157,10 +157,10 @@ context "using the default credentials provider" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com" - }] + })] end context "and a valid AWS response" do @@ -175,7 +175,7 @@ end it "returns updated, valid credentials" do - expect(found_credentials).to eq( + expect(found_credentials.to_h).to eq( "type" => "docker_registry", "registry" => "695729449481.dkr.ecr.eu-west-2.amazonaws.com", "username" => "foo", diff --git a/docker/spec/fixtures/docker/dockerfiles/multi_stage_different_variants_with_digests b/docker/spec/fixtures/docker/dockerfiles/multi_stage_different_variants_with_digests new file mode 100644 index 0000000000..2ee0afecc0 --- /dev/null +++ b/docker/spec/fixtures/docker/dockerfiles/multi_stage_different_variants_with_digests @@ -0,0 +1,11 @@ +FROM python:3.10.5@sha256:bdf0079de4094afdb26b94d9f89b716499436282c972461d945a87899c015c23 AS base + +ENV PIP_NO_CACHE_DIR=off \ + PIP_DEFAULT_TIMEOUT=100 \ + PIP_DISABLE_PIP_VERSION_CHECK=on + +FROM python:3.10.5-slim@sha256:bdf0079de4094afdb26b94d9f89b716499436282c972461d945a87899c015c23 AS production + +ENV PORT=8000 \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 diff --git a/git_submodules/.rubocop.yml b/git_submodules/.rubocop.yml index b8168698e8..e5270530f5 100644 --- a/git_submodules/.rubocop.yml +++ b/git_submodules/.rubocop.yml @@ -1,4 +1,4 @@ inherit_from: ../.rubocop.yml -Sorbet/TrueSigil: +Sorbet/StrictSigil: Enabled: true diff --git a/git_submodules/lib/dependabot/git_submodules/file_fetcher.rb b/git_submodules/lib/dependabot/git_submodules/file_fetcher.rb index f5de3ea22b..379207ccbb 100644 --- a/git_submodules/lib/dependabot/git_submodules/file_fetcher.rb +++ b/git_submodules/lib/dependabot/git_submodules/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "parseconfig" @@ -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?(".gitmodules") end + sig { override.returns(String) } def self.required_files_message "Repo must contain a .gitmodules file." end @@ -31,26 +33,40 @@ def fetch_files private + sig { returns(Dependabot::DependencyFile) } def gitmodules_file - @gitmodules_file ||= fetch_file_from_host(".gitmodules") + @gitmodules_file ||= + T.let( + fetch_file_from_host(".gitmodules"), + T.nilable(Dependabot::DependencyFile) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def submodule_refs @submodule_refs ||= - submodule_paths - .map { |path| fetch_submodule_ref_from_host(path) } - .tap { |refs| refs.each { |f| f.support_file = true } } - .uniq + T.let( + submodule_paths + .map { |path| fetch_submodule_ref_from_host(path) } + .tap { |refs| refs.each { |f| f.support_file = true } } + .uniq, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[String]) } def submodule_paths @submodule_paths ||= - Dependabot::SharedHelpers.in_a_temporary_directory do - File.write(".gitmodules", gitmodules_file.content) - ParseConfig.new(".gitmodules").params.values.map { |p| p["path"] } - end + T.let( + Dependabot::SharedHelpers.in_a_temporary_directory do + File.write(".gitmodules", gitmodules_file.content) + ParseConfig.new(".gitmodules").params.values.map { |p| p["path"] } + end, + T.nilable(T::Array[String]) + ) end + sig { params(submodule_path: T.nilable(String)).returns(Dependabot::DependencyFile) } def fetch_submodule_ref_from_host(submodule_path) path = Pathname.new(File.join(directory, submodule_path)) .cleanpath.to_path.gsub(%r{^/*}, "") @@ -61,7 +77,7 @@ def fetch_submodule_ref_from_host(submodule_path) tmp_path = path.gsub(%r{^/*}, "") T.unsafe(gitlab_client).get_file(repo, tmp_path, commit).blob_id when "azure" - azure_client.fetch_file_contents(commit, path) + azure_client.fetch_file_contents(T.must(commit), path) else raise "Unsupported provider '#{source.provider}'." end @@ -77,6 +93,7 @@ def fetch_submodule_ref_from_host(submodule_path) raise Dependabot::DependencyFileNotFound, path end + sig { params(path: String).returns(String) } def fetch_github_submodule_commit(path) content = T.unsafe(github_client).contents( repo, diff --git a/git_submodules/lib/dependabot/git_submodules/file_parser.rb b/git_submodules/lib/dependabot/git_submodules/file_parser.rb index 00495af838..e55bad0256 100644 --- a/git_submodules/lib/dependabot/git_submodules/file_parser.rb +++ b/git_submodules/lib/dependabot/git_submodules/file_parser.rb @@ -1,7 +1,9 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "parseconfig" +require "sorbet-runtime" + require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" @@ -10,6 +12,9 @@ module Dependabot module GitSubmodules class FileParser < Dependabot::FileParsers::Base + extend T::Sig + + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse Dependabot::SharedHelpers.in_a_temporary_directory do File.write(".gitmodules", gitmodules_file.content) @@ -39,6 +44,7 @@ def parse private + sig { params(url: String).returns(String) } def absolute_url(url) # Submodules can be specified with a relative URL (e.g., ../repo.git) # which we want to expand out into a full URL if present. @@ -48,6 +54,7 @@ def absolute_url(url) "https://#{source&.hostname}/#{path.cleanpath}" end + sig { params(path: String).returns(T.nilable(String)) } def submodule_sha(path) submodule = dependency_files.find { |f| f.name == path } raise "Submodule not found #{path}" unless submodule @@ -55,10 +62,16 @@ def submodule_sha(path) submodule.content end + sig { returns(Dependabot::DependencyFile) } def gitmodules_file - @gitmodules_file ||= get_original_file(".gitmodules") + @gitmodules_file ||= + T.let( + T.must(get_original_file(".gitmodules")), + T.nilable(Dependabot::DependencyFile) + ) end + sig { override.void } def check_required_files %w(.gitmodules).each do |filename| raise "No #{filename}!" unless get_original_file(filename) diff --git a/git_submodules/lib/dependabot/git_submodules/file_updater.rb b/git_submodules/lib/dependabot/git_submodules/file_updater.rb index ed0c2f1b7d..355264577e 100644 --- a/git_submodules/lib/dependabot/git_submodules/file_updater.rb +++ b/git_submodules/lib/dependabot/git_submodules/file_updater.rb @@ -1,37 +1,52 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/file_updaters" require "dependabot/file_updaters/base" module Dependabot module GitSubmodules class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + + sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [] end + sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files - [updated_file(file: submodule, content: dependency.version)] + [updated_file(file: submodule, content: T.must(dependency.version))] end private + sig { returns(Dependabot::Dependency) } def dependency # Git submodules will only ever be updating a single dependency - dependencies.first + T.must(dependencies.first) end + sig { override.void } def check_required_files %w(.gitmodules).each do |filename| raise "No #{filename}!" unless get_original_file(filename) end end + sig { returns(Dependabot::DependencyFile) } def submodule - @submodule ||= dependency_files.find do |file| - file.name == dependency.name - end + @submodule ||= + T.let( + T.must( + dependency_files.find do |file| + file.name == dependency.name + end + ), + T.nilable(Dependabot::DependencyFile) + ) end end end diff --git a/git_submodules/lib/dependabot/git_submodules/metadata_finder.rb b/git_submodules/lib/dependabot/git_submodules/metadata_finder.rb index dec406fae4..6e28a8a476 100644 --- a/git_submodules/lib/dependabot/git_submodules/metadata_finder.rb +++ b/git_submodules/lib/dependabot/git_submodules/metadata_finder.rb @@ -1,14 +1,19 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/metadata_finders" require "dependabot/metadata_finders/base" module Dependabot module GitSubmodules class MetadataFinder < Dependabot::MetadataFinders::Base + extend T::Sig + private + sig { override.returns(T.nilable(Dependabot::Source)) } def look_up_source url = dependency.requirements.first&.fetch(:source)&.fetch(:url) || dependency.requirements.first&.fetch(:source)&.fetch("url") diff --git a/git_submodules/lib/dependabot/git_submodules/requirement.rb b/git_submodules/lib/dependabot/git_submodules/requirement.rb index 00c6e2994e..6d7575245a 100644 --- a/git_submodules/lib/dependabot/git_submodules/requirement.rb +++ b/git_submodules/lib/dependabot/git_submodules/requirement.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "sorbet-runtime" @@ -21,9 +21,10 @@ def self.requirements_array(requirement_string) # Patches Gem::Requirement to make it accept requirement strings like # "~> 4.2.5, >= 4.2.5.1" without first needing to split them. + sig { params(requirements: T.nilable(String)).void } def initialize(*requirements) requirements = requirements.flatten.flat_map do |req_string| - req_string.split(",").map(&:strip) + req_string&.split(",")&.map(&:strip) end super(requirements) diff --git a/git_submodules/lib/dependabot/git_submodules/update_checker.rb b/git_submodules/lib/dependabot/git_submodules/update_checker.rb index eea6843eac..7c81c7cb82 100644 --- a/git_submodules/lib/dependabot/git_submodules/update_checker.rb +++ b/git_submodules/lib/dependabot/git_submodules/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/git_submodules/version" @@ -10,20 +12,30 @@ module Dependabot module GitSubmodules class UpdateChecker < Dependabot::UpdateCheckers::Base + extend T::Sig + + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_version - @latest_version ||= fetch_latest_version + @latest_version ||= + T.let( + fetch_latest_version, + T.nilable(T.any(String, Gem::Version)) + ) end + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_resolvable_version # Resolvability isn't an issue for submodules. latest_version end + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_resolvable_version_with_no_unlock # No concept of "unlocking" for submodules latest_version end + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements # Submodule requirements are the URL and branch to use for the # submodule. We never want to update either. @@ -32,15 +44,18 @@ def updated_requirements private + sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? # Full unlock checks aren't relevant for submodules false end + sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock raise NotImplementedError end + sig { returns(T.nilable(String)) } def fetch_latest_version git_commit_checker = Dependabot::GitCommitChecker.new( dependency: dependency, diff --git a/go_modules/Dockerfile b/go_modules/Dockerfile index 71309401af..c2de4de3f9 100644 --- a/go_modules/Dockerfile +++ b/go_modules/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.21.6-bookworm as go +FROM docker.io/library/golang:1.22.0-bookworm as go FROM ghcr.io/dependabot/dependabot-updater-core ARG TARGETARCH diff --git a/go_modules/lib/dependabot/go_modules/file_updater/go_mod_updater.rb b/go_modules/lib/dependabot/go_modules/file_updater/go_mod_updater.rb index 922a68295a..22462caab8 100644 --- a/go_modules/lib/dependabot/go_modules/file_updater/go_mod_updater.rb +++ b/go_modules/lib/dependabot/go_modules/file_updater/go_mod_updater.rb @@ -58,7 +58,8 @@ class GoModUpdater OUT_OF_DISK_REGEXES = [ %r{input/output error}, - /no space left on device/ + /no space left on device/, + /Out of diskspace/ ].freeze GO_MOD_VERSION = /^go 1\.\d+(\.\d+)?$/ diff --git a/go_modules/spec/dependabot/go_modules/file_updater/go_mod_updater_spec.rb b/go_modules/spec/dependabot/go_modules/file_updater/go_mod_updater_spec.rb index 323d3b64a5..a8d1c46e4b 100644 --- a/go_modules/spec/dependabot/go_modules/file_updater/go_mod_updater_spec.rb +++ b/go_modules/spec/dependabot/go_modules/file_updater/go_mod_updater_spec.rb @@ -962,6 +962,17 @@ expect(error.message).to include("info/attributes: no space left on device") end end + + it "detects 'Out of diskspace'" do + stderr = <<~ERROR + rsc.io/sampler imports + write fatal: sha1 file '/home/dependabot/dependabot-updater/repo/.git/index.lock' write error. Out of diskspace + ERROR + + expect { updater.send(:handle_subprocess_error, stderr) }.to raise_error(Dependabot::OutOfDisk) do |error| + expect(error.message).to include("write error. Out of diskspace") + end + end end end end diff --git a/gradle/lib/dependabot/gradle/file_parser/repositories_finder.rb b/gradle/lib/dependabot/gradle/file_parser/repositories_finder.rb index df5e9fca22..43925d2ff7 100644 --- a/gradle/lib/dependabot/gradle/file_parser/repositories_finder.rb +++ b/gradle/lib/dependabot/gradle/file_parser/repositories_finder.rb @@ -8,6 +8,7 @@ module Gradle class FileParser class RepositoriesFinder SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts).freeze + SUPPORTED_SETTINGS_FILE_NAMES = %w(settings.gradle settings.gradle.kts).freeze # The Central Repo doesn't have special status for Gradle, but until # we're confident we're selecting repos correctly it's wise to include @@ -37,6 +38,7 @@ def repository_urls repository_urls += inherited_repository_urls(dependency_file) end repository_urls += own_buildfile_repository_urls + repository_urls += settings_file_repository_urls(top_level_settings_file) repository_urls = repository_urls.uniq return repository_urls unless repository_urls.empty? @@ -91,6 +93,21 @@ def own_buildfile_repository_urls own_buildfile_urls end + def settings_file_repository_urls(settings_file) + return [] unless settings_file + + settings_file_content = comment_free_content(settings_file) + dependency_resolution_management_repositories = [] + + settings_file_content.scan(/(?:^|\s)dependencyResolutionManagement\s*\{/) do + mtch = Regexp.last_match + dependency_resolution_management_repositories << + mtch.post_match[0..closing_bracket_index(mtch.post_match)] + end + + repository_urls_from(dependency_resolution_management_repositories.join("\n")) + end + def repository_urls_from(buildfile_content) repository_urls = [] @@ -154,6 +171,12 @@ def top_level_buildfile SUPPORTED_BUILD_FILE_NAMES.include?(f.name) end end + + def top_level_settings_file + @top_level_settings_file ||= dependency_files.find do |f| + SUPPORTED_SETTINGS_FILE_NAMES.include?(f.name) + end + end end end end diff --git a/gradle/spec/dependabot/gradle/file_parser/repositories_finder_spec.rb b/gradle/spec/dependabot/gradle/file_parser/repositories_finder_spec.rb index c5cc46296a..afcce1a8d8 100644 --- a/gradle/spec/dependabot/gradle/file_parser/repositories_finder_spec.rb +++ b/gradle/spec/dependabot/gradle/file_parser/repositories_finder_spec.rb @@ -116,6 +116,28 @@ end end + context "with dependencyResolutionManagement in the settings file" do + let(:dependency_files) { [buildfile, settings_file] } + let(:settings_file) do + Dependabot::DependencyFile.new( + name: "settings.gradle", + content: fixture("settings_files", settings_file_fixture_name) + ) + end + let(:settings_file_fixture_name) { "dependency_resolution_management.gradle" } + + it "finds the repositories" do + expect(repository_urls).to match_array( + %w( + https://dependency_resolution_management.example.com + https://jcenter.bintray.com + https://dl.bintray.com/magnusja/maven + https://maven.google.com + ) + ) + end + end + context "that get URLs from a variable" do let(:buildfile_fixture_name) { "variable_repos_build.gradle" } diff --git a/gradle/spec/fixtures/settings_files/dependency_resolution_management.gradle b/gradle/spec/fixtures/settings_files/dependency_resolution_management.gradle new file mode 100644 index 0000000000..44b8210b31 --- /dev/null +++ b/gradle/spec/fixtures/settings_files/dependency_resolution_management.gradle @@ -0,0 +1,7 @@ +include ':app' + +dependencyResolutionManagement { + repositories { + maven { url 'https://dependency_resolution_management.example.com/' } + } +} diff --git a/hex/lib/dependabot/hex/credential_helpers.rb b/hex/lib/dependabot/hex/credential_helpers.rb index 07701fc9ae..d246e0036c 100644 --- a/hex/lib/dependabot/hex/credential_helpers.rb +++ b/hex/lib/dependabot/hex/credential_helpers.rb @@ -9,7 +9,7 @@ def self.hex_credentials(credentials) end def self.organization_credentials(credentials) - defaults = { "organization" => "", "token" => "" } + defaults = Dependabot::Credential.new({ "organization" => "", "token" => "" }) keys = %w(type organization token) credentials @@ -20,7 +20,7 @@ def self.organization_credentials(credentials) def self.repo_credentials(credentials) # Credentials are serialized as an array that may not have optional fields. Using a # default ensures that the array is always the same length, even if values are empty. - defaults = { "url" => "", "auth_key" => "", "public_key_fingerprint" => "" } + defaults = Dependabot::Credential.new({ "url" => "", "auth_key" => "", "public_key_fingerprint" => "" }) keys = %w(type repo url auth_key public_key_fingerprint) credentials diff --git a/hex/spec/dependabot/hex/credential_helpers_spec.rb b/hex/spec/dependabot/hex/credential_helpers_spec.rb new file mode 100644 index 0000000000..eaf3c986ff --- /dev/null +++ b/hex/spec/dependabot/hex/credential_helpers_spec.rb @@ -0,0 +1,37 @@ +# typed: false +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/hex/credential_helpers" + +RSpec.describe Dependabot::Hex::CredentialHelpers do + describe ".organization_credentials" do + subject(:organization_credentials) { described_class.organization_credentials(credentials) } + + let(:credentials) do + [ + Dependabot::Credential.new({ "type" => "hex_organization", "organization" => "organization", + "token" => "token" }) + ] + end + + it "populates the credentials with default properties" do + expect(organization_credentials).to eq(%w(hex_organization organization token)) + end + end + + describe ".repo_credentials" do + subject(:repo_credentials) { described_class.repo_credentials(credentials) } + + let(:credentials) do + [ + Dependabot::Credential.new({ "type" => "hex_repository", "url" => "url", "auth_key" => "auth_key", + "public_key_fingerprint" => "public_key_fingerprint" }) + ] + end + + it "populates the credentials with default properties" do + expect(repo_credentials).to eq(%w(hex_repository url auth_key public_key_fingerprint)) + end + end +end diff --git a/hex/spec/dependabot/hex/file_updater/lockfile_updater_spec.rb b/hex/spec/dependabot/hex/file_updater/lockfile_updater_spec.rb index 5d8dde79d8..e0d9f5c9df 100644 --- a/hex/spec/dependabot/hex/file_updater/lockfile_updater_spec.rb +++ b/hex/spec/dependabot/hex/file_updater/lockfile_updater_spec.rb @@ -351,12 +351,12 @@ let(:lockfile_fixture_name) { "private_repo" } let(:credentials) do - { + Dependabot::Credential.new({ "type" => "hex_repository", "repo" => "dependabot", "auth_key" => "d6fc2b6n6h7katic6vuq6k5e2csahcm4", "url" => "https://dependabot-private.fly.dev" - } + }) end let(:dependency) do diff --git a/hex/spec/dependabot/hex/update_checker_spec.rb b/hex/spec/dependabot/hex/update_checker_spec.rb index ab75ff24ca..b687ad15cd 100644 --- a/hex/spec/dependabot/hex/update_checker_spec.rb +++ b/hex/spec/dependabot/hex/update_checker_spec.rb @@ -22,12 +22,12 @@ end let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }] + })] end let(:ignored_versions) { [] } let(:raise_on_ignored) { false } @@ -278,16 +278,16 @@ context "with good credentials" do let(:hex_pm_org_token) { ENV.fetch("HEX_PM_ORGANIZATION_TOKEN", nil) } let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }, { + }), Dependabot::Credential.new({ "type" => "hex_organization", "organization" => "dependabot", "token" => hex_pm_org_token - }] + })] end it "returns the expected version" do @@ -298,16 +298,16 @@ context "with bad credentials" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }, { + }), Dependabot::Credential.new({ "type" => "hex_organization", "organization" => "dependabot", "token" => "111f6cbeffc6e14c6a884f0111caff3e" - }] + })] end it "raises a helpful error" do @@ -321,15 +321,15 @@ context "with no token" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }, { + }), Dependabot::Credential.new({ "type" => "hex_organization", "organization" => "dependabot" - }] + })] end # This needs to changes to the Elixir helper @@ -344,12 +344,12 @@ context "with no credentials" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "git_source", "host" => "github.com", "username" => "x-access-token", "password" => "token" - }] + })] end # The Elixir process hangs waiting for input in this case. This spec @@ -378,12 +378,12 @@ context "with good credentials" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "hex_repository", "repo" => "dependabot", "auth_key" => "d6fc2b6n6h7katic6vuq6k5e2csahcm4", "url" => "https://dependabot-private.fly.dev" - }] + })] end it { is_expected.to eq(Dependabot::Hex::Version.new("1.1.0")) } @@ -391,12 +391,12 @@ context "with bad credentials" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "hex_repository", "repo" => "dependabot", "auth_key" => "111f6cbeffc6e14c6a884f0111caff3e", "url" => "https://dependabot-private.fly.dev" - }] + })] end it "raises a helpful error" do @@ -411,13 +411,13 @@ context "with correct public key fingerprint verification" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "hex_repository", "repo" => "dependabot", "auth_key" => "d6fc2b6n6h7katic6vuq6k5e2csahcm4", "url" => "https://dependabot-private.fly.dev", "public_key_fingerprint" => "SHA256:jn36tNgSXuEljoob8fkejX9LIyXqCcwShjRGps7RVgw" - }] + })] end it { is_expected.to eq(Dependabot::Hex::Version.new("1.1.0")) } @@ -425,13 +425,13 @@ context "with incorrect public key fingerprint verification" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "hex_repository", "repo" => "dependabot", "auth_key" => "d6fc2b6n6h7katic6vuq6k5e2csahcm4", "url" => "https://dependabot-private.fly.dev", "public_key_fingerprint" => "SHA256:kejX9LIyXqCcwShjRGps7RVgjn36tNgSXuEljoob8fw" - }] + })] end it "raises a helpful error" do @@ -446,16 +446,16 @@ context "with dependencies on both a private organization and private repo" do let(:credentials) do - [{ + [Dependabot::Credential.new({ "type" => "hex_organization", "organization" => "dependabot", "token" => "855f6cbeffc6e14c6a884f0111caff3e" - }, { + }), Dependabot::Credential.new({ "type" => "hex_repository", "repo" => "dependabot", "auth_key" => "d6fc2b6n6h7katic6vuq6k5e2csahcm4", "url" => "https://dependabot-private.fly.dev" - }] + })] end it { is_expected.to eq(Dependabot::Hex::Version.new("1.1.0")) } diff --git a/maven/lib/dependabot/maven/update_checker/version_finder.rb b/maven/lib/dependabot/maven/update_checker/version_finder.rb index 4716529c92..f8a7380264 100644 --- a/maven/lib/dependabot/maven/update_checker/version_finder.rb +++ b/maven/lib/dependabot/maven/update_checker/version_finder.rb @@ -217,7 +217,7 @@ def pom_repository_details def credentials_repository_details credentials - .select { |cred| cred["type"] == "maven_repository" } + .select { |cred| cred["type"] == "maven_repository" && cred["url"] } .map do |cred| { "url" => cred.fetch("url").gsub(%r{/+$}, ""), diff --git a/npm_and_yarn/Dockerfile b/npm_and_yarn/Dockerfile index b76c08c752..6d4dc70b9a 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.14.3 +ARG PNPM_VERSION=8.15.2 # Check for updates at https://github.com/yarnpkg/berry/releases ARG YARN_VERSION=3.7.0 diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb index 3d755304cd..a822032807 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_parser/yarn_lock.rb @@ -22,7 +22,11 @@ def parsed function: "yarn:parseLockfile", args: [Dir.pwd] ) - rescue SharedHelpers::HelperSubprocessFailed + rescue SharedHelpers::HelperSubprocessFailed => e + raise Dependabot::OutOfDisk, e.message if e.message.end_with?("No space left on device") + raise Dependabot::OutOfDisk, e.message if e.message.end_with?("Out of diskspace") + raise Dependabot::OutOfMemory, e.message if e.message.end_with?("MemoryError") + raise Dependabot::DependencyFileNotParseable, @dependency_file.path end end diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/registry_parser.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/registry_parser.rb index 525ba0abfa..a78be58545 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/registry_parser.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/registry_parser.rb @@ -45,12 +45,13 @@ def dependency_name attr_reader :resolved_url, :credentials + # rubocop:disable Metrics/PerceivedComplexity def url_for_relevant_cred resolved_url_host = URI(resolved_url).host credential_matching_url = credentials - .select { |cred| cred["type"] == "npm_registry" } + .select { |cred| cred["type"] == "npm_registry" && cred["registry"] } .sort_by { |cred| cred["registry"].length } .find do |details| next true if resolved_url_host == details["registry"] @@ -70,6 +71,7 @@ def url_for_relevant_cred reg = credential_matching_url["registry"] resolved_url.gsub(/#{Regexp.quote(reg)}.*/, "") + reg end + # rubocop:enable Metrics/PerceivedComplexity end end end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_parser/lockfile_parser_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_parser/lockfile_parser_spec.rb index 7aaa17d3f4..0822e8c095 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_parser/lockfile_parser_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_parser/lockfile_parser_spec.rb @@ -69,6 +69,46 @@ end end end + + context "that contain out of disk/memory error" do + let(:dependency_files) { project_dependency_files("yarn/broken_lockfile") } + + context "because it ran out of disk space" do + before do + allow(Dependabot::SharedHelpers) + .to receive(:run_helper_subprocess) + .and_raise( + Dependabot::SharedHelpers::HelperSubprocessFailed.new( + message: "No space left on device", + error_context: {} + ) + ) + end + + it "raises a helpful error" do + expect { subject } + .to raise_error(Dependabot::OutOfDisk) + end + end + + context "because it ran out of memory" do + before do + allow(Dependabot::SharedHelpers) + .to receive(:run_helper_subprocess) + .and_raise( + Dependabot::SharedHelpers::HelperSubprocessFailed.new( + message: "MemoryError", + error_context: {} + ) + ) + end + + it "raises a helpful error" do + expect { subject } + .to raise_error(Dependabot::OutOfMemory) + end + end + end end context "for pnpm lockfiles" do diff --git a/nuget/helpers/lib/NuGetUpdater/.editorconfig b/nuget/helpers/lib/NuGetUpdater/.editorconfig index 9cf15a88d5..c5fa72bccd 100644 --- a/nuget/helpers/lib/NuGetUpdater/.editorconfig +++ b/nuget/helpers/lib/NuGetUpdater/.editorconfig @@ -24,6 +24,8 @@ insert_final_newline = true #### .NET Coding Conventions #### [*.{cs,vb}] +max_line_length = 0 + # Organize usings dotnet_separate_import_directive_groups = true dotnet_sort_system_directives_first = true @@ -131,7 +133,8 @@ csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true +# The following line is commented out to work around a Rider formatter bug. `true` is the default value. +# csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true @@ -143,6 +146,9 @@ csharp_indent_case_contents_when_block = true csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true +resharper_outdent_statement_labels = true +resharper_indent_raw_literal_string = indent + # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true @@ -256,31 +262,31 @@ dotnet_naming_rule.non_field_members_should_be_pascalcase.style = pascalcase dotnet_naming_symbols.interfaces.applicable_kinds = interface dotnet_naming_symbols.interfaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interfaces.required_modifiers = +dotnet_naming_symbols.interfaces.required_modifiers = dotnet_naming_symbols.enums.applicable_kinds = enum dotnet_naming_symbols.enums.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.enums.required_modifiers = +dotnet_naming_symbols.enums.required_modifiers = dotnet_naming_symbols.events.applicable_kinds = event dotnet_naming_symbols.events.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.events.required_modifiers = +dotnet_naming_symbols.events.required_modifiers = dotnet_naming_symbols.methods.applicable_kinds = method dotnet_naming_symbols.methods.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.methods.required_modifiers = +dotnet_naming_symbols.methods.required_modifiers = dotnet_naming_symbols.properties.applicable_kinds = property dotnet_naming_symbols.properties.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.properties.required_modifiers = +dotnet_naming_symbols.properties.required_modifiers = dotnet_naming_symbols.public_fields.applicable_kinds = field dotnet_naming_symbols.public_fields.applicable_accessibilities = public, internal -dotnet_naming_symbols.public_fields.required_modifiers = +dotnet_naming_symbols.public_fields.required_modifiers = dotnet_naming_symbols.private_fields.applicable_kinds = field dotnet_naming_symbols.private_fields.applicable_accessibilities = private, protected, protected_internal, private_protected -dotnet_naming_symbols.private_fields.required_modifiers = +dotnet_naming_symbols.private_fields.required_modifiers = dotnet_naming_symbols.private_static_fields.applicable_kinds = field dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected @@ -288,15 +294,15 @@ dotnet_naming_symbols.private_static_fields.required_modifiers = static dotnet_naming_symbols.types_and_namespaces.applicable_kinds = namespace, class, struct, interface, enum dotnet_naming_symbols.types_and_namespaces.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types_and_namespaces.required_modifiers = +dotnet_naming_symbols.types_and_namespaces.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +dotnet_naming_symbols.non_field_members.required_modifiers = dotnet_naming_symbols.type_parameters.applicable_kinds = namespace dotnet_naming_symbols.type_parameters.applicable_accessibilities = * -dotnet_naming_symbols.type_parameters.required_modifiers = +dotnet_naming_symbols.type_parameters.required_modifiers = dotnet_naming_symbols.private_constant_fields.applicable_kinds = field dotnet_naming_symbols.private_constant_fields.applicable_accessibilities = private, protected, protected_internal, private_protected @@ -304,7 +310,7 @@ dotnet_naming_symbols.private_constant_fields.required_modifiers = const dotnet_naming_symbols.local_variables.applicable_kinds = local dotnet_naming_symbols.local_variables.applicable_accessibilities = local -dotnet_naming_symbols.local_variables.required_modifiers = +dotnet_naming_symbols.local_variables.required_modifiers = dotnet_naming_symbols.local_constants.applicable_kinds = local dotnet_naming_symbols.local_constants.applicable_accessibilities = local @@ -312,7 +318,7 @@ dotnet_naming_symbols.local_constants.required_modifiers = const dotnet_naming_symbols.parameters.applicable_kinds = parameter dotnet_naming_symbols.parameters.applicable_accessibilities = * -dotnet_naming_symbols.parameters.required_modifiers = +dotnet_naming_symbols.parameters.required_modifiers = dotnet_naming_symbols.public_constant_fields.applicable_kinds = field dotnet_naming_symbols.public_constant_fields.applicable_accessibilities = public, internal @@ -328,37 +334,40 @@ dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = readon dotnet_naming_symbols.local_functions.applicable_kinds = local_function dotnet_naming_symbols.local_functions.applicable_accessibilities = * -dotnet_naming_symbols.local_functions.required_modifiers = +dotnet_naming_symbols.local_functions.required_modifiers = # Naming styles -dotnet_naming_style.pascalcase.required_prefix = -dotnet_naming_style.pascalcase.required_suffix = -dotnet_naming_style.pascalcase.word_separator = +dotnet_naming_style.pascalcase.required_prefix = +dotnet_naming_style.pascalcase.required_suffix = +dotnet_naming_style.pascalcase.word_separator = dotnet_naming_style.pascalcase.capitalization = pascal_case dotnet_naming_style.ipascalcase.required_prefix = I -dotnet_naming_style.ipascalcase.required_suffix = -dotnet_naming_style.ipascalcase.word_separator = +dotnet_naming_style.ipascalcase.required_suffix = +dotnet_naming_style.ipascalcase.word_separator = dotnet_naming_style.ipascalcase.capitalization = pascal_case dotnet_naming_style.tpascalcase.required_prefix = T -dotnet_naming_style.tpascalcase.required_suffix = -dotnet_naming_style.tpascalcase.word_separator = +dotnet_naming_style.tpascalcase.required_suffix = +dotnet_naming_style.tpascalcase.word_separator = dotnet_naming_style.tpascalcase.capitalization = pascal_case dotnet_naming_style._camelcase.required_prefix = _ -dotnet_naming_style._camelcase.required_suffix = -dotnet_naming_style._camelcase.word_separator = +dotnet_naming_style._camelcase.required_suffix = +dotnet_naming_style._camelcase.word_separator = dotnet_naming_style._camelcase.capitalization = camel_case -dotnet_naming_style.camelcase.required_prefix = -dotnet_naming_style.camelcase.required_suffix = -dotnet_naming_style.camelcase.word_separator = +dotnet_naming_style.camelcase.required_prefix = +dotnet_naming_style.camelcase.required_suffix = +dotnet_naming_style.camelcase.word_separator = dotnet_naming_style.camelcase.capitalization = camel_case dotnet_naming_style.s_camelcase.required_prefix = s_ -dotnet_naming_style.s_camelcase.required_suffix = -dotnet_naming_style.s_camelcase.word_separator = +dotnet_naming_style.s_camelcase.required_suffix = +dotnet_naming_style.s_camelcase.word_separator = dotnet_naming_style.s_camelcase.capitalization = camel_case +[NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs] +generated_code = true +disable_formatter = true diff --git a/nuget/helpers/lib/NuGetUpdater/.gitignore b/nuget/helpers/lib/NuGetUpdater/.gitignore index 4c5efb30e9..ce73752685 100644 --- a/nuget/helpers/lib/NuGetUpdater/.gitignore +++ b/nuget/helpers/lib/NuGetUpdater/.gitignore @@ -3,3 +3,4 @@ artifacts/ bin/ obj/ Properties/launchSettings.json +NuGetUpdater.sln.DotSettings.user diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs b/nuget/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs index f90fa93895..72885b846d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetProjects/NuGet.CommandLine/AssemblyMetadataExtractor.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Reflection; + using NuGet.Common; using NuGet.Packaging; using NuGet.Packaging.Core; 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 9db66d7997..8f5d0850ef 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -16,186 +16,187 @@ public class Update : UpdateWorkerTestBase [Fact] public async Task WithSolution() { - await Run(path => new[] - { - "update", - "--repo-root", - path, - "--solution-or-project", - Path.Combine(path, "path/to/solution.sln"), - "--dependency", - "Newtonsoft.Json", - "--new-version", - "13.0.1", - "--previous-version", - "7.0.1", - }, - new[] - { - ("path/to/solution.sln", """ - Microsoft Visual Studio Solution File, Format Version 12.00 - # Visual Studio 14 - VisualStudioVersion = 14.0.22705.0 - MinimumVisualStudioVersion = 10.0.40219.1 - Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "my", "my.csproj", "{782E0C0A-10D3-444D-9640-263D03D2B20C}" - EndProject - Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - 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 - HideSolutionNode = FALSE - EndGlobalSection - EndGlobal - """), - ("path/to/my.csproj", """ - - - - v4.5 - - - - - - - packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - """), - ("path/to/packages.config", """ - - - - """) - }, - new[] - { - ("path/to/my.csproj", """ - - - - v4.5 - - - - - - - packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - """), - ("path/to/packages.config", """ - - - - - """) - }); + await Run(path => + [ + "update", + "--repo-root", + path, + "--solution-or-project", + Path.Combine(path, "path/to/solution.sln"), + "--dependency", + "Newtonsoft.Json", + "--new-version", + "13.0.1", + "--previous-version", + "7.0.1", + ], + [ + ("path/to/solution.sln", """ + Microsoft Visual Studio Solution File, Format Version 12.00 + # Visual Studio 14 + VisualStudioVersion = 14.0.22705.0 + MinimumVisualStudioVersion = 10.0.40219.1 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "my", "my.csproj", "{782E0C0A-10D3-444D-9640-263D03D2B20C}" + EndProject + Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + 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 + HideSolutionNode = FALSE + EndGlobalSection + EndGlobal + """), + ("path/to/my.csproj", """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + """) + ], + [ + ("path/to/my.csproj", """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + + """) + ]); } [Fact] public async Task WithProject() { - await Run(path => new[] - { - "update", - "--repo-root", - path, - "--solution-or-project", - Path.Combine(path, "path/to/my.csproj"), - "--dependency", - "Newtonsoft.Json", - "--new-version", - "13.0.1", - "--previous-version", - "7.0.1", - "--verbose" - }, - new[] - { - ("path/to/my.csproj", """ - - - - v4.5 - - - - - - - packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - """), - ("path/to/packages.config", """ - - - - """) - }, - new[] - { - ("path/to/my.csproj", """ - - - - v4.5 - - - - - - - packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - """), - ("path/to/packages.config", """ - - - - - """) - }); + await Run(path => + [ + "update", + "--repo-root", + path, + "--solution-or-project", + Path.Combine(path, "path/to/my.csproj"), + "--dependency", + "Newtonsoft.Json", + "--new-version", + "13.0.1", + "--previous-version", + "7.0.1", + "--verbose" + ], + [ + ("path/to/my.csproj", """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + """) + ], + [ + ("path/to/my.csproj", """ + + + + v4.5 + + + + + + + packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + """), + ("path/to/packages.config", """ + + + + + """) + ]); } [Fact] public async Task WithDirsProjAndDirectoryBuildPropsThatIsOutOfDirectoryButStillMatchingThePackage() { - await Run(path => new[] - { + await Run(path => + [ "update", - "--repo-root", path, - "--solution-or-project", $"{path}/some-dir/dirs.proj", - "--dependency", "NuGet.Versioning", - "--new-version", "6.6.1", - "--previous-version", "6.1.0", + "--repo-root", + path, + "--solution-or-project", + $"{path}/some-dir/dirs.proj", + "--dependency", + "NuGet.Versioning", + "--new-version", + "6.6.1", + "--previous-version", + "6.1.0", "--verbose" - }, - initialFiles: new[] - { + ], + initialFiles: + [ ("some-dir/dirs.proj", """ @@ -232,16 +233,16 @@ await Run(path => new[] """), ("other-dir/Directory.Build.props", """ - + """) - }, - expectedFiles: new[] - { + ], + expectedFiles: + [ ("some-dir/dirs.proj", """ @@ -250,7 +251,8 @@ await Run(path => new[] """), - ("some-dir/project1/project.csproj", """ + ("some-dir/project1/project.csproj", + """ Exe @@ -278,20 +280,20 @@ await Run(path => new[] """), ("other-dir/Directory.Build.props", """ - + """) - } + ] ); } private static async Task Run(Func getArgs, (string Path, string Content)[] initialFiles, (string, string)[] expectedFiles) { - var actualFiles = await RunUpdate(initialFiles, async (path) => + var actualFiles = await RunUpdate(initialFiles, async path => { var sb = new StringBuilder(); var writer = new StringWriter(sb); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs index bfc5d3e0e5..6549831eba 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs @@ -11,9 +11,9 @@ internal sealed class Program internal static async Task Main(string[] args) { var exitCode = 0; - Action setExitCode = (int code) => exitCode = code; + Action setExitCode = code => exitCode = code; - var command = new RootCommand() + var command = new RootCommand { FrameworkCheckCommand.GetCommand(setExitCode), UpdateCommand.GetCommand(setExitCode), diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs index 3e66e3cc2f..f937725c41 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Files/ProjectBuildFileTests.cs @@ -133,17 +133,26 @@ public void EmptyProject_GetDependencies_ReturnsNoDependencies() } [Theory] - [InlineData( // no change made - @"path\to\file.dll", - @"path\to\file.dll" + // no change made + [InlineData( + // language=csproj + @"path\to\file.dll", + // language=csproj + @"path\to\file.dll" )] - [InlineData( // change from `/` to `\` - "path/to/file.dll", - @"path\to\file.dll" + // change from `/` to `\` + [InlineData( + // language=csproj + "path/to/file.dll", + // language=csproj + @"path\to\file.dll" )] - [InlineData( // multiple changes made - "path1/to1/file1.dllpath2/to2/file2.dll", - @"path1\to1\file1.dllpath2\to2\file2.dll" + // multiple changes made + [InlineData( + // language=csproj + "path1/to1/file1.dllpath2/to2/file2.dll", + // language=csproj + @"path1\to1\file1.dllpath2\to2\file2.dll" )] public void ReferenceHintPathsCanBeNormalized(string originalXml, string expectedXml) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs index 9aef2b7048..87378fe3e1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/CompatibilityCheckerFacts.cs @@ -20,7 +20,7 @@ public class CompatibilityCheckerFacts [InlineData("net4.8", "netstandard1.3")] public void PackageContainsCompatibleFramework(string projectTfm, string packageTfm) { - var result = CompatibilityChecker.IsCompatible(new[] { projectTfm }, new[] { packageTfm }, new Logger(verbose: true)); + var result = CompatibilityChecker.IsCompatible([projectTfm], [packageTfm], new Logger(verbose: true)); Assert.True(result); } @@ -37,7 +37,7 @@ public void PackageContainsCompatibleFramework(string projectTfm, string package [InlineData("net7.0", "net48")] public void PackageContainsIncompatibleFramework(string projectTfm, string packageTfm) { - var result = CompatibilityChecker.IsCompatible(new[] { projectTfm }, new[] { packageTfm }, new Logger(verbose: true)); + var result = CompatibilityChecker.IsCompatible([projectTfm], [packageTfm], new Logger(verbose: true)); Assert.False(result); } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs index 507ea7e072..b77608420b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/FrameworkCompatibilityServiceFacts.cs @@ -41,11 +41,11 @@ public void EmptyPackageFrameworksReturnsEmptySet() public void UnknownSupportedPackageReturnsSetWithSameFramework() { var framework = NuGetFramework.Parse("net45-client"); - var frameworks = new List() { framework }; + var frameworks = new List { framework }; var compatible = _service.GetCompatibleFrameworks(frameworks); Assert.False(framework.IsUnsupported); - Assert.Equal(expected: 1, compatible.Count); + Assert.Single(compatible); Assert.Contains(framework, compatible); } @@ -57,10 +57,10 @@ public void UnsupportedPackageFrameworksReturnsEmptySet(string unsupportedFramew { var unsupportedFramework = NuGetFramework.Parse(unsupportedFrameworkName); - var result = _service.GetCompatibleFrameworks(new List() { unsupportedFramework }); + var result = _service.GetCompatibleFrameworks([unsupportedFramework]); Assert.True(unsupportedFramework.IsUnsupported); - Assert.Equal(expected: 0, actual: result.Count); + Assert.Empty(result); } [Theory] @@ -71,10 +71,10 @@ public void PCLPackageFrameworksReturnsEmptySet(string pclFrameworkName) { var portableFramework = NuGetFramework.Parse(pclFrameworkName); - var result = _service.GetCompatibleFrameworks(new List() { portableFramework }); + var result = _service.GetCompatibleFrameworks([portableFramework]); Assert.True(portableFramework.IsPCL); - Assert.Equal(expected: 0, actual: result.Count); + Assert.Empty(result); } [Theory] @@ -113,7 +113,7 @@ public void WindowsPlatformVersionsShouldContainAllSpecifiedFrameworks(string wi projectFrameworks.Add(NuGetFramework.Parse(frameworkName)); } - var compatibleFrameworks = _service.GetCompatibleFrameworks(new HashSet() { packageFramework }); + var compatibleFrameworks = _service.GetCompatibleFrameworks([packageFramework]); Assert.Equal(windowsProjectFrameworks.Length, compatibleFrameworks.Count); var containsAllCompatibleFrameworks = compatibleFrameworks.All(cf => projectFrameworks.Contains(cf)); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs index bd72daeaad..c52c656384 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/FrameworkChecker/SupportedFrameworkFacts.cs @@ -49,7 +49,7 @@ public void SupportedFrameworksContainsCommonFrameworksWithNoDeprecatedFramework foreach (var field in fields) { - var framework = (NuGetFramework)field.GetValue(null); + var framework = (NuGetFramework)field.GetValue(null)!; if (DeprecatedFrameworks.Contains(framework)) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs index 3ba8115816..fdb2f0d74c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/PackagesConfigUpdaterTests.cs @@ -23,8 +23,8 @@ public void PathToPackagesDirectoryCanBeDetermined(string projectContents, strin public static IEnumerable PackagesDirectoryPathTestData() { // project with namespace - yield return new object[] - { + yield return + [ """ @@ -38,11 +38,11 @@ public static IEnumerable PackagesDirectoryPathTestData() "Newtonsoft.Json", "7.0.1", @"..\packages" - }; + ]; // project without namespace - yield return new object[] - { + yield return + [ """ @@ -56,11 +56,11 @@ public static IEnumerable PackagesDirectoryPathTestData() "Newtonsoft.Json", "7.0.1", @"..\packages" - }; + ]; // project with non-standard packages path - yield return new object[] - { + yield return + [ """ @@ -74,6 +74,6 @@ public static IEnumerable PackagesDirectoryPathTestData() "Newtonsoft.Json", "7.0.1", @"..\not-a-path-you-would-expect" - }; + ]; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs index 71cba8a306..c74025d214 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorker.DirsProj.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -23,53 +24,53 @@ await TestUpdateForDirsProj("Newtonsoft.Json", "9.0.1", "13.0.1", // initial projectContents: """ - + """, - additionalFiles: new (string Path, string Content)[] - { + additionalFiles: + [ ("src/test-project.csproj", - """ - - - netstandard2.0 - - - - - - - """) - }, + """ + + + netstandard2.0 + + + + + + + """) + ], // expected expectedProjectContents: """ - + """, - additionalFilesExpected: new (string Path, string Content)[] - { + additionalFilesExpected: + [ ("src/test-project.csproj", - """ - - - netstandard2.0 - - - - - - - """) - }); + """ + + + netstandard2.0 + + + + + + + """) + ]); } [Fact] @@ -100,73 +101,225 @@ await TestUpdateForDirsProj("Newtonsoft.Json", "9.0.1", "13.0.1", // initial projectContents: """ + + + + + + + """, + additionalFiles: + [ + ("src/dirs.proj", + """ + + + + + + + """), + ("src/test-project/test-project.csproj", + """ + + + netstandard2.0 + + + + + + + """) + ], + // expected + expectedProjectContents: """ + + """, - additionalFiles: new (string Path, string Content)[] - { + additionalFilesExpected: + [ ("src/dirs.proj", - """ - + """ + + + + + - - - + + """), + ("src/test-project/test-project.csproj", + """ + + + netstandard2.0 + + + + + + + """) + ]); + } + + [Fact] + public async Task UpdateSingleDependencyInNestedDirsProjUsingWildcard() + { + await TestUpdateForDirsProj("Newtonsoft.Json", "9.0.1", "13.0.1", + // initial + projectContents: """ + + + + + + + + """, + additionalFiles: + [ + ("src/dirs.proj", + """ + + + + + - - """), + + """), ("src/test-project/test-project.csproj", - """ - - - netstandard2.0 - - - - - - - """) - }, + """ + + + netstandard2.0 + + + + + + + """) + ], // expected expectedProjectContents: """ + + + + + + + """, + additionalFilesExpected: + [ + ("src/dirs.proj", + """ + + + + + + + """), + ("src/test-project/test-project.csproj", + """ + + + netstandard2.0 + + + + + + + """) + ]); + } + + [Fact] + public async Task UpdateSingleDependencyInNestedDirsProjUsingRecursiveWildcard() + { + await TestUpdateForDirsProj("Newtonsoft.Json", "9.0.1", "13.0.1", + // initial + projectContents: """ + + - + """, - additionalFilesExpected: new (string Path, string Content)[] - { + additionalFiles: + [ ("src/dirs.proj", - """ - + """ + + + + + - - - + + """), + ("src/test-project/test-project.csproj", + """ + + + netstandard2.0 + + + + + + + """) + ], + // expected + expectedProjectContents: """ + + + + + + + + """, + additionalFilesExpected: + [ + ("src/dirs.proj", + """ + + + + + - - """), + + """), ("src/test-project/test-project.csproj", - """ - - - netstandard2.0 - - - - - - - """) - }); + """ + + + netstandard2.0 + + + + + + + """) + ]); } static async Task TestUpdateForDirsProj( @@ -179,8 +332,8 @@ static async Task TestUpdateForDirsProj( (string Path, string Content)[]? additionalFiles = null, (string Path, string Content)[]? additionalFilesExpected = null) { - additionalFiles ??= Array.Empty<(string Path, string Content)>(); - additionalFilesExpected ??= Array.Empty<(string Path, string Content)>(); + additionalFiles ??= []; + additionalFilesExpected ??= []; var projectName = "dirs"; var projectFileName = $"{projectName}.proj"; 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 4af9edaaae..7dff9be25f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs @@ -18,7 +18,15 @@ protected static Task TestNoChangeforProject( bool isTransitive = false, (string Path, string Content)[]? additionalFiles = null, string projectFilePath = "test-project.csproj") - => TestUpdateForProject(dependencyName, oldVersion, newVersion, (projectFilePath, projectContents), expectedProjectContents: projectContents, isTransitive, additionalFiles, additionalFilesExpected: additionalFiles); + => TestUpdateForProject( + dependencyName, + oldVersion, + newVersion, + (projectFilePath, projectContents), + expectedProjectContents: projectContents, + isTransitive, + additionalFiles, + additionalFilesExpected: additionalFiles); protected static Task TestUpdateForProject( string dependencyName, @@ -30,10 +38,15 @@ protected static Task TestUpdateForProject( (string Path, string Content)[]? additionalFiles = null, (string Path, string Content)[]? additionalFilesExpected = null, string projectFilePath = "test-project.csproj") - { - var projectFile = (Path: projectFilePath, Content: projectContents); - return TestUpdateForProject(dependencyName, oldVersion, newVersion, projectFile, expectedProjectContents, isTransitive, additionalFiles, additionalFilesExpected); - } + => TestUpdateForProject( + dependencyName, + oldVersion, + newVersion, + (Path: projectFilePath, Content: projectContents), + expectedProjectContents, + isTransitive, + additionalFiles, + additionalFilesExpected); protected static async Task TestUpdateForProject( string dependencyName, @@ -45,8 +58,8 @@ protected static async Task TestUpdateForProject( (string Path, string Content)[]? additionalFiles = null, (string Path, string Content)[]? additionalFilesExpected = null) { - additionalFiles ??= Array.Empty<(string Path, string Content)>(); - additionalFilesExpected ??= Array.Empty<(string Path, string Content)>(); + additionalFiles ??= []; + additionalFilesExpected ??= []; var projectFilePath = projectFile.Path; var projectName = Path.GetFileNameWithoutExtension(projectFilePath); @@ -76,7 +89,7 @@ protected static async Task TestUpdateForProject( """; var testFiles = new[] { (slnName, slnContent), projectFile }.Concat(additionalFiles).ToArray(); - var actualResult = await RunUpdate(testFiles, async (temporaryDirectory) => + var actualResult = await RunUpdate(testFiles, async temporaryDirectory => { var slnPath = Path.Combine(temporaryDirectory, slnName); var worker = new UpdaterWorker(new Logger(verbose: true)); @@ -94,7 +107,7 @@ protected static async Task TestUpdateForProject( using var tempDir = new TemporaryDirectory(); foreach (var file in files) { - var localPath = file.Path.StartsWith("/") ? file.Path[1..] : file.Path; // remove path rooting character + var localPath = file.Path.StartsWith('/') ? file.Path[1..] : file.Path; // remove path rooting character var filePath = Path.Combine(tempDir.DirectoryPath, localPath); var directoryPath = Path.GetDirectoryName(filePath); Directory.CreateDirectory(directoryPath!); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs index 4547ed8f97..f1880e12a2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.DotNetTools.cs @@ -23,7 +23,7 @@ await TestNoChangeforProject("Microsoft.BotSay", "1.0.0", "1.1.0", netstandard2.0 - + @@ -41,29 +41,29 @@ await TestNoChangeforProject("Microsoft.BotSay", "1.0.0", "1.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ (".config/dotnet-tools.json", """ - { - "version": 1, - "isRoot": true, - "tools": { - "dotnetsay": { - "version": "2.1.3", - "commands": [ - "dotnetsay" - ] + { + "version": 1, + "isRoot": true, + "tools": { + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } } } - } - """) - }); + """) + ]); } [Fact] @@ -77,29 +77,29 @@ await TestNoChangeforProject("Microsoft.BotSay", "1.0.0", "1.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("eng/.config/dotnet-tools.json", """ - { - "version": 1, - "isRoot": true, - "tools": { - "dotnetsay": { - "version": "2.1.3", - "commands": [ - "dotnetsay" - ] + { + "version": 1, + "isRoot": true, + "tools": { + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } } } - } - """) - }); + """) + ]); } [Fact] @@ -112,70 +112,70 @@ await TestUpdateForProject("Microsoft.BotSay", "1.0.0", "1.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ (".config/dotnet-tools.json", """ - { - "version": 1, - "isRoot": true, - "tools": { - "microsoft.botsay": { - "version": "1.0.0", - "commands": [ - "botsay" - ] - }, - "dotnetsay": { - "version": "2.1.3", - "commands": [ - "dotnetsay" - ] + { + "version": 1, + "isRoot": true, + "tools": { + "microsoft.botsay": { + "version": "1.0.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } } } - } - """) - }, + """) + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ (".config/dotnet-tools.json", """ - { - "version": 1, - "isRoot": true, - "tools": { - "microsoft.botsay": { - "version": "1.1.0", - "commands": [ - "botsay" - ] - }, - "dotnetsay": { - "version": "2.1.3", - "commands": [ - "dotnetsay" - ] + { + "version": 1, + "isRoot": true, + "tools": { + "microsoft.botsay": { + "version": "1.1.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } } } - } - """) - }); + """) + ]); } [Fact] @@ -188,74 +188,74 @@ await TestUpdateForProject("Microsoft.BotSay", "1.0.0", "1.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ (".config/dotnet-tools.json", """ - { - // this is a comment - "version": 1, - "isRoot": true, - "tools": { - "microsoft.botsay": { - // this is a deep comment - "version": "1.0.0", - "commands": [ - "botsay" - ] - }, - "dotnetsay": { - "version": "2.1.3", - "commands": [ - "dotnetsay" - ] + { + // this is a comment + "version": 1, + "isRoot": true, + "tools": { + "microsoft.botsay": { + // this is a deep comment + "version": "1.0.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } } } - } - """) - }, + """) + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ (".config/dotnet-tools.json", """ - { - // this is a comment - "version": 1, - "isRoot": true, - "tools": { - "microsoft.botsay": { - // this is a deep comment - "version": "1.1.0", - "commands": [ - "botsay" - ] - }, - "dotnetsay": { - "version": "2.1.3", - "commands": [ - "dotnetsay" - ] + { + // this is a comment + "version": 1, + "isRoot": true, + "tools": { + "microsoft.botsay": { + // this is a deep comment + "version": "1.1.0", + "commands": [ + "botsay" + ] + }, + "dotnetsay": { + "version": "2.1.3", + "commands": [ + "dotnetsay" + ] + } } } - } - """) - }); + """) + ]); } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs index a26022147b..458a63d2fc 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.GlobalJson.cs @@ -23,7 +23,7 @@ await TestNoChangeforProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", netstandard2.0 - + @@ -41,14 +41,14 @@ await TestNoChangeforProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("global.json", """ { "sdk": { @@ -57,7 +57,7 @@ await TestNoChangeforProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", } } """) - }); + ]); } [Fact] @@ -71,14 +71,14 @@ await TestNoChangeforProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", netstandard2.0 - + > """, - additionalFiles: new[] - { + additionalFiles: + [ ("eng/global.json", """ { "sdk": { @@ -90,7 +90,7 @@ await TestNoChangeforProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", } } """) - }); + ]); } [Fact] @@ -104,14 +104,14 @@ await TestUpdateForProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("src/global.json", """ { "sdk": { @@ -123,21 +123,21 @@ await TestUpdateForProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", } } """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("src/global.json", """ { "sdk": { @@ -149,7 +149,7 @@ await TestUpdateForProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", } } """) - }); + ]); } [Fact] @@ -162,14 +162,14 @@ await TestUpdateForProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("global.json", """ { // this is a comment @@ -183,21 +183,21 @@ await TestUpdateForProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", } } """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("global.json", """ { // this is a comment @@ -211,7 +211,7 @@ await TestUpdateForProject("Microsoft.Build.Traversal", "3.2.0", "4.1.0", } } """) - }); + ]); } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs index 04289c3611..4cd72ed88b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Mixed.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -38,8 +37,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFiles: new[] - { + additionalFiles: + [ ("packages.config", """ @@ -52,8 +51,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", - """), - }, + """) + ], // expected expectedProjectContents: """ @@ -73,8 +72,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("packages.config", """ @@ -87,8 +86,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", - """), - }); + """) + ]); } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs index 9c750eeb07..f052c81854 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs @@ -278,8 +278,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFiles: new[] - { + additionalFiles: + [ ("app.config", """ @@ -292,7 +292,7 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """) - }, + ], expectedProjectContents: """ @@ -320,8 +320,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("app.config", """ @@ -334,7 +334,7 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -425,8 +425,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFiles: new[] - { + additionalFiles: + [ ("web.config", """ @@ -439,7 +439,7 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """) - }, + ], expectedProjectContents: """ @@ -525,8 +525,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("web.config", """ @@ -539,7 +539,7 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -630,15 +630,15 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFiles: new[] - { + additionalFiles: + [ ("web.config", """ """) - }, + ], expectedProjectContents: """ @@ -724,8 +724,8 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("web.config", """ @@ -738,7 +738,7 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -903,7 +903,176 @@ await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", """); } - protected static async Task TestUpdateForProject( + [Fact] + public async Task PackagesConfigUpdateIsNotThwartedBy_VSToolsPath_PropertyBeingSetInUserCode() + { + await TestUpdateForProject("Newtonsoft.Json", "7.0.1", "13.0.1", + projectContents: """ + + + Debug + AnyCPU + + + 2.0 + 68ed3303-52a0-47b8-a687-3abbb07530da + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + TestProject + TestProject + v4.5 + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + + packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + C:\some\path\that\does\not\exist + + + + + + """, + packagesConfigContents: """ + + + + """, + expectedProjectContents: """ + + + Debug + AnyCPU + + + 2.0 + 68ed3303-52a0-47b8-a687-3abbb07530da + {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + TestProject + TestProject + v4.5 + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\ + TRACE + prompt + 4 + + + + + packages\Newtonsoft.Json.13.0.1\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + C:\some\path\that\does\not\exist + + + + + + """, + expectedPackagesConfigContents: """ + + + + + """); + } + + protected static Task TestUpdateForProject( string dependencyName, string oldVersion, string newVersion, @@ -914,7 +1083,7 @@ protected static async Task TestUpdateForProject( (string Path, string Content)[]? additionalFiles = null, (string Path, string Content)[]? additionalFilesExpected = null) { - var realizedAdditionalFiles = new List<(string Path, string Content)>() + var realizedAdditionalFiles = new List<(string Path, string Content)> { ("packages.config", packagesConfigContents), }; @@ -923,7 +1092,7 @@ protected static async Task TestUpdateForProject( realizedAdditionalFiles.AddRange(additionalFiles); } - var realizedAdditionalFilesExpected = new List<(string Path, string Content)>() + var realizedAdditionalFilesExpected = new List<(string Path, string Content)> { ("packages.config", expectedPackagesConfigContents), }; @@ -932,7 +1101,14 @@ protected static async Task TestUpdateForProject( realizedAdditionalFilesExpected.AddRange(additionalFilesExpected); } - await TestUpdateForProject(dependencyName, oldVersion, newVersion, projectContents, expectedProjectContents, additionalFiles: realizedAdditionalFiles.ToArray(), additionalFilesExpected: realizedAdditionalFilesExpected.ToArray()); + return TestUpdateForProject( + dependencyName, + oldVersion, + newVersion, + projectContents, + expectedProjectContents, + additionalFiles: realizedAdditionalFiles.ToArray(), + additionalFilesExpected: realizedAdditionalFilesExpected.ToArray()); } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs index ec548689e4..220a62a867 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs @@ -30,7 +30,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", {tfm} - + @@ -42,7 +42,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", {tfm} - + @@ -50,6 +50,38 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """); } + [Fact] + public async Task UpdateVersionAttribute_InProjectFile_ForPackageReferenceInclude_Windows() + { + // update Newtonsoft.Json from 9.0.1 to 13.0.1 + await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", + // initial + projectContents: $""" + + + net8.0-windows10.0.19041.0 + win-x64 + + + + + + + """, + // expected + expectedProjectContents: $""" + + + net8.0-windows10.0.19041.0 + win-x64 + + + + + + + """); + } [Theory] [InlineData("$(NewtonsoftJsonVersion")] @@ -76,12 +108,12 @@ await TestNoChangeforProject("Newtonsoft.Json", "9.0.1", "13.0.1", public async Task UpdateFindsNearestNugetConfig_AndSucceeds() { // Clean the cache to ensure we don't find a cached version of packages. - await ProcessEx.RunAsync("dotnet", $"nuget locals -c all"); + await ProcessEx.RunAsync("dotnet", "nuget locals -c all"); // If the Top-Level NugetConfig was found we would have failed. - var privateNugetContent = $""" + var privateNugetContent = """ - + @@ -90,16 +122,16 @@ public async Task UpdateFindsNearestNugetConfig_AndSucceeds() """; await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", projectFile: (Path: "Directory/Project.csproj", Content: """ - - - netstandard2.0 - - - - - - """), - $""" + + + netstandard2.0 + + + + + + """), + """ netstandard2.0 @@ -109,7 +141,8 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """, - additionalFiles: new (string Path, string Content)[] { + additionalFiles: + [ (Path: "NuGet.config", Content: $""" @@ -120,7 +153,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """), (Path: "Directory/NuGet.config", Content: privateNugetContent) - }); + ]); } [Fact] @@ -139,8 +172,9 @@ await TestNoChangeforProject("Newtonsoft.Json", "9.0.1", "13.0.1", """, - additionalFiles: new (string Path, string Content)[] { - (Path: "NuGet.config", Content: $""" + additionalFiles: + [ + (Path: "NuGet.config", Content: """ @@ -151,8 +185,8 @@ await TestNoChangeforProject("Newtonsoft.Json", "9.0.1", "13.0.1", - """), - }); + """) + ]); } [Fact] @@ -161,24 +195,24 @@ public async Task UpdateExactMatchVersionAttribute_InProjectFile_ForPackageRefer // update Newtonsoft.Json from 9.0.1 to 13.0.1 await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", // initial - projectContents: $""" + projectContents: """ net6.0 - + """, // expected - expectedProjectContents: $""" + expectedProjectContents: """ net6.0 - + @@ -194,11 +228,11 @@ await TestUpdateForProject("System.Text.Json", "5.0.1", "5.0.2", isTransitive: t // initial projectContents: """ - + netcoreapp3.1 - + @@ -208,11 +242,11 @@ await TestUpdateForProject("System.Text.Json", "5.0.1", "5.0.2", isTransitive: t // expected expectedProjectContents: """ - + netcoreapp3.1 - + @@ -233,7 +267,7 @@ await TestUpdateForProject("Microsoft.CodeAnalysis.Analyzers", "3.3.0", "3.3.4", netstandard2.0 - + all @@ -248,7 +282,7 @@ await TestUpdateForProject("Microsoft.CodeAnalysis.Analyzers", "3.3.0", "3.3.4", netstandard2.0 - + all @@ -270,7 +304,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + @@ -283,7 +317,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + @@ -303,7 +337,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + @@ -316,7 +350,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + @@ -336,52 +370,52 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true - + """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true - + """) - }); + ]); } [Fact] @@ -395,52 +429,52 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true - + """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true - + """) - }); + ]); } [Fact] @@ -454,7 +488,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 9.0.1 - + @@ -467,7 +501,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 13.0.1 - + @@ -486,7 +520,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 9.0.1 - + @@ -499,7 +533,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 13.0.1 - + @@ -518,7 +552,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 9.0.1 - + @@ -531,7 +565,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 13.0.1 - + @@ -550,7 +584,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 [9.0.1] - + @@ -563,7 +597,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 [13.0.1] - + @@ -582,7 +616,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 9.0.1 - + @@ -596,7 +630,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 13.0.1 - + @@ -616,7 +650,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 9.0.1 - + @@ -630,7 +664,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 13.0.1 - + @@ -650,54 +684,54 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true 9.0.1 - + """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true 13.0.1 - + """) - }); + ]); } [Fact] @@ -711,54 +745,54 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true [9.0.1] - + """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true [13.0.1] - + """) - }); + ]); } [Fact] @@ -772,54 +806,54 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true 9.0.1 - + """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true 13.0.1 - + """) - }); + ]); } [Fact] @@ -835,20 +869,20 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true - + """) - }, + ], // expected expectedProjectContents: """ @@ -857,20 +891,20 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true - + """) - }); + ]); } [Fact] @@ -886,21 +920,21 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ true 9.0.1 - + """) - }, + ], // expected expectedProjectContents: """ @@ -909,21 +943,21 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ true 13.0.1 - + """) - }); + ]); } [Fact] @@ -936,14 +970,14 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props file ("Directory.Build.props", """ @@ -952,21 +986,21 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }, + ], // expected project expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props file ("Directory.Build.props", """ @@ -975,7 +1009,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -986,18 +1020,18 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", projectContents: """ - + netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props file ("my-properties.props", """ @@ -1006,23 +1040,23 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }, + ], // expected project expectedProjectContents: """ - + netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props file ("my-properties.props", """ @@ -1031,7 +1065,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -1045,14 +1079,14 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props files ("Directory.Packages.props", """ @@ -1060,7 +1094,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", true - + @@ -1073,21 +1107,21 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props files ("Directory.Packages.props", """ @@ -1095,7 +1129,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", true - + @@ -1108,7 +1142,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -1122,14 +1156,14 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props files ("Directory.Packages.props", """ @@ -1138,7 +1172,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", true $(NewtonsoftJsonVersion) - + @@ -1151,21 +1185,21 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props files ("Directory.Packages.props", """ @@ -1174,7 +1208,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", true $(NewtonsoftJsonVersion) - + @@ -1187,7 +1221,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -1201,14 +1235,14 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", netstandard2.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props files ("Directory.Packages.props", """ @@ -1217,7 +1251,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", true $(NewtonsoftJsonVersion) - + @@ -1231,21 +1265,21 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }, + ], // expected expectedProjectContents: """ netstandard2.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props files ("Directory.Packages.props", """ @@ -1254,7 +1288,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", true $(NewtonsoftJsonVersion) - + @@ -1268,7 +1302,7 @@ await TestUpdateForProject("Newtonsoft.Json", "9.0.1", "13.0.1", """) - }); + ]); } [Fact] @@ -1347,8 +1381,8 @@ await TestUpdateForProject("Microsoft.Extensions.Http", "2.2.0", "7.0.0", """, - additionalFiles: new[] - { + additionalFiles: + [ ("Versions.props", """ @@ -1357,7 +1391,7 @@ await TestUpdateForProject("Microsoft.Extensions.Http", "2.2.0", "7.0.0", """) - }, + ], expectedProjectContents: """ @@ -1370,8 +1404,8 @@ await TestUpdateForProject("Microsoft.Extensions.Http", "2.2.0", "7.0.0", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Versions.props", """ @@ -1380,7 +1414,7 @@ await TestUpdateForProject("Microsoft.Extensions.Http", "2.2.0", "7.0.0", """) - }); + ]); } [Fact] @@ -1428,8 +1462,8 @@ await TestUpdateForProject("Microsoft.Identity.Web", "2.13.0", "2.13.2", """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ @@ -1444,7 +1478,7 @@ await TestUpdateForProject("Microsoft.Identity.Web", "2.13.0", "2.13.2", """) - }, + ], expectedProjectContents: """ @@ -1458,8 +1492,8 @@ await TestUpdateForProject("Microsoft.Identity.Web", "2.13.0", "2.13.2", """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ @@ -1474,7 +1508,7 @@ await TestUpdateForProject("Microsoft.Identity.Web", "2.13.0", "2.13.2", """) - }); + ]); } [Fact] @@ -1563,13 +1597,13 @@ public async Task AvoidPackageDowngradeWhenUpdatingDependency() await TestUpdateForProject("Microsoft.VisualStudio.Sdk.TestFramework.Xunit", "17.2.7", "17.6.16", projectContents: """ - + $(PreferredTargetFramework) false - + @@ -1581,8 +1615,8 @@ await TestUpdateForProject("Microsoft.VisualStudio.Sdk.TestFramework.Xunit", "17 """, - additionalFiles: new[] - { + additionalFiles: + [ ("Directory.Packages.props", """ @@ -1608,16 +1642,16 @@ await TestUpdateForProject("Microsoft.VisualStudio.Sdk.TestFramework.Xunit", "17 """) - }, + ], expectedProjectContents: """ - + $(PreferredTargetFramework) false - + @@ -1629,8 +1663,8 @@ await TestUpdateForProject("Microsoft.VisualStudio.Sdk.TestFramework.Xunit", "17 """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ ("Directory.Packages.props", """ @@ -1656,7 +1690,7 @@ await TestUpdateForProject("Microsoft.VisualStudio.Sdk.TestFramework.Xunit", "17 """) - }); + ]); } [Fact] @@ -1666,19 +1700,19 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t // initial projectContents: """ - + net5.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props files ("Directory.Packages.props", """ @@ -1690,15 +1724,15 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t """) - }, + ], // expected expectedProjectContents: """ - + net5.0 - + @@ -1706,8 +1740,8 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props files ("Directory.Packages.props", """ @@ -1720,7 +1754,7 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t """) - }); + ]); } [Fact] @@ -1730,20 +1764,20 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t // initial projectContents: """ - + $(NoWarn);NETSDK1138 net5.0 - + """, - additionalFiles: new[] - { + additionalFiles: + [ // initial props files ("Directory.Packages.props", """ @@ -1756,24 +1790,24 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t """) - }, + ], // expected expectedProjectContents: """ - + $(NoWarn);NETSDK1138 net5.0 - + """, - additionalFilesExpected: new[] - { + additionalFilesExpected: + [ // expected props files ("Directory.Packages.props", """ @@ -1787,7 +1821,7 @@ await TestUpdateForProject("System.Text.Json", "5.0.0", "5.0.2", isTransitive: t """) - }); + ]); } [Fact] @@ -1795,17 +1829,17 @@ public async Task PropsFileNameWithDifferentCasing() { await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", projectContents: """ - - - net7.0 - - - - - - """, - additionalFiles: new[] - { + + + net7.0 + + + + + + """, + additionalFiles: + [ ("Directory.Build.props", """ @@ -1819,20 +1853,20 @@ await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", """) - }, + ], // no change expectedProjectContents: """ - - - net7.0 - - - - - - """, - additionalFilesExpected: new[] - { + + + net7.0 + + + + + + """, + additionalFilesExpected: + [ // no change ("Directory.Build.props", """ @@ -1847,7 +1881,7 @@ await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", """) - } + ] ); } @@ -1857,25 +1891,25 @@ public async Task VersionAttributeWithDifferentCasing_VersionNumberInline() // the version attribute in the project has an all lowercase name await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", projectContents: """ - - - net7.0 - - - - - - """, + + + net7.0 + + + + + + """, expectedProjectContents: """ - - - net7.0 - - - - - - """ + + + net7.0 + + + + + + """ ); } @@ -1885,17 +1919,17 @@ public async Task VersionAttributeWithDifferentCasing_VersionNumberInProperty() // the version attribute in the project has an all lowercase name await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", projectContents: """ - - - net7.0 - - - - - - """, - additionalFiles: new[] - { + + + net7.0 + + + + + + """, + additionalFiles: + [ ("Directory.Build.props", """ @@ -1908,20 +1942,20 @@ await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", """) - }, + ], // no change expectedProjectContents: """ - - - net7.0 - - - - - - """, - additionalFilesExpected: new[] - { + + + net7.0 + + + + + + """, + additionalFilesExpected: + [ // no change ("Directory.Build.props", """ @@ -1936,7 +1970,7 @@ await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", """) - } + ] ); } @@ -1946,17 +1980,17 @@ public async Task DirectoryPackagesPropsDoesCentralPackagePinningGetsUpdatedIfTr await TestUpdateForProject("xunit.assert", "2.5.2", "2.5.3", isTransitive: true, projectContents: """ - - - net7.0 - - - - - - """, - additionalFiles: new[] - { + + + net7.0 + + + + + + """, + additionalFiles: + [ ("Directory.Packages.props", """ @@ -1969,19 +2003,19 @@ await TestUpdateForProject("xunit.assert", "2.5.2", "2.5.3", """) - }, + ], expectedProjectContents: """ - - - net7.0 - - - - - - """, - additionalFilesExpected: new[] - { + + + net7.0 + + + + + + """, + additionalFilesExpected: + [ ("Directory.Packages.props", """ @@ -1994,7 +2028,7 @@ await TestUpdateForProject("xunit.assert", "2.5.2", "2.5.3", """) - } + ] ); } @@ -2004,17 +2038,17 @@ public async Task DirectoryPackagesPropsDoesNotGetDuplicateEntryIfCentralTransit await TestUpdateForProject("xunit.assert", "2.5.2", "2.5.3", isTransitive: true, projectContents: """ - - - net7.0 - - - - - - """, - additionalFiles: new[] - { + + + net7.0 + + + + + + """, + additionalFiles: + [ ("Directory.Packages.props", """ @@ -2027,19 +2061,19 @@ await TestUpdateForProject("xunit.assert", "2.5.2", "2.5.3", """) - }, + ], expectedProjectContents: """ - - - net7.0 - - - - - - """, - additionalFilesExpected: new[] - { + + + net7.0 + + + + + + """, + additionalFilesExpected: + [ ("Directory.Packages.props", """ @@ -2052,7 +2086,7 @@ await TestUpdateForProject("xunit.assert", "2.5.2", "2.5.3", """) - } + ] ); } @@ -2061,25 +2095,25 @@ public async Task PackageWithFourPartVersionCanBeUpdated() { await TestUpdateForProject("AWSSDK.Core", "3.7.204.13", "3.7.204.14", projectContents: """ - - - net7.0 - - - - - - """, + + + net7.0 + + + + + + """, expectedProjectContents: """ - - - net7.0 - - - - - - """ + + + net7.0 + + + + + + """ ); } @@ -2088,25 +2122,25 @@ public async Task PackageWithOnlyBuildTargetsCanBeUpdated() { await TestUpdateForProject("Microsoft.Windows.Compatibility", "7.0.0", "8.0.0", projectContents: """ - - - net5.0 - - - - - - """, + + + net5.0 + + + + + + """, expectedProjectContents: """ - - - net5.0 - - - - - - """ + + + net5.0 + + + + + + """ ); } @@ -2115,31 +2149,31 @@ public async Task UpdatePackageVersionFromPropertiesWithAndWithoutConditions() { await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", projectContents: """ - - - netstandard2.0 - 7.0.1 - 12.0.1 - 9.0.1 - - - - - - """, + + + netstandard2.0 + 7.0.1 + 12.0.1 + 9.0.1 + + + + + + """, expectedProjectContents: """ - - - netstandard2.0 - 7.0.1 - 13.0.1 - 9.0.1 - - - - - - """ + + + netstandard2.0 + 7.0.1 + 13.0.1 + 9.0.1 + + + + + + """ ); } @@ -2148,29 +2182,29 @@ public async Task UpdatePackageVersionFromPropertyWithConditionCheckingForEmptyS { await TestUpdateForProject("Newtonsoft.Json", "12.0.1", "13.0.1", projectContents: """ - - - netstandard2.0 - 12.0.1 - 9.0.1 - - - - - - """, + + + netstandard2.0 + 12.0.1 + 9.0.1 + + + + + + """, expectedProjectContents: """ - - - netstandard2.0 - 13.0.1 - 9.0.1 - - - - - - """ + + + netstandard2.0 + 13.0.1 + 9.0.1 + + + + + + """ ); } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs index 7268509aa5..87737f4982 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/JsonHelperTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; + using NuGetUpdater.Core.Utilities; + using Xunit; namespace NuGetUpdater.Core.Test.Utilities; @@ -18,8 +20,8 @@ public void UpdateJsonPreservingComments(string json, string[] propertyPath, str public static IEnumerable JsonUpdaterTestData() { - yield return new object[] - { + yield return + [ // json """ { @@ -75,10 +77,10 @@ public static IEnumerable JsonUpdaterTestData() } } """ - }; + ]; - yield return new object[] - { + yield return + [ // json """ { @@ -119,11 +121,11 @@ public static IEnumerable JsonUpdaterTestData() } } """ - }; + ]; // differing case between `propertyPath` and the actual property values - yield return new object[] - { + yield return + [ // json """ { @@ -177,11 +179,11 @@ public static IEnumerable JsonUpdaterTestData() } } """ - }; + ]; // shallow property path - yield return new object[] - { + yield return + [ // original json """ { @@ -209,11 +211,11 @@ public static IEnumerable JsonUpdaterTestData() "path2": "new-value" } """ - }; + ]; // line comment after comma - yield return new object[] - { + yield return + [ // original json """ { @@ -234,6 +236,6 @@ public static IEnumerable JsonUpdaterTestData() "property2": "updated-value" } """ - }; + ]; } } 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 839ba1f23b..441b67a070 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -128,7 +128,7 @@ public void TfmsCanBeDeterminedFromProjectContents(string projectContents, strin } [Theory] - [MemberData(nameof(GetTopLevelPackageDependenyInfosTestData))] + [MemberData(nameof(GetTopLevelPackageDependencyInfosTestData))] public async Task TopLevelPackageDependenciesCanBeDetermined((string Path, string Content)[] buildFileContents, Dependency[] expectedTopLevelDependencies) { using var testDirectory = new TemporaryDirectory(); @@ -140,7 +140,7 @@ public async Task TopLevelPackageDependenciesCanBeDetermined((string Path, strin buildFiles.Add(ProjectBuildFile.Parse(testDirectory.DirectoryPath, fullPath, content)); } - var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependenyInfos(buildFiles.ToImmutableArray()); + var actualTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles.ToImmutableArray()); Assert.Equal(expectedTopLevelDependencies, actualTopLevelDependencies); } @@ -167,7 +167,11 @@ public async Task AllPackageDependenciesCanBeTraversed() new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown), new("NETStandard.Library", "2.0.3", DependencyType.Unknown), }; - var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(temp.DirectoryPath, temp.DirectoryPath, "netstandard2.0", new[] { new Dependency("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown) }); + var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + temp.DirectoryPath, + temp.DirectoryPath, + "netstandard2.0", + [new Dependency("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown)]); Assert.Equal(expectedDependencies, actualDependencies); } @@ -251,7 +255,8 @@ public async Task AllPackageDependencies_DoNotTruncateLongDependencyLists() new("MSTest.TestAdapter", "2.1.0", DependencyType.Unknown), new("NETStandard.Library", "2.0.3", DependencyType.Unknown), }; - var packages = new[] { + var packages = new[] + { new Dependency("System", "4.1.311.2", DependencyType.Unknown), new Dependency("System.Core", "3.5.21022.801", DependencyType.Unknown), new Dependency("Moq", "4.16.1", DependencyType.Unknown), @@ -269,12 +274,13 @@ public async Task AllPackageDependencies_DoNotTruncateLongDependencyLists() new Dependency("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) }; var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(temp.DirectoryPath, temp.DirectoryPath, "netstandard2.0", packages); - for(int i = 0; i < actualDependencies.Length; i++) + for (int i = 0; i < actualDependencies.Length; i++) { var ad = actualDependencies[i]; var ed = expectedDependencies[i]; Assert.Equal(ed, ad); } + Assert.Equal(expectedDependencies, actualDependencies); } @@ -301,7 +307,8 @@ public async Task AllPackageDependencies_DoNotIncludeUpdateOnlyPackages() new("System.Threading.Tasks.Extensions", "4.5.4", DependencyType.Unknown), new("NETStandard.Library", "2.0.3", DependencyType.Unknown), }; - var packages = new[] { + var packages = new[] + { new Dependency("Microsoft.Extensions.Http", "7.0.0", DependencyType.Unknown), new Dependency("Newtonsoft.Json", "12.0.1", DependencyType.Unknown, IsUpdate: true) }; @@ -326,11 +333,16 @@ public async Task AllPackageDependenciesCanBeFoundWithNuGetConfig() Environment.SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", tempNuGetHttpCacheDirectory); // First validate that we are unable to find dependencies for the package version without a NuGet.config. - var dependenciesNoNuGetConfig = await MSBuildHelper.GetAllPackageDependenciesAsync(temp.DirectoryPath, temp.DirectoryPath, "netstandard2.0", new[] { new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown) }); - Assert.Equal(Array.Empty(), dependenciesNoNuGetConfig); + var dependenciesNoNuGetConfig = await MSBuildHelper.GetAllPackageDependenciesAsync( + temp.DirectoryPath, + temp.DirectoryPath, + "netstandard2.0", + [new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown)]); + Assert.Equal([], dependenciesNoNuGetConfig); // Write the NuGet.config and try again. - await File.WriteAllTextAsync(Path.Combine(temp.DirectoryPath, "NuGet.Config"), """ + await File.WriteAllTextAsync( + Path.Combine(temp.DirectoryPath, "NuGet.Config"), """ @@ -355,7 +367,12 @@ await File.WriteAllTextAsync(Path.Combine(temp.DirectoryPath, "NuGet.Config"), " new("Microsoft.CodeAnalysis.Analyzers", "3.3.4", DependencyType.Unknown), new("NETStandard.Library", "2.0.3", DependencyType.Unknown), }; - var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync(temp.DirectoryPath, temp.DirectoryPath, "netstandard2.0", new[] { new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown) }); + var actualDependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + temp.DirectoryPath, + temp.DirectoryPath, + "netstandard2.0", + [new Dependency("Microsoft.CodeAnalysis.Common", "4.8.0-3.23457.5", DependencyType.Unknown)] + ); Assert.Equal(expectedDependencies, actualDependencies); } finally @@ -366,11 +383,11 @@ await File.WriteAllTextAsync(Path.Combine(temp.DirectoryPath, "NuGet.Config"), " } } - public static IEnumerable GetTopLevelPackageDependenyInfosTestData() + public static IEnumerable GetTopLevelPackageDependencyInfosTestData() { // simple case - yield return new object[] - { + yield return + [ // build file contents new[] { @@ -387,11 +404,11 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() { new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) } - }; + ]; // version is a child-node of the package reference - yield return new object[] - { + yield return + [ // build file contents new[] { @@ -410,11 +427,11 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() { new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) } - }; + ]; // version is in property in same file - yield return new object[] - { + yield return + [ // build file contents new[] { @@ -434,11 +451,11 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() { new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) } - }; + ]; // version is a property not triggered by a condition - yield return new object[] - { + yield return + [ // build file contents new[] { @@ -460,7 +477,7 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() { new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) } - }; + ]; // version is a property not triggered by a quoted condition yield return new object[] @@ -489,8 +506,8 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() }; // version is a property with a condition checking for an empty string - yield return new object[] - { + yield return + [ // build file contents new[] { @@ -512,7 +529,7 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() { new("Newtonsoft.Json", "12.0.1", DependencyType.Unknown) } - }; + ]; // version is a property with a quoted condition checking for an empty string yield return new object[] @@ -541,19 +558,19 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() }; // version is set in one file, used in another - yield return new object[] - { + yield return + [ // build file contents new[] { ("Packages.props", """ - - - - - - - """), + + + + + + + """), ("project.csproj", """ @@ -571,11 +588,11 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() new("Azure.Identity", "1.6.0", DependencyType.Unknown), new("Microsoft.Data.SqlClient", "5.1.4", DependencyType.Unknown, IsUpdate: true) } - }; + ]; // version is set in one file, used in another - yield return new object[] - { + yield return + [ // build file contents new[] { @@ -590,13 +607,13 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() """), ("Packages.props", """ - - - - - - - """) + + + + + + + """) }, // expected dependencies new Dependency[] @@ -604,13 +621,13 @@ public static IEnumerable GetTopLevelPackageDependenyInfosTestData() new("Azure.Identity", "1.6.0", DependencyType.Unknown), new("Microsoft.Data.SqlClient", "5.1.4", DependencyType.Unknown, IsUpdate: true) } - }; + ]; } public static IEnumerable SolutionProjectPathTestData() { - yield return new object[] - { + yield return + [ """ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 @@ -644,7 +661,7 @@ public static IEnumerable SolutionProjectPathTestData() { "src/Some.Project/SomeProject.csproj", "src/Some.Project.Test/Some.Project.Test.csproj", - }, - }; + } + ]; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs index 8b96a52d8a..c0f2048995 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs @@ -31,7 +31,7 @@ public static IEnumerable GetDependencyUpdates() // Simple case yield return new object[] { - new [] + new[] { (Path: "src/Project.csproj", Content: """ @@ -44,7 +44,7 @@ public static IEnumerable GetDependencyUpdates() """) }, // starting contents - new [] + new[] { (Path: "src/Project.csproj", Content: """ @@ -61,98 +61,104 @@ public static IEnumerable GetDependencyUpdates() }; // Dependency package has version constraint - yield return new object[] - { + yield return + [ new[] { (Path: "src/Project/Project.csproj", Content: """ - - - netstandard2.0 - - - - - - - """), + + + netstandard2.0 + + + + + + + """), }, // starting contents new[] { // If a dependency has a version constraint outside of our new-version, we don't update anything (Path: "src/Project/Project.csproj", Content: """ - - - netstandard2.0 - - - - - - - """), - },// expected contents - "AWSSDK.Core", "3.3.21.19", "3.7.300.20", false // isTransitive - }; + + + netstandard2.0 + + + + + + + """), + }, // expected contents + "AWSSDK.Core", + "3.3.21.19", + "3.7.300.20", + false // isTransitive + ]; // Dependency project has version constraint - yield return new object[] - { + yield return + [ new[] { (Path: "src/Project2/Project2.csproj", Content: """ - - - netstandard2.0 - - - - - - - """), + + + netstandard2.0 + + + + + + + """), (Path: "src/Project/Project.csproj", Content: """ - - - netstandard2.0 - - - - - - """), + + + netstandard2.0 + + + + + + """), }, // starting contents new[] { (Path: "src/Project2/Project2.csproj", Content: """ - - - netstandard2.0 - - - - - - - """), // starting contents + + + netstandard2.0 + + + + + + + """), // starting contents (Path: "src/Project/Project.csproj", Content: """ - - - netstandard2.0 - - - - - - """), - },// expected contents - "Newtonsoft.Json", "12.0.1", "13.0.1", false // isTransitive - }; + + + netstandard2.0 + + + + + + """), + }, // expected contents + "Newtonsoft.Json", + "12.0.1", + "13.0.1", + false // isTransitive + ]; // Multiple references - yield return new object[] - { - new [] + yield return + [ + new[] { (Path: "src/Project.csproj", Content: """ @@ -168,7 +174,7 @@ public static IEnumerable GetDependencyUpdates() """) }, // starting contents - new [] + new[] { (Path: "src/Project.csproj", Content: """ @@ -184,12 +190,16 @@ public static IEnumerable GetDependencyUpdates() """) }, // expected contents - "Newtonsoft.Json", "12.0.1", "13.0.1", false // isTransitive - }; + "Newtonsoft.Json", + "12.0.1", + "13.0.1", + false // isTransitive + ]; // Make sure we don't update if there are incoherent versions - yield return new object[] { - new [] + yield return + [ + new[] { (Path: "src/Project.csproj", Content: """ @@ -218,7 +228,7 @@ public static IEnumerable GetDependencyUpdates() """) }, // starting contents - new [] + new[] { (Path: "src/Project.csproj", Content: """ @@ -247,13 +257,16 @@ public static IEnumerable GetDependencyUpdates() """) }, // expected contents - "Microsoft.EntityFrameworkCore.SqlServer", "2.1.0", "2.2.0", false // isTransitive - }; + "Microsoft.EntityFrameworkCore.SqlServer", + "2.1.0", + "2.2.0", + false // isTransitive + ]; // PackageReference with Version as child element - yield return new object[] - { - new [] + yield return + [ + new[] { (Path: "src/Project.csproj", Content: """ @@ -268,7 +281,7 @@ public static IEnumerable GetDependencyUpdates() """) }, // starting contents - new [] + new[] { (Path: "src/Project.csproj", Content: """ @@ -283,8 +296,11 @@ public static IEnumerable GetDependencyUpdates() """) }, // expected contents - "Newtonsoft.Json", "12.0.1", "13.0.1", false // isTransitive - }; + "Newtonsoft.Json", + "12.0.1", + "13.0.1", + false // isTransitive + ]; } private static void AssertContentsEqual((string Path, string Contents)[] expectedContents, TemporaryDirectory directory) diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs index b057265670..009692c7bf 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/JsonBuildFile.cs @@ -1,4 +1,5 @@ using System; +using System.Text.Json; using System.Text.Json.Nodes; using NuGetUpdater.Core.Utilities; @@ -35,7 +36,7 @@ private void ResetNode() { return JsonHelper.ParseNode(Contents); } - catch (System.Text.Json.JsonException ex) + catch (JsonException ex) { // We can't police that people have legal JSON files. // If they don't, we just return null. diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs index c9c3794098..2f4b892141 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/PackagesConfigBuildFile.cs @@ -11,6 +11,7 @@ internal sealed class PackagesConfigBuildFile : XmlBuildFile { public static PackagesConfigBuildFile Open(string repoRootPath, string path) => Parse(repoRootPath, path, File.ReadAllText(path)); + public static PackagesConfigBuildFile Parse(string repoRootPath, string path, string xml) => new(repoRootPath, path, Parser.ParseText(xml)); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs index e20602da46..cb4e79df0d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Files/ProjectBuildFile.cs @@ -11,6 +11,7 @@ internal sealed class ProjectBuildFile : XmlBuildFile { public static ProjectBuildFile Open(string repoRootPath, string path) => Parse(repoRootPath, path, File.ReadAllText(path)); + public static ProjectBuildFile Parse(string repoRootPath, string path, string xml) => new(repoRootPath, path, Parser.ParseText(xml)); @@ -31,9 +32,9 @@ public IEnumerable> GetProperties() => PropertyNode .SelectMany(e => e.Elements); public IEnumerable PackageItemNodes => ItemNodes.Where(e => - e.Name.Equals("PackageReference", StringComparison.OrdinalIgnoreCase) || - e.Name.Equals("GlobalPackageReference", StringComparison.OrdinalIgnoreCase) || - e.Name.Equals("PackageVersion", StringComparison.OrdinalIgnoreCase)); + e.Name.Equals("PackageReference", StringComparison.OrdinalIgnoreCase) || + e.Name.Equals("GlobalPackageReference", StringComparison.OrdinalIgnoreCase) || + e.Name.Equals("PackageVersion", StringComparison.OrdinalIgnoreCase)); public IEnumerable GetDependencies() => PackageItemNodes .Select(GetDependency) @@ -42,7 +43,7 @@ public IEnumerable GetDependencies() => PackageItemNodes private static Dependency? GetDependency(IXmlElementSyntax element) { var name = element.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) - ?? element.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase); + ?? element.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase); if (name is null || name.StartsWith("@(")) { return null; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs index 3dc1b8b4d4..e49f4c9faf 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs @@ -33,6 +33,7 @@ static NuGetFramework ParseFramework(string tfm) // effort by including just the platform. framework = new NuGetFramework(framework.Framework, framework.Version, framework.Platform, FrameworkConstants.EmptyVersion); } + return framework; } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs index adca1b8b79..e3e6642e15 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using NuGet.Frameworks; + using NuGetGallery.Frameworks; namespace NuGetUpdater.Core.FrameworkChecker; @@ -15,7 +16,7 @@ public class FrameworkCompatibilityService private static readonly IReadOnlyList AllSupportedFrameworks = SupportedFrameworks.AllSupportedNuGetFrameworks; private static readonly IReadOnlyDictionary> CompatibilityMatrix = GetCompatibilityMatrix(); - public ISet GetCompatibleFrameworks(IEnumerable packageFrameworks) + public ISet GetCompatibleFrameworks(IEnumerable? packageFrameworks) { if (packageFrameworks == null) { @@ -63,10 +64,14 @@ private static IReadOnlyDictionary> GetComp } } - matrix.Add(SupportedFrameworks.Net60Windows7, - new HashSet() { - SupportedFrameworks.Net60Windows, SupportedFrameworks.Net60Windows7, - SupportedFrameworks.Net70Windows, SupportedFrameworks.Net70Windows7 }); + matrix.Add( + SupportedFrameworks.Net60Windows7, + new HashSet + { + SupportedFrameworks.Net60Windows, SupportedFrameworks.Net60Windows7, + SupportedFrameworks.Net70Windows, SupportedFrameworks.Net70Windows7 + } + ); return matrix; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs index 6482212558..9ded2bf4f2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; + using NuGet.Frameworks; + using static NuGet.Frameworks.FrameworkConstants; using static NuGet.Frameworks.FrameworkConstants.CommonFrameworks; @@ -42,7 +44,9 @@ public static class SupportedFrameworks public static readonly NuGetFramework Net70MacCatalyst = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version7, "maccatalyst", EmptyVersion); public static readonly NuGetFramework Net70TvOs = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version7, "tvos", EmptyVersion); public static readonly NuGetFramework Net70Windows = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version7, "windows", EmptyVersion); + public static readonly NuGetFramework Net80 = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version8); // https://github.com/NuGet/Engineering/issues/5112 + public static readonly NuGetFramework Net80Android = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version8, "android", EmptyVersion); public static readonly NuGetFramework Net80Ios = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version8, "ios", EmptyVersion); public static readonly NuGetFramework Net80MacOs = new NuGetFramework(FrameworkIdentifiers.NetCoreApp, Version8, "macos", EmptyVersion); @@ -91,16 +95,16 @@ static SupportedFrameworks() /// public static class TfmFilters { - public static readonly List NetTfms = new List - { + public static readonly List NetTfms = + [ Net80, Net70, Net60, Net50 - }; + ]; - public static readonly List NetCoreAppTfms = new List - { + public static readonly List NetCoreAppTfms = + [ NetCoreApp31, NetCoreApp30, NetCoreApp22, @@ -108,10 +112,10 @@ public static class TfmFilters NetCoreApp20, NetCoreApp11, NetCoreApp10 - }; + ]; - public static readonly List NetStandardTfms = new List - { + public static readonly List NetStandardTfms = + [ NetStandard21, NetStandard20, NetStandard16, @@ -121,10 +125,10 @@ public static class TfmFilters NetStandard12, NetStandard11, NetStandard10 - }; + ]; - public static readonly List NetFrameworkTfms = new List - { + public static readonly List NetFrameworkTfms = + [ Net481, Net48, Net472, @@ -140,7 +144,7 @@ public static class TfmFilters Net35, Net3, Net2 - }; + ]; } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs index eadec691ff..81fde97a04 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs @@ -7,11 +7,13 @@ using System.Threading.Tasks; using System.Xml.Linq; +using CoreV2::NuGet.Runtime; + using Microsoft.Language.Xml; using NuGet.ProjectManagement; -using AssemblyBinding = CoreV2::NuGet.Runtime.AssemblyBinding; +using Runtime_AssemblyBinding = CoreV2::NuGet.Runtime.AssemblyBinding; namespace NuGetUpdater.Core; @@ -150,7 +152,7 @@ static bool IsConfigFile(IXmlElementSyntax element) var path = Path.GetFileName(content); return (element.Name == "None" && string.Equals(path, "app.config", StringComparison.OrdinalIgnoreCase)) - || (element.Name == "Content" && string.Equals(path, "web.config", StringComparison.OrdinalIgnoreCase)); + || (element.Name == "Content" && string.Equals(path, "web.config", StringComparison.OrdinalIgnoreCase)); } static string GetConfigFileName(XmlDocumentSyntax document) @@ -171,13 +173,13 @@ static string GenerateDefaultAppConfig(XmlDocumentSyntax document) { var frameworkVersion = GetFrameworkVersion(document); return $""" - - - - - - - """; + + + + + + + """; } static string? GetFrameworkVersion(XmlDocumentSyntax document) @@ -190,7 +192,7 @@ static string GenerateDefaultAppConfig(XmlDocumentSyntax document) } } - private static string AddBindingRedirects(ConfigurationFile configFile, IEnumerable bindingRedirects) + private static string AddBindingRedirects(ConfigurationFile configFile, IEnumerable bindingRedirects) { // Do nothing if there are no binding redirects to add, bail out if (!bindingRedirects.Any()) @@ -261,7 +263,7 @@ static void RemoveElement(XElement element) static void UpdateBindingRedirectElement( XElement existingDependentAssemblyElement, - AssemblyBinding newBindingRedirect) + Runtime_AssemblyBinding newBindingRedirect) { var existingBindingRedirectElement = existingDependentAssemblyElement.Element(BindingRedirectName); // Since we've successfully parsed this node, it has to be valid and this child must exist. @@ -287,12 +289,11 @@ static void UpdateBindingRedirectElement( .Elements(DependentAssemblyName); // We're going to need to know which element is associated with what binding for removal - var assemblyElementPairs = from dependentAssemblyElement in dependencyAssemblyElements - select new - { - Binding = AssemblyBinding.Parse(dependentAssemblyElement), - Element = dependentAssemblyElement - }; + var assemblyElementPairs = dependencyAssemblyElements.Select(dependentAssemblyElement => new + { + Binding = Runtime_AssemblyBinding.Parse(dependentAssemblyElement), + Element = dependentAssemblyElement + }); // Return a mapping from binding to element return assemblyElementPairs.ToDictionary(p => (p.Binding.Name, p.Binding.PublicKeyToken), p => p.Element); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs index 12682aa502..79c5764686 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs @@ -8,14 +8,14 @@ using System.Reflection; using System.Text.RegularExpressions; -using AssemblyBinding = CoreV2::NuGet.Runtime.AssemblyBinding; -using IAssembly = CoreV2::NuGet.Runtime.IAssembly; +using Runtime_AssemblyBinding = CoreV2::NuGet.Runtime.AssemblyBinding; +using Runtime_IAssembly = CoreV2::NuGet.Runtime.IAssembly; namespace NuGetUpdater.Core; public static partial class BindingRedirectResolver { - public static IEnumerable GetBindingRedirects(string projectPath, IEnumerable includes) + public static IEnumerable GetBindingRedirects(string projectPath, IEnumerable includes) { var directoryPath = Path.GetDirectoryName(projectPath); if (directoryPath is null) @@ -27,7 +27,7 @@ public static IEnumerable GetBindingRedirects(string projectPat { if (TryParseIncludesString(include, out var assemblyInfo)) { - yield return new AssemblyBinding(assemblyInfo); + yield return new Runtime_AssemblyBinding(assemblyInfo); } } @@ -63,9 +63,9 @@ static bool TryParseIncludesString(string include, [NotNullWhen(true)] out Assem private static readonly Regex IncludesRegex = IncludesPattern(); /// - /// Wraps system type in the nuget interface to interop with nuget apis + /// Wraps system type in the nuget interface to interop with nuget apis /// - private class AssemblyWrapper : IAssembly + private class AssemblyWrapper : Runtime_IAssembly { public AssemblyWrapper(string name, Version version, string? publicKeyToken = null, string? culture = null) { @@ -79,7 +79,7 @@ public AssemblyWrapper(string name, Version version, string? publicKeyToken = nu public Version Version { get; } public string? PublicKeyToken { get; } public string? Culture { get; } - public IEnumerable ReferencedAssemblies { get; } = Enumerable.Empty(); + public IEnumerable ReferencedAssemblies { get; } = Enumerable.Empty(); } [GeneratedRegex("(?\\w+)=(?[^,]+)")] diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs index 9cccf5b2b2..a7aa673a6b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/DotNetToolsJsonUpdater.cs @@ -1,27 +1,27 @@ using System; using System.Collections.Immutable; -using System.IO; using System.Linq; using System.Threading.Tasks; namespace NuGetUpdater.Core; -internal static partial class DotNetToolsJsonUpdater +internal static class DotNetToolsJsonUpdater { - public static async Task UpdateDependencyAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, Logger logger) + public static async Task UpdateDependencyAsync(string repoRootPath, string workspacePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, + Logger logger) { var buildFiles = LoadBuildFiles(repoRootPath, workspacePath, logger); if (buildFiles.Length == 0) { - logger.Log($" No dotnet-tools.json files found."); + logger.Log(" No dotnet-tools.json files found."); return; } - logger.Log($" Updating dotnet-tools.json files."); + logger.Log(" Updating dotnet-tools.json files."); var filesToUpdate = buildFiles.Where(f => - f.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))) + f.GetDependencies().Any(d => d.Name.Equals(dependencyName, StringComparison.OrdinalIgnoreCase))) .ToImmutableArray(); if (filesToUpdate.Length == 0) { @@ -39,7 +39,7 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string works if (toolObject is not null && toolObject["version"]?.GetValue() == previousDependencyVersion) { - buildFile.UpdateProperty(new[] { "tools", dependencyName, "version" }, newDependencyVersion); + buildFile.UpdateProperty(["tools", dependencyName, "version"], newDependencyVersion); if (await buildFile.SaveAsync()) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs index a507c0fe0f..fa8d9aa99e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/GlobalJsonUpdater.cs @@ -5,9 +5,15 @@ namespace NuGetUpdater.Core; -internal static partial class GlobalJsonUpdater +internal static class GlobalJsonUpdater { - public static async Task UpdateDependencyAsync(string repoRootPath, string globalJsonPath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, Logger logger) + public static async Task UpdateDependencyAsync( + string repoRootPath, + string globalJsonPath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + Logger logger) { if (!File.Exists(globalJsonPath)) { @@ -29,7 +35,7 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string globa if (globalJsonFile.MSBuildSdks?.TryGetPropertyValue(dependencyName, out var version) != true || version?.GetValue() is not string versionString) { - logger.Log($" Unable to determine dependency version."); + logger.Log(" Unable to determine dependency version."); return; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs index 428cc3451d..28bff1c020 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs @@ -4,15 +4,28 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using System.Xml.Linq; using Microsoft.Language.Xml; +using NuGet.CommandLine; + +using NuGetUpdater.Core.Updater; + +using Console = System.Console; + namespace NuGetUpdater.Core; internal static class PackagesConfigUpdater { - public static async Task UpdateDependencyAsync(string repoRootPath, string projectPath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive, Logger logger) + public static async Task UpdateDependencyAsync( + string repoRootPath, + string projectPath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + bool isTransitive, + Logger logger + ) { logger.Log($" Found {NuGetHelper.PackagesConfigFileName}; running with NuGet.exe"); @@ -35,18 +48,18 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string proje var packagesDirectory = PathHelper.JoinPath(projectDirectory, packagesSubDirectory); Directory.CreateDirectory(packagesDirectory); - var args = new List() - { - "update", - packagesConfigPath, - "-Id", - dependencyName, - "-Version", - newDependencyVersion, - "-RepositoryPath", - packagesDirectory, - "-NonInteractive", - }; + var args = new List + { + "update", + packagesConfigPath, + "-Id", + dependencyName, + "-Version", + newDependencyVersion, + "-RepositoryPath", + packagesDirectory, + "-NonInteractive", + }; logger.Log(" Finding MSBuild..."); var msbuildDirectory = MSBuildHelper.MSBuildPath; @@ -56,7 +69,10 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string proje args.Add(msbuildDirectory); // e.g., /usr/share/dotnet/sdk/7.0.203 } - RunNuget(args, packagesDirectory, logger); + using (new WebApplicationTargetsConditionPatcher(projectPath)) + { + RunNuget(args, packagesDirectory, logger); + } projectBuildFile = ProjectBuildFile.Open(repoRootPath, projectPath); projectBuildFile.NormalizeDirectorySeparatorsInProject(); @@ -84,7 +100,7 @@ private static void RunNuget(List args, string packagesDirectory, Logger logger.Log($" Running NuGet.exe with args: {string.Join(" ", args)}"); Environment.CurrentDirectory = packagesDirectory; - var result = NuGet.CommandLine.Program.Main(args.ToArray()); + var result = Program.Main(args.ToArray()); var fullOutput = outputBuilder.ToString(); logger.Log($" Result: {result}"); logger.Log($" Output:\n{fullOutput}"); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 61686e0910..fe51270c32 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -5,16 +5,23 @@ using System.Linq; using System.Threading.Tasks; -using Microsoft.Build.Evaluation; using Microsoft.Language.Xml; using NuGet.Versioning; namespace NuGetUpdater.Core; -internal static partial class SdkPackageUpdater +internal static class SdkPackageUpdater { - public static async Task UpdateDependencyAsync(string repoRootPath, string projectPath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive, Logger logger) + public static async Task UpdateDependencyAsync( + string repoRootPath, + string projectPath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + bool isTransitive, + Logger logger + ) { // SDK-style project, modify the XML directly logger.Log(" Running for SDK-style project"); @@ -26,7 +33,7 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string proje var tfms = MSBuildHelper.GetTargetFrameworkMonikers(buildFiles); // Get the set of all top-level dependencies in the current project - var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependenyInfos(buildFiles).ToArray(); + var topLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles).ToArray(); var packageFoundInDependencies = false; var packageNeedsUpdating = false; @@ -74,7 +81,7 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string proje // stop update process if we find conflicting package versions var conflictingPackageVersionsFound = false; var packagesAndVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var (tfm, dependencies) in tfmsAndDependencies) + foreach (var (_, dependencies) in tfmsAndDependencies) { foreach (var (packageName, packageVersion, _, _, _, _) in dependencies) { @@ -118,10 +125,10 @@ public static async Task UpdateDependencyAsync(string repoRootPath, string proje } else { - await UpdateTopLevelDepdendencyAsync(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, packagesAndVersions, logger); + UpdateTopLevelDepdendency(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, packagesAndVersions, logger); } - var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependenyInfos(buildFiles); + var updatedTopLevelDependencies = MSBuildHelper.GetTopLevelPackageDependencyInfos(buildFiles); foreach (var tfm in tfms) { var updatedPackages = await MSBuildHelper.GetAllPackageDependenciesAsync(repoRootPath, projectPath, tfm, updatedTopLevelDependencies.ToArray(), logger); @@ -239,7 +246,14 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin } } - private static async Task UpdateTopLevelDepdendencyAsync(ImmutableArray buildFiles, string dependencyName, string previousDependencyVersion, string newDependencyVersion, Dictionary packagesAndVersions, Logger logger) + private static void UpdateTopLevelDepdendency( + ImmutableArray buildFiles, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + IDictionary packagesAndVersions, + Logger logger + ) { var result = TryUpdateDependencyVersion(buildFiles, dependencyName, previousDependencyVersion, newDependencyVersion, logger); if (result == UpdateResult.NotFound) @@ -254,7 +268,12 @@ private static async Task UpdateTopLevelDepdendencyAsync(ImmutableArray buildFiles, string dependencyName, string? previousDependencyVersion, string newDependencyVersion, Logger logger) + private static UpdateResult TryUpdateDependencyVersion( + ImmutableArray buildFiles, + string dependencyName, + string? previousDependencyVersion, + string newDependencyVersion, + Logger logger) { var foundCorrect = false; var foundUnsupported = false; @@ -275,9 +294,9 @@ private static UpdateResult TryUpdateDependencyVersion(ImmutableArray e.Name.Equals("Version", StringComparison.OrdinalIgnoreCase)) - ?? packageNode.Elements.FirstOrDefault(e => e.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase)); + ?? packageNode.Elements.FirstOrDefault(e => e.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase)); if (versionAttribute is not null) { // Is this the case where version is specified with property substitution? @@ -373,31 +392,29 @@ private static UpdateResult TryUpdateDependencyVersion(ImmutableArray 0) { var updatedXml = buildFile.Contents - .ReplaceNodes(updateNodes, (o, n) => + .ReplaceNodes(updateNodes, (_, n) => { if (n is XmlAttributeSyntax attributeSyntax) { return attributeSyntax.WithValue(attributeSyntax.Value.Replace(previousPackageVersion!, newDependencyVersion)); } - else if (n is XmlElementSyntax elementsSyntax) + + if (n is XmlElementSyntax elementsSyntax) { var modifiedContent = elementsSyntax.GetContentValue().Replace(previousPackageVersion!, newDependencyVersion); var textSyntax = SyntaxFactory.XmlText(SyntaxFactory.Token(null, SyntaxKind.XmlTextLiteralToken, null, modifiedContent)); return elementsSyntax.WithContent(SyntaxFactory.SingletonList(textSyntax)); } - else - { - throw new InvalidDataException($"Unsupported SyntaxType {n.GetType().Name} marked for update"); - } + + throw new InvalidDataException($"Unsupported SyntaxType {n.GetType().Name} marked for update"); }); buildFile.Update(updatedXml); updateWasPerformed = true; @@ -489,10 +506,13 @@ private static UpdateResult TryUpdateDependencyVersion(ImmutableArray FindPackageNodes(ProjectBuildFile buildFile, string packageName) - { - return buildFile.PackageItemNodes.Where(e => - string.Equals(e.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase), packageName, StringComparison.OrdinalIgnoreCase) && + private static IEnumerable FindPackageNodes( + ProjectBuildFile buildFile, + string packageName) + => buildFile.PackageItemNodes.Where(e => + string.Equals( + e.GetAttributeOrSubElementValue("Include", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("Update", StringComparison.OrdinalIgnoreCase), + packageName, + StringComparison.OrdinalIgnoreCase) && (e.GetAttributeOrSubElementValue("Version", StringComparison.OrdinalIgnoreCase) ?? e.GetAttributeOrSubElementValue("VersionOverride", StringComparison.OrdinalIgnoreCase)) is not null); - } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs index 23b7302e22..76d4ac3257 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs @@ -5,7 +5,7 @@ namespace NuGetUpdater.Core; -public partial class UpdaterWorker +public class UpdaterWorker { private readonly Logger _logger; private readonly HashSet _processedGlobalJsonPaths = new(StringComparer.OrdinalIgnoreCase); @@ -51,7 +51,13 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string dep _processedGlobalJsonPaths.Clear(); } - private async Task RunForSolutionAsync(string repoRootPath, string solutionPath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive) + private async Task RunForSolutionAsync( + string repoRootPath, + string solutionPath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + bool isTransitive) { _logger.Log($"Running for solution [{Path.GetRelativePath(repoRootPath, solutionPath)}]"); var projectPaths = MSBuildHelper.GetProjectPathsFromSolution(solutionPath); @@ -61,7 +67,13 @@ private async Task RunForSolutionAsync(string repoRootPath, string solutionPath, } } - private async Task RunForProjFileAsync(string repoRootPath, string projFilePath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive) + private async Task RunForProjFileAsync( + string repoRootPath, + string projFilePath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + bool isTransitive) { _logger.Log($"Running for proj file [{Path.GetRelativePath(repoRootPath, projFilePath)}]"); if (!File.Exists(projFilePath)) @@ -69,6 +81,7 @@ private async Task RunForProjFileAsync(string repoRootPath, string projFilePath, _logger.Log($"File [{projFilePath}] does not exist."); return; } + var projectFilePaths = MSBuildHelper.GetProjectPathsFromProject(projFilePath); foreach (var projectFullPath in projectFilePaths) { @@ -80,12 +93,18 @@ private async Task RunForProjFileAsync(string repoRootPath, string projFilePath, } } - private async Task RunForProjectAsync(string repoRootPath, string projectPath, string dependencyName, string previousDependencyVersion, string newDependencyVersion, bool isTransitive) + private async Task RunForProjectAsync( + string repoRootPath, + string projectPath, + string dependencyName, + string previousDependencyVersion, + string newDependencyVersion, + bool isTransitive) { _logger.Log($"Running for project [{projectPath}]"); if (!isTransitive - && MSBuildHelper.GetGlobalJsonPath(repoRootPath, projectPath) is string globalJsonPath + && MSBuildHelper.GetGlobalJsonPath(repoRootPath, projectPath) is { } globalJsonPath && !_processedGlobalJsonPaths.Contains(globalJsonPath)) { _processedGlobalJsonPaths.Add(globalJsonPath); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs new file mode 100644 index 0000000000..8468cdb8c5 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs @@ -0,0 +1,47 @@ +using System; +using System.IO; +using System.Linq; + +using Microsoft.Language.Xml; + +namespace NuGetUpdater.Core.Updater +{ + internal class WebApplicationTargetsConditionPatcher : IDisposable + { + private string? _capturedCondition; + private readonly XmlFilePreAndPostProcessor _processor; + + public WebApplicationTargetsConditionPatcher(string projectFilePath) + { + _processor = new XmlFilePreAndPostProcessor( + getContent: () => File.ReadAllText(projectFilePath), + setContent: s => File.WriteAllText(projectFilePath, s), + nodeFinder: doc => doc.Descendants() + .FirstOrDefault(e => e.Name == "Import" && e.GetAttributeValue("Project") == @"$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets") + as XmlNodeSyntax, + preProcessor: n => + { + var element = (IXmlElementSyntax)n; + _capturedCondition = element.GetAttributeValue("Condition"); + return (XmlNodeSyntax)element.RemoveAttributeByName("Condition").WithAttribute("Condition", "false"); + }, + postProcessor: n => + { + var element = (IXmlElementSyntax)n; + var newElement = element.RemoveAttributeByName("Condition"); + if (_capturedCondition is not null) + { + newElement = newElement.WithAttribute("Condition", _capturedCondition); + } + + return (XmlNodeSyntax)newElement; + } + ); + } + + public void Dispose() + { + _processor.Dispose(); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs new file mode 100644 index 0000000000..5b9af19eda --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs @@ -0,0 +1,55 @@ +using System; + +using Microsoft.Language.Xml; + +namespace NuGetUpdater.Core.Updater +{ + internal class XmlFilePreAndPostProcessor : IDisposable + { + public Func GetContent { get; } + public Action SetContent { get; } + public Func NodeFinder { get; } + public Func PreProcessor { get; } + public Func PostProcessor { get; } + + public XmlFilePreAndPostProcessor(Func getContent, Action setContent, Func nodeFinder, Func preProcessor, Func postProcessor) + { + GetContent = getContent; + SetContent = setContent; + NodeFinder = nodeFinder; + PreProcessor = preProcessor; + PostProcessor = postProcessor; + PreProcess(); + } + + public void Dispose() + { + PostProcess(); + } + + private void PreProcess() => RunProcessor(PreProcessor); + + private void PostProcess() => RunProcessor(PostProcessor); + + private void RunProcessor(Func processor) + { + var content = GetContent(); + var xml = Parser.ParseText(content); + if (xml is null) + { + return; + } + + var node = NodeFinder(xml); + if (node is null) + { + return; + } + + var replacementElement = processor(node); + var replacementXml = xml.ReplaceNode(node, replacementElement); + var replacementString = replacementXml.ToFullString(); + SetContent(replacementString); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs index 73940bc2d3..1c5683ddd6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; @@ -11,7 +12,7 @@ namespace NuGetUpdater.Core.Utilities { internal static class JsonHelper { - public static JsonDocumentOptions DocumentOptions { get; } = new JsonDocumentOptions() + public static JsonDocumentOptions DocumentOptions { get; } = new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip, }; @@ -24,16 +25,16 @@ internal static class JsonHelper public static string UpdateJsonProperty(string json, string[] propertyPath, string newValue, StringComparison comparisonType = StringComparison.Ordinal) { - var readerOptions = new JsonReaderOptions() + var readerOptions = new JsonReaderOptions { CommentHandling = JsonCommentHandling.Allow, }; var bytes = Encoding.UTF8.GetBytes(json); var reader = new Utf8JsonReader(bytes, readerOptions); using var ms = new MemoryStream(); - var writerOptions = new JsonWriterOptions() + var writerOptions = new JsonWriterOptions { - Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, Indented = true, }; var writer = new Utf8JsonWriter(ms, writerOptions); @@ -70,6 +71,7 @@ public static string UpdateJsonProperty(string json, string[] propertyPath, stri // let the default block comment writer handle it writer.WriteCommentValue(reader.GetComment()); } + break; case JsonTokenType.EndArray: writer.WriteEndArray(); @@ -126,7 +128,7 @@ public static string UpdateJsonProperty(string json, string[] propertyPath, stri { currentPath.RemoveAt(currentPath.Count - 1); } - + currentPath[pathDepth] = pathValue; if (IsPathMatch(currentPath, propertyPath, comparisonType)) { @@ -199,6 +201,7 @@ private static string GetCurrentTokenTriviaPrefix(int tokenStartIndex, string or { prefixStart--; } + goto done; default: // found regular character; move forward one and quit @@ -215,8 +218,8 @@ private static string GetCurrentTokenTriviaPrefix(int tokenStartIndex, string or private static bool IsPreceedingCharacterEqual(string originalText, int currentIndex, char expectedCharacter) { return currentIndex > 0 - && currentIndex < originalText.Length - && originalText[currentIndex - 1] == expectedCharacter; + && currentIndex < originalText.Length + && originalText[currentIndex - 1] == expectedCharacter; } } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 0d302053eb..e6e0fd241c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -15,6 +15,7 @@ using Microsoft.Build.Evaluation; using Microsoft.Build.Exceptions; using Microsoft.Build.Locator; +using Microsoft.Extensions.FileSystemGlobbing; using NuGetUpdater.Core.Utilities; @@ -101,6 +102,7 @@ public static IEnumerable GetProjectPathsFromProject(string projFilePath { var projectStack = new Stack<(string folderPath, ProjectRootElement)>(); var projectRootElement = ProjectRootElement.Open(projFilePath); + var processedProjectFiles = new HashSet(StringComparer.OrdinalIgnoreCase); projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(projFilePath)!), projectRootElement)); @@ -114,27 +116,42 @@ public static IEnumerable GetProjectPathsFromProject(string projFilePath continue; } - projectPath = PathHelper.GetFullPathFromRelative(folderPath, projectPath); + Matcher matcher = new Matcher(); + matcher.AddInclude(PathHelper.NormalizePathToUnix(projectReference.Include)); - var projectExtension = Path.GetExtension(projectPath).ToLowerInvariant(); - if (projectExtension == ".proj") + string searchDirectory = PathHelper.NormalizePathToUnix(folderPath); + + IEnumerable files = matcher.GetResultsInFullPath(searchDirectory); + + foreach (var file in files) { - // If there is some MSBuild logic that needs to run to fully resolve the path skip the project - if (File.Exists(projectPath)) + // Check that we haven't already processed this file + if (processedProjectFiles.Contains(file)) { - var additionalProjectRootElement = ProjectRootElement.Open(projectPath); - projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(projectPath)!), additionalProjectRootElement)); + continue; + } + + var projectExtension = Path.GetExtension(file).ToLowerInvariant(); + if (projectExtension == ".proj") + { + // If there is some MSBuild logic that needs to run to fully resolve the path skip the project + if (File.Exists(file)) + { + var additionalProjectRootElement = ProjectRootElement.Open(file); + projectStack.Push((Path.GetFullPath(Path.GetDirectoryName(file)!), additionalProjectRootElement)); + processedProjectFiles.Add(file); + } + } + else if (projectExtension == ".csproj" || projectExtension == ".vbproj" || projectExtension == ".fsproj") + { + yield return file; } - } - else if (projectExtension == ".csproj" || projectExtension == ".vbproj" || projectExtension == ".fsproj") - { - yield return projectPath; } } } } - public static IEnumerable GetTopLevelPackageDependenyInfos(ImmutableArray buildFiles) + public static IEnumerable GetTopLevelPackageDependencyInfos(ImmutableArray buildFiles) { Dictionary packageInfo = new(StringComparer.OrdinalIgnoreCase); Dictionary packageVersionInfo = new(StringComparer.OrdinalIgnoreCase); @@ -145,11 +162,11 @@ public static IEnumerable GetTopLevelPackageDependenyInfos(Immutable var projectRoot = CreateProjectRootElement(buildFile); foreach (var packageItem in projectRoot.Items - .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference"))) + .Where(i => (i.ItemType == "PackageReference" || i.ItemType == "GlobalPackageReference"))) { var versionSpecification = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value - ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value - ?? string.Empty; + ?? packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("VersionOverride", StringComparison.OrdinalIgnoreCase))?.Value + ?? string.Empty; foreach (var attributeValue in new[] { packageItem.Include, packageItem.Update }) { if (!string.IsNullOrWhiteSpace(attributeValue)) @@ -175,10 +192,10 @@ public static IEnumerable GetTopLevelPackageDependenyInfos(Immutable } foreach (var packageItem in projectRoot.Items - .Where(i => i.ItemType == "PackageVersion" && !string.IsNullOrEmpty(i.Include))) + .Where(i => i.ItemType == "PackageVersion" && !string.IsNullOrEmpty(i.Include))) { packageVersionInfo[packageItem.Include] = packageItem.Metadata.FirstOrDefault(m => m.Name.Equals("Version", StringComparison.OrdinalIgnoreCase))?.Value - ?? string.Empty; + ?? string.Empty; } foreach (var property in projectRoot.Properties) @@ -286,7 +303,12 @@ private static ProjectRootElement CreateProjectRootElement(ProjectBuildFile buil return projectRoot; } - private static async Task CreateTempProjectAsync(DirectoryInfo tempDir, string repoRoot, string projectPath, string targetFramework, Dependency[] packages) + private static async Task CreateTempProjectAsync( + DirectoryInfo tempDir, + string repoRoot, + string projectPath, + string targetFramework, + IReadOnlyCollection packages) { var projectDirectory = Path.GetDirectoryName(projectPath); projectDirectory ??= repoRoot; @@ -300,44 +322,55 @@ private static async Task CreateTempProjectAsync(DirectoryInfo tempDir, var packageReferences = string.Join( Environment.NewLine, packages - .Where(p => !string.IsNullOrWhiteSpace(p.Version)) // empty `Version` attributes will cause the temporary project to not build + // empty `Version` attributes will cause the temporary project to not build + .Where(p => !string.IsNullOrWhiteSpace(p.Version)) // If all PackageReferences for a package are update-only mark it as such, otherwise it can cause package incoherence errors which do not exist in the repo. .Select(static p => $"")); var projectContents = $""" - - - {targetFramework} - true - false - - - {packageReferences} - - - - <_NuGetPackageData Include="@(NativeCopyLocalItems)" /> - <_NuGetPackageData Include="@(ResourceCopyLocalItems)" /> - <_NuGetPackageData Include="@(RuntimeCopyLocalItems)" /> - <_NuGetPackageData Include="@(ResolvedAnalyzers)" /> - <_NuGetPackageData Include="@(_PackageDependenciesDesignTime)"> - %(_PackageDependenciesDesignTime.Name) - %(_PackageDependenciesDesignTime.Version) - - - - - - - - """; + + + {targetFramework} + true + false + + + {packageReferences} + + + + <_NuGetPackageData Include="@(NativeCopyLocalItems)" /> + <_NuGetPackageData Include="@(ResourceCopyLocalItems)" /> + <_NuGetPackageData Include="@(RuntimeCopyLocalItems)" /> + <_NuGetPackageData Include="@(ResolvedAnalyzers)" /> + <_NuGetPackageData Include="@(_PackageDependenciesDesignTime)"> + %(_PackageDependenciesDesignTime.Name) + %(_PackageDependenciesDesignTime.Version) + + + + + + + + """; var tempProjectPath = Path.Combine(tempDir.FullName, "Project.csproj"); await File.WriteAllTextAsync(tempProjectPath, projectContents); // prevent directory crawling - await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.props"), ""); + await File.WriteAllTextAsync( + Path.Combine(tempDir.FullName, "Directory.Build.props"), + """ + + + + true + + + """); + await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Build.targets"), ""); await File.WriteAllTextAsync(Path.Combine(tempDir.FullName, "Directory.Packages.props"), ""); @@ -345,7 +378,7 @@ private static async Task CreateTempProjectAsync(DirectoryInfo tempDir, } internal static async Task GetAllPackageDependenciesAsync( - string repoRoot, string projectPath, string targetFramework, Dependency[] packages, Logger? logger = null) + string repoRoot, string projectPath, string targetFramework, IReadOnlyCollection packages, Logger? logger = null) { var tempDirectory = Directory.CreateTempSubdirectory("package-dependency-resolution_"); try @@ -369,7 +402,7 @@ internal static async Task GetAllPackageDependenciesAsync( else { logger?.Log($"dotnet build in {nameof(GetAllPackageDependenciesAsync)} failed. STDOUT: {stdout} STDERR: {stderr}"); - return Array.Empty(); + return []; } } finally @@ -391,7 +424,7 @@ internal static async Task GetAllPackageDependenciesAsync( internal static async Task> LoadBuildFiles(string repoRootPath, string projectPath) { - var buildFileList = new List() + var buildFileList = new List { projectPath.NormalizePathToUnix() // always include the starting project }; @@ -410,12 +443,12 @@ internal static async Task> LoadBuildFiles(stri // create a safe version with only certain top-level keys var globalJsonContent = await File.ReadAllTextAsync(safeGlobalJsonName); var json = JsonHelper.ParseNode(globalJsonContent); - var sdks = json["msbuild-sdks"]; + var sdks = json?["msbuild-sdks"]; if (sdks is not null) { var newObject = new Dictionary() { - { "msbuild-sdks", sdks } + ["msbuild-sdks"] = sdks, }; var newContent = JsonSerializer.Serialize(newObject); await File.WriteAllTextAsync(globalJsonPath, newContent); @@ -427,7 +460,7 @@ internal static async Task> LoadBuildFiles(stri // load the project even if it imports a file that doesn't exist (e.g. a file that's generated at restore // or build time). using var projectCollection = new ProjectCollection(); // do this in a one-off instance and don't pollute the global collection - var project = Project.FromFile(projectPath, new ProjectOptions() + var project = Project.FromFile(projectPath, new ProjectOptions { LoadSettings = ProjectLoadSettings.IgnoreMissingImports, ProjectCollection = projectCollection, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs index 6056a45aa1..1a0bf60fed 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs @@ -10,6 +10,7 @@ internal static class PathHelper { MatchCasing = MatchCasing.CaseInsensitive, }; + private static readonly EnumerationOptions _caseSensitiveEnumerationOptions = new() { MatchCasing = MatchCasing.CaseSensitive, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs index 39fccb0d33..c7b5da8758 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs @@ -45,14 +45,14 @@ public static class ProcessEx // than enter right back into the Process type and start a wait which isn't guaranteed to be safe. Task.Run(() => { - redirectInitiated.Wait(); - redirectInitiated.Dispose(); - redirectInitiated = null; + redirectInitiated.Wait(); + redirectInitiated.Dispose(); + redirectInitiated = null; - process.WaitForExit(); + process.WaitForExit(); - tcs.TrySetResult((process.ExitCode, stdout.ToString(), stderr.ToString())); - process.Dispose(); + tcs.TrySetResult((process.ExitCode, stdout.ToString(), stderr.ToString())); + process.Dispose(); }); }; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs index e31bb65b94..1a38affa7d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs @@ -30,6 +30,17 @@ public static IEnumerable GetElements(this IXmlElementSyntax return element.Attributes.FirstOrDefault(a => a.Name.Equals(name, comparisonType)); } + public static IXmlElementSyntax RemoveAttributeByName(this IXmlElementSyntax element, string attributeName, StringComparison comparisonType = StringComparison.Ordinal) + { + var attribute = element.GetAttribute(attributeName, comparisonType); + if (attribute is null) + { + return element; + } + + return element.RemoveAttribute(attribute); + } + public static string GetAttributeValue(this IXmlElementSyntax element, string name, StringComparison comparisonType) { return element.Attributes.First(a => a.Name.Equals(name, comparisonType)).Value; diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index 005825f113..430238a9ee 100644 --- a/nuget/lib/dependabot/nuget/file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "nokogiri" @@ -6,12 +6,15 @@ require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" +require "sorbet-runtime" # For details on how dotnet handles version constraints, see: # https://docs.microsoft.com/en-us/nuget/reference/package-versioning module Dependabot module Nuget class FileParser < Dependabot::FileParsers::Base + extend T::Sig + require "dependabot/file_parsers/base/dependency_set" require_relative "file_parser/project_file_parser" require_relative "file_parser/packages_config_parser" @@ -20,6 +23,7 @@ class FileParser < Dependabot::FileParsers::Base PACKAGE_CONF_DEPENDENCY_SELECTOR = "packages > packages" + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse dependency_set = DependencySet.new dependency_set += project_file_dependencies @@ -31,6 +35,7 @@ def parse private + sig { returns(Dependabot::FileParsers::Base::DependencySet) } def project_file_dependencies dependency_set = DependencySet.new @@ -42,6 +47,7 @@ def project_file_dependencies dependency_set end + sig { returns(Dependabot::FileParsers::Base::DependencySet) } def packages_config_dependencies dependency_set = DependencySet.new @@ -53,26 +59,33 @@ def packages_config_dependencies dependency_set end + sig { returns(Dependabot::FileParsers::Base::DependencySet) } def global_json_dependencies return DependencySet.new unless global_json GlobalJsonParser.new(global_json: global_json).dependency_set end + sig { returns(Dependabot::FileParsers::Base::DependencySet) } def dotnet_tools_json_dependencies return DependencySet.new unless dotnet_tools_json DotNetToolsJsonParser.new(dotnet_tools_json: dotnet_tools_json).dependency_set end + sig { returns(Dependabot::Nuget::FileParser::ProjectFileParser) } def project_file_parser - @project_file_parser ||= + @project_file_parser ||= T.let( ProjectFileParser.new( dependency_files: dependency_files, - credentials: credentials - ) + credentials: credentials, + repo_contents_path: @repo_contents_path + ), + T.nilable(Dependabot::Nuget::FileParser::ProjectFileParser) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def project_files projfile = /\.([a-z]{2})?proj$/ packageprops = /[Dd]irectory.[Pp]ackages.props/ @@ -83,12 +96,14 @@ def project_files end end + sig { returns(T::Array[Dependabot::DependencyFile]) } def packages_config_files dependency_files.select do |f| f.name.split("/").last&.casecmp("packages.config")&.zero? end end + sig { returns(T::Array[Dependabot::DependencyFile]) } def project_import_files dependency_files - project_files - @@ -98,18 +113,22 @@ def project_import_files [dotnet_tools_json] end + sig { returns(T::Array[Dependabot::DependencyFile]) } def nuget_configs dependency_files.select { |f| f.name.match?(/nuget\.config$/i) } end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def global_json dependency_files.find { |f| f.name.casecmp("global.json")&.zero? } end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def dotnet_tools_json dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json")&.zero? } end + sig { override.void } def check_required_files return if project_files.any? || packages_config_files.any? diff --git a/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb b/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb index 8f7e7accfd..9c2dfd4824 100644 --- a/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser/project_file_parser.rb @@ -14,7 +14,9 @@ module Dependabot module Nuget class FileParser - class ProjectFileParser + class ProjectFileParser # rubocop:disable Metrics/ClassLength + extend T::Sig + require "dependabot/file_parsers/base/dependency_set" require_relative "property_value_finder" require_relative "../update_checker/repository_finder" @@ -46,16 +48,20 @@ def self.dependency_url_search_cache CacheManager.cache("dependency_url_search_cache") end - def initialize(dependency_files:, credentials:) + def initialize(dependency_files:, credentials:, repo_contents_path:) @dependency_files = dependency_files @credentials = credentials + @repo_contents_path = repo_contents_path end - def dependency_set(project_file:) + def dependency_set(project_file:, visited_project_files: Set.new) key = "#{project_file.name.downcase}::#{project_file.content.hash}" cache = ProjectFileParser.dependency_set_cache - cache[key] ||= parse_dependencies(project_file) + visited_project_files.add(cache[key]) + + # Pass the visited_project_files set to parse_dependencies + cache[key] ||= parse_dependencies(project_file, visited_project_files) end def downstream_file_references(project_file:) @@ -70,7 +76,10 @@ def downstream_file_references(project_file:) dep_file = get_attribute_value(project_reference_node, "Include") full_project_path = full_path(project_file, dep_file) full_project_path = full_project_path[1..-1] if full_project_path.start_with?("/") - file_set << full_project_path if full_project_path + full_project_paths = expand_wildcards_in_project_reference_path(full_project_path) + full_project_paths.each do |full_project_path_expanded| + file_set << full_project_path_expanded if full_project_path_expanded + end end file_set @@ -115,7 +124,7 @@ def full_path(project_file, ref_path) result end - def parse_dependencies(project_file) + def parse_dependencies(project_file, visited_project_files) dependency_set = Dependabot::FileParsers::Base::DependencySet.new doc = Nokogiri::XML(project_file.content) @@ -134,7 +143,7 @@ def parse_dependencies(project_file) add_global_package_references(dependency_set) - add_transitive_dependencies(project_file, doc, dependency_set) + add_transitive_dependencies(project_file, doc, dependency_set, visited_project_files) # Look for SDK references; see: # https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk @@ -160,12 +169,16 @@ def add_global_package_references(dependency_set) end end - def add_transitive_dependencies(project_file, doc, dependency_set) + def add_transitive_dependencies(project_file, doc, dependency_set, visited_project_files) add_transitive_dependencies_from_packages(dependency_set) - add_transitive_dependencies_from_project_references(project_file, doc, dependency_set) + add_transitive_dependencies_from_project_references(project_file, doc, dependency_set, visited_project_files) end - def add_transitive_dependencies_from_project_references(project_file, doc, dependency_set) + def add_transitive_dependencies_from_project_references(project_file, doc, dependency_set, + visited_project_files) + + # if visited_project_files is an empty set then new up a new set + visited_project_files = Set.new if visited_project_files.nil? # Look for regular project references project_refs = doc.css(PROJECT_REFERENCE_SELECTOR) # Look for ProjectFile references (dirs.proj) @@ -179,21 +192,51 @@ def add_transitive_dependencies_from_project_references(project_file, doc, depen full_project_path = full_path(project_file, relative_path) - referenced_file = dependency_files.find { |f| f.name == full_project_path } - next unless referenced_file - - dependency_set(project_file: referenced_file).dependencies.each do |dep| - dependency = Dependency.new( - name: dep.name, - version: dep.version, - package_manager: dep.package_manager, - requirements: [] - ) - dependency_set << dependency + full_project_paths = expand_wildcards_in_project_reference_path(full_project_path) + + full_project_paths.each do |path| + # Check if we've already visited this project file + next if visited_project_files.include?(path) + + visited_project_files.add(path) + referenced_file = dependency_files.find { |f| f.name == path } + next unless referenced_file + + dependency_set(project_file: referenced_file, + visited_project_files: visited_project_files).dependencies.each do |dep| + dependency = Dependency.new( + name: dep.name, + version: dep.version, + package_manager: dep.package_manager, + requirements: [] + ) + dependency_set << dependency + end end end end + sig { params(full_path: T.untyped).returns(T::Array[T.nilable(String)]) } + def expand_wildcards_in_project_reference_path(full_path) + full_path = T.let(File.join(@repo_contents_path, full_path), T.nilable(String)) + expanded_wildcard = Dir.glob(T.must(full_path)) + + filtered_paths = [] + + # For each expanded path, remove the @repo_contents_path prefix and leading slash + expanded_wildcard.map do |path| + # Remove @repo_contents_path prefix + path = path.sub(@repo_contents_path, "") + # Remove leading slash + path = path[1..-1] if path.start_with?("/") + filtered_paths << path + path # Return the modified path + end + + # If the wildcard didn't match anything, strip the @repo_contents_path prefix and return the original path. + filtered_paths.any? ? filtered_paths : [T.must(full_path).sub(@repo_contents_path, "")[1..-1]] + end + def add_transitive_dependencies_from_packages(dependency_set) transitive_dependencies_from_packages(dependency_set.dependencies).each { |dep| dependency_set << dep } end @@ -205,7 +248,8 @@ def transitive_dependencies_from_packages(dependencies) UpdateChecker::DependencyFinder.new( dependency: dependency, dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: @repo_contents_path ).transitive_dependencies.each do |transitive_dep| visited_dep = transitive_dependencies[transitive_dep.name.downcase] next if !visited_dep.nil? && visited_dep.numeric_version > transitive_dep.numeric_version diff --git a/nuget/lib/dependabot/nuget/file_updater.rb b/nuget/lib/dependabot/nuget/file_updater.rb index 14c42c7168..45599be2be 100644 --- a/nuget/lib/dependabot/nuget/file_updater.rb +++ b/nuget/lib/dependabot/nuget/file_updater.rb @@ -66,6 +66,8 @@ def try_update_projects(dependency) next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? } + next unless repo_contents_path + checked_key = "#{project_file.name}-#{dependency.name}#{dependency.version}" call_nuget_updater_tool(dependency, proj_path) unless checked_files.include?(checked_key) @@ -89,6 +91,8 @@ def try_update_json(dependency) project_file = project_files.first proj_path = dependency_file_path(project_file) + return false unless repo_contents_path + call_nuget_updater_tool(dependency, proj_path) return true end @@ -98,7 +102,7 @@ def try_update_json(dependency) sig { params(dependency: Dependency, proj_path: String).void } def call_nuget_updater_tool(dependency, proj_path) - NativeHelpers.run_nuget_updater_tool(repo_root: repo_contents_path, proj_path: proj_path, + NativeHelpers.run_nuget_updater_tool(repo_root: T.must(repo_contents_path), proj_path: proj_path, dependency: dependency, is_transitive: !dependency.top_level?, credentials: credentials) @@ -140,7 +144,8 @@ def project_file_parser @project_file_parser ||= FileParser::ProjectFileParser.new( dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: repo_contents_path ) end diff --git a/nuget/lib/dependabot/nuget/native_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index b1770dade2..1e98cc329f 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -1,13 +1,17 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "shellwords" +require "sorbet-runtime" require_relative "nuget_config_credential_helpers" module Dependabot module Nuget module NativeHelpers + extend T::Sig + + sig { returns(String) } def self.native_helpers_root helpers_root = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil) return File.join(helpers_root, "nuget") unless helpers_root.nil? @@ -15,6 +19,7 @@ def self.native_helpers_root File.expand_path("../../../helpers", __dir__) end + sig { params(project_tfms: T::Array[String], package_tfms: T::Array[String]).returns(T::Boolean) } def self.run_nuget_framework_check(project_tfms, package_tfms) exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") command_parts = [ @@ -51,6 +56,15 @@ def self.run_nuget_framework_check(project_tfms, package_tfms) end # rubocop:disable Metrics/MethodLength + sig do + params( + repo_root: String, + proj_path: String, + dependency: Dependency, + is_transitive: T::Boolean, + credentials: T::Array[T.untyped] + ).void + end def self.run_nuget_updater_tool(repo_root:, proj_path:, dependency:, is_transitive:, credentials:) exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") command_parts = [ diff --git a/nuget/lib/dependabot/nuget/nuget_client.rb b/nuget/lib/dependabot/nuget/nuget_client.rb index d337b843b7..e0e6f4238d 100644 --- a/nuget/lib/dependabot/nuget/nuget_client.rb +++ b/nuget/lib/dependabot/nuget/nuget_client.rb @@ -1,12 +1,19 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "dependabot/nuget/cache_manager" require "dependabot/nuget/update_checker/repository_finder" +require "sorbet-runtime" module Dependabot module Nuget class NugetClient + extend T::Sig + + sig do + params(dependency_name: String, repository_details: T::Hash[Symbol, String]) + .returns(T.nilable(T::Set[String])) + end def self.get_package_versions(dependency_name, repository_details) repository_type = repository_details.fetch(:repository_type) if repository_type == "v3" @@ -18,6 +25,10 @@ def self.get_package_versions(dependency_name, repository_details) end end + sig do + params(dependency_name: String, repository_details: T::Hash[Symbol, String]) + .returns(T.nilable(T::Set[String])) + end private_class_method def self.get_package_versions_v3(dependency_name, repository_details) # Use the registration URL if possible because it is fast and correct if repository_details[:registration_url] @@ -28,9 +39,15 @@ def self.get_package_versions(dependency_name, repository_details) # Otherwise, use the versions URL (fast but wrong because it includes unlisted versions) elsif repository_details[:versions_url] get_versions_from_versions_url_v3(repository_details) + else + raise "No version sources were available for #{dependency_name} in #{repository_details}" end end + sig do + params(dependency_name: String, repository_details: T::Hash[Symbol, String]) + .returns(T.nilable(T::Set[String])) + end private_class_method def self.get_package_versions_v2(dependency_name, repository_details) doc = execute_xml_nuget_request(repository_details.fetch(:versions_url), repository_details) return unless doc @@ -49,38 +66,33 @@ def self.get_package_versions(dependency_name, repository_details) matching_versions end + sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) } private_class_method def self.get_versions_from_versions_url_v3(repository_details) - body = execute_json_nuget_request(repository_details[:versions_url], repository_details) - body&.fetch("versions") + body = execute_json_nuget_request(repository_details.fetch(:versions_url), repository_details) + ver_array = T.let(body&.fetch("versions"), T.nilable(T::Array[String])) + ver_array&.to_set end + sig { params(repository_details: T::Hash[Symbol, String]).returns(T.nilable(T::Set[String])) } private_class_method def self.get_versions_from_registration_v3(repository_details) - url = repository_details[:registration_url] + url = repository_details.fetch(:registration_url) body = execute_json_nuget_request(url, repository_details) return unless body pages = body.fetch("items") - versions = Set.new + versions = T.let(Set.new, T::Set[String]) pages.each do |page| items = page["items"] if items # inlined entries - items.each do |item| - catalog_entry = item["catalogEntry"] - - # a package is considered listed if the `listed` property is either `true` or missing - listed_property = catalog_entry["listed"] - is_listed = listed_property.nil? || listed_property == true - if is_listed - vers = catalog_entry["version"] - versions << vers - end - end + get_versions_from_inline_page(items, versions) else # paged entries page_url = page["@id"] page_body = execute_json_nuget_request(page_url, repository_details) + next unless page_body + items = page_body.fetch("items") items.each do |item| catalog_entry = item.fetch("catalogEntry") @@ -92,8 +104,27 @@ def self.get_package_versions(dependency_name, repository_details) versions end + sig { params(items: T::Array[T::Hash[String, T.untyped]], versions: T::Set[String]).void } + private_class_method def self.get_versions_from_inline_page(items, versions) + items.each do |item| + catalog_entry = item["catalogEntry"] + + # a package is considered listed if the `listed` property is either `true` or missing + listed_property = catalog_entry["listed"] + is_listed = listed_property.nil? || listed_property == true + if is_listed + vers = catalog_entry["version"] + versions << vers + end + end + end + + sig do + params(repository_details: T::Hash[Symbol, String], dependency_name: String) + .returns(T.nilable(T::Set[String])) + end private_class_method def self.get_versions_from_search_url_v3(repository_details, dependency_name) - search_url = repository_details[:search_url] + search_url = repository_details.fetch(:search_url) body = execute_json_nuget_request(search_url, repository_details) body&.fetch("data") @@ -102,11 +133,14 @@ def self.get_package_versions(dependency_name, repository_details) &.map { |d| d.fetch("version") } end + sig do + params(url: String, repository_details: T::Hash[Symbol, T.untyped]).returns(T.nilable(Nokogiri::XML::Document)) + end private_class_method def self.execute_xml_nuget_request(url, repository_details) response = execute_nuget_request_internal( url: url, - auth_header: repository_details[:auth_header], - repository_url: repository_details[:repository_url] + auth_header: repository_details.fetch(:auth_header), + repository_url: repository_details.fetch(:repository_url) ) return unless response.status == 200 @@ -115,11 +149,16 @@ def self.get_package_versions(dependency_name, repository_details) doc end + sig do + params(url: String, + repository_details: T::Hash[Symbol, T.untyped]) + .returns(T.nilable(T::Hash[T.untyped, T.untyped])) + end private_class_method def self.execute_json_nuget_request(url, repository_details) response = execute_nuget_request_internal( url: url, - auth_header: repository_details[:auth_header], - repository_url: repository_details[:repository_url] + auth_header: repository_details.fetch(:auth_header), + repository_url: repository_details.fetch(:repository_url) ) return unless response.status == 200 @@ -127,11 +166,10 @@ def self.get_package_versions(dependency_name, repository_details) JSON.parse(body) end - private_class_method def self.execute_nuget_request_internal( - url: String, - auth_header: String, - repository_url: String - ) + sig do + params(url: String, auth_header: T::Hash[Symbol, T.untyped], repository_url: String).returns(Excon::Response) + end + private_class_method def self.execute_nuget_request_internal(url:, auth_header:, repository_url:) cache = CacheManager.cache("dependency_url_search_cache") if cache[url].nil? response = Dependabot::RegistryClient.get( @@ -156,6 +194,7 @@ def self.get_package_versions(dependency_name, repository_details) raise PrivateSourceTimedOut, repo_url end + sig { params(string: String).returns(String) } private_class_method def self.remove_wrapping_zero_width_chars(string) string.force_encoding("UTF-8").encode .gsub(/\A[\u200B-\u200D\uFEFF]/, "") diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index 5e98b0700a..541b8ae752 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -107,7 +107,8 @@ def updated_dependencies_after_full_unlock updated_dependencies += DependencyFinder.new( dependency: updated_dependency, dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: @repo_contents_path ).updated_peer_dependencies updated_dependencies end @@ -135,7 +136,8 @@ def version_finder credentials: credentials, ignored_versions: ignored_versions, raise_on_ignored: @raise_on_ignored, - security_advisories: security_advisories + security_advisories: security_advisories, + repo_contents_path: @repo_contents_path ) end @@ -147,7 +149,8 @@ def property_updater target_version_details: latest_version_details, credentials: credentials, ignored_versions: ignored_versions, - raise_on_ignored: @raise_on_ignored + raise_on_ignored: @raise_on_ignored, + repo_contents_path: @repo_contents_path ) end diff --git a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb index fbd8c7e917..60252c755c 100644 --- a/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker/compatibility_checker.rb @@ -21,9 +21,10 @@ def compatible?(version) nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version) return false unless nuspec_xml - # development dependencies are packages such as analyzers which need to be - # compatible with the compiler not the project itself. - return true if development_dependency?(nuspec_xml) + # development dependencies are packages such as analyzers which need to be compatible with the compiler not the + # project itself, but some packages that report themselves as development dependencies still contain target + # framework dependencies and should be checked for compatibility through the regular means + return true if pure_development_dependency?(nuspec_xml) package_tfms = parse_package_tfms(nuspec_xml) package_tfms = fetch_package_tfms(version) if package_tfms.empty? @@ -40,11 +41,18 @@ def compatible?(version) attr_reader :dependency_urls, :dependency, :tfm_finder - def development_dependency?(nuspec_xml) + def pure_development_dependency?(nuspec_xml) contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip - return false unless contents + return false unless contents # no `developmentDependency` element - contents.casecmp("true").zero? + self_reports_as_development_dependency = contents.casecmp?("true") + return false unless self_reports_as_development_dependency + + # even though a package self-reports as a development dependency, it might not be if it has dependency groups + # with a target framework + dependency_groups_with_target_framework = + nuspec_xml.at_xpath("/package/metadata/dependencies/group[@targetFramework]") + dependency_groups_with_target_framework.to_a.empty? end def parse_package_tfms(nuspec_xml) diff --git a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb b/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb index fc57382055..0fdc51402f 100644 --- a/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/dependency_finder.rb @@ -26,10 +26,11 @@ def self.fetch_dependencies_cache CacheManager.cache("dependency_finder_fetch_dependencies") end - def initialize(dependency:, dependency_files:, credentials:) + def initialize(dependency:, dependency_files:, credentials:, repo_contents_path:) @dependency = dependency @dependency_files = dependency_files @credentials = credentials + @repo_contents_path = repo_contents_path end def transitive_dependencies @@ -93,7 +94,7 @@ def updated_peer_dependencies private - attr_reader :dependency, :dependency_files, :credentials + attr_reader :dependency, :dependency_files, :credentials, :repo_contents_path def updated_requirements(dep, target_version_details) @updated_requirements ||= {} @@ -219,7 +220,8 @@ def version_finder(dep) credentials: credentials, ignored_versions: [], raise_on_ignored: false, - security_advisories: [] + security_advisories: [], + repo_contents_path: repo_contents_path ) end end diff --git a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb b/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb index 7b8503da6a..2f937e088e 100644 --- a/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +++ b/nuget/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb @@ -73,7 +73,7 @@ def self.fetch_stream(stream_url, auth_header, max_redirects = 5) response_block: response_block ) - if response.status == 303 + if response.status == 303 || response.status == 307 current_redirects += 1 return nil if current_redirects > max_redirects diff --git a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb b/nuget/lib/dependabot/nuget/update_checker/property_updater.rb index c905a5a54a..f589390df4 100644 --- a/nuget/lib/dependabot/nuget/update_checker/property_updater.rb +++ b/nuget/lib/dependabot/nuget/update_checker/property_updater.rb @@ -14,7 +14,7 @@ class PropertyUpdater def initialize(dependency:, dependency_files:, credentials:, target_version_details:, ignored_versions:, - raise_on_ignored: false) + raise_on_ignored: false, repo_contents_path:) @dependency = dependency @dependency_files = dependency_files @credentials = credentials @@ -23,6 +23,7 @@ def initialize(dependency:, dependency_files:, credentials:, @target_version = target_version_details&.fetch(:version) @source_details = target_version_details &.slice(:nuspec_url, :repo_url, :source_url) + @repo_contents_path = repo_contents_path end def update_possible? @@ -36,7 +37,8 @@ def update_possible? credentials: credentials, ignored_versions: ignored_versions, raise_on_ignored: @raise_on_ignored, - security_advisories: [] + security_advisories: [], + repo_contents_path: repo_contents_path ).versions.map { |v| v.fetch(:version) } versions.include?(target_version) || versions.none? @@ -74,13 +76,14 @@ def updated_dependencies private attr_reader :dependency, :dependency_files, :target_version, - :source_details, :credentials, :ignored_versions + :source_details, :credentials, :ignored_versions, :repo_contents_path def process_updated_peer_dependencies(dependency, dependencies) DependencyFinder.new( dependency: dependency, dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: repo_contents_path ).updated_peer_dependencies.each do |peer_dependency| # Only keep one copy of each dependency, the one with the highest target version. visited_dependency = dependencies[peer_dependency.name.downcase] diff --git a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb b/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb index a43d0fdfd4..ea1ca79055 100644 --- a/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/repository_finder.rb @@ -33,6 +33,9 @@ def known_repositories @known_repositories << { url: DEFAULT_REPOSITORY_URL, token: nil } if @known_repositories.empty? + @known_repositories = @known_repositories.map do |repo| + { url: URI::DEFAULT_PARSER.escape(repo[:url]), token: repo[:token] } + end @known_repositories.uniq end @@ -192,7 +195,7 @@ def handle_timeout(repo_metadata_url:) def credential_repositories @credential_repositories ||= credentials - .select { |cred| cred["type"] == "nuget_feed" } + .select { |cred| cred["type"] == "nuget_feed" && cred["url"] } .map { |c| { url: c.fetch("url"), token: c["token"] } } end diff --git a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb index 2089856747..5547f8b0e2 100644 --- a/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/tfm_finder.rb @@ -16,9 +16,10 @@ class TfmFinder require "dependabot/nuget/file_parser/packages_config_parser" require "dependabot/nuget/file_parser/project_file_parser" - def initialize(dependency_files:, credentials:) + def initialize(dependency_files:, credentials:, repo_contents_path:) @dependency_files = dependency_files @credentials = credentials + @repo_contents_path = repo_contents_path end def frameworks(dependency) @@ -30,7 +31,7 @@ def frameworks(dependency) private - attr_reader :dependency_files, :credentials + attr_reader :dependency_files, :credentials, :repo_contents_path def project_file_tfms(dependency) project_files_with_dependency(dependency).flat_map do |file| @@ -80,7 +81,8 @@ def project_file_parser @project_file_parser ||= FileParser::ProjectFileParser.new( dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: repo_contents_path ) end diff --git a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb b/nuget/lib/dependabot/nuget/update_checker/version_finder.rb index 980b16e068..75c5e70d8f 100644 --- a/nuget/lib/dependabot/nuget/update_checker/version_finder.rb +++ b/nuget/lib/dependabot/nuget/update_checker/version_finder.rb @@ -18,13 +18,15 @@ class VersionFinder def initialize(dependency:, dependency_files:, credentials:, ignored_versions:, raise_on_ignored: false, - security_advisories:) + security_advisories:, + repo_contents_path:) @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 def latest_version_details @@ -58,7 +60,7 @@ def versions end attr_reader :dependency, :dependency_files, :credentials, - :ignored_versions, :security_advisories + :ignored_versions, :security_advisories, :repo_contents_path private @@ -101,7 +103,8 @@ def compatibility_checker dependency: dependency, tfm_finder: TfmFinder.new( dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: repo_contents_path ) ) end diff --git a/nuget/lib/dependabot/nuget/version.rb b/nuget/lib/dependabot/nuget/version.rb index 74b43289a9..b2ff760f72 100644 --- a/nuget/lib/dependabot/nuget/version.rb +++ b/nuget/lib/dependabot/nuget/version.rb @@ -1,8 +1,9 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "dependabot/version" require "dependabot/utils" +require "sorbet-runtime" # Dotnet pre-release versions use 1.0.1-rc1 syntax, which Gem::Version # converts into 1.0.1.pre.rc1. We override the `to_s` method to stop that @@ -11,30 +12,37 @@ module Dependabot module Nuget class Version < Dependabot::Version - VERSION_PATTERN = Gem::Version::VERSION_PATTERN + '(\+[0-9a-zA-Z\-.]+)?' + extend T::Sig + + VERSION_PATTERN = T.let(Gem::Version::VERSION_PATTERN + '(\+[0-9a-zA-Z\-.]+)?', String) ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ + sig { override.params(version: T.nilable(T.any(String, Integer, Float, Gem::Version))).returns(T::Boolean) } def self.correct?(version) return false if version.nil? version.to_s.match?(ANCHORED_VERSION_PATTERN) end + sig { override.params(version: T.nilable(T.any(String, Integer, Float, Gem::Version))).void } def initialize(version) version = version.to_s.split("+").first || "" - @version_string = version + @version_string = T.let(version, String) super end + sig { returns(String) } def to_s @version_string end + sig { returns(String) } def inspect # :nodoc: "#<#{self.class} #{@version_string}>" end + sig { params(other: Object).returns(Integer) } def <=>(other) version_comparison = compare_release(other) return version_comparison unless version_comparison.zero? @@ -42,14 +50,16 @@ def <=>(other) compare_prerelease_part(other) end + sig { params(other: Object).returns(Integer) } def compare_release(other) release_str = @version_string.split("-").first || "" other_release_str = other.to_s.split("-").first || "" - Gem::Version.new(release_str) <=> Gem::Version.new(other_release_str) + T.must(Gem::Version.new(release_str) <=> Gem::Version.new(other_release_str)) end # rubocop:disable Metrics/PerceivedComplexity + sig { params(other: Object).returns(Integer) } def compare_prerelease_part(other) release_str = @version_string.split("-").first || "" prerelease_string = @version_string @@ -67,8 +77,8 @@ def compare_prerelease_part(other) return 1 if !prerelease_string && other_prerelease_string return 0 if !prerelease_string && !other_prerelease_string - split_prerelease_string = prerelease_string.split(".") - other_split_prerelease_string = other_prerelease_string.split(".") + split_prerelease_string = T.must(prerelease_string).split(".") + other_split_prerelease_string = T.must(other_prerelease_string).split(".") length = [split_prerelease_string.length, other_split_prerelease_string.length].max - 1 (0..length).to_a.each do |index| @@ -82,13 +92,14 @@ def compare_prerelease_part(other) end # rubocop:enable Metrics/PerceivedComplexity + sig { params(lhs: T.nilable(String), rhs: T.nilable(String)).returns(Integer) } def compare_dot_separated_part(lhs, rhs) return -1 if lhs.nil? return 1 if rhs.nil? return lhs.to_i <=> rhs.to_i if lhs.match?(/^\d+$/) && rhs.match?(/^\d+$/) - lhs.upcase <=> rhs.upcase + T.must(lhs.upcase <=> rhs.upcase) end end end diff --git a/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb index ea02ba4052..105d7a75e6 100644 --- a/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser/project_file_parser_spec.rb @@ -17,7 +17,9 @@ Dependabot::DependencyFile.new(name: "my.csproj", content: file_body) end let(:file_body) { fixture("csproj", "basic.csproj") } - let(:parser) { described_class.new(dependency_files: [file], credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: [file], credentials: credentials, repo_contents_path: "/test/repo") + end let(:credentials) do [{ "type" => "git_source", @@ -68,7 +70,9 @@ ) ] end - let(:parser) { described_class.new(dependency_files: files, credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: files, credentials: credentials, repo_contents_path: "/test/repo") + end let(:dependencies) { dependency_set.dependencies } subject(:transitive_dependencies) { dependencies.reject(&:top_level?) } @@ -374,7 +378,10 @@ def dependencies_from(dep_info) ] end - let(:parser) { described_class.new(dependency_files: files, credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: files, credentials: credentials, + repo_contents_path: "/test/repo") + end subject(:dependency) do top_level_dependencies.find { |d| d.name == "Newtonsoft.Json" } @@ -427,7 +434,10 @@ def dependencies_from(dep_info) ] end - let(:parser) { described_class.new(dependency_files: files, credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: files, credentials: credentials, + repo_contents_path: "/test/repo") + end subject(:dependency) do top_level_dependencies.find { |d| d.name == "Newtonsoft.Json" } @@ -488,7 +498,10 @@ def dependencies_from(dep_info) ] end - let(:parser) { described_class.new(dependency_files: files, credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: files, credentials: credentials, + repo_contents_path: "/test/repo") + end subject(:dependency) do top_level_dependencies.find { |d| d.name == "Newtonsoft.Json" } @@ -623,7 +636,10 @@ def dependencies_from(dep_info) let(:nuget_config_file) do Dependabot::DependencyFile.new(name: "NuGet.config", content: nuget_config_body) end - let(:parser) { described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, + repo_contents_path: "/test/repo") + end before do # no results @@ -694,7 +710,10 @@ def dependencies_from(dep_info) let(:nuget_config_file) do Dependabot::DependencyFile.new(name: "NuGet.config", content: nuget_config_body) end - let(:parser) { described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, + repo_contents_path: "/test/repo") + end before do stub_request(:get, "https://www.nuget.org/api/v2/") @@ -729,7 +748,10 @@ def dependencies_from(dep_info) let(:nuget_config_file) do Dependabot::DependencyFile.new(name: "../../NuGet.Config", content: nuget_config_body) end - let(:parser) { described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, + repo_contents_path: "/test/repo") + end it "finds the config file up several directories" do nuget_configs = parser.nuget_configs @@ -748,7 +770,10 @@ def dependencies_from(dep_info) let(:nuget_config_file) do Dependabot::DependencyFile.new(name: "../../not-NuGet.Config", content: nuget_config_body) end - let(:parser) { described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: [file, nuget_config_file], credentials: credentials, + repo_contents_path: "/test/repo") + end it "does not return a name with a partial match" do nuget_configs = parser.nuget_configs @@ -788,7 +813,10 @@ def dependencies_from(dep_info) let(:file2) do Dependabot::DependencyFile.new(name: "my2.csproj", content: file_2_body) end - let(:parser) { described_class.new(dependency_files: [file, file2], credentials: credentials) } + let(:parser) do + described_class.new(dependency_files: [file, file2], credentials: credentials, + repo_contents_path: "/test/repo") + end before do stub_no_search_results("this.dependency.does.not.exist") diff --git a/nuget/spec/dependabot/nuget/file_updater_spec.rb b/nuget/spec/dependabot/nuget/file_updater_spec.rb index 51f2141601..364703dc4e 100644 --- a/nuget/spec/dependabot/nuget/file_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/file_updater_spec.rb @@ -81,4 +81,28 @@ end end end + + describe "#updated_dependency_files_with_wildcard" do + subject(:updated_files) { file_updater_instance.updated_dependency_files } + + let(:project_name) { "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 match_array([ + "Proj1/Proj1/Proj1.csproj", + "Proj2/Proj2.csproj" + ]) + + expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( + { + "#{repo_contents_path}/dirs.projMicrosoft.Extensions.DependencyModel" => 1, + "#{repo_contents_path}/Proj2/Proj2.csprojMicrosoft.Extensions.DependencyModel" => 1 + } + ) + end + end end diff --git a/nuget/spec/dependabot/nuget/native_helpers_spec.rb b/nuget/spec/dependabot/nuget/native_helpers_spec.rb index 3e85a02f03..1939fb039f 100644 --- a/nuget/spec/dependabot/nuget/native_helpers_spec.rb +++ b/nuget/spec/dependabot/nuget/native_helpers_spec.rb @@ -45,4 +45,35 @@ end end end + + describe "#native_csharp_format" do + let(:command) do + [ + "dotnet", + "format", + lib_path, + "--exclude", + except_path, + "--verify-no-changes", + "-v", + "diag" + ].join(" ") + end + + subject(:dotnet_test) do + Dependabot::SharedHelpers.run_shell_command(command) + end + + context "`dotnet format NuGetUpdater` output" do + let(:lib_path) do + File.absolute_path(File.join("helpers", "lib", "NuGetUpdater")) + end + + let(:except_path) { "helpers/lib/NuGet.Client" } + + it "contains the expected output" do + expect(dotnet_test).to include("Format complete") + 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 index 0e0e8a0d0e..10376286ce 100644 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb @@ -50,7 +50,8 @@ let(:tfm_finder) do Dependabot::Nuget::TfmFinder.new( dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: "test/repo" ) end @@ -74,41 +75,109 @@ context "#compatible?" do subject(:compatible) { checker.compatible?(version) } - 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/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: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") + 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/registration5-gz-semver2/microsoft.appcenter.crashes/index.json") + stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") .to_return( status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, - version: "5.0.2" - } - }, - { - catalogEntry: { - listed: true, - version: "5.0.3" - } - } - ] - ] - }.to_json + body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") ) 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 index ed56ee5e43..ba0431fcd5 100644 --- a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb @@ -11,7 +11,8 @@ described_class.new( dependency: dependency, dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: "test/repo" ) end let(:dependency) do diff --git a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb index 8e44452e25..f631503e8b 100644 --- a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb @@ -5,6 +5,7 @@ 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 @@ -81,4 +82,42 @@ 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 end + + describe "#fetch_nupkg_buffer" do + let(:package_id) { "Newtonsoft.Json" } + let(:package_version) { "13.0.1" } + let(:repository_details) { Dependabot::Nuget::RepositoryFinder.get_default_repository_details(package_id) } + let(:dependency_urls) { [repository_details] } + subject(:nupkg_buffer) do + described_class.fetch_nupkg_buffer(dependency_urls, package_id, package_version) + end + + before do + stub_request(:get, "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") + .to_return( + status: 303, + headers: { + "Location" => "https://api.nuget.org/redirect-on-303" + }, + body: "not the final contents" + ) + stub_request(:get, "https://api.nuget.org/redirect-on-303") + .to_return( + status: 307, + headers: { + "Location" => "https://api.nuget.org/redirect-on-307" + }, + body: "almost final contents" + ) + stub_request(:get, "https://api.nuget.org/redirect-on-307") + .to_return( + status: 200, + body: "the final contents" + ) + end + + it "fetches the nupkg after multiple redirects" do + expect(nupkg_buffer.string).to eq("the final contents") + 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 index 42d6d93301..99a1d1a10c 100644 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb @@ -181,6 +181,36 @@ end end + context "that has URLs that need to be escaped" do + let(:custom_repo_url) { "https://www.myget.org/F/exceptionless/api with spaces/v3/index.json" } + before do + stub_request(:get, "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json") + .to_return( + status: 200, + body: fixture("nuget_responses", "myget_base.json") + ) + end + + it "gets the right URL" do + expect(dependency_urls).to eq( + [{ + base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", + registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ + "microsoft.extensions.dependencymodel/index.json", + repository_url: "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json", + versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ + "flatcontainer/microsoft.extensions." \ + "dependencymodel/index.json", + search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ + "query?q=microsoft.extensions.dependencymodel" \ + "&prerelease=true&semVerLevel=2.0.0", + auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, + repository_type: "v3" + }] + ) + end + end + context "that 404s" do before { stub_request(:get, custom_repo_url).to_return(status: 404) } diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb index dfb2a7cae1..d6533e46d0 100644 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb @@ -10,7 +10,8 @@ subject(:finder) do described_class.new( dependency_files: dependency_files, - credentials: credentials + credentials: credentials, + repo_contents_path: "test/repo" ) 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 index 0bf8fdafb6..0f2b6b854c 100644 --- a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb @@ -15,7 +15,8 @@ credentials: credentials, ignored_versions: ignored_versions, raise_on_ignored: raise_on_ignored, - security_advisories: security_advisories + security_advisories: security_advisories, + repo_contents_path: "test/repo" ) end diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/Proj1/Proj1.csproj b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/Proj1/Proj1.csproj new file mode 100644 index 0000000000..d2f2db4218 --- /dev/null +++ b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/Proj1/Proj1.csproj @@ -0,0 +1,8 @@ + + + net461 + + + + + diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/dirs.proj b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/dirs.proj new file mode 100644 index 0000000000..044d2a884c --- /dev/null +++ b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj1/dirs.proj @@ -0,0 +1,5 @@ + + + + + diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/Proj2.csproj b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/Proj2.csproj new file mode 100644 index 0000000000..d2f2db4218 --- /dev/null +++ b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/Proj2.csproj @@ -0,0 +1,8 @@ + + + net461 + + + + + diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/dirs.proj b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/dirs.proj new file mode 100644 index 0000000000..5c108193eb --- /dev/null +++ b/nuget/spec/fixtures/projects/dirsproj_wildcards/Proj2/dirs.proj @@ -0,0 +1,4 @@ + + + + diff --git a/nuget/spec/fixtures/projects/dirsproj_wildcards/dirs.proj b/nuget/spec/fixtures/projects/dirsproj_wildcards/dirs.proj new file mode 100644 index 0000000000..25bf99004d --- /dev/null +++ b/nuget/spec/fixtures/projects/dirsproj_wildcards/dirs.proj @@ -0,0 +1,6 @@ + + + + + + diff --git a/python/Dockerfile b/python/Dockerfile index 360519be77..b4c12e0437 100644 --- a/python/Dockerfile +++ b/python/Dockerfile @@ -37,7 +37,7 @@ RUN mkdir "${PYENV_ROOT}/versions" ## 3.8 # Docker doesn't support parametrizing `COPY --from:python:$PY_1_23-bookworm`, so work around it using an alias. # TODO: If upstream adds support for Ubuntu, use that instead of Debian as the base suffix: https://github.com/docker-library/python/pull/791 -FROM python:$PY_3_8-bookworm as upstream-python-3.8 +FROM docker.io/library/python:$PY_3_8-bookworm as upstream-python-3.8 FROM python-core as python-3.8 ARG PYTHON_INSTALL_LOCATION="$PYENV_ROOT/versions/$PY_3_8" COPY --from=upstream-python-3.8 --chown=dependabot:dependabot /usr/local/bin $PYTHON_INSTALL_LOCATION/bin @@ -55,7 +55,7 @@ RUN cd $PYENV_ROOT/versions \ ## 3.9 # Docker doesn't support parametrizing `COPY --from:python:$PY_1_23-bookworm`, so work around it using an alias. # TODO: If upstream adds support for Ubuntu, use that instead of Debian as the base suffix: https://github.com/docker-library/python/pull/791 -FROM python:$PY_3_9-bookworm as upstream-python-3.9 +FROM docker.io/library/python:$PY_3_9-bookworm as upstream-python-3.9 FROM python-core as python-3.9 ARG PYTHON_INSTALL_LOCATION="$PYENV_ROOT/versions/$PY_3_9" COPY --from=upstream-python-3.9 --chown=dependabot:dependabot /usr/local/bin $PYTHON_INSTALL_LOCATION/bin @@ -73,7 +73,7 @@ RUN cd $PYENV_ROOT/versions \ ## 3.10 # Docker doesn't support parametrizing `COPY --from:python:$PY_1_23-bookworm`, so work around it using an alias. # TODO: If upstream adds support for Ubuntu, use that instead of Debian as the base suffix: https://github.com/docker-library/python/pull/791 -FROM python:$PY_3_10-bookworm as upstream-python-3.10 +FROM docker.io/library/python:$PY_3_10-bookworm as upstream-python-3.10 FROM python-core as python-3.10 ARG PYTHON_INSTALL_LOCATION="$PYENV_ROOT/versions/$PY_3_10" COPY --from=upstream-python-3.10 --chown=dependabot:dependabot /usr/local/bin $PYTHON_INSTALL_LOCATION/bin @@ -91,7 +91,7 @@ RUN cd $PYENV_ROOT/versions \ ## 3.11 # Docker doesn't support parametrizing `COPY --from:python:$PY_1_23-bookworm`, so work around it using an alias. # TODO: If upstream adds support for Ubuntu, use that instead of Debian as the base suffix: https://github.com/docker-library/python/pull/791 -FROM python:$PY_3_11-bookworm as upstream-python-3.11 +FROM docker.io/library/python:$PY_3_11-bookworm as upstream-python-3.11 FROM python-core as python-3.11 ARG PYTHON_INSTALL_LOCATION="$PYENV_ROOT/versions/$PY_3_11" COPY --from=upstream-python-3.11 --chown=dependabot:dependabot /usr/local/bin $PYTHON_INSTALL_LOCATION/bin @@ -109,7 +109,7 @@ RUN cd $PYENV_ROOT/versions \ ## 3.12 # Docker doesn't support parametrizing `COPY --from:python:$PY_1_23-bookworm`, so work around it using an alias. # TODO: If upstream adds support for Ubuntu, use that instead of Debian as the base suffix: https://github.com/docker-library/python/pull/791 -FROM python:$PY_3_12-bookworm as upstream-python-3.12 +FROM docker.io/library/python:$PY_3_12-bookworm as upstream-python-3.12 FROM python-core as python-3.12 ARG PYTHON_INSTALL_LOCATION="$PYENV_ROOT/versions/$PY_3_12" COPY --from=upstream-python-3.12 --chown=dependabot:dependabot /usr/local/bin $PYTHON_INSTALL_LOCATION/bin diff --git a/python/lib/dependabot/python/authed_url_builder.rb b/python/lib/dependabot/python/authed_url_builder.rb index d349ac8a31..37c10eb560 100644 --- a/python/lib/dependabot/python/authed_url_builder.rb +++ b/python/lib/dependabot/python/authed_url_builder.rb @@ -6,7 +6,7 @@ module Python class AuthedUrlBuilder def self.authed_url(credential:) token = credential.fetch("token", nil) - url = credential.fetch("index-url") + url = credential.fetch("index-url", nil) return url unless token basic_auth_details = diff --git a/python/lib/dependabot/python/file_updater/pipfile_preparer.rb b/python/lib/dependabot/python/file_updater/pipfile_preparer.rb index 1cc66d8c5e..a7732b385b 100644 --- a/python/lib/dependabot/python/file_updater/pipfile_preparer.rb +++ b/python/lib/dependabot/python/file_updater/pipfile_preparer.rb @@ -52,7 +52,7 @@ def sub_auth_url(source, credentials) base_url = source["url"].sub(/\${.*}@/, "") source_cred = credentials - .select { |cred| cred["type"] == "python_index" } + .select { |cred| cred["type"] == "python_index" && cred["index-url"] } .find { |c| c["index-url"].sub(/\${.*}@/, "") == base_url } return nil if source_cred.nil? diff --git a/python/lib/dependabot/python/version.rb b/python/lib/dependabot/python/version.rb index f832d93198..d3ff1648e8 100644 --- a/python/lib/dependabot/python/version.rb +++ b/python/lib/dependabot/python/version.rb @@ -1,4 +1,4 @@ -# typed: false +# typed: true # frozen_string_literal: true require "dependabot/version" @@ -59,7 +59,7 @@ def <=>(other) return epoch_comparison unless epoch_comparison.zero? version_comparison = super(other) - return version_comparison unless version_comparison.zero? + return version_comparison unless version_comparison&.zero? post_version_comparison = post_version_comparison(other) return post_version_comparison unless post_version_comparison.zero? @@ -96,7 +96,7 @@ def local_version_comparison(other) local_comparison = Gem::Version.new(lhs) <=> Gem::Version.new(rhs) - return local_comparison unless local_comparison.zero? + return local_comparison unless local_comparison&.zero? lhsegments.count <=> rhsegments.count end diff --git a/script/_common b/script/_common index e4cf21e978..ae2e13f826 100755 --- a/script/_common +++ b/script/_common @@ -47,6 +47,7 @@ function docker_build() { --build-arg BUILDKIT_INLINE_CACHE=1 \ --build-arg USER_UID=$DEPENDABOT_USER_UID \ --build-arg USER_GID=$DEPENDABOT_USER_GID \ + --build-arg DEPENDABOT_UPDATER_VERSION=$DEPENDABOT_UPDATER_VERSION \ --cache-from "$UPDATER_CORE_IMAGE" \ -t "$UPDATER_CORE_IMAGE" \ -f Dockerfile.updater-core \ diff --git a/silent/tests/testdata/su-err-dependency-not-found.txt b/silent/tests/testdata/su-err-dependency-not-found.txt new file mode 100644 index 0000000000..b9d589c4fc --- /dev/null +++ b/silent/tests/testdata/su-err-dependency-not-found.txt @@ -0,0 +1,37 @@ +! dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr security_update_dependency_not_found +stdout '{"data":{"error-type":"security_update_dependency_not_found","error-details":{}},"type":"record_update_job_error"}' +! stdout create_pull_request + +# Since 'not-found' is not in the manifest, it errors with security_update_dependency_not_found. + +-- manifest.json -- +{ + "dependency-a": { "version": "1.2.3" } +} + +-- dependency-a -- +{ + "versions": [ + "1.2.3" + ] +} + +-- input.yml -- +job: + package-manager: "silent" + dependencies: + - not-found + source: + directory: "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + security-advisories: + - dependency-name: not-found + affected-versions: + - <= 1.2.3 + patched-versions: [] + unaffected-versions: [] + security-updates-only: true diff --git a/silent/tests/testdata/su-err-not-needed.txt b/silent/tests/testdata/su-err-not-needed.txt new file mode 100644 index 0000000000..ae8cc327e2 --- /dev/null +++ b/silent/tests/testdata/su-err-not-needed.txt @@ -0,0 +1,37 @@ +! dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr security_update_not_needed +stdout '{"data":{"error-type":"security_update_not_needed","error-details":{"dependency-name":"dependency-a"}},"type":"record_update_job_error"}' + +# The security update is not needed because 1.2.3 is not vulnerable according to the advisory given. + +-- manifest.json -- +{ + "dependency-a": { "version": "1.2.3" } +} + +-- dependency-a -- +{ + "versions": [ + "1.2.3", + "1.2.4" + ] +} + +-- input.yml -- +job: + package-manager: "silent" + dependencies: + - dependency-a + source: + directory: "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + security-advisories: + - dependency-name: dependency-a + affected-versions: + - < 1.0.0 + patched-versions: [] + unaffected-versions: [] + security-updates-only: true diff --git a/silent/tests/testdata/su-err-pr-exists-latest.txt b/silent/tests/testdata/su-err-pr-exists-latest.txt new file mode 100644 index 0000000000..9cad9f2ddb --- /dev/null +++ b/silent/tests/testdata/su-err-pr-exists-latest.txt @@ -0,0 +1,41 @@ +! dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr pull_request_exists_for_latest_version +stdout '{"data":{"error-type":"pull_request_exists_for_latest_version","error-details":{"dependency-name":"dependency-a","dependency-version":"1.2.5"}},"type":"record_update_job_error"}' + +# An existing pull request exists for 1.2.5 which is the latest version of dependency-a. + +-- manifest.json -- +{ + "dependency-a": { "version": "1.2.3" } +} + +-- dependency-a -- +{ + "versions": [ + "1.2.3", + "1.2.4", + "1.2.5" + ] +} + +-- input.yml -- +job: + package-manager: "silent" + dependencies: + - dependency-a + source: + directory: "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + security-advisories: + - dependency-name: dependency-a + affected-versions: + - <= 1.2.3 + patched-versions: [] + unaffected-versions: [] + security-updates-only: true + existing-pull-requests: + - - dependency-name: dependency-a + dependency-version: 1.2.5 diff --git a/silent/tests/testdata/su-err-pr-exists-security.txt b/silent/tests/testdata/su-err-pr-exists-security.txt new file mode 100644 index 0000000000..fc63996996 --- /dev/null +++ b/silent/tests/testdata/su-err-pr-exists-security.txt @@ -0,0 +1,41 @@ +! dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr pull_request_exists_for_security_update +stdout '{"data":{"error-type":"pull_request_exists_for_security_update","error-details":{"updated-dependencies":\[{"dependency-name":"dependency-a","dependency-version":"1.2.4"}\]}},"type":"record_update_job_error"}' + +# An existing pull request exists for 1.2.4, which is the security version required, but not the latest. + +-- manifest.json -- +{ + "dependency-a": { "version": "1.2.3" } +} + +-- dependency-a -- +{ + "versions": [ + "1.2.3", + "1.2.4", + "1.2.5" + ] +} + +-- input.yml -- +job: + package-manager: "silent" + dependencies: + - dependency-a + source: + directory: "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + security-advisories: + - dependency-name: dependency-a + affected-versions: + - <= 1.2.3 + patched-versions: [] + unaffected-versions: [] + security-updates-only: true + existing-pull-requests: + - - dependency-name: dependency-a + dependency-version: 1.2.4 diff --git a/silent/tests/testdata/su-group-pattern.txt b/silent/tests/testdata/su-group-pattern.txt index 29379c9a5e..4b05958ad9 100644 --- a/silent/tests/testdata/su-group-pattern.txt +++ b/silent/tests/testdata/su-group-pattern.txt @@ -83,6 +83,7 @@ job: security-updates-only: true dependency-groups: - name: related + applies-to: "security-updates" rules: patterns: - "related-*" diff --git a/silent/tests/testdata/su-group-semver.txt b/silent/tests/testdata/su-group-semver.txt index b429767399..3205109b34 100644 --- a/silent/tests/testdata/su-group-semver.txt +++ b/silent/tests/testdata/su-group-semver.txt @@ -83,6 +83,7 @@ job: security-updates-only: true dependency-groups: - name: dev + applies-to: security-updates rules: update-types: - minor diff --git a/silent/tests/testdata/su-group-type.txt b/silent/tests/testdata/su-group-type.txt index 0bf11c9166..e8109038a5 100644 --- a/silent/tests/testdata/su-group-type.txt +++ b/silent/tests/testdata/su-group-type.txt @@ -85,8 +85,10 @@ job: grouped-update: true dependency-groups: - name: dev + applies-to: security-updates rules: dependency-type: development - name: prod + applies-to: security-updates rules: dependency-type: production diff --git a/silent/tests/testdata/vs-close-up-to-date.txt b/silent/tests/testdata/vs-close-up-to-date.txt new file mode 100644 index 0000000000..3e7fed940e --- /dev/null +++ b/silent/tests/testdata/vs-close-up-to-date.txt @@ -0,0 +1,37 @@ +dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr 'closed: up_to_date \| dependency-a' +stdout '{"data":{"dependency-names":\["dependency-a"\],"reason":"up_to_date"},"type":"close_pull_request"}' +! stdout 'create_pull_request' +! stdout 'update_pull_request' + +# This tests the scenario where a manifest was updated and the existing pull request should be closed. + +-- manifest.json -- +{ + "dependency-a": { "version": "1.2.5" } +} + +-- dependency-a -- +{ + "versions": [ + "1.2.3", + "1.2.4", + "1.2.5" + ] +} + +-- input.yml -- +job: + package-manager: "silent" + source: + directory: "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + dependencies: + - dependency-a + updating-a-pull-request: true + existing-pull-requests: + - - dependency-name: dependency-a + dependency-version: 1.2.5 diff --git a/silent/tests/testdata/vu-basic-directories.txt b/silent/tests/testdata/vu-basic-directories.txt new file mode 100644 index 0000000000..5f0836360b --- /dev/null +++ b/silent/tests/testdata/vu-basic-directories.txt @@ -0,0 +1,54 @@ +dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr 'created \| dependency-a \( from 1.2.3 to 1.2.5 \)' +pr-created expected.json + +dependabot update -f input-2.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent +stderr 'updated \| dependency-a \( from 1.2.3 to 1.2.5 \)' +pr-updated expected.json + +-- manifest.json -- +{ + "dependency-a": { "version": "1.2.3" } +} + +-- expected.json -- +{ + "dependency-a": { "version": "1.2.5" } +} + +-- dependency-a -- +{ + "versions": [ + "1.2.3", + "1.2.4", + "1.2.5" + ] +} + +-- input.yml -- +job: + package-manager: "silent" + source: + directories: + - "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + +-- input-2.yml -- +job: + package-manager: "silent" + source: + directories: + - "/" + provider: example + hostname: example.com + api-endpoint: https://example.com/api/v3 + repo: dependabot/smoke-tests + dependencies: + - dependency-a + updating-a-pull-request: true + existing-pull-requests: + - - dependency-name: dependency-a + dependency-version: 1.2.5 diff --git a/silent/tests/testdata/vu-group-incidental.txt b/silent/tests/testdata/vu-group-incidental.txt index 651c2255bb..0e2f6d7fd1 100644 --- a/silent/tests/testdata/vu-group-incidental.txt +++ b/silent/tests/testdata/vu-group-incidental.txt @@ -1,9 +1,7 @@ dependabot update -f input.yml --local . --updater-image ghcr.io/dependabot/dependabot-updater-silent -stdout -count=2 create_pull_request -stderr 'created \| dependency-a \( from 1.2.3 to 1.2.5 \)' -stderr 'created \| dependency-b \( from 1.2.3 to 1.2.5 \)' -pr-created expected-1.json -pr-created expected-2.json +stdout -count=1 create_pull_request +stderr 'created \| dependency-a \( from 1.2.3 to 1.2.5 \), dependency-b \( from 1.2.3 to 1.2.5 \)' +pr-created expected.json # When Dependabot goes to update dependency-a it will also bump dependency-b to the same version. # This test checks what the behavior is when using grouped updates. @@ -14,18 +12,12 @@ pr-created expected-2.json "dependency-b": { "version": "1.2.3" } } --- expected-1.json -- +-- expected.json -- { "dependency-a": { "version": "1.2.5", "depends-on": "dependency-b" }, "dependency-b": { "version": "1.2.5" } } --- expected-2.json -- -{ - "dependency-a": { "version": "1.2.3", "depends-on": "dependency-b" }, - "dependency-b": { "version": "1.2.5" } -} - -- dependency-a -- { "versions": [ diff --git a/sorbet/rbi/shims/sentry-ruby.rbi b/sorbet/rbi/shims/sentry-ruby.rbi index 4e0cc189ba..fcf185aa27 100644 --- a/sorbet/rbi/shims/sentry-ruby.rbi +++ b/sorbet/rbi/shims/sentry-ruby.rbi @@ -11,6 +11,9 @@ module Sentry end class Configuration + sig { returns(T.nilable(String)) } + attr_accessor :release + sig { returns(T.nilable(::Logger)) } attr_accessor :logger diff --git a/sorbet/rbi/todo.rbi b/sorbet/rbi/todo.rbi index 8d40db2d08..b63008ebcd 100644 --- a/sorbet/rbi/todo.rbi +++ b/sorbet/rbi/todo.rbi @@ -4,7 +4,6 @@ # typed: false -module ::Azure::Error::NotFound; end module Bundler::CompactIndexClient::Updater; end module Bundler::SolveFailure; end module Dependabot::NpmAndYarn::FileFetcher::Pysch::SyntaxError; end diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index 2947f23b08..f643552f0d 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -1,19 +1,19 @@ PATH remote: ../bundler specs: - dependabot-bundler (0.242.1) - dependabot-common (= 0.242.1) + dependabot-bundler (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../cargo specs: - dependabot-cargo (0.242.1) - dependabot-common (= 0.242.1) + dependabot-cargo (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../common specs: - dependabot-common (0.242.1) + dependabot-common (0.244.0) aws-sdk-codecommit (~> 1.28) aws-sdk-ecr (~> 1.5) bundler (>= 1.16, < 3.0.0) @@ -35,107 +35,107 @@ PATH PATH remote: ../composer specs: - dependabot-composer (0.242.1) - dependabot-common (= 0.242.1) + dependabot-composer (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../devcontainers specs: - dependabot-devcontainers (0.242.1) - dependabot-common (= 0.242.1) + dependabot-devcontainers (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../docker specs: - dependabot-docker (0.242.1) - dependabot-common (= 0.242.1) + dependabot-docker (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../elm specs: - dependabot-elm (0.242.1) - dependabot-common (= 0.242.1) + dependabot-elm (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../git_submodules specs: - dependabot-git_submodules (0.242.1) - dependabot-common (= 0.242.1) + dependabot-git_submodules (0.244.0) + dependabot-common (= 0.244.0) parseconfig (~> 1.0, < 1.1.0) PATH remote: ../github_actions specs: - dependabot-github_actions (0.242.1) - dependabot-common (= 0.242.1) + dependabot-github_actions (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../go_modules specs: - dependabot-go_modules (0.242.1) - dependabot-common (= 0.242.1) + dependabot-go_modules (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../gradle specs: - dependabot-gradle (0.242.1) - dependabot-common (= 0.242.1) - dependabot-maven (= 0.242.1) + dependabot-gradle (0.244.0) + dependabot-common (= 0.244.0) + dependabot-maven (= 0.244.0) PATH remote: ../hex specs: - dependabot-hex (0.242.1) - dependabot-common (= 0.242.1) + dependabot-hex (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../maven specs: - dependabot-maven (0.242.1) - dependabot-common (= 0.242.1) + dependabot-maven (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../npm_and_yarn specs: - dependabot-npm_and_yarn (0.242.1) - dependabot-common (= 0.242.1) + dependabot-npm_and_yarn (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../nuget specs: - dependabot-nuget (0.242.1) - dependabot-common (= 0.242.1) + dependabot-nuget (0.244.0) + dependabot-common (= 0.244.0) rubyzip (>= 2.3.2, < 3.0) PATH remote: ../pub specs: - dependabot-pub (0.242.1) - dependabot-common (= 0.242.1) + dependabot-pub (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../python specs: - dependabot-python (0.242.1) - dependabot-common (= 0.242.1) + dependabot-python (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../silent specs: - dependabot-silent (0.242.1) - dependabot-common (= 0.242.1) + dependabot-silent (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../swift specs: - dependabot-swift (0.242.1) - dependabot-common (= 0.242.1) + dependabot-swift (0.244.0) + dependabot-common (= 0.244.0) PATH remote: ../terraform specs: - dependabot-terraform (0.242.1) - dependabot-common (= 0.242.1) + dependabot-terraform (0.244.0) + dependabot-common (= 0.244.0) GEM remote: https://rubygems.org/ diff --git a/updater/lib/dependabot/api_client.rb b/updater/lib/dependabot/api_client.rb index 959d7f75a9..3e68699c56 100644 --- a/updater/lib/dependabot/api_client.rb +++ b/updater/lib/dependabot/api_client.rb @@ -170,7 +170,7 @@ def mark_job_as_processed(base_commit_sha) span&.finish end - sig { params(dependencies: T::Array[T::Hash[Symbol, T.untyped]], dependency_files: T::Array[DependencyFile]).void } + sig { params(dependencies: T::Array[T::Hash[Symbol, T.untyped]], dependency_files: T::Array[String]).void } def update_dependency_list(dependencies, dependency_files) span = ::Dependabot::OpenTelemetry.tracer&.start_span("update_dependency_list", kind: :internal) span&.set_attribute(::Dependabot::OpenTelemetry::Attributes::JOB_ID, job_id) @@ -290,8 +290,6 @@ def create_pull_request_data(dependency_change, base_commit_sha) "base-commit-sha": base_commit_sha }.merge(dependency_group_hash(dependency_change)) - return data unless dependency_change.pr_message - data["commit-message"] = dependency_change.pr_message.commit_message data["pr-title"] = dependency_change.pr_message.pr_name data["pr-body"] = dependency_change.pr_message.pr_message diff --git a/updater/lib/dependabot/dependency_change.rb b/updater/lib/dependabot/dependency_change.rb index bba8ffba09..529e83e88d 100644 --- a/updater/lib/dependabot/dependency_change.rb +++ b/updater/lib/dependabot/dependency_change.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + # This class describes a change to the project's Dependencies which has been # determined by a Dependabot operation. # @@ -12,19 +14,42 @@ # by adapters to create a Pull Request, apply the changes on disk, etc. module Dependabot class DependencyChange - attr_reader :job, :updated_dependencies, :updated_dependency_files, :dependency_group + extend T::Sig + + sig { returns(Dependabot::Job) } + attr_reader :job + + sig { returns(T::Array[Dependabot::Dependency]) } + attr_reader :updated_dependencies + + sig { returns(T::Array[Dependabot::DependencyFile]) } + attr_reader :updated_dependency_files + sig { returns(T.nilable(Dependabot::DependencyGroup)) } + attr_reader :dependency_group + + sig do + params( + job: Dependabot::Job, + updated_dependencies: T::Array[Dependabot::Dependency], + updated_dependency_files: T::Array[Dependabot::DependencyFile], + dependency_group: T.nilable(Dependabot::DependencyGroup) + ).void + end def initialize(job:, updated_dependencies:, updated_dependency_files:, dependency_group: nil) @job = job @updated_dependencies = updated_dependencies @updated_dependency_files = updated_dependency_files @dependency_group = dependency_group + + @pr_message = T.let(nil, T.nilable(Dependabot::PullRequestCreator::Message)) end + sig { returns(Dependabot::PullRequestCreator::Message) } def pr_message - return @pr_message if defined?(@pr_message) + return @pr_message unless @pr_message.nil? - case job.source&.provider + case job.source.provider when "github" pr_message_max_length = Dependabot::PullRequestCreator::Github::PR_DESCRIPTION_MAX_LENGTH when "azure" @@ -38,7 +63,7 @@ def pr_message pr_message_max_length = Dependabot::PullRequestCreator::Github::PR_DESCRIPTION_MAX_LENGTH end - @pr_message = Dependabot::PullRequestCreator::MessageBuilder.new( + message = Dependabot::PullRequestCreator::MessageBuilder.new( source: job.source, dependencies: updated_dependencies, files: updated_dependency_files, @@ -49,18 +74,23 @@ def pr_message pr_message_encoding: pr_message_encoding, ignore_conditions: job.ignore_conditions ).message + + @pr_message = message end + sig { returns(String) } def humanized updated_dependencies.map do |dependency| "#{dependency.name} ( from #{dependency.humanized_previous_version} to #{dependency.humanized_version} )" end.join(", ") end + sig { returns(T::Array[T::Hash[String, T.untyped]]) } def updated_dependency_files_hash updated_dependency_files.map(&:to_h) end + sig { returns(T::Boolean) } def grouped_update? !!dependency_group end @@ -72,19 +102,17 @@ def grouped_update? # rather than supersede it as the new changes don't necessarily follow # from the previous ones; dependencies could have been removed from the # project, or pinned by other changes. + sig { returns(T::Boolean) } def should_replace_existing_pr? return false unless job.updating_a_pull_request? # NOTE: Gradle, Maven and Nuget dependency names can be case-insensitive # and the dependency name injected from a security advisory often doesn't # match what users have specified in their manifest. - updated_dependencies.map { |x| x.name.downcase }.uniq.sort != job.dependencies.map(&:downcase).uniq.sort - end - - def matches_existing_pr? - !!existing_pull_request + updated_dependencies.map { |x| x.name.downcase }.uniq.sort != T.must(job.dependencies).map(&:downcase).uniq.sort end + sig { params(dependency_changes: T::Array[DependencyChange]).void } def merge_changes!(dependency_changes) dependency_changes.each do |dependency_change| updated_dependencies.concat(dependency_change.updated_dependencies) @@ -94,20 +122,22 @@ def merge_changes!(dependency_changes) updated_dependency_files.compact! end - private - - def existing_pull_request + sig { returns(T::Boolean) } + def matches_existing_pr? if grouped_update? # We only want PRs for the same group that have the same versions - job.existing_group_pull_requests.find do |pr| - pr["dependency-group-name"] == dependency_group.name && + job.existing_group_pull_requests.any? do |pr| + pr["dependency-group-name"] == dependency_group&.name && Set.new(pr["dependencies"]) == updated_dependencies_set end else - job.existing_pull_requests.find { |pr| Set.new(pr) == updated_dependencies_set } + job.existing_pull_requests.any? { |pr| Set.new(pr) == updated_dependencies_set } end end + private + + sig { returns(T::Set[T::Hash[String, T.any(String, T::Boolean)]]) } def updated_dependencies_set Set.new( updated_dependencies.map do |dep| diff --git a/updater/lib/dependabot/dependency_change_builder.rb b/updater/lib/dependabot/dependency_change_builder.rb index 620fe9b3cc..6e09dcf217 100644 --- a/updater/lib/dependabot/dependency_change_builder.rb +++ b/updater/lib/dependabot/dependency_change_builder.rb @@ -1,6 +1,7 @@ -# typed: false +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" require "dependabot/dependency" require "dependabot/dependency_change" require "dependabot/file_updaters" @@ -22,15 +23,39 @@ # a DependencyGroup module Dependabot class DependencyChangeBuilder - def self.create_from(**kwargs) - new(**kwargs).run + extend T::Sig + + sig do + params( + job: Dependabot::Job, + dependency_files: T::Array[Dependabot::DependencyFile], + updated_dependencies: T::Array[Dependabot::Dependency], + change_source: T.any(Dependabot::Dependency, Dependabot::DependencyGroup) + ).returns(Dependabot::DependencyChange) + end + def self.create_from(job:, dependency_files:, updated_dependencies:, change_source:) + new( + job: job, + dependency_files: dependency_files, + updated_dependencies: updated_dependencies, + change_source: change_source + ).run end + sig do + params( + job: Dependabot::Job, + dependency_files: T::Array[Dependabot::DependencyFile], + updated_dependencies: T::Array[Dependabot::Dependency], + change_source: T.any(Dependabot::Dependency, Dependabot::DependencyGroup) + ).void + end def initialize(job:, dependency_files:, updated_dependencies:, change_source:) @job = job dir = Pathname.new(job.source.directory).cleanpath - @dependency_files = dependency_files.select { |f| Pathname.new(f.directory).cleanpath == dir } + @dependency_files = T.let(dependency_files.select { |f| Pathname.new(f.directory).cleanpath == dir }, + T::Array[Dependabot::DependencyFile]) raise "Missing directory in dependency files: #{dir}" unless @dependency_files.any? @@ -38,6 +63,7 @@ def initialize(job:, dependency_files:, updated_dependencies:, change_source:) @change_source = change_source end + sig { returns(Dependabot::DependencyChange) } def run updated_files = generate_dependency_files raise DependabotError, "FileUpdater failed" unless updated_files.any? @@ -52,7 +78,7 @@ def run d.version == d.previous_version end - updated_deps.each { |d| d.metadata[:directory] = job.source.directory } if job.source&.directory + updated_deps.each { |d| d.metadata[:directory] = job.source.directory } if job.source.directory Dependabot::DependencyChange.new( job: job, @@ -64,23 +90,36 @@ def run private - attr_reader :job, :dependency_files, :updated_dependencies, :change_source + sig { returns(Dependabot::Job) } + attr_reader :job + + sig { returns(T::Array[Dependabot::DependencyFile]) } + attr_reader :dependency_files + + sig { returns(T::Array[Dependabot::Dependency]) } + attr_reader :updated_dependencies + + sig { returns(T.any(Dependabot::Dependency, Dependabot::DependencyGroup)) } + attr_reader :change_source + sig { returns(T.nilable(String)) } def source_dependency_name return nil unless change_source.is_a? Dependabot::Dependency - change_source.name + T.cast(change_source, Dependabot::Dependency).name end + sig { returns(T.nilable(Dependabot::DependencyGroup)) } def source_dependency_group return nil unless change_source.is_a? Dependabot::DependencyGroup - change_source + T.cast(change_source, Dependabot::DependencyGroup) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def generate_dependency_files if updated_dependencies.count == 1 - updated_dependency = updated_dependencies.first + updated_dependency = T.must(updated_dependencies.first) Dependabot.logger.info("Updating #{updated_dependency.name} from " \ "#{updated_dependency.previous_version} to " \ "#{updated_dependency.version}") @@ -96,6 +135,7 @@ def generate_dependency_files file_updater_for(relevant_dependencies).updated_dependency_files end + sig { params(dependencies: T::Array[Dependabot::Dependency]).returns(Dependabot::FileUpdaters::Base) } def file_updater_for(dependencies) Dependabot::FileUpdaters.for_package_manager(job.package_manager).new( dependencies: dependencies, diff --git a/updater/lib/dependabot/dependency_group_engine.rb b/updater/lib/dependabot/dependency_group_engine.rb index 6dcbc8038a..9cd9fcafee 100644 --- a/updater/lib/dependabot/dependency_group_engine.rb +++ b/updater/lib/dependabot/dependency_group_engine.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "dependabot/dependency_group" @@ -18,8 +18,11 @@ # module Dependabot class DependencyGroupEngine + extend T::Sig + class ConfigurationError < StandardError; end + sig { params(job: Dependabot::Job).returns(Dependabot::DependencyGroupEngine) } def self.from_job_config(job:) if job.security_updates_only? && job.source.directories && job.dependency_groups.empty? # The indication that this should be a grouped update is: @@ -28,7 +31,8 @@ def self.from_job_config(job:) # Since there are no groups, the default behavior is to group all dependencies, so create a fake group. job.dependency_groups << { "name" => "#{job.package_manager} group", - "rules" => { "patterns" => ["*"] } + "rules" => { "patterns" => ["*"] }, + "applies-to" => "security-updates" } # This ensures refreshes work for these dynamic groups. @@ -38,18 +42,31 @@ def self.from_job_config(job:) end groups = job.dependency_groups.map do |group| - Dependabot::DependencyGroup.new(name: group["name"], rules: group["rules"]) + Dependabot::DependencyGroup.new(name: group["name"], rules: group["rules"], applies_to: group["applies-to"]) end + # Filter out version updates when doing security updates and visa versa + groups = if job.security_updates_only? + groups.select { |group| group.applies_to == "security-updates" } + else + groups.select { |group| group.applies_to == "version-updates" } + end + new(dependency_groups: groups) end - attr_reader :dependency_groups, :groups_calculated, :ungrouped_dependencies + sig { returns(T::Array[Dependabot::DependencyGroup]) } + attr_reader :dependency_groups + + sig { returns(T::Array[Dependabot::Dependency]) } + attr_reader :ungrouped_dependencies + sig { params(name: String).returns(T.nilable(Dependabot::DependencyGroup)) } def find_group(name:) dependency_groups.find { |group| group.name == name } end + sig { params(dependencies: T::Array[Dependabot::Dependency]).void } def assign_to_groups!(dependencies:) raise ConfigurationError, "dependency groups have already been configured!" if @groups_calculated @@ -75,17 +92,20 @@ def assign_to_groups!(dependencies:) private + sig { params(dependency_groups: T::Array[Dependabot::DependencyGroup]).void } def initialize(dependency_groups:) @dependency_groups = dependency_groups - @ungrouped_dependencies = [] - @groups_calculated = false + @ungrouped_dependencies = T.let([], T::Array[Dependabot::Dependency]) + @groups_calculated = T.let(false, T::Boolean) end + sig { void } def validate_groups empty_groups = dependency_groups.select { |group| group.dependencies.empty? } warn_misconfigured_groups(empty_groups) if empty_groups.any? end + sig { params(groups: T::Array[Dependabot::DependencyGroup]).void } def warn_misconfigured_groups(groups) Dependabot.logger.warn <<~WARN Please check your configuration as there are groups where no dependencies match: diff --git a/updater/lib/dependabot/dependency_snapshot.rb b/updater/lib/dependabot/dependency_snapshot.rb index fa049b935c..914551bf58 100644 --- a/updater/lib/dependabot/dependency_snapshot.rb +++ b/updater/lib/dependabot/dependency_snapshot.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "base64" @@ -14,6 +14,9 @@ module Dependabot class DependencySnapshot extend T::Sig + sig do + params(job: Dependabot::Job, job_definition: T::Hash[String, T.untyped]).returns(Dependabot::DependencySnapshot) + end def self.create_from_job_definition(job:, job_definition:) decoded_dependency_files = job_definition.fetch("base64_dependency_files").map do |a| file = Dependabot::DependencyFile.new(**a.transform_keys(&:to_sym)) @@ -30,55 +33,38 @@ def self.create_from_job_definition(job:, job_definition:) ) end - attr_reader :base_commit_sha, :dependency_files, :dependencies, :handled_dependencies + sig { returns(String) } + attr_reader :base_commit_sha - def add_handled_dependencies(dependency_names) - @handled_dependencies += Array(dependency_names) - end + sig { returns(T::Array[Dependabot::DependencyFile]) } + attr_reader :dependency_files - # Returns the subset of all project dependencies which are permitted - # by the project configuration. - def allowed_dependencies - @allowed_dependencies ||= if job.security_updates_only? - dependencies.select { |d| T.must(job.dependencies).include?(d.name) } - else - dependencies.select { |d| job.allowed_update?(d) } - end - end + sig { returns(T::Array[Dependabot::Dependency]) } + attr_reader :dependencies - # Returns the subset of all project dependencies which are specifically - # requested to be updated by the job definition. - def job_dependencies - return [] unless job.dependencies&.any? - return @job_dependencies if defined? @job_dependencies + sig { returns(T::Set[String]) } + attr_reader :handled_dependencies - # Gradle, Maven and Nuget dependency names can be case-insensitive and - # the dependency name in the security advisory often doesn't match what - # users have specified in their manifest. - # - # It's technically possibly to publish case-sensitive npm packages to a - # private registry but shouldn't cause problems here as job.dependencies - # is set either from an existing PR rebase/recreate or a security - # advisory. - job_dependency_names = T.must(job.dependencies).map(&:downcase) - @job_dependencies = dependencies.select do |dep| - job_dependency_names.include?(dep.name.downcase) - end - end + sig { returns(T::Array[Dependabot::Dependency]) } + attr_reader :allowed_dependencies - # Returns just the group that is specifically requested to be updated by - # the job definition - def job_group - return nil unless job.dependency_group_to_refresh - return @job_group if defined?(@job_group) + sig { returns(T::Array[Dependabot::Dependency]) } + attr_reader :job_dependencies + + sig { returns(T.nilable(Dependabot::DependencyGroup)) } + attr_reader :job_group - @job_group = @dependency_group_engine.find_group(name: job.dependency_group_to_refresh) + sig { params(dependency_names: T.any(String, T::Array[String])).void } + def add_handled_dependencies(dependency_names) + @handled_dependencies += Array(dependency_names) end + sig { returns(T::Array[Dependabot::DependencyGroup]) } def groups @dependency_group_engine.dependency_groups end + sig { returns(T::Array[Dependabot::Dependency]) } def ungrouped_dependencies # If no groups are defined, all dependencies are ungrouped by default. return allowed_dependencies unless groups.any? @@ -89,25 +75,34 @@ def ungrouped_dependencies private + sig do + params(job: Dependabot::Job, base_commit_sha: String, dependency_files: T::Array[Dependabot::DependencyFile]).void + end def initialize(job:, base_commit_sha:, dependency_files:) @job = job @base_commit_sha = base_commit_sha @dependency_files = dependency_files - @handled_dependencies = Set.new + @handled_dependencies = T.let(Set.new, T::Set[String]) - @dependencies = parse_files! + @dependencies = T.let(parse_files!, T::Array[Dependabot::Dependency]) + @allowed_dependencies = T.let(calculate_allowed_dependencies, T::Array[Dependabot::Dependency]) + @job_dependencies = T.let(calculate_job_dependencies, T::Array[Dependabot::Dependency]) - @dependency_group_engine = DependencyGroupEngine.from_job_config(job: job) + @dependency_group_engine = T.let(DependencyGroupEngine.from_job_config(job: job), + Dependabot::DependencyGroupEngine) @dependency_group_engine.assign_to_groups!(dependencies: allowed_dependencies) + @job_group = T.let(calculate_job_group, T.nilable(Dependabot::DependencyGroup)) end sig { returns(Dependabot::Job) } attr_reader :job + sig { returns(T::Array[Dependabot::Dependency]) } def parse_files! dependency_file_parser.parse end + sig { returns(Dependabot::FileParsers::Base) } def dependency_file_parser Dependabot::FileParsers.for_package_manager(job.package_manager).new( dependency_files: dependency_files, @@ -118,5 +113,45 @@ def dependency_file_parser options: job.experiments ) end + + # Returns the subset of all project dependencies which are permitted + # by the project configuration. + sig { returns(T::Array[Dependabot::Dependency]) } + def calculate_allowed_dependencies + if job.security_updates_only? + dependencies.select { |d| T.must(job.dependencies).include?(d.name) } + else + dependencies.select { |d| job.allowed_update?(d) } + end + end + + # Returns the subset of all project dependencies which are specifically + # requested to be updated by the job definition. + sig { returns(T::Array[Dependabot::Dependency]) } + def calculate_job_dependencies + return [] unless job.dependencies&.any? + + # Gradle, Maven and Nuget dependency names can be case-insensitive and + # the dependency name in the security advisory often doesn't match what + # users have specified in their manifest. + # + # It's technically possibly to publish case-sensitive npm packages to a + # private registry but shouldn't cause problems here as job.dependencies + # is set either from an existing PR rebase/recreate or a security + # advisory. + job_dependency_names = T.must(job.dependencies).map(&:downcase) + dependencies.select do |dep| + job_dependency_names.include?(dep.name.downcase) + end + end + + # Returns just the group that is specifically requested to be updated by + # the job definition + sig { returns(T.nilable(Dependabot::DependencyGroup)) } + def calculate_job_group + return nil unless job.dependency_group_to_refresh + + @dependency_group_engine.find_group(name: T.must(job.dependency_group_to_refresh)) + end end end diff --git a/updater/lib/dependabot/service.rb b/updater/lib/dependabot/service.rb index 7de1f7ab56..2bdf9c98f9 100644 --- a/updater/lib/dependabot/service.rb +++ b/updater/lib/dependabot/service.rb @@ -19,7 +19,7 @@ class Service extend T::Sig extend Forwardable - sig { returns(T::Array[T::Array[String]]) } + sig { returns(T::Array[T.untyped]) } attr_reader :pull_requests sig { returns(T::Array[T::Array[T.untyped]]) } @@ -162,7 +162,7 @@ def pull_request_summary T.unsafe(Terminal::Table).new do |t| t.title = "Changes to Dependabot Pull Requests" - t.rows = pull_requests.map { |deps, action| [action, truncate(T.must(deps))] } + t.rows = pull_requests.map { |deps, action| [action, truncate(deps)] } end end diff --git a/updater/lib/dependabot/setup.rb b/updater/lib/dependabot/setup.rb index 187f4e8184..2e06316da7 100644 --- a/updater/lib/dependabot/setup.rb +++ b/updater/lib/dependabot/setup.rb @@ -8,6 +8,7 @@ require "dependabot/logger/formats" require "dependabot/opentelemetry" require "dependabot/sentry" +require "dependabot/sorbet/runtime" Dependabot.logger = Logger.new($stdout).tap do |logger| logger.level = Dependabot::Environment.log_level @@ -15,6 +16,7 @@ end Sentry.init do |config| + config.release = ENV.fetch("DEPENDABOT_UPDATER_VERSION") config.logger = Dependabot.logger config.project_root = File.expand_path("../../..", __dir__) @@ -50,6 +52,7 @@ end Dependabot::OpenTelemetry.configure +Dependabot::Sorbet::Runtime.silently_report_errors! # Ecosystems require "dependabot/python" diff --git a/updater/lib/dependabot/sorbet/runtime.rb b/updater/lib/dependabot/sorbet/runtime.rb new file mode 100644 index 0000000000..b5664b139c --- /dev/null +++ b/updater/lib/dependabot/sorbet/runtime.rb @@ -0,0 +1,23 @@ +# typed: strict +# frozen_string_literal: true + +require "sorbet-runtime" + +module Dependabot + module Sorbet + module Runtime + class InformationalError < StandardError; end + extend T::Sig + + sig { void } + def self.silently_report_errors! + T::Configuration.call_validation_error_handler = lambda do |_sig, opts| + error = InformationalError.new(opts[:pretty_message]) + error.set_backtrace(caller.dup) + + ::Sentry.capture_exception(error) + end + end + end + end +end diff --git a/updater/lib/dependabot/updater/dependency_group_change_batch.rb b/updater/lib/dependabot/updater/dependency_group_change_batch.rb index 826106a40a..96f542b70c 100644 --- a/updater/lib/dependabot/updater/dependency_group_change_batch.rb +++ b/updater/lib/dependabot/updater/dependency_group_change_batch.rb @@ -55,6 +55,11 @@ def merge(dependency_change) debug_current_file_state end + # add an updated dependency without changing any files, useful for incidental updates + def add_updated_dependency(dependency) + merge_dependency_changes([dependency]) + end + private # We should retain a list of all dependencies that we change, in future we may need to account for the folder diff --git a/updater/lib/dependabot/updater/group_update_creation.rb b/updater/lib/dependabot/updater/group_update_creation.rb index 27db6ee46b..dfb3b29d52 100644 --- a/updater/lib/dependabot/updater/group_update_creation.rb +++ b/updater/lib/dependabot/updater/group_update_creation.rb @@ -19,12 +19,21 @@ module GroupUpdateCreation # Returns a Dependabot::DependencyChange object that encapsulates the # outcome of attempting to update every dependency iteratively which # can be used for PR creation. - def compile_all_dependency_changes_for(group) # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity + def compile_all_dependency_changes_for(group) prepare_workspace group_changes = Dependabot::Updater::DependencyGroupChangeBatch.new( initial_dependency_files: dependency_snapshot.dependency_files ) + # TODO: add directory to the dependencies to avoid reparsing? + job_directory = Pathname.new(job.source.directory).cleanpath + original_dependency_files = dependency_snapshot.dependency_files.select do |f| + Pathname.new(f.directory).cleanpath == job_directory + end + original_dependencies = dependency_file_parser(original_dependency_files).parse group.dependencies.each do |dependency| if dependency_snapshot.handled_dependencies.include?(dependency.name) @@ -45,8 +54,15 @@ def compile_all_dependency_changes_for(group) # rubocop:disable Metrics/AbcSize # dependency update next if dependency.nil? - updated_dependencies = compile_updates_for(dependency, dependency_files, group) + # If the dependency version changed, then we can deduce that the dependency was updated already. + original_dependency = original_dependencies.find { |d| d.name == dependency.name } + updated_dependency = deduce_updated_dependency(dependency, original_dependency) + unless updated_dependency.nil? + group_changes.add_updated_dependency(updated_dependency) + next + end + updated_dependencies = compile_updates_for(dependency, dependency_files, group) next unless updated_dependencies.any? lead_dependency = updated_dependencies.find do |dep| @@ -75,6 +91,9 @@ def compile_all_dependency_changes_for(group) # rubocop:disable Metrics/AbcSize ensure cleanup_workspace end + # rubocop:enable Metrics/AbcSize + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity def dependency_file_parser(dependency_files) Dependabot::FileParsers.for_package_manager(job.package_manager).new( @@ -319,6 +338,24 @@ def dependencies_in_existing_pr_for_group(group) pr["dependency-group-name"] == group.name end.fetch("dependencies", []) end + + def deduce_updated_dependency(dependency, original_dependency) + return nil if original_dependency.version == dependency.version + + Dependabot.logger.info( + "Skipping #{dependency.name} as it has already been updated to #{dependency.version}" + ) + dependency_snapshot.handled_dependencies << dependency.name + + Dependabot::Dependency.new( + name: dependency.name, + version: dependency.version, + previous_version: original_dependency.version, + requirements: dependency.requirements, + previous_requirements: original_dependency.requirements, + package_manager: dependency.package_manager + ) + end end end end diff --git a/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb b/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb index 3d4e9c0d6f..f2321824c3 100644 --- a/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb +++ b/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb @@ -30,6 +30,10 @@ def initialize(service:, job:, dependency_snapshot:, error_handler:) @job = job @dependency_snapshot = dependency_snapshot @error_handler = error_handler + + return unless job.source.directory.nil? && job.source.directories.count == 1 + + job.source.directory = job.source.directories.first end def perform diff --git a/updater/lib/dependabot/updater/operations/update_all_versions.rb b/updater/lib/dependabot/updater/operations/update_all_versions.rb index eda72b5033..96f3a39297 100644 --- a/updater/lib/dependabot/updater/operations/update_all_versions.rb +++ b/updater/lib/dependabot/updater/operations/update_all_versions.rb @@ -27,6 +27,10 @@ def initialize(service:, job:, dependency_snapshot:, error_handler:) @error_handler = error_handler # TODO: Collect @created_pull_requests on the Job object? @created_pull_requests = [] + + return unless job.source.directory.nil? && job.source.directories.count == 1 + + job.source.directory = job.source.directories.first end def perform diff --git a/updater/spec/dependabot/api_client_spec.rb b/updater/spec/dependabot/api_client_spec.rb index 4e8219e253..9f99ae7c46 100644 --- a/updater/spec/dependabot/api_client_spec.rb +++ b/updater/spec/dependabot/api_client_spec.rb @@ -20,9 +20,12 @@ updated_dependency_files: dependency_files ) end + let(:source) do + instance_double(Dependabot::Source, provider: "github", repo: "gocardless/bump", directory: "/") + end let(:job) do instance_double(Dependabot::Job, - source: nil, + source: source, credentials: [], commit_message_options: [], updating_a_pull_request?: false, @@ -219,9 +222,12 @@ updated_dependency_files: dependency_files ) end + let(:source) do + instance_double(Dependabot::Source, provider: "github", repo: "gocardless/bump", directory: "/") + end let(:job) do instance_double(Dependabot::Job, - source: nil, + source: source, credentials: [], commit_message_options: [], updating_a_pull_request?: true) diff --git a/updater/spec/dependabot/dependency_change_spec.rb b/updater/spec/dependabot/dependency_change_spec.rb index d615e36369..e8c26f850f 100644 --- a/updater/spec/dependabot/dependency_change_spec.rb +++ b/updater/spec/dependabot/dependency_change_spec.rb @@ -86,7 +86,14 @@ end let(:message_builder_mock) do - instance_double(Dependabot::PullRequestCreator::MessageBuilder, message: "Hello World!") + instance_double( + Dependabot::PullRequestCreator::MessageBuilder, + message: Dependabot::PullRequestCreator::Message.new( + pr_name: "Title", + pr_message: "Hello World!", + commit_message: "Commit message" + ) + ) end before do @@ -110,7 +117,7 @@ ignore_conditions: [] ) - expect(dependency_change.pr_message).to eql("Hello World!") + expect(dependency_change.pr_message.pr_message).to eql("Hello World!") end context "when a dependency group is assigned" do @@ -137,7 +144,7 @@ ignore_conditions: [] ) - expect(dependency_change.pr_message).to eql("Hello World!") + expect(dependency_change.pr_message&.pr_message).to eql("Hello World!") end end end diff --git a/updater/spec/dependabot/dependency_group_engine_spec.rb b/updater/spec/dependabot/dependency_group_engine_spec.rb index 034d7f0c6d..c180b6c6cd 100644 --- a/updater/spec/dependabot/dependency_group_engine_spec.rb +++ b/updater/spec/dependabot/dependency_group_engine_spec.rb @@ -13,11 +13,20 @@ include DependencyFileHelpers let(:dependency_group_engine) { described_class.from_job_config(job: job) } - + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/", + branch: "master" + ) + end + let(:security_updates_only) { false } let(:job) do instance_double(Dependabot::Job, dependency_groups: dependency_groups_config, - security_updates_only?: false) + source: source, + security_updates_only?: security_updates_only) end let(:dummy_pkg_a) do @@ -108,6 +117,45 @@ end end + context "when a job has grouped configured, and it's a version update" do + let(:dependency_groups_config) do + [ + { + "name" => "group-a", + "rules" => { + "patterns" => ["dummy-pkg-*"], + "exclude-patterns" => ["dummy-pkg-b"] + } + }, + { + "name" => "group-b", + "applies-to" => "security-updates", + "rules" => { + "patterns" => %w(dummy-pkg-b dummy-pkg-c) + } + } + ] + end + + describe "::from_job_config" do + it "filters out the security update" do + expect(dependency_group_engine.dependency_groups.length).to eql(1) + expect(dependency_group_engine.dependency_groups.map(&:name)).to eql(%w(group-a)) + end + end + + context "when it's a security update" do + let(:security_updates_only) { true } + + describe "::from_job_config" do + it "filters out the version update" do + expect(dependency_group_engine.dependency_groups.length).to eql(1) + expect(dependency_group_engine.dependency_groups.map(&:name)).to eql(%w(group-b)) + end + end + end + end + context "when a job has groups configured" do let(:dependency_groups_config) do [ diff --git a/updater/spec/dependabot/dependency_snapshot_spec.rb b/updater/spec/dependabot/dependency_snapshot_spec.rb index e5f0a8138b..adaa91613d 100644 --- a/updater/spec/dependabot/dependency_snapshot_spec.rb +++ b/updater/spec/dependabot/dependency_snapshot_spec.rb @@ -36,6 +36,8 @@ source: source, dependency_groups: dependency_groups, allowed_update?: true, + dependency_group_to_refresh: nil, + dependencies: nil, experiments: { large_hadron_collider: true }) end @@ -150,6 +152,7 @@ dependency_groups: dependency_groups, dependencies: ["dummy-pkg-a"], allowed_update?: false, + dependency_group_to_refresh: nil, experiments: { large_hadron_collider: true }) end diff --git a/updater/spec/dependabot/service_spec.rb b/updater/spec/dependabot/service_spec.rb index 283e096216..a487d450fb 100644 --- a/updater/spec/dependabot/service_spec.rb +++ b/updater/spec/dependabot/service_spec.rb @@ -25,12 +25,25 @@ allow(api_client).to receive(:is_a?).with(Dependabot::ApiClient).and_return(true) api_client end + subject(:service) { described_class.new(client: mock_client) } shared_context :a_pr_was_created do + let(:source) do + instance_double(Dependabot::Source, provider: "github", repo: "dependabot/dependabot-core", directory: "/") + end + + let(:job) do + instance_double(Dependabot::Job, + source: source, + credentials: [], + commit_message_options: [], + ignore_conditions: []) + end + let(:dependency_change) do Dependabot::DependencyChange.new( - job: instance_double(Dependabot::Job, source: nil, credentials: [], commit_message_options: []), + job: job, updated_dependencies: dependencies, updated_dependency_files: dependency_files ) @@ -74,16 +87,34 @@ before do allow(Dependabot::PullRequestCreator::MessageBuilder) - .to receive_message_chain(:new, :message).and_return(pr_message) + .to receive_message_chain(:new, :message).and_return( + Dependabot::PullRequestCreator::Message.new( + pr_name: "Test PR", + pr_message: pr_message, + commit_message: "Commit message" + ) + ) service.create_pull_request(dependency_change, base_sha) end end shared_context :a_pr_was_updated do + let(:source) do + instance_double(Dependabot::Source, provider: "github", repo: "dependabot/dependabot-core", directory: "/") + end + + let(:job) do + instance_double(Dependabot::Job, + source: source, + credentials: [], + commit_message_options: [], + ignore_conditions: []) + end + let(:dependency_change) do Dependabot::DependencyChange.new( - job: anything, + job: job, updated_dependencies: dependencies, updated_dependency_files: dependency_files )