diff --git a/.github/workflows/add-to-core-project.yml b/.github/workflows/add-to-core-project.yml deleted file mode 100644 index 725aed3c16f..00000000000 --- a/.github/workflows/add-to-core-project.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Add issues to core project - -on: # yamllint disable-line rule:truthy - issues: - types: - - opened - -jobs: - add-to-project: - name: Add issue to project - runs-on: ubuntu-latest - steps: - - uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 - with: - project-url: https://github.com/orgs/dependabot/projects/5 - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c62d38a01e3..6ed7f561f87 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive # using bundler as the test updater @@ -91,8 +91,8 @@ jobs: env: BUNDLE_GEMFILE: updater/Gemfile steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0 with: bundler-cache: true - run: ./bin/lint @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive - name: Build ecosystem image diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 813d22635e2..7c379c2b07d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,13 +47,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL (ruby) - uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 + uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 with: languages: ${{ matrix.language }} config: | @@ -63,7 +63,7 @@ jobs: if: matrix.language == 'ruby' - name: Initialize CodeQL (others) - uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 + uses: github/codeql-action/init@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 with: languages: ${{ matrix.language }} if: matrix.language != 'ruby' @@ -71,7 +71,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 + uses: github/codeql-action/autobuild@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 # ℹī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -85,4 +85,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 + uses: github/codeql-action/analyze@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index d968710134b..f16cb661059 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -17,6 +17,6 @@ jobs: steps: - name: Checkout - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Codespell uses: codespell-project/actions-codespell@94259cd8be02ad2903ba34a22d9c13de21a74461 # v2.0 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 5b442a2c641..7eeb4bf9bb9 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -20,6 +20,6 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Perform Dependency Review uses: actions/dependency-review-action@72eb03d02c7872a771aacd928f3123ac62ad6d3a # v4.3.3 diff --git a/.github/workflows/gems-bump-version.yml b/.github/workflows/gems-bump-version.yml index a4ca0cd92d5..981b00b45ed 100644 --- a/.github/workflows/gems-bump-version.yml +++ b/.github/workflows/gems-bump-version.yml @@ -19,19 +19,19 @@ jobs: steps: - name: Generate token id: generate_token - uses: actions/create-github-app-token@c8f55efbd427e7465d6da1106e7979bc8aaee856 # v1.10.1 + uses: actions/create-github-app-token@31c86eb3b33c9b601a1f60f98dcbfd1d70f379b4 # v1.10.3 with: app-id: ${{ secrets.DEPENDABOT_CORE_ACTION_AUTOMATION_APP_ID }} private-key: ${{ secrets.DEPENDABOT_CORE_ACTION_AUTOMATION_PRIVATE_KEY }} - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: token: ${{ steps.generate_token.outputs.token }} # Ensure we start from main in case the workflow is run from a branch ref: "main" # bump-version.rb needs bundler - - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 + - uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0 with: # Use the version of bundler specified in `updater/Gemfile.lock`. # Otherwise the generated PR will change `BUNDLED WITH` in diff --git a/.github/workflows/gems-release-to-rubygems.yml b/.github/workflows/gems-release-to-rubygems.yml index 8bf3b6623f3..7c115e41619 100644 --- a/.github/workflows/gems-release-to-rubygems.yml +++ b/.github/workflows/gems-release-to-rubygems.yml @@ -15,8 +15,8 @@ jobs: contents: read steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0 - run: | [ -d ~/.gem ] || mkdir ~/.gem echo "---" > ~/.gem/credentials diff --git a/.github/workflows/images-branch.yml b/.github/workflows/images-branch.yml index cd0efd62ff1..874ae24e48d 100644 --- a/.github/workflows/images-branch.yml +++ b/.github/workflows/images-branch.yml @@ -24,7 +24,7 @@ jobs: decision: ${{ steps.decision.outputs.decision }} steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive @@ -77,7 +77,7 @@ jobs: DEPENDABOT_UPDATER_VERSION: ${{ github.sha }} steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive diff --git a/.github/workflows/images-latest.yml b/.github/workflows/images-latest.yml index a072ec2543a..df26498c231 100644 --- a/.github/workflows/images-latest.yml +++ b/.github/workflows/images-latest.yml @@ -57,7 +57,7 @@ jobs: ECOSYSTEM: ${{ matrix.suite.ecosystem }} steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive diff --git a/.github/workflows/images-updater-core.yml b/.github/workflows/images-updater-core.yml index b84be6e7d09..db330b73c39 100644 --- a/.github/workflows/images-updater-core.yml +++ b/.github/workflows/images-updater-core.yml @@ -18,7 +18,7 @@ jobs: packages: write steps: - name: Checkout code - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive diff --git a/.github/workflows/scorecards.yaml b/.github/workflows/scorecards.yaml index 5a9ece7df12..0749d780df3 100644 --- a/.github/workflows/scorecards.yaml +++ b/.github/workflows/scorecards.yaml @@ -20,7 +20,7 @@ jobs: id-token: write steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: persist-credentials: false @@ -30,6 +30,6 @@ jobs: results_format: sarif publish_results: true - - uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 + - uses: github/codeql-action/upload-sarif@b611370bb5703a7efb587f9d136a52ea24c5c38c # v3.25.11 with: sarif_file: results.sarif diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 40c652970d5..f53b380464b 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -16,13 +16,14 @@ concurrency: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SMOKE_TEST_BRANCH: main jobs: discover: runs-on: ubuntu-latest outputs: suites: ${{ steps.suites.outputs.suites }} steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive @@ -44,7 +45,7 @@ jobs: cat filtered.json # Curl the smoke-test tests directory to get a list of tests to run - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests?ref=${{ env.SMOKE_TEST_BRANCH }} curl $URL > tests.json # Select the names that match smoke-$test*.yaml, where $test is the .text value from filtered.json @@ -64,7 +65,7 @@ jobs: matrix: suite: ${{ fromJSON(needs.discover.outputs.suites) }} steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 with: submodules: recursive @@ -84,7 +85,7 @@ jobs: - name: Download test if: steps.cache-smoke-test.outputs.cache-hit != 'true' run: | - URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }} + URL=https://api.github.com/repos/dependabot/smoke-tests/contents/tests/${{ matrix.suite.name }}?ref=${{ env.SMOKE_TEST_BRANCH }} curl $(gh api $URL --jq .download_url) -o smoke.yaml - name: Cache Smoke Test diff --git a/.github/workflows/sorbet.yml b/.github/workflows/sorbet.yml index 3132cee626b..71665b17c60 100644 --- a/.github/workflows/sorbet.yml +++ b/.github/workflows/sorbet.yml @@ -14,9 +14,9 @@ jobs: name: Sorbet runs-on: ubuntu-latest steps: - - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - - uses: ruby/setup-ruby@3783f195e29b74ae398d7caca108814bbafde90e # v1.180.1 + - uses: ruby/setup-ruby@3a77c29278ae80936b4cb030fefc7d21c96c786f # v1.185.0 with: bundler-cache: true @@ -34,7 +34,7 @@ jobs: - run: bundle exec spoom srb coverage snapshot --save - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: spoom_data path: ./spoom_data/ @@ -45,7 +45,7 @@ jobs: GH_TOKEN: ${{ github.token }} - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4 with: name: spoom_report path: ./spoom_report.html diff --git a/.rubocop.yml b/.rubocop.yml index 75991bfccfb..3a9503f0619 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -341,6 +341,7 @@ Style/SpecialGlobalVars: Style/SelectByRegexp: Enabled: false Sorbet/TrueSigil: + Enabled: true Exclude: - "**/spec/**/*" Sorbet/StrictSigil: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7f4389dad8a..64ea1d09a6e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -47,12 +47,7 @@ RSpec/FilePath: # Configuration parameters: AssignmentOnly. RSpec/InstanceVariable: Exclude: - - 'bundler/helpers/v2/spec/ruby_version_spec.rb' - - 'common/spec/dependabot/clients/azure_spec.rb' - 'go_modules/spec/dependabot/go_modules/file_updater_spec.rb' - - 'pub/spec/dependabot/pub/file_updater_spec.rb' - - 'pub/spec/dependabot/pub/infer_sdk_versions_spec.rb' - - 'pub/spec/dependabot/pub/update_checker_spec.rb' # Offense count: 22 RSpec/IteratedExpectation: diff --git a/Gemfile.lock b/Gemfile.lock index 55c17dce298..63140e7d823 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,20 +1,20 @@ PATH remote: bundler specs: - dependabot-bundler (0.262.0) - dependabot-common (= 0.262.0) + dependabot-bundler (0.265.0) + dependabot-common (= 0.265.0) parallel (~> 1.24) PATH remote: cargo specs: - dependabot-cargo (0.262.0) - dependabot-common (= 0.262.0) + dependabot-cargo (0.265.0) + dependabot-common (= 0.265.0) PATH remote: common specs: - dependabot-common (0.262.0) + dependabot-common (0.265.0) aws-sdk-codecommit (~> 1.28) aws-sdk-ecr (~> 1.5) bundler (>= 1.16, < 3.0.0) @@ -37,107 +37,107 @@ PATH PATH remote: composer specs: - dependabot-composer (0.262.0) - dependabot-common (= 0.262.0) + dependabot-composer (0.265.0) + dependabot-common (= 0.265.0) PATH remote: devcontainers specs: - dependabot-devcontainers (0.262.0) - dependabot-common (= 0.262.0) + dependabot-devcontainers (0.265.0) + dependabot-common (= 0.265.0) PATH remote: docker specs: - dependabot-docker (0.262.0) - dependabot-common (= 0.262.0) + dependabot-docker (0.265.0) + dependabot-common (= 0.265.0) PATH remote: elm specs: - dependabot-elm (0.262.0) - dependabot-common (= 0.262.0) + dependabot-elm (0.265.0) + dependabot-common (= 0.265.0) PATH remote: git_submodules specs: - dependabot-git_submodules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-git_submodules (0.265.0) + dependabot-common (= 0.265.0) parseconfig (~> 1.0, < 1.1.0) PATH remote: github_actions specs: - dependabot-github_actions (0.262.0) - dependabot-common (= 0.262.0) + dependabot-github_actions (0.265.0) + dependabot-common (= 0.265.0) PATH remote: go_modules specs: - dependabot-go_modules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-go_modules (0.265.0) + dependabot-common (= 0.265.0) PATH remote: gradle specs: - dependabot-gradle (0.262.0) - dependabot-common (= 0.262.0) - dependabot-maven (= 0.262.0) + dependabot-gradle (0.265.0) + dependabot-common (= 0.265.0) + dependabot-maven (= 0.265.0) PATH remote: hex specs: - dependabot-hex (0.262.0) - dependabot-common (= 0.262.0) + dependabot-hex (0.265.0) + dependabot-common (= 0.265.0) PATH remote: maven specs: - dependabot-maven (0.262.0) - dependabot-common (= 0.262.0) + dependabot-maven (0.265.0) + dependabot-common (= 0.265.0) PATH remote: npm_and_yarn specs: - dependabot-npm_and_yarn (0.262.0) - dependabot-common (= 0.262.0) + dependabot-npm_and_yarn (0.265.0) + dependabot-common (= 0.265.0) PATH remote: nuget specs: - dependabot-nuget (0.262.0) - dependabot-common (= 0.262.0) + dependabot-nuget (0.265.0) + dependabot-common (= 0.265.0) rubyzip (>= 2.3.2, < 3.0) PATH remote: pub specs: - dependabot-pub (0.262.0) - dependabot-common (= 0.262.0) + dependabot-pub (0.265.0) + dependabot-common (= 0.265.0) PATH remote: python specs: - dependabot-python (0.262.0) - dependabot-common (= 0.262.0) + dependabot-python (0.265.0) + dependabot-common (= 0.265.0) PATH remote: silent specs: - dependabot-silent (0.262.0) - dependabot-common (= 0.262.0) + dependabot-silent (0.265.0) + dependabot-common (= 0.265.0) PATH remote: swift specs: - dependabot-swift (0.262.0) - dependabot-common (= 0.262.0) + dependabot-swift (0.265.0) + dependabot-common (= 0.265.0) PATH remote: terraform specs: - dependabot-terraform (0.262.0) - dependabot-common (= 0.262.0) + dependabot-terraform (0.265.0) + dependabot-common (= 0.265.0) GEM remote: https://rubygems.org/ @@ -255,7 +255,7 @@ GEM sorbet-runtime (>= 0.5.9204) rdoc (6.6.3.1) psych (>= 4.0.0) - regexp_parser (2.9.0) + regexp_parser (2.9.2) reline (0.5.2) io-console (~> 0.5) rest-client (2.1.0) @@ -283,13 +283,13 @@ GEM rspec-sorbet (1.9.2) sorbet-runtime rspec-support (3.12.1) - rubocop (1.63.2) + rubocop (1.65.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) + regexp_parser (>= 2.4, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) @@ -404,7 +404,7 @@ DEPENDENCIES rake (~> 13) rspec-its (~> 1.3) rspec-sorbet (~> 1.9.2) - rubocop (~> 1.63.2) + rubocop (~> 1.65.0) rubocop-performance (~> 1.21.0) rubocop-rspec (~> 2.29.1) rubocop-sorbet (~> 0.8.1) diff --git a/README.md b/README.md index 9e29f9aaf44..1e15891b067 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ It is intended as a starting point for advanced users to run a self-hosted versi ## Dependabot CLI The [Dependabot CLI](https://github.com/dependabot/cli) is a newer tool that may eventually replace [`dependabot-script`](#dependabot-script) for standalone use cases. -While it creates dependency diffs, it's currently missing the logic to turn those diffs into actual PR's. Nevertheless, it +While it creates dependency diffs, it's currently missing the logic to turn those diffs into actual PRs. Nevertheless, it may be useful for advanced users looking for examples of how to hack on Dependabot. ## Dependabot on CI diff --git a/bin/bump-version.rb b/bin/bump-version.rb index e50d7ca5efc..80b0386622c 100755 --- a/bin/bump-version.rb +++ b/bin/bump-version.rb @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# typed: false +# typed: true # frozen_string_literal: true unless %w(minor patch).include?(ARGV[0]) diff --git a/bin/dry-run.rb b/bin/dry-run.rb index 847613b572d..662953b562a 100755 --- a/bin/dry-run.rb +++ b/bin/dry-run.rb @@ -1,5 +1,5 @@ #!/usr/bin/env ruby -# typed: false +# typed: true # frozen_string_literal: true # This script executes a full update run for a given repo (optionally for a diff --git a/bundler/.rubocop.yml b/bundler/.rubocop.yml index f04967a814a..507d22b2def 100644 --- a/bundler/.rubocop.yml +++ b/bundler/.rubocop.yml @@ -5,6 +5,5 @@ inherit_mode: - Exclude Sorbet/TrueSigil: - Enabled: true Exclude: - "helpers/**/monkey_patches/*.rb" diff --git a/bundler/helpers/v1/monkey_patches/resolver_spec_group_sane_eql.rb b/bundler/helpers/v1/monkey_patches/resolver_spec_group_sane_eql.rb index ce57399f054..c3e2e701261 100644 --- a/bundler/helpers/v1/monkey_patches/resolver_spec_group_sane_eql.rb +++ b/bundler/helpers/v1/monkey_patches/resolver_spec_group_sane_eql.rb @@ -11,7 +11,7 @@ module BundlerResolverSpecGroupSaneEql def eql?(other) return false unless other.is_a?(self.class) - super(other) + super end end diff --git a/bundler/helpers/v2/spec/ruby_version_spec.rb b/bundler/helpers/v2/spec/ruby_version_spec.rb index 6acf0e36603..f3787a48382 100644 --- a/bundler/helpers/v2/spec/ruby_version_spec.rb +++ b/bundler/helpers/v2/spec/ruby_version_spec.rb @@ -9,13 +9,13 @@ include_context "when stubbing rubygems compact index" let(:project_name) { "ruby_version_implied" } + let(:ui) { Bundler.ui } before do - @ui = Bundler.ui Bundler.ui = Bundler::UI::Silent.new end - after { Bundler.ui = @ui } + after { Bundler.ui = ui } it "updates to the most recent version" do in_tmp_folder do diff --git a/bundler/lib/dependabot/bundler/file_fetcher.rb b/bundler/lib/dependabot/bundler/file_fetcher.rb index bcbd18add95..e25ab963653 100644 --- a/bundler/lib/dependabot/bundler/file_fetcher.rb +++ b/bundler/lib/dependabot/bundler/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" @@ -17,18 +17,21 @@ class FileFetcher < Dependabot::FileFetchers::Base require "dependabot/bundler/file_fetcher/gemspec_finder" require "dependabot/bundler/file_fetcher/path_gemspec_finder" require "dependabot/bundler/file_fetcher/child_gemfile_finder" - require "dependabot/bundler/file_fetcher/require_relative_finder" + require "dependabot/bundler/file_fetcher/included_path_finder" + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) return true if filenames.any? { |name| name.match?(%r{^[^/]*\.gemspec$}) } filenames.include?("Gemfile") || filenames.include?("gems.rb") end + sig { override.returns(String) } def self.required_files_message "Repo must contain either a Gemfile, a gemspec, or a gems.rb." end + sig { override.returns(T.nilable(T::Hash[Symbol, T.nilable(String)])) } def ecosystem_versions { package_managers: { @@ -39,41 +42,47 @@ def ecosystem_versions sig { override.returns(T::Array[DependencyFile]) } def fetch_files - fetched_files = [] - fetched_files << gemfile if gemfile - fetched_files << lockfile if gemfile && lockfile + fetched_files = T.let([], T::Array[DependencyFile]) + fetched_files << T.must(gemfile) if gemfile + fetched_files << T.must(lockfile) if gemfile && lockfile fetched_files += child_gemfiles fetched_files += gemspecs - fetched_files << ruby_version_file if ruby_version_file - fetched_files << tool_versions_file if tool_versions_file + fetched_files << T.must(ruby_version_file) if ruby_version_file + fetched_files << T.must(tool_versions_file) if tool_versions_file fetched_files += path_gemspecs - fetched_files += require_relative_files(fetched_files) + fetched_files += find_included_files(fetched_files) uniq_files(fetched_files) end private + sig { params(fetched_files: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) } def uniq_files(fetched_files) uniq_files = fetched_files.reject(&:support_file?).uniq uniq_files += fetched_files .reject { |f| uniq_files.map(&:name).include?(f.name) } end + sig { returns(T.nilable(DependencyFile)) } def gemfile return @gemfile if defined?(@gemfile) - @gemfile = fetch_file_if_present("gems.rb") || fetch_file_if_present("Gemfile") + @gemfile = T.let(fetch_file_if_present("gems.rb") || fetch_file_if_present("Gemfile"), + T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(DependencyFile)) } def lockfile return @lockfile if defined?(@lockfile) - @lockfile = fetch_file_if_present("gems.locked") || fetch_file_if_present("Gemfile.lock") + @lockfile = T.let(fetch_file_if_present("gems.locked") || fetch_file_if_present("Gemfile.lock"), + T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def gemspecs - return @gemspecs if defined?(@gemspecs) + return T.must(@gemspecs) if defined?(@gemspecs) gemspecs_paths = gemspec_directories @@ -83,11 +92,17 @@ def gemspecs .map { |f| File.join(d, f.name) } end - @gemspecs = gemspecs_paths.map { |n| fetch_file_from_host(n) } + @gemspecs ||= T.let( + gemspecs_paths.map do |n| + fetch_file_from_host(n) + end, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) rescue Octokit::NotFound [] end + sig { returns(T::Array[String]) } def gemspec_directories gemfiles = ([gemfile] + child_gemfiles).compact directories = @@ -98,18 +113,21 @@ def gemspec_directories directories.empty? ? ["."] : directories end + sig { returns(T.nilable(DependencyFile)) } def ruby_version_file return unless gemfile - @ruby_version_file ||= fetch_support_file(".ruby-version") + @ruby_version_file ||= T.let(fetch_support_file(".ruby-version"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(DependencyFile)) } def tool_versions_file return unless gemfile - @tool_versions_file ||= fetch_support_file(".tool-versions") + @tool_versions_file ||= T.let(fetch_support_file(".tool-versions"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Array[DependencyFile]) } def path_gemspecs gemspec_files = T.let([], T::Array[Dependabot::DependencyFile]) unfetchable_gems = [] @@ -141,21 +159,25 @@ def path_gemspecs gemspec_files end + sig { returns(T::Array[Pathname]) } def path_gemspec_paths fetch_path_gemspec_paths.map { |path| Pathname.new(path) } end - def require_relative_files(files) + sig { params(files: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) } + def find_included_files(files) ruby_files = files.select { |f| f.name.end_with?(".rb", "Gemfile", ".gemspec") } paths = ruby_files.flat_map do |file| - RequireRelativeFinder.new(file: file).require_relative_paths + IncludedPathFinder.new(file: file).find_included_paths end - @require_relative_files ||= + @find_included_files ||= T.let( paths.map { |path| fetch_file_from_host(path) } - .tap { |req_files| req_files.each { |f| f.support_file = true } } + .tap { |req_files| req_files.each { |f| f.support_file = true } }, + T.nilable(T::Array[DependencyFile]) + ) end sig { params(dir_path: T.any(String, Pathname)).returns(T::Array[DependencyFile]) } @@ -166,9 +188,10 @@ def fetch_gemspecs_from_directory(dir_path) .map { |fp| fetch_file_from_host(fp, fetch_submodules: true) } end + sig { returns(T::Array[String]) } def fetch_path_gemspec_paths if lockfile - parsed_lockfile = CachedLockfileParser.parse(sanitized_lockfile_content) + parsed_lockfile = CachedLockfileParser.parse(T.must(sanitized_lockfile_content)) parsed_lockfile.specs .select { |s| s.source.instance_of?(::Bundler::Source::Path) } .map { |s| s.source.path }.uniq @@ -179,26 +202,35 @@ def fetch_path_gemspec_paths end.uniq end rescue ::Bundler::LockfileError - raise Dependabot::DependencyFileNotParseable, lockfile.path + raise Dependabot::DependencyFileNotParseable, T.must(lockfile).path rescue ::Bundler::Plugin::UnknownSourceError # Quietly ignore plugin errors - we'll raise a better error during # parsing [] end + sig { returns(T::Array[DependencyFile]) } def child_gemfiles return [] unless gemfile - @child_gemfiles ||= - fetch_child_gemfiles(file: gemfile, previously_fetched_files: []) + @child_gemfiles ||= T.let( + fetch_child_gemfiles(file: T.must(gemfile), previously_fetched_files: []), + T.nilable(T::Array[DependencyFile]) + ) end # TODO: Stop sanitizing the lockfile once we have bundler 2 installed + + sig { returns T.nilable(String) } def sanitized_lockfile_content regex = FileUpdater::LockfileUpdater::LOCKFILE_ENDING - lockfile.content.gsub(regex, "") + lockfile&.content&.gsub(regex, "") end + sig do + params(file: DependencyFile, + previously_fetched_files: T::Array[DependencyFile]).returns(T::Array[DependencyFile]) + end def fetch_child_gemfiles(file:, previously_fetched_files:) paths = ChildGemfileFinder.new(gemfile: file).child_gemfile_paths diff --git a/bundler/lib/dependabot/bundler/file_fetcher/included_path_finder.rb b/bundler/lib/dependabot/bundler/file_fetcher/included_path_finder.rb new file mode 100644 index 00000000000..41e62a4fdb9 --- /dev/null +++ b/bundler/lib/dependabot/bundler/file_fetcher/included_path_finder.rb @@ -0,0 +1,133 @@ +# typed: strict +# frozen_string_literal: true + +require "pathname" +require "parser/current" +require "dependabot/bundler/file_fetcher" +require "dependabot/errors" +require "sorbet-runtime" + +module Dependabot + module Bundler + class FileFetcher + # Finds the paths of any files included using `require_relative` and `eval` in the + # passed file. + class IncludedPathFinder + extend T::Sig + + sig { params(file: Dependabot::DependencyFile).void } + def initialize(file:) + @file = file + end + + sig { returns(T::Array[String]) } + def find_included_paths + ast = Parser::CurrentRuby.parse(file.content) + find_require_relative_paths(ast) + find_eval_paths(ast) + rescue Parser::SyntaxError + raise Dependabot::DependencyFileNotParseable, file.path + end + + private + + sig { returns(Dependabot::DependencyFile) } + attr_reader :file + + sig { params(node: T.untyped).returns(T::Array[String]) } + def find_require_relative_paths(node) + return [] unless node.is_a?(Parser::AST::Node) + + if declares_require_relative?(node) + return [] unless node.children[2].type == :str + + path = node.children[2].loc.expression.source.gsub(/['"]/, "") + path = File.join(current_dir, path) unless current_dir.nil? + path += ".rb" unless path.end_with?(".rb") + return [Pathname.new(path).cleanpath.to_path] + end + + node.children.flat_map do |child_node| + find_require_relative_paths(child_node) + end + end + + sig { params(node: T.untyped).returns(T::Array[String]) } + def find_eval_paths(node) + return [] unless node.is_a?(Parser::AST::Node) + + if declares_eval?(node) + eval_arg = node.children[2] + if eval_arg.is_a?(Parser::AST::Node) + file_read_node = find_file_read_node(eval_arg) + path = extract_path_from_file_read(file_read_node) if file_read_node + return [path] if path + end + end + + node.children.flat_map do |child_node| + find_eval_paths(child_node) + end + end + + sig { params(node: Parser::AST::Node).returns(T.nilable(Parser::AST::Node)) } + def find_file_read_node(node) + return nil unless node.is_a?(Parser::AST::Node) + + # Check if the node represents a method call (:send) + # and if the method name is :read + method_name = node.children[1] + receiver_node = node.children[0] + + if node.type == :send && method_name == :read && receiver_node.is_a?(Parser::AST::Node) + # Check if the receiver of the :read method call is :File + receiver_const = receiver_node.children[1] + return node if receiver_const == :File + end + + # Recursively search for a file read node in the children + node.children.each do |child| + next unless child.is_a?(Parser::AST::Node) + + result = find_file_read_node(child) + return result if result + end + + nil + end + + sig { params(node: Parser::AST::Node).returns(T.nilable(String)) } + def extract_path_from_file_read(node) + return nil unless node.is_a?(Parser::AST::Node) + + expand_path_node = node.children[2] + if expand_path_node.type == :send && expand_path_node.children[1] == :expand_path + path_node = expand_path_node.children[2] + return path_node.loc.expression.source.gsub(/['"]/, "") if path_node.type == :str + end + nil + end + + sig { returns(T.nilable(String)) } + def current_dir + @current_dir ||= T.let(file.name.rpartition("/").first, T.nilable(String)) + @current_dir = nil if @current_dir == "" + @current_dir + end + + sig { params(node: Parser::AST::Node).returns(T::Boolean) } + def declares_require_relative?(node) + return false unless node.is_a?(Parser::AST::Node) + + node.children[1] == :require_relative + end + + sig { params(node: Parser::AST::Node).returns(T::Boolean) } + def declares_eval?(node) + return false unless node.is_a?(Parser::AST::Node) + + node.children[1] == :eval + end + end + end + end +end diff --git a/bundler/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb b/bundler/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb deleted file mode 100644 index cd31d72f2f4..00000000000 --- a/bundler/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +++ /dev/null @@ -1,70 +0,0 @@ -# typed: strict -# frozen_string_literal: true - -require "pathname" -require "parser/current" -require "dependabot/bundler/file_fetcher" -require "dependabot/errors" -require "sorbet-runtime" - -module Dependabot - module Bundler - class FileFetcher - # Finds the paths of any files included using `require_relative` in the - # passed file. - class RequireRelativeFinder - extend T::Sig - - sig { params(file: Dependabot::DependencyFile).void } - def initialize(file:) - @file = file - end - - sig { returns(T::Array[String]) } - def require_relative_paths - ast = Parser::CurrentRuby.parse(file.content) - find_require_relative_paths(ast) - rescue Parser::SyntaxError - raise Dependabot::DependencyFileNotParseable, file.path - end - - private - - sig { returns(Dependabot::DependencyFile) } - attr_reader :file - - sig { params(node: T.untyped).returns(T::Array[T.untyped]) } - def find_require_relative_paths(node) - return [] unless node.is_a?(Parser::AST::Node) - - if declares_require_relative?(node) - return [] unless node.children[2].type == :str - - path = node.children[2].loc.expression.source.gsub(/['"]/, "") - path = File.join(current_dir, path) unless current_dir.nil? - path += ".rb" unless path.end_with?(".rb") - return [Pathname.new(path).cleanpath.to_path] - end - - node.children.flat_map do |child_node| - find_require_relative_paths(child_node) - end - end - - sig { returns(T.nilable(String)) } - def current_dir - @current_dir ||= T.let(file.name.rpartition("/").first, T.nilable(String)) - @current_dir = nil if @current_dir == "" - @current_dir - end - - sig { params(node: Parser::AST::Node).returns(T::Boolean) } - def declares_require_relative?(node) - return false unless node.is_a?(Parser::AST::Node) - - node.children[1] == :require_relative - end - end - end - end -end diff --git a/bundler/spec/dependabot/bundler/file_fetcher/require_relative_finder_spec.rb b/bundler/spec/dependabot/bundler/file_fetcher/included_path_finder_spec.rb similarity index 69% rename from bundler/spec/dependabot/bundler/file_fetcher/require_relative_finder_spec.rb rename to bundler/spec/dependabot/bundler/file_fetcher/included_path_finder_spec.rb index 8a2d2c47d8d..500ab6c3629 100644 --- a/bundler/spec/dependabot/bundler/file_fetcher/require_relative_finder_spec.rb +++ b/bundler/spec/dependabot/bundler/file_fetcher/included_path_finder_spec.rb @@ -4,9 +4,9 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" -require "dependabot/bundler/file_fetcher/require_relative_finder" +require "dependabot/bundler/file_fetcher/included_path_finder" -RSpec.describe Dependabot::Bundler::FileFetcher::RequireRelativeFinder do +RSpec.describe Dependabot::Bundler::FileFetcher::IncludedPathFinder do let(:finder) { described_class.new(file: file) } let(:file) do @@ -14,8 +14,8 @@ end let(:file_name) { "Gemfile" } - describe "#require_relative_paths" do - subject(:require_relative_paths) { finder.require_relative_paths } + describe "#find_included_paths" do + subject(:find_included_paths) { finder.find_included_paths } context "when the file does not include any relative paths" do let(:file_body) { bundler_project_dependency_file("gemfile", filename: "Gemfile").content } @@ -28,7 +28,7 @@ it "raises a helpful error" do suppress_output do - expect { finder.require_relative_paths }.to raise_error do |error| + expect { finder.find_included_paths }.to raise_error do |error| expect(error).to be_a(Dependabot::DependencyFileNotParseable) expect(error.file_name).to eq("Gemfile") end @@ -36,7 +36,7 @@ end end - context "when the file does include a relative path" do + context "when the file includes a require_relative path" do let(:file_body) do bundler_project_dependency_file("includes_require_relative_gemfile", filename: "nested/Gemfile").content end @@ -68,11 +68,23 @@ it { is_expected.to eq([]) } end # rubocop:enable Lint/InterpolationCheck + end - context "when dealing with a file that is already nested" do - let(:file_name) { "deeply/nested/Gemfile" } + context "when the file includes an eval statement" do + context "with File.read" do + let(:file_body) do + 'eval File.read(File.expand_path("some_other_file.rb", __dir__))' + end + + it { is_expected.to eq(["some_other_file.rb"]) } + end - it { is_expected.to eq(["deeply/some_other_file.rb"]) } + context "when the eval does not read a file" do + let(:file_body) do + 'eval "puts \'Hello, world!\'"' + end + + it { is_expected.to eq([]) } end end end diff --git a/cargo/.rubocop.yml b/cargo/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/cargo/.rubocop.yml +++ b/cargo/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/cargo/Dockerfile b/cargo/Dockerfile index c799ede70bd..97b4f2d3183 100644 --- a/cargo/Dockerfile +++ b/cargo/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/rust:1.78.0-bookworm as rust +FROM docker.io/library/rust:1.79.0-bookworm as rust FROM ghcr.io/dependabot/dependabot-updater-core diff --git a/common/dependabot-common.gemspec b/common/dependabot-common.gemspec index 82225282dd5..fc94b721e85 100644 --- a/common/dependabot-common.gemspec +++ b/common/dependabot-common.gemspec @@ -51,7 +51,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec", "~> 3.12" spec.add_development_dependency "rspec-its", "~> 1.3" spec.add_development_dependency "rspec-sorbet", "~> 1.9.2" - spec.add_development_dependency "rubocop", "~> 1.63.2" + spec.add_development_dependency "rubocop", "~> 1.65.0" spec.add_development_dependency "rubocop-performance", "~> 1.21.0" spec.add_development_dependency "rubocop-rspec", "~> 2.29.1" spec.add_development_dependency "rubocop-sorbet", "~> 0.8.1" diff --git a/common/lib/dependabot.rb b/common/lib/dependabot.rb index 573023fe55c..6f20f1fa5bf 100644 --- a/common/lib/dependabot.rb +++ b/common/lib/dependabot.rb @@ -2,5 +2,5 @@ # frozen_string_literal: true module Dependabot - VERSION = "0.262.0" + VERSION = "0.265.0" end diff --git a/common/lib/dependabot/clients/bitbucket.rb b/common/lib/dependabot/clients/bitbucket.rb index 2129d0fc4a5..3d4d4878549 100644 --- a/common/lib/dependabot/clients/bitbucket.rb +++ b/common/lib/dependabot/clients/bitbucket.rb @@ -142,7 +142,7 @@ def pull_requests(repo, source_branch, target_branch, status = %w(OPEN MERGED DE next_page_url = base_url + pr_path pull_requests = paginate({ "next" => next_page_url }) - pull_requests unless source_branch && target_branch + pull_requests unless source_branch && target_branch # rubocop:disable Lint/Void pull_requests.select do |pr| if source_branch.nil? diff --git a/common/lib/dependabot/errors.rb b/common/lib/dependabot/errors.rb index 1df7447a0cf..4f6b0e968c2 100644 --- a/common/lib/dependabot/errors.rb +++ b/common/lib/dependabot/errors.rb @@ -17,6 +17,7 @@ module ErrorAttributes DEPENDENCY_GROUPS = "job-dependency-groups" JOB_ID = "job-id" PACKAGE_MANAGER = "package-manager" + SECURITY_UPDATE = "security-update" end # rubocop:disable Metrics/MethodLength diff --git a/common/lib/dependabot/file_fetchers/base.rb b/common/lib/dependabot/file_fetchers/base.rb index d714a0400d3..dadf75083a3 100644 --- a/common/lib/dependabot/file_fetchers/base.rb +++ b/common/lib/dependabot/file_fetchers/base.rb @@ -93,12 +93,13 @@ def self.required_files_message # # options supports custom feature enablement sig do - params( - source: Dependabot::Source, - credentials: T::Array[Dependabot::Credential], - repo_contents_path: T.nilable(String), - options: T::Hash[String, String] - ) + overridable + .params( + source: Dependabot::Source, + credentials: T::Array[Dependabot::Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[String, String] + ) .void end def initialize(source:, credentials:, repo_contents_path: nil, options: {}) diff --git a/common/lib/dependabot/git_commit_checker.rb b/common/lib/dependabot/git_commit_checker.rb index 55753f15ce2..ff52536978e 100644 --- a/common/lib/dependabot/git_commit_checker.rb +++ b/common/lib/dependabot/git_commit_checker.rb @@ -144,14 +144,14 @@ def local_tag_for_latest_version max_local_tag(allowed_version_tags) end - sig { returns(T::Array[T.nilable(T::Hash[Symbol, T.untyped])]) } + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } def local_tags_for_allowed_versions_matching_existing_precision - select_matching_existing_precision(allowed_version_tags).map { |t| to_local_tag(t) } + select_matching_existing_precision(allowed_version_tags).filter_map { |t| to_local_tag(t) } end - sig { returns(T::Array[T.nilable(T::Hash[Symbol, T.untyped])]) } + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } def local_tags_for_allowed_versions - allowed_version_tags.map { |t| to_local_tag(t) } + allowed_version_tags.filter_map { |t| to_local_tag(t) } end sig { returns(T::Array[Dependabot::GitRef]) } diff --git a/common/lib/dependabot/shared_helpers.rb b/common/lib/dependabot/shared_helpers.rb index c16aa6f8316..9d6bfe3a7e3 100644 --- a/common/lib/dependabot/shared_helpers.rb +++ b/common/lib/dependabot/shared_helpers.rb @@ -131,16 +131,18 @@ def self.escape_command(command) params( command: String, function: String, - args: T.any(T::Array[String], T::Hash[Symbol, String]), + args: T.any(T::Array[T.any(String, T::Array[T::Hash[String, T.untyped]])], T::Hash[Symbol, String]), env: T.nilable(T::Hash[String, String]), stderr_to_stdout: T::Boolean, - allow_unsafe_shell_command: T::Boolean + allow_unsafe_shell_command: T::Boolean, + error_class: T.class_of(HelperSubprocessFailed) ) .returns(T.nilable(T.any(String, T::Hash[String, T.untyped], T::Array[T::Hash[String, T.untyped]]))) end def self.run_helper_subprocess(command:, function:, args:, env: nil, stderr_to_stdout: false, - allow_unsafe_shell_command: false) + allow_unsafe_shell_command: false, + error_class: HelperSubprocessFailed) start = Time.now stdin_data = JSON.dump(function: function, args: args) cmd = allow_unsafe_shell_command ? command : escape_command(command) @@ -180,33 +182,54 @@ def self.run_helper_subprocess(command:, function:, args:, env: nil, process_termsig: process.termsig } - check_out_of_memory_error(stderr, error_context) + check_out_of_memory_error(stderr, error_context, error_class) begin response = JSON.parse(stdout) return response["result"] if process.success? - raise HelperSubprocessFailed.new( + raise error_class.new( message: response["error"], error_class: response["error_class"], error_context: error_context, trace: response["trace"] ) rescue JSON::ParserError - raise HelperSubprocessFailed.new( - message: stdout || "No output from command", - error_class: "JSON::ParserError", - error_context: error_context - ) + raise handle_json_parse_error(stdout, stderr, error_context, error_class) end end + sig do + params(stdout: String, stderr: String, error_context: T::Hash[Symbol, T.untyped], + error_class: T.class_of(HelperSubprocessFailed)) + .returns(HelperSubprocessFailed) + end + def self.handle_json_parse_error(stdout, stderr, error_context, error_class) + # If the JSON is invalid, the helper has likely failed + # We should raise a more helpful error message + message = if !stdout.strip.empty? + stdout + elsif !stderr.strip.empty? + stderr + else + "No output from command" + end + error_class.new( + message: message, + error_class: "JSON::ParserError", + error_context: error_context + ) + end + # rubocop:enable Metrics/MethodLength - sig { params(stderr: T.nilable(String), error_context: T::Hash[Symbol, String]).void } - def self.check_out_of_memory_error(stderr, error_context) + sig do + params(stderr: T.nilable(String), error_context: T::Hash[Symbol, String], + error_class: T.class_of(HelperSubprocessFailed)).void + end + def self.check_out_of_memory_error(stderr, error_context, error_class) return unless stderr&.include?("JavaScript heap out of memory") - raise HelperSubprocessFailed.new( + raise error_class.new( message: "JavaScript heap out of memory", error_class: "Dependabot::OutOfMemoryError", error_context: error_context diff --git a/common/lib/dependabot/update_checkers/base.rb b/common/lib/dependabot/update_checkers/base.rb index 308549c8abf..05501b2d5ce 100644 --- a/common/lib/dependabot/update_checkers/base.rb +++ b/common/lib/dependabot/update_checkers/base.rb @@ -136,7 +136,7 @@ def latest_resolvable_version # Lowest available security fix version not checking resolvability # @return [Dependabot::::Version, #to_s] version class - sig { overridable.returns(Dependabot::Version) } + sig { overridable.returns(T.nilable(Dependabot::Version)) } def lowest_security_fix_version raise NotImplementedError, "#{self.class} must implement #lowest_security_fix_version" end @@ -363,7 +363,7 @@ def requirements_up_to_date? end # TODO: Should this return Dependabot::Version? - sig { returns(T.nilable(Gem::Version)) } + sig { returns(T.nilable(Dependabot::Version)) } def current_version @current_version ||= T.let( diff --git a/common/lib/dependabot/workspace/git.rb b/common/lib/dependabot/workspace/git.rb index bd9411bf833..98114e4f011 100644 --- a/common/lib/dependabot/workspace/git.rb +++ b/common/lib/dependabot/workspace/git.rb @@ -19,7 +19,7 @@ class Git < Base sig { params(path: T.any(Pathname, String)).void } def initialize(path) - super(path) + super @initial_head_sha = T.let(head_sha, String) configure_git end diff --git a/common/spec/dependabot/clients/azure_spec.rb b/common/spec/dependabot/clients/azure_spec.rb index 73dbdedf7ee..815baf60448 100644 --- a/common/spec/dependabot/clients/azure_spec.rb +++ b/common/spec/dependabot/clients/azure_spec.rb @@ -441,26 +441,24 @@ end context "when dealing with POST" do - before do - @request_body = "request body" - end + let(:request_body) { "request body" } it "with failure count <= max_retries" do # Request succeeds on third attempt stub_request(:post, base_url) - .with(basic_auth: [username, password], body: @request_body) + .with(basic_auth: [username, password], body: request_body) .to_return({ status: 503 }, { status: 503 }, { status: 200 }) - response = client.post(base_url, @request_body) + response = client.post(base_url, request_body) expect(response.status).to eq(200) end it "with failure count > max_retries raises an error" do stub_request(:post, base_url) - .with(basic_auth: [username, password], body: @request_body) + .with(basic_auth: [username, password], body: request_body) .to_return({ status: 503 }, { status: 503 }, { status: 503 }, { status: 503 }) - expect { client.post(base_url, @request_body) } + expect { client.post(base_url, request_body) } .to raise_error(Dependabot::Clients::Azure::ServiceNotAvailable) end end diff --git a/common/spec/dependabot/shared_helpers_spec.rb b/common/spec/dependabot/shared_helpers_spec.rb index 6663bfbdda4..c45fc3fba65 100644 --- a/common/spec/dependabot/shared_helpers_spec.rb +++ b/common/spec/dependabot/shared_helpers_spec.rb @@ -9,6 +9,9 @@ require "dependabot/simple_instrumentor" require "dependabot/workspace" +# Custom error class for testing +class EcoSystemHelperSubprocessFailed < Dependabot::SharedHelpers::HelperSubprocessFailed; end + RSpec.describe Dependabot::SharedHelpers do let(:spec_root) { File.join(File.dirname(__FILE__), "..") } let(:tmp) { Dependabot::Utils::BUMP_TMP_DIR_PATH } @@ -135,7 +138,8 @@ def existing_tmp_folders function: function, args: args, env: env, - stderr_to_stdout: stderr_to_stdout + stderr_to_stdout: stderr_to_stdout, + error_class: error_class ) end @@ -143,6 +147,7 @@ def existing_tmp_folders let(:args) { ["foo"] } let(:env) { nil } let(:stderr_to_stdout) { false } + let(:error_class) { Dependabot::SharedHelpers::HelperSubprocessFailed } context "when the subprocess is successful" do it "returns the result" do @@ -214,6 +219,16 @@ def existing_tmp_folders end) end end + + context "when a custom error class is passed" do + let(:error_class) { EcoSystemHelperSubprocessFailed } + let(:function) { "hard_error" } + + it "raises the custom error class" do + expect { run_subprocess } + .to raise_error(EcoSystemHelperSubprocessFailed) + end + end end describe ".run_shell_command" do @@ -566,4 +581,50 @@ def with_git_configured(&block) specify { expect { configured_git_config }.to raise_error(Dependabot::OutOfDisk) } end end + + describe ".handle_json_parse_error" do + subject(:handle_json_parse_error) do + described_class.handle_json_parse_error(stdout, stderr, error_context, error_class) + end + + let(:stdout) { "" } + let(:stderr) { "" } + let(:error_context) { { command: "test_command", function: "test_function", args: [] } } + let(:error_class) { Dependabot::SharedHelpers::HelperSubprocessFailed } + + context "when stdout is not empty" do + let(:stdout) { "Some stdout message" } + + it "raises HelperSubprocessFailed with stdout message" do + expect { raise handle_json_parse_error } + .to raise_error(Dependabot::SharedHelpers::HelperSubprocessFailed, "Some stdout message") + end + end + + context "when stdout is empty but stderr is not empty" do + let(:stderr) { "Some stderr message" } + + it "raises HelperSubprocessFailed with stderr message" do + expect { raise handle_json_parse_error } + .to raise_error(Dependabot::SharedHelpers::HelperSubprocessFailed, "Some stderr message") + end + end + + context "when both stdout and stderr are empty" do + it "raises HelperSubprocessFailed with default message" do + expect { raise handle_json_parse_error } + .to raise_error(Dependabot::SharedHelpers::HelperSubprocessFailed, "No output from command") + end + end + + context "when a custom error class is passed" do + let(:stdout) { "Some stdout message" } + let(:error_class) { EcoSystemHelperSubprocessFailed } + + it "raises the custom error class with stdout message" do + expect { raise handle_json_parse_error } + .to raise_error(EcoSystemHelperSubprocessFailed, "Some stdout message") + end + end + end end diff --git a/composer/.rubocop.yml b/composer/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/composer/.rubocop.yml +++ b/composer/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/composer/Dockerfile b/composer/Dockerfile index 46b0010d206..11513eda85b 100644 --- a/composer/Dockerfile +++ b/composer/Dockerfile @@ -1,6 +1,6 @@ FROM ghcr.io/dependabot/dependabot-updater-core ARG COMPOSER_V1_VERSION=1.10.27 -ARG COMPOSER_V2_VERSION=2.7.4 +ARG COMPOSER_V2_VERSION=2.7.7 ENV COMPOSER_ALLOW_SUPERUSER=1 RUN apt-get update \ && apt-get upgrade -y \ diff --git a/composer/helpers/v2/composer.lock b/composer/helpers/v2/composer.lock index 742794027e2..b9eb43fad3a 100644 --- a/composer/helpers/v2/composer.lock +++ b/composer/helpers/v2/composer.lock @@ -84,16 +84,16 @@ }, { "name": "composer/class-map-generator", - "version": "1.1.1", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/composer/class-map-generator.git", - "reference": "8286a62d243312ed99b3eee20d5005c961adb311" + "reference": "b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", - "reference": "8286a62d243312ed99b3eee20d5005c961adb311", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3", + "reference": "b1b3fd0b4eaf3ddf3ee230bc340bf3fff454a1a3", "shasum": "" }, "require": { @@ -137,7 +137,7 @@ ], "support": { "issues": "https://github.com/composer/class-map-generator/issues", - "source": "https://github.com/composer/class-map-generator/tree/1.1.1" + "source": "https://github.com/composer/class-map-generator/tree/1.3.4" }, "funding": [ { @@ -153,28 +153,28 @@ "type": "tidelift" } ], - "time": "2024-03-15T12:53:41+00:00" + "time": "2024-06-12T14:13:04+00:00" }, { "name": "composer/composer", - "version": "2.7.4", + "version": "2.7.7", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "a625e50598e12171d3f60b1149eb530690c43474" + "reference": "291942978f39435cf904d33739f98d7d4eca7b23" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/a625e50598e12171d3f60b1149eb530690c43474", - "reference": "a625e50598e12171d3f60b1149eb530690c43474", + "url": "https://api.github.com/repos/composer/composer/zipball/291942978f39435cf904d33739f98d7d4eca7b23", + "reference": "291942978f39435cf904d33739f98d7d4eca7b23", "shasum": "" }, "require": { "composer/ca-bundle": "^1.0", - "composer/class-map-generator": "^1.0", + "composer/class-map-generator": "^1.3.3", "composer/metadata-minifier": "^1.0", "composer/pcre": "^2.1 || ^3.1", - "composer/semver": "^3.2.5", + "composer/semver": "^3.3", "composer/spdx-licenses": "^1.5.7", "composer/xdebug-handler": "^2.0.2 || ^3.0.3", "justinrainbow/json-schema": "^5.2.11", @@ -193,11 +193,11 @@ "symfony/process": "^5.4 || ^6.0 || ^7" }, "require-dev": { - "phpstan/phpstan": "^1.9.3", - "phpstan/phpstan-deprecation-rules": "^1", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-strict-rules": "^1", - "phpstan/phpstan-symfony": "^1.2.10", + "phpstan/phpstan": "^1.11.0", + "phpstan/phpstan-deprecation-rules": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.4.0", + "phpstan/phpstan-strict-rules": "^1.6.0", + "phpstan/phpstan-symfony": "^1.4.0", "symfony/phpunit-bridge": "^6.4.1 || ^7.0.1" }, "suggest": { @@ -251,7 +251,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.7.4" + "source": "https://github.com/composer/composer/tree/2.7.7" }, "funding": [ { @@ -267,7 +267,7 @@ "type": "tidelift" } ], - "time": "2024-04-22T19:17:03+00:00" + "time": "2024-06-10T20:11:12+00:00" }, { "name": "composer/metadata-minifier", @@ -340,16 +340,16 @@ }, { "name": "composer/pcre", - "version": "3.1.3", + "version": "3.1.4", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + "reference": "04229f163664973f68f38f6f73d917799168ef24" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", - "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "url": "https://api.github.com/repos/composer/pcre/zipball/04229f163664973f68f38f6f73d917799168ef24", + "reference": "04229f163664973f68f38f6f73d917799168ef24", "shasum": "" }, "require": { @@ -391,7 +391,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.1.3" + "source": "https://github.com/composer/pcre/tree/3.1.4" }, "funding": [ { @@ -407,7 +407,7 @@ "type": "tidelift" } ], - "time": "2024-03-19T10:26:25+00:00" + "time": "2024-05-27T13:40:54+00:00" }, { "name": "composer/semver", @@ -572,16 +572,16 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.4", + "version": "3.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", - "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", "shasum": "" }, "require": { @@ -618,7 +618,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" }, "funding": [ { @@ -634,19 +634,19 @@ "type": "tidelift" } ], - "time": "2024-03-26T18:29:49+00:00" + "time": "2024-05-06T16:37:16+00:00" }, { "name": "justinrainbow/json-schema", "version": "v5.2.13", "source": { "type": "git", - "url": "https://github.com/justinrainbow/json-schema.git", + "url": "https://github.com/jsonrainbow/json-schema.git", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fbbe7e5d79f618997bc3332a6f49246036c45793", "reference": "fbbe7e5d79f618997bc3332a6f49246036c45793", "shasum": "" }, @@ -701,8 +701,8 @@ "schema" ], "support": { - "issues": "https://github.com/justinrainbow/json-schema/issues", - "source": "https://github.com/justinrainbow/json-schema/tree/v5.2.13" + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/v5.2.13" }, "time": "2023-09-26T02:20:38+00:00" }, @@ -806,16 +806,16 @@ }, { "name": "react/promise", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/reactphp/promise.git", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c" + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", - "reference": "e563d55d1641de1dea9f5e84f3cccc66d2bfe02c", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", "shasum": "" }, "require": { @@ -867,7 +867,7 @@ ], "support": { "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.1.0" + "source": "https://github.com/reactphp/promise/tree/v3.2.0" }, "funding": [ { @@ -875,7 +875,7 @@ "type": "open_collective" } ], - "time": "2023-11-16T16:21:57+00:00" + "time": "2024-05-24T10:39:05+00:00" }, { "name": "seld/jsonlint", @@ -1052,16 +1052,16 @@ }, { "name": "symfony/console", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e" + "reference": "aa73115c0c24220b523625bfcfa655d7d73662dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", - "reference": "39f75d9d73d0c11952fdcecf4877b4d0f62a8f6e", + "url": "https://api.github.com/repos/symfony/console/zipball/aa73115c0c24220b523625bfcfa655d7d73662dd", + "reference": "aa73115c0c24220b523625bfcfa655d7d73662dd", "shasum": "" }, "require": { @@ -1131,7 +1131,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.36" + "source": "https://github.com/symfony/console/tree/v5.4.40" }, "funding": [ { @@ -1147,7 +1147,7 @@ "type": "tidelift" } ], - "time": "2024-02-20T16:33:57+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/deprecation-contracts", @@ -1218,16 +1218,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.4.38", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "899330a01056077271e2f614c7b28b0379a671eb" + "reference": "26dd9912df6940810ea00f8f53ad48d6a3424995" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/899330a01056077271e2f614c7b28b0379a671eb", - "reference": "899330a01056077271e2f614c7b28b0379a671eb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/26dd9912df6940810ea00f8f53ad48d6a3424995", + "reference": "26dd9912df6940810ea00f8f53ad48d6a3424995", "shasum": "" }, "require": { @@ -1236,6 +1236,9 @@ "symfony/polyfill-mbstring": "~1.8", "symfony/polyfill-php80": "^1.16" }, + "require-dev": { + "symfony/process": "^5.4|^6.4" + }, "type": "library", "autoload": { "psr-4": { @@ -1262,7 +1265,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.38" + "source": "https://github.com/symfony/filesystem/tree/v5.4.40" }, "funding": [ { @@ -1278,20 +1281,20 @@ "type": "tidelift" } ], - "time": "2024-03-21T08:05:07+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/finder", - "version": "v5.4.35", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435" + "reference": "f51cff4687547641c7d8180d74932ab40b2205ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/abe6d6f77d9465fed3cd2d029b29d03b56b56435", - "reference": "abe6d6f77d9465fed3cd2d029b29d03b56b56435", + "url": "https://api.github.com/repos/symfony/finder/zipball/f51cff4687547641c7d8180d74932ab40b2205ce", + "reference": "f51cff4687547641c7d8180d74932ab40b2205ce", "shasum": "" }, "require": { @@ -1325,7 +1328,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.35" + "source": "https://github.com/symfony/finder/tree/v5.4.40" }, "funding": [ { @@ -1341,7 +1344,7 @@ "type": "tidelift" } ], - "time": "2024-01-23T13:51:25+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/polyfill-ctype", @@ -1895,16 +1898,16 @@ }, { "name": "symfony/process", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975" + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4fdf34004f149cc20b2f51d7d119aa500caad975", - "reference": "4fdf34004f149cc20b2f51d7d119aa500caad975", + "url": "https://api.github.com/repos/symfony/process/zipball/deedcb3bb4669cae2148bc920eafd2b16dc7c046", + "reference": "deedcb3bb4669cae2148bc920eafd2b16dc7c046", "shasum": "" }, "require": { @@ -1937,7 +1940,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.4.36" + "source": "https://github.com/symfony/process/tree/v5.4.40" }, "funding": [ { @@ -1953,7 +1956,7 @@ "type": "tidelift" } ], - "time": "2024-02-12T15:49:53+00:00" + "time": "2024-05-31T14:33:22+00:00" }, { "name": "symfony/service-contracts", @@ -2040,16 +2043,16 @@ }, { "name": "symfony/string", - "version": "v5.4.36", + "version": "v5.4.40", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b" + "reference": "142877285aa974a6f7685e292ab5ba9aae86b143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4e232c83622bd8cd32b794216aa29d0d266d353b", - "reference": "4e232c83622bd8cd32b794216aa29d0d266d353b", + "url": "https://api.github.com/repos/symfony/string/zipball/142877285aa974a6f7685e292ab5ba9aae86b143", + "reference": "142877285aa974a6f7685e292ab5ba9aae86b143", "shasum": "" }, "require": { @@ -2106,7 +2109,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.36" + "source": "https://github.com/symfony/string/tree/v5.4.40" }, "funding": [ { @@ -2122,7 +2125,7 @@ "type": "tidelift" } ], - "time": "2024-02-01T08:49:30+00:00" + "time": "2024-05-31T14:33:22+00:00" } ], "packages-dev": [ diff --git a/composer/helpers/v2/src/UpdateChecker.php b/composer/helpers/v2/src/UpdateChecker.php index 462b1a80c07..cb849941ccf 100644 --- a/composer/helpers/v2/src/UpdateChecker.php +++ b/composer/helpers/v2/src/UpdateChecker.php @@ -8,15 +8,13 @@ use Composer\Factory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; -use Composer\Package\Link; use Composer\Package\PackageInterface; -use Composer\Package\Version\VersionParser; final class UpdateChecker { public static function getLatestResolvableVersion(array $args): ?string { - [$workingDirectory, $dependencyName, $gitCredentials, $registryCredentials, $latestAllowableVersion] = $args; + [$workingDirectory, $dependencyName, $gitCredentials, $registryCredentials] = $args; $httpBasicCredentials = []; @@ -50,22 +48,10 @@ public static function getLatestResolvableVersion(array $args): ?string $io->loadConfiguration($config); } - $package = $composer->getPackage(); - - $versionParser = new VersionParser(); - - $constraint = $versionParser->parseConstraints($latestAllowableVersion); // your version constraint - $packageLink = new Link($package->getName(), $dependencyName, $constraint); - - $requires = $package->getRequires(); - $requires[$dependencyName] = $packageLink; - - $package->setRequires($requires); - $install = new Installer( $io, $config, - $package, // @phpstan-ignore-line + $composer->getPackage(), // @phpstan-ignore-line $composer->getDownloadManager(), $composer->getRepositoryManager(), $composer->getLocker(), diff --git a/composer/lib/dependabot/composer/update_checker/version_resolver.rb b/composer/lib/dependabot/composer/update_checker/version_resolver.rb index 501e3522eaa..31c3d16a3d9 100644 --- a/composer/lib/dependabot/composer/update_checker/version_resolver.rb +++ b/composer/lib/dependabot/composer/update_checker/version_resolver.rb @@ -148,8 +148,7 @@ def run_update_checker Dir.pwd, dependency.name.downcase, git_credentials, - registry_credentials, - @latest_allowable_version.to_s + registry_credentials ] ) end @@ -239,9 +238,9 @@ def updated_version_requirement_string # If the original requirement is just a stability flag we append that # flag to the requirement - return "<=#{latest_allowable_version}#{lower_bound.strip}" if lower_bound.strip.start_with?("@") + return "==#{latest_allowable_version}#{lower_bound.strip}" if lower_bound.strip.start_with?("@") - lower_bound + ", <= #{latest_allowable_version}" + lower_bound + ", == #{latest_allowable_version}" end # rubocop:enable Metrics/PerceivedComplexity # rubocop:enable Metrics/AbcSize diff --git a/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb b/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb index 564f8188799..483c2d60843 100644 --- a/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb +++ b/composer/spec/dependabot/composer/update_checker/version_resolver_spec.rb @@ -54,12 +54,69 @@ end end - context "with a library using a >= PHP constraint" do + # version constraint: >= 2.0.4, == 3.3.2 (debugging logs) + context "when version constraint is set as requirement" do let(:project_name) { "php_specified_in_library" } let(:dependency_name) { "phpdocumentor/reflection-docblock" } + let(:latest_allowable_version) { Gem::Version.new("3.3.2") } let(:dependency_version) { "2.0.4" } let(:string_req) { "2.0.4" } - let(:latest_allowable_version) { Gem::Version.new("3.3.2") } + + it { is_expected.to eq(Dependabot::Composer::Version.new("3.3.2")) } + end + + # combined constraint: >= 2.0.4, == 3.0.0 (debugging logs) + # But latest allowable version is 3.0.0 + context "when version constraint is set as requirement, but pushing to the latest_allowable_version 3.0.0 now" do + let(:project_name) { "php_specified_in_library" } + let(:dependency_name) { "phpdocumentor/reflection-docblock" } + let(:latest_allowable_version) { Gem::Version.new("3.0.0") } + let(:dependency_version) { "2.0.4" } + let(:string_req) { "2.0.4" } + + it { is_expected.to eq(Dependabot::Composer::Version.new("3.0.0")) } + end + + # combined constraint: >= 1.0.1, == 1.1.0 (debugging logs) + context "when version constraint is set as dev-requirement" do + let(:project_name) { "php_specified_in_library" } + let(:dependency_name) { "monolog/monolog" } + let(:latest_allowable_version) { Gem::Version.new("1.1.0") } + let(:dependency_version) { "1.0.1" } + let(:string_req) { "1.0.1" } + + it { is_expected.to eq(Dependabot::Composer::Version.new("1.1.0")) } + end + + # combined constraint: >= 2.0.4 (debugging logs) + context "when latest_allowable_version is not set" do + let(:project_name) { "php_specified_in_library" } + let(:dependency_name) { "phpdocumentor/reflection-docblock" } + let(:latest_allowable_version) { nil } + let(:dependency_version) { "2.0.4" } + let(:string_req) { "2.0.4" } + + it { is_expected.to eq(Dependabot::Composer::Version.new("3.3.2")) } + end + + # combined constraint: ==3.0.0 (debugging logs) not set to the latest in the registry. + context "when version constraint is not set (in existing composer.json)" do + let(:project_name) { "php_specified_in_library" } + let(:dependency_name) { "phpdocumentor/reflection-docblock" } + let(:latest_allowable_version) { Gem::Version.new("3.0.0") } + let(:dependency_version) { nil } + let(:string_req) { nil } + + it { is_expected.to eq(Dependabot::Composer::Version.new("3.0.0")) } + end + + # combined constraint: >= 0 + context "when both version constraint and latest_allowable_version are not set" do + let(:project_name) { "php_specified_in_library" } + let(:dependency_name) { "phpdocumentor/reflection-docblock" } + let(:latest_allowable_version) { nil } + let(:dependency_version) { nil } + let(:string_req) { nil } it { is_expected.to eq(Dependabot::Composer::Version.new("3.3.2")) } end @@ -73,12 +130,11 @@ it { is_expected.to eq(Dependabot::Composer::Version.new("3.3.2")) } - context "when the minimum version is invalid" do + context "when the minimum version is invalid, 3.3.2 is less than 4.2.0" do let(:dependency_version) { "4.2.0" } let(:string_req) { "4.2.0" } - let(:latest_allowable_version) { Gem::Version.new("4.3.1") } - it { is_expected.to be >= Dependabot::Composer::Version.new("4.3.1") } + it { is_expected.to be_nil } end end @@ -107,11 +163,11 @@ context "with a dependency that's provided by another dep" do let(:project_name) { "provided_dependency" } let(:string_req) { "^1.0" } - let(:latest_allowable_version) { Gem::Version.new("1.0.0") } + let(:latest_allowable_version) { Gem::Version.new("6.0.0") } let(:dependency_name) { "php-http/client-implementation" } let(:dependency_version) { nil } - it { is_expected.to eq(Dependabot::Composer::Version.new("1.0")) } + it { is_expected.to be_nil } end context "with a dependency that uses a stability flag" do diff --git a/composer/spec/dependabot/composer/update_checker_spec.rb b/composer/spec/dependabot/composer/update_checker_spec.rb index ac5934bdef4..d395ab23cde 100644 --- a/composer/spec/dependabot/composer/update_checker_spec.rb +++ b/composer/spec/dependabot/composer/update_checker_spec.rb @@ -194,7 +194,6 @@ describe "#latest_resolvable_version" do subject(:latest_resolvable_version) { checker.latest_resolvable_version } - # setting the latest allowable version to 1.22.0 before do allow(checker).to receive(:latest_version_from_registry) .and_return(Gem::Version.new("1.22.0")) @@ -234,7 +233,6 @@ }] end - # setting the latest allowable version to 4.3.0 before do allow(checker).to receive(:latest_version_from_registry) .and_return(Gem::Version.new("4.3.0")) @@ -256,7 +254,6 @@ }] end - # setting the latest allowable version to 5.2.45 before do allow(checker).to receive(:latest_version_from_registry) .and_return(Gem::Version.new("5.2.45")) @@ -299,7 +296,6 @@ }] end - # setting the latest allowable version to 5.2.45 before do allow(checker).to receive(:latest_version_from_registry) .and_return(Gem::Version.new("5.2.45")) @@ -490,7 +486,7 @@ # v1 url doesn't always return 404 for missing packages stub_request(:get, v1_metadata_url).to_return(status: 200, body: '{"error":{"code":404,"message":"Not Found"}}') allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("2.4.2")) + .and_return(Gem::Version.new("2.4.1")) end it "is between 2.0.0 and 3.0.0" do @@ -513,7 +509,6 @@ end let(:ignored_versions) { [">= 2.8.0"] } - # set latest allowable version from registry to 2.1.7 before do allow(checker).to receive(:latest_version_from_registry) .and_return(Gem::Version.new("2.1.7")) @@ -586,16 +581,12 @@ context "when there is no lockfile" do let(:project_name) { "version_conflict_on_update_without_lockfile" } - it "raises a helpful error" do - expect { latest_resolvable_version }.to raise_error(Dependabot::DependencyFileNotResolvable) - end + it { is_expected.to be_nil } context "when the conflict comes from a loose PHP version" do let(:project_name) { "version_conflict_library" } - it "raises a helpful error" do - expect { latest_resolvable_version }.to raise_error(Dependabot::DependencyFileNotResolvable) - end + it { is_expected.to be_nil } end end end @@ -686,12 +677,6 @@ }] end - # set latest allowable version from registry to 1.3.0 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("1.3.0")) - end - # Alternatively, this could raise an error. Either behaviour would be # fine - the below is just what we get with Composer at the moment # because we disabled downloading the files in @@ -765,13 +750,13 @@ end before do + allow(checker).to receive(:latest_version_from_registry) + .and_return(Gem::Version.new("3.0.2")) stub_request(:get, "https://wpackagist.org/packages.json") .to_return( status: 200, body: fixture("wpackagist_response.json") ) - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("3.0.2")) end it { is_expected.to be >= Gem::Version.new("3.0.2") } @@ -790,13 +775,7 @@ }] end - # set latest allowable version from registry to 5.2.7 - before do - allow(checker).to receive(:latest_version_from_registry) - .and_return(Gem::Version.new("5.2.7")) - end - - it { is_expected.to be >= Gem::Version.new("5.2.7") } + it { is_expected.to be_nil } end context "when a sub-dependency would block the update" do @@ -812,7 +791,6 @@ }] end - # setting the latest allowable version to 5.6.23 before do allow(checker).to receive(:latest_version_from_registry) .and_return(Gem::Version.new("5.6.23")) diff --git a/composer/spec/fixtures/projects/php_specified_in_library/composer.json b/composer/spec/fixtures/projects/php_specified_in_library/composer.json index f7ad28affdf..65a83aa7187 100644 --- a/composer/spec/fixtures/projects/php_specified_in_library/composer.json +++ b/composer/spec/fixtures/projects/php_specified_in_library/composer.json @@ -5,5 +5,8 @@ "php": ">=5.6.0", "phpdocumentor/reflection-docblock": "2.0.4", "illuminate/support": "^5.2.0" + }, + "require-dev": { + "monolog/monolog": "1.0.1" } } diff --git a/devcontainers/.rubocop.yml b/devcontainers/.rubocop.yml index b8168698e85..e5270530f5a 100644 --- a/devcontainers/.rubocop.yml +++ b/devcontainers/.rubocop.yml @@ -1,4 +1,4 @@ inherit_from: ../.rubocop.yml -Sorbet/TrueSigil: +Sorbet/StrictSigil: Enabled: true diff --git a/docker/.rubocop.yml b/docker/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/docker/.rubocop.yml +++ b/docker/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/elm/.rubocop.yml b/elm/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/elm/.rubocop.yml +++ b/elm/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/github_actions/.rubocop.yml b/github_actions/.rubocop.yml index b8168698e85..e5270530f5a 100644 --- a/github_actions/.rubocop.yml +++ b/github_actions/.rubocop.yml @@ -1,4 +1,4 @@ inherit_from: ../.rubocop.yml -Sorbet/TrueSigil: +Sorbet/StrictSigil: Enabled: true diff --git a/github_actions/lib/dependabot/github_actions/file_fetcher.rb b/github_actions/lib/dependabot/github_actions/file_fetcher.rb index c00e16e8c1b..81000266dec 100644 --- a/github_actions/lib/dependabot/github_actions/file_fetcher.rb +++ b/github_actions/lib/dependabot/github_actions/file_fetcher.rb @@ -36,7 +36,7 @@ def self.required_files_message end def initialize(source:, credentials:, repo_contents_path: nil, options: {}) @workflow_files = T.let([], T::Array[DependencyFile]) - super(source: source, credentials: credentials, repo_contents_path: repo_contents_path, options: options) + super end sig { override.returns(T::Array[DependencyFile]) } diff --git a/github_actions/lib/dependabot/github_actions/update_checker.rb b/github_actions/lib/dependabot/github_actions/update_checker.rb index b384217d5d2..56daa9160ed 100644 --- a/github_actions/lib/dependabot/github_actions/update_checker.rb +++ b/github_actions/lib/dependabot/github_actions/update_checker.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" @@ -15,29 +15,41 @@ module GithubActions 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 GitHub Actions. latest_version end + sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) } def latest_resolvable_version_with_no_unlock # No concept of "unlocking" for GitHub Actions (since no lockfile) dependency.version end + sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_security_fix_version - @lowest_security_fix_version ||= fetch_lowest_security_fix_version + @lowest_security_fix_version ||= T.let( + fetch_lowest_security_fix_version, + T.nilable(Dependabot::Version) + ) end + sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_resolvable_security_fix_version # Resolvability isn't an issue for GitHub Actions. lowest_security_fix_version end + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements dependency.requirements.map do |req| source = req[:source] @@ -61,21 +73,25 @@ def updated_requirements private + sig { returns(T::Array[Dependabot::SecurityAdvisory]) } def active_advisories security_advisories.select do |advisory| advisory.vulnerable?(version_class.new(git_commit_checker.most_specific_tag_equivalent_to_pinned_ref)) end end + sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? # Full unlock checks aren't relevant for GitHub Actions false end + sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock raise NotImplementedError end + sig { returns(T.nilable(T.any(Dependabot::Version, String))) } def fetch_latest_version # TODO: Support Docker sources return unless git_dependency? @@ -83,20 +99,21 @@ def fetch_latest_version fetch_latest_version_for_git_dependency end + sig { returns(T.nilable(T.any(Dependabot::Version, String))) } def fetch_latest_version_for_git_dependency return current_commit unless git_commit_checker.pinned? # If the dependency is pinned to a tag that looks like a version then # we want to update that tag. if git_commit_checker.pinned_ref_looks_like_version? && latest_version_tag - latest_version = latest_version_tag.fetch(:version) + latest_version = latest_version_tag&.fetch(:version) return current_version if shortened_semver_eq?(dependency.version, latest_version.to_s) return latest_version end if git_commit_checker.pinned_ref_looks_like_commit_sha? && latest_version_tag - latest_version = latest_version_tag.fetch(:version) + latest_version = latest_version_tag&.fetch(:version) return latest_commit_for_pinned_ref unless git_commit_checker.local_tag_for_pinned_sha return latest_version @@ -107,6 +124,7 @@ def fetch_latest_version_for_git_dependency nil end + sig { returns(T.nilable(Dependabot::Version)) } def fetch_lowest_security_fix_version # TODO: Support Docker sources return unless git_dependency? @@ -114,23 +132,34 @@ def fetch_lowest_security_fix_version fetch_lowest_security_fix_version_for_git_dependency end + sig { returns(T.nilable(Dependabot::Version)) } def fetch_lowest_security_fix_version_for_git_dependency - lowest_security_fix_version_tag.fetch(:version) + lowest_security_fix_version_tag&.fetch(:version) end + sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } def lowest_security_fix_version_tag - @lowest_security_fix_version_tag ||= begin - tags_matching_precision = git_commit_checker.local_tags_for_allowed_versions_matching_existing_precision - lowest_fixed_version = find_lowest_secure_version(tags_matching_precision) - if lowest_fixed_version - lowest_fixed_version - else - tags = git_commit_checker.local_tags_for_allowed_versions - find_lowest_secure_version(tags) - end - end + @lowest_security_fix_version_tag ||= T.let( + begin + tags_matching_precision = git_commit_checker.local_tags_for_allowed_versions_matching_existing_precision + lowest_fixed_version = find_lowest_secure_version(tags_matching_precision) + if lowest_fixed_version + lowest_fixed_version + else + tags = git_commit_checker.local_tags_for_allowed_versions + find_lowest_secure_version(tags) + end + end, + T.nilable(T::Hash[Symbol, String]) + ) end + sig do + params( + tags: T::Array[T::Hash[Symbol, T.untyped]] + ) + .returns(T.nilable(T::Hash[Symbol, T.untyped])) + end def find_lowest_secure_version(tags) relevant_tags = Dependabot::UpdateCheckers::VersionFilters.filter_vulnerable_versions(tags, security_advisories) relevant_tags = filter_lower_tags(relevant_tags) @@ -138,40 +167,54 @@ def find_lowest_secure_version(tags) relevant_tags.min_by { |tag| tag.fetch(:version) } end + sig { returns(T.nilable(String)) } def latest_commit_for_pinned_ref - @latest_commit_for_pinned_ref ||= begin - head_commit_for_ref_sha = git_commit_checker.head_commit_for_pinned_ref - if head_commit_for_ref_sha - head_commit_for_ref_sha - else - url = git_commit_checker.dependency_source_details[:url] - source = T.must(Source.from_url(url)) - - SharedHelpers.in_a_temporary_directory(File.dirname(source.repo)) do |temp_dir| - repo_contents_path = File.join(temp_dir, File.basename(source.repo)) - - SharedHelpers.run_shell_command("git clone --no-recurse-submodules #{url} #{repo_contents_path}") - - Dir.chdir(repo_contents_path) do - ref_branch = find_container_branch(git_commit_checker.dependency_source_details[:ref]) - git_commit_checker.head_commit_for_local_branch(ref_branch) if ref_branch + @latest_commit_for_pinned_ref ||= T.let( + begin + head_commit_for_ref_sha = git_commit_checker.head_commit_for_pinned_ref + if head_commit_for_ref_sha + head_commit_for_ref_sha + else + url = git_commit_checker.dependency_source_details&.fetch(:url) + source = T.must(Source.from_url(url)) + + SharedHelpers.in_a_temporary_directory(File.dirname(source.repo)) do |temp_dir| + repo_contents_path = File.join(temp_dir, File.basename(source.repo)) + + SharedHelpers.run_shell_command("git clone --no-recurse-submodules #{url} #{repo_contents_path}") + + Dir.chdir(repo_contents_path) do + ref_branch = find_container_branch(git_commit_checker.dependency_source_details&.fetch(:ref)) + git_commit_checker.head_commit_for_local_branch(ref_branch) if ref_branch + end end end - end - end + end, + T.nilable(String) + ) end + sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } def latest_version_tag - @latest_version_tag ||= begin - return git_commit_checker.local_tag_for_latest_version if dependency.version.nil? + @latest_version_tag ||= T.let( + begin + return git_commit_checker.local_tag_for_latest_version if dependency.version.nil? - ref = git_commit_checker.local_ref_for_latest_version_matching_existing_precision - return ref if ref && ref.fetch(:version) > current_version + ref = git_commit_checker.local_ref_for_latest_version_matching_existing_precision + return ref if ref && ref.fetch(:version) > current_version - git_commit_checker.local_ref_for_latest_version_lower_precision - end + git_commit_checker.local_ref_for_latest_version_lower_precision + end, + T.nilable(T::Hash[Symbol, T.untyped]) + ) end + sig do + params( + tags_array: T::Array[T::Hash[Symbol, T.untyped]] + ) + .returns(T::Array[T::Hash[Symbol, T.untyped]]) + end def filter_lower_tags(tags_array) return tags_array unless current_version @@ -179,6 +222,7 @@ def filter_lower_tags(tags_array) .select { |tag| tag.fetch(:version) > current_version } end + sig { params(source: T.nilable(T::Hash[Symbol, String])).returns(T.nilable(String)) } def updated_ref(source) # TODO: Support Docker sources return unless git_dependency? @@ -206,6 +250,7 @@ def updated_ref(source) nil end + sig { returns(T.nilable(String)) } def latest_commit_sha new_tag = latest_version_tag return unless new_tag @@ -217,20 +262,30 @@ def latest_commit_sha end end + sig { returns(T.nilable(String)) } def current_commit git_commit_checker.head_commit_for_current_branch end + sig { returns(T::Boolean) } def git_dependency? git_commit_checker.git_dependency? end + sig { returns(Dependabot::GitCommitChecker) } def git_commit_checker - @git_commit_checker ||= git_commit_checker_for(nil) + @git_commit_checker ||= T.let( + git_commit_checker_for(nil), + T.nilable(Dependabot::GitCommitChecker) + ) end + sig { params(source: T.nilable(T::Hash[Symbol, String])).returns(Dependabot::GitCommitChecker) } def git_commit_checker_for(source) - @git_commit_checkers ||= {} + @git_commit_checkers ||= T.let( + {}, + T.nilable(T::Hash[T.nilable(T::Hash[Symbol, String]), Dependabot::GitCommitChecker]) + ) @git_commit_checkers[source] ||= Dependabot::GitCommitChecker.new( dependency: dependency, @@ -242,6 +297,7 @@ def git_commit_checker_for(source) ) end + sig { params(base: T.nilable(String), other: String).returns(T::Boolean) } def shortened_semver_eq?(base, other) return false unless base @@ -252,6 +308,7 @@ def shortened_semver_eq?(base, other) other_split[0..base_split.length - 1] == base_split end + sig { params(sha: String).returns(T.nilable(String)) } def find_container_branch(sha) branches_including_ref = SharedHelpers.run_shell_command( "git branch --remotes --contains #{sha}", diff --git a/github_actions/spec/dependabot/github_actions/update_checker_spec.rb b/github_actions/spec/dependabot/github_actions/update_checker_spec.rb index b5fa53d32bd..f89992c4115 100644 --- a/github_actions/spec/dependabot/github_actions/update_checker_spec.rb +++ b/github_actions/spec/dependabot/github_actions/update_checker_spec.rb @@ -606,9 +606,13 @@ describe "#lowest_resolvable_security_fix_version" do subject(:lowest_resolvable_security_fix_version) { checker.lowest_resolvable_security_fix_version } - before { allow(checker).to receive(:lowest_security_fix_version).and_return("delegate") } + before do + allow(checker) + .to receive(:lowest_security_fix_version) + .and_return(Dependabot::GithubActions::Version.new("2.0.0")) + end - it { is_expected.to eq("delegate") } + it { is_expected.to eq(Dependabot::GithubActions::Version.new("2.0.0")) } end describe "#updated_requirements" do diff --git a/go_modules/.rubocop.yml b/go_modules/.rubocop.yml index fc2019d46a3..e5270530f5a 100644 --- a/go_modules/.rubocop.yml +++ b/go_modules/.rubocop.yml @@ -1 +1,4 @@ inherit_from: ../.rubocop.yml + +Sorbet/StrictSigil: + Enabled: true diff --git a/go_modules/Dockerfile b/go_modules/Dockerfile index 2a313b8253f..5e6d3a59b15 100644 --- a/go_modules/Dockerfile +++ b/go_modules/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/golang:1.22.4-bookworm as go +FROM docker.io/library/golang:1.22.5-bookworm as go FROM ghcr.io/dependabot/dependabot-updater-core ARG TARGETARCH @@ -27,3 +27,10 @@ ENV GOTOOLCHAIN="go1.20.10" # This pre-installs go 1.20 so that each job doesn't have to do it. RUN go version ENV GO_LEGACY=$GOTOOLCHAIN + +# Enable automatic pulling of files stored with Git LFS. +# This is crucial for the Go toolchain to correctly compute module hashes in repositories +# with LFS committed files. +# This is relevant only for Go Modules embedding (with //go:embed) large files and does not +# affect repositories that do not commit files to LFS. +ENV GIT_LFS_SKIP_SMUDGE=1 diff --git a/go_modules/helpers/go.mod b/go_modules/helpers/go.mod index 56db04ad0be..9622157e93c 100644 --- a/go_modules/helpers/go.mod +++ b/go_modules/helpers/go.mod @@ -2,4 +2,7 @@ module github.com/dependabot/dependabot-core/go_modules/helpers go 1.20 -require github.com/Masterminds/vcs v1.13.3 +require ( + github.com/Masterminds/vcs v1.13.3 + golang.org/x/mod v0.19.0 +) diff --git a/go_modules/helpers/go.sum b/go_modules/helpers/go.sum index 89365af4e39..ca30242305f 100644 --- a/go_modules/helpers/go.sum +++ b/go_modules/helpers/go.sum @@ -1,2 +1,4 @@ github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= diff --git a/go_modules/helpers/version_test.go b/go_modules/helpers/version_test.go new file mode 100644 index 00000000000..74031f9aacc --- /dev/null +++ b/go_modules/helpers/version_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "encoding/json" + "golang.org/x/mod/semver" + "os" + "reflect" + "testing" +) + +// TestVersionComparison verifies that the ordered version fixture is sorted correctly. +func TestVersionComparison(t *testing.T) { + data, err := os.ReadFile("../spec/fixtures/ordered_versions.json") + if err != nil { + t.Fatalf("failed to read file: %v", err) + } + var expected []string + if err = json.Unmarshal(data, &expected); err != nil { + t.Fatalf("failed to unmarshal json: %v", err) + } + + actual := make([]string, len(expected)) + copy(actual, expected) + semver.Sort(actual) + + // The sorted order should equal the original order in the file. + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("got %v", actual) + } +} diff --git a/go_modules/lib/dependabot/go_modules/file_updater.rb b/go_modules/lib/dependabot/go_modules/file_updater.rb index f3e15e8e72d..514bcae499c 100644 --- a/go_modules/lib/dependabot/go_modules/file_updater.rb +++ b/go_modules/lib/dependabot/go_modules/file_updater.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/shared_helpers" require "dependabot/file_updaters" require "dependabot/file_updaters/base" @@ -9,16 +11,29 @@ module Dependabot module GoModules class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + require_relative "file_updater/go_mod_updater" - def initialize(dependencies:, dependency_files:, repo_contents_path: nil, - credentials:, options: {}) + sig do + override + .params( + dependencies: T::Array[Dependabot::Dependency], + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Dependabot::Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[Symbol, T.untyped] + ) + .void + end + def initialize(dependencies:, dependency_files:, credentials:, repo_contents_path: nil, options: {}) super - @goprivate = options.fetch(:goprivate, "*") + @goprivate = T.let(options.fetch(:goprivate, "*"), String) use_repo_contents_stub if repo_contents_path.nil? end + sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ /^go\.mod$/, @@ -26,25 +41,26 @@ def self.updated_files_regex ] end + sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files updated_files = [] - if go_mod && dependency_changed?(go_mod) + if go_mod && dependency_changed?(T.must(go_mod)) updated_files << updated_file( - file: go_mod, - content: file_updater.updated_go_mod_content + file: T.must(go_mod), + content: T.must(file_updater.updated_go_mod_content) ) - if go_sum && go_sum.content != file_updater.updated_go_sum_content + if go_sum && T.must(go_sum).content != file_updater.updated_go_sum_content updated_files << updated_file( - file: go_sum, - content: file_updater.updated_go_sum_content + file: T.must(go_sum), + content: T.must(file_updater.updated_go_sum_content) ) end - vendor_updater.updated_vendor_cache_files(base_directory: directory) + vendor_updater.updated_files(base_directory: T.must(directory)) .each do |file| updated_files << file end @@ -57,19 +73,22 @@ def updated_dependency_files private + sig { params(go_mod: Dependabot::DependencyFile).returns(T::Boolean) } def dependency_changed?(go_mod) # file_changed? only checks for changed requirements. Need to check for indirect dep version changes too. file_changed?(go_mod) || dependencies.any? { |dep| dep.previous_version != dep.version } end + sig { override.void } def check_required_files return if go_mod raise "No go.mod!" end + sig { returns(String) } def use_repo_contents_stub - @repo_contents_stub = true + @repo_contents_stub = T.let(true, T.nilable(T::Boolean)) @repo_contents_path = Dir.mktmpdir Dir.chdir(@repo_contents_path) do @@ -92,22 +111,27 @@ def use_repo_contents_stub end end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def go_mod - @go_mod ||= get_original_file("go.mod") + @go_mod ||= T.let(get_original_file("go.mod"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def go_sum - @go_sum ||= get_original_file("go.sum") + @go_sum ||= T.let(get_original_file("go.sum"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(String)) } def directory dependency_files.first&.directory end + sig { returns(String) } def vendor_dir File.join(repo_contents_path, directory, "vendor") end + sig { returns(Dependabot::FileUpdaters::VendorUpdater) } def vendor_updater Dependabot::FileUpdaters::VendorUpdater.new( repo_contents_path: repo_contents_path, @@ -115,22 +139,27 @@ def vendor_updater ) end + sig { returns(GoModUpdater) } def file_updater - @file_updater ||= + @file_updater ||= T.let( GoModUpdater.new( dependencies: dependencies, dependency_files: dependency_files, credentials: credentials, repo_contents_path: repo_contents_path, - directory: directory, + directory: T.must(directory), options: { tidy: tidy?, vendor: vendor?, goprivate: @goprivate } - ) + ), + T.nilable(Dependabot::GoModules::FileUpdater::GoModUpdater) + ) end + sig { returns(T::Boolean) } def tidy? !@repo_contents_stub end + sig { returns(T::Boolean) } def vendor? File.exist?(File.join(vendor_dir, "modules.txt")) end diff --git a/go_modules/lib/dependabot/go_modules/metadata_finder.rb b/go_modules/lib/dependabot/go_modules/metadata_finder.rb index e5e007a8d70..c848e57a7f7 100644 --- a/go_modules/lib/dependabot/go_modules/metadata_finder.rb +++ b/go_modules/lib/dependabot/go_modules/metadata_finder.rb @@ -1,4 +1,4 @@ -# typed: strict +# typed: strong # frozen_string_literal: true require "sorbet-runtime" diff --git a/go_modules/lib/dependabot/go_modules/native_helpers.rb b/go_modules/lib/dependabot/go_modules/native_helpers.rb index fd955f37513..dbd08d3e523 100644 --- a/go_modules/lib/dependabot/go_modules/native_helpers.rb +++ b/go_modules/lib/dependabot/go_modules/native_helpers.rb @@ -1,18 +1,25 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module GoModules module NativeHelpers + extend T::Sig + + sig { returns(String) } def self.helper_path clean_path(File.join(native_helpers_root, "go_modules/bin/helper")) end + sig { returns(String) } def self.native_helpers_root default_path = File.join(__dir__, "../../../helpers/install-dir") ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path) end + sig { params(path: String).returns(String) } def self.clean_path(path) Pathname.new(path).cleanpath.to_path end diff --git a/go_modules/lib/dependabot/go_modules/path_converter.rb b/go_modules/lib/dependabot/go_modules/path_converter.rb index 97715287011..42901f57a36 100644 --- a/go_modules/lib/dependabot/go_modules/path_converter.rb +++ b/go_modules/lib/dependabot/go_modules/path_converter.rb @@ -1,19 +1,32 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/go_modules/native_helpers" module Dependabot module GoModules module PathConverter + extend T::Sig + + sig do + params(path: String) + .returns( + T.nilable(String) + ) + end def self.git_url_for_path(path) # Save a query by manually converting golang.org/x names import_path = path.gsub(%r{^golang\.org/x}, "github.com/golang") - SharedHelpers.run_helper_subprocess( - command: NativeHelpers.helper_path, - function: "getVcsRemoteForImport", - args: { import: import_path } + T.cast( + SharedHelpers.run_helper_subprocess( + command: NativeHelpers.helper_path, + function: "getVcsRemoteForImport", + args: { import: import_path } + ), + T.nilable(String) ) end end diff --git a/go_modules/lib/dependabot/go_modules/resolvability_errors.rb b/go_modules/lib/dependabot/go_modules/resolvability_errors.rb index 0c1820df613..398f0a89cdb 100644 --- a/go_modules/lib/dependabot/go_modules/resolvability_errors.rb +++ b/go_modules/lib/dependabot/go_modules/resolvability_errors.rb @@ -1,11 +1,16 @@ -# typed: true +# typed: strong # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module GoModules module ResolvabilityErrors + extend T::Sig + GITHUB_REPO_REGEX = %r{github.com/[^:@]*} + sig { params(message: String, goprivate: T.untyped).void } def self.handle(message, goprivate:) mod_path = message.scan(GITHUB_REPO_REGEX).last unless mod_path && message.include?("If this is a private repository") @@ -17,9 +22,10 @@ def self.handle(message, goprivate:) SharedHelpers.in_a_temporary_directory do File.write("go.mod", "module dummy\n") + mod_path = T.cast(mod_path, String) mod_split = mod_path.split("/") repo_path = if mod_split.size > 3 - mod_split[0..2].join("/") + T.must(mod_split[0..2]).join("/") else mod_path end diff --git a/go_modules/lib/dependabot/go_modules/update_checker.rb b/go_modules/lib/dependabot/go_modules/update_checker.rb index 1a884dd7884..882e52371e1 100644 --- a/go_modules/lib/dependabot/go_modules/update_checker.rb +++ b/go_modules/lib/dependabot/go_modules/update_checker.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/update_checkers" require "dependabot/update_checkers/base" require "dependabot/shared_helpers" @@ -10,8 +12,11 @@ module Dependabot module GoModules class UpdateChecker < Dependabot::UpdateCheckers::Base + extend T::Sig + require_relative "update_checker/latest_version_finder" + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_resolvable_version latest_version_finder.latest_version end @@ -19,25 +24,30 @@ def latest_resolvable_version # This is currently used to short-circuit latest_resolvable_version, # with the assumption that it'll be quicker than checking # resolvability. As this is quite quick in Go anyway, we just alias. + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_version latest_resolvable_version end + sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_resolvable_security_fix_version raise "Dependency not vulnerable!" unless vulnerable? lowest_security_fix_version end + sig { override.returns(T.nilable(Dependabot::Version)) } def lowest_security_fix_version latest_version_finder.lowest_security_fix_version end + sig { override.returns(T.nilable(T.any(String, Dependabot::Version))) } def latest_resolvable_version_with_no_unlock # Irrelevant, since Go modules uses a single dependency file nil end + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements dependency.requirements.map do |req| req.merge(requirement: latest_version) @@ -46,8 +56,9 @@ def updated_requirements private + sig { returns(Dependabot::GoModules::UpdateChecker::LatestVersionFinder) } def latest_version_finder - @latest_version_finder ||= + @latest_version_finder ||= T.let( LatestVersionFinder.new( dependency: dependency, dependency_files: dependency_files, @@ -56,23 +67,29 @@ def latest_version_finder security_advisories: security_advisories, raise_on_ignored: raise_on_ignored, goprivate: options.fetch(:goprivate, "*") - ) + ), + T.nilable(Dependabot::GoModules::UpdateChecker::LatestVersionFinder) + ) end + sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? # Full unlock checks aren't implemented for Go (yet) false end + sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock raise NotImplementedError end # Go only supports semver and semver-compliant pseudo-versions, so it can't be a SHA. + sig { returns(T::Boolean) } def existing_version_is_sha? false end + sig { params(tag: T.nilable(T::Hash[Symbol, String])).returns(T.untyped) } def version_from_tag(tag) # To compare with the current version we either use the commit SHA # (if that's what the parser picked up) or the tag name. @@ -81,6 +98,7 @@ def version_from_tag(tag) tag&.fetch(:tag) end + sig { returns(T::Hash[Symbol, T.untyped]) } def default_source { type: "default", source: dependency.name } end diff --git a/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb b/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb index 8f2f06007f7..05cfb4e116d 100644 --- a/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb +++ b/go_modules/lib/dependabot/go_modules/update_checker/latest_version_finder.rb @@ -1,7 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "excon" +require "sorbet-runtime" require "dependabot/go_modules/update_checker" require "dependabot/update_checkers/version_filters" @@ -9,7 +10,6 @@ require "dependabot/errors" require "dependabot/go_modules/requirement" require "dependabot/go_modules/resolvability_errors" -require "sorbet-runtime" module Dependabot module GoModules @@ -17,26 +17,47 @@ class UpdateChecker class LatestVersionFinder extend T::Sig - RESOLVABILITY_ERROR_REGEXES = [ - # Package url/proxy doesn't include any redirect meta tags - /no go-import meta tags/, - # Package url 404s - /404 Not Found/, - /Repository not found/, - /unrecognized import path/, - /malformed module path/, - # (Private) module could not be fetched - /module .*: git ls-remote .*: exit status 128/m - ].freeze + RESOLVABILITY_ERROR_REGEXES = T.let( + [ + # Package url/proxy doesn't include any redirect meta tags + /no go-import meta tags/, + # Package url 404s + /404 Not Found/, + /Repository not found/, + /unrecognized import path/, + /malformed module path/, + # (Private) module could not be fetched + /module .*: git ls-remote .*: exit status 128/m + ].freeze, + T::Array[Regexp] + ) # The module was retracted from the proxy # OR the version of Go required is greater than what Dependabot supports # OR other go.mod version errors INVALID_VERSION_REGEX = /(go: loading module retractions for)|(version "[^"]+" invalid)/m PSEUDO_VERSION_REGEX = /\b\d{14}-[0-9a-f]{12}$/ - def initialize(dependency:, dependency_files:, credentials:, - ignored_versions:, security_advisories:, raise_on_ignored: false, - goprivate:) + sig do + params( + dependency: Dependabot::Dependency, + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Dependabot::Credential], + ignored_versions: T::Array[String], + security_advisories: T::Array[Dependabot::SecurityAdvisory], + goprivate: String, + raise_on_ignored: T::Boolean + ) + .void + end + def initialize( + dependency:, + dependency_files:, + credentials:, + ignored_versions:, + security_advisories:, + goprivate:, + raise_on_ignored: false + ) @dependency = dependency @dependency_files = dependency_files @credentials = credentials @@ -46,32 +67,45 @@ def initialize(dependency:, dependency_files:, credentials:, @goprivate = goprivate end + sig { returns(T.nilable(Dependabot::Version)) } def latest_version - @latest_version ||= fetch_latest_version + @latest_version ||= T.let(fetch_latest_version, T.nilable(Dependabot::Version)) end + sig { returns(T.nilable(Dependabot::Version)) } def lowest_security_fix_version - @lowest_security_fix_version ||= fetch_lowest_security_fix_version + @lowest_security_fix_version ||= T.let(fetch_lowest_security_fix_version, T.nilable(Dependabot::Version)) end private + sig { returns(Dependabot::Dependency) } attr_reader :dependency + + sig { returns(T::Array[Dependabot::DependencyFile]) } attr_reader :dependency_files + + sig { returns(T::Array[Dependabot::Credential]) } attr_reader :credentials + + sig { returns(T::Array[String]) } attr_reader :ignored_versions + + sig { returns(T::Array[Dependabot::SecurityAdvisory]) } attr_reader :security_advisories + sig { returns(T.nilable(Dependabot::Version)) } def fetch_latest_version candidate_versions = available_versions candidate_versions = filter_prerelease_versions(candidate_versions) candidate_versions = filter_ignored_versions(candidate_versions) # Adding the psuedo-version to the list to avoid downgrades - candidate_versions << dependency.version if PSEUDO_VERSION_REGEX.match?(dependency.version) + candidate_versions << version_class.new(dependency.version) if PSEUDO_VERSION_REGEX.match?(dependency.version) candidate_versions.max end + sig { returns(T.nilable(Dependabot::Version)) } def fetch_lowest_security_fix_version relevant_versions = available_versions relevant_versions = filter_prerelease_versions(relevant_versions) @@ -83,10 +117,12 @@ def fetch_lowest_security_fix_version relevant_versions.min end + sig { returns(T::Array[Dependabot::Version]) } def available_versions - @available_versions ||= fetch_available_versions + @available_versions ||= T.let(fetch_available_versions, T.nilable(T::Array[Dependabot::Version])) end + sig { returns(T::Array[Dependabot::Version]) } def fetch_available_versions SharedHelpers.in_a_temporary_directory do SharedHelpers.with_git_configured(credentials: credentials) do @@ -124,26 +160,29 @@ def fetch_available_versions ResolvabilityErrors.handle(e.message, goprivate: @goprivate) end + sig { params(error: StandardError).returns(T::Boolean) } def transitory_failure?(error) return true if error.message.include?("EOF") error.message.include?("Internal Server Error") end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def go_mod - @go_mod ||= dependency_files.find { |f| f.name == "go.mod" } + @go_mod ||= T.let(dependency_files.find { |f| f.name == "go.mod" }, T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Hash[String, T.untyped]) } def parse_manifest SharedHelpers.in_a_temporary_directory do - File.write("go.mod", go_mod.content) + File.write("go.mod", T.must(go_mod).content) json = SharedHelpers.run_shell_command("go mod edit -json") JSON.parse(json) || {} end end - sig { params(versions_array: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) } + sig { params(versions_array: T::Array[Dependabot::Version]).returns(T::Array[Dependabot::Version]) } def filter_prerelease_versions(versions_array) return versions_array if wants_prerelease? @@ -154,6 +193,7 @@ def filter_prerelease_versions(versions_array) filtered end + sig { params(versions_array: T::Array[Dependabot::Version]).returns(T::Array[Dependabot::Version]) } def filter_lower_versions(versions_array) return versions_array unless dependency.numeric_version @@ -161,7 +201,7 @@ def filter_lower_versions(versions_array) .select { |version| version > dependency.numeric_version } end - sig { params(versions_array: T::Array[Gem::Version]).returns(T::Array[Gem::Version]) } + sig { params(versions_array: T::Array[Dependabot::Version]).returns(T::Array[Dependabot::Version]) } def filter_ignored_versions(versions_array) filtered = versions_array .reject { |v| ignore_requirements.any? { |r| r.satisfied_by?(v) } } @@ -176,22 +216,28 @@ def filter_ignored_versions(versions_array) filtered end + sig { returns(T::Boolean) } def wants_prerelease? - @wants_prerelease ||= + @wants_prerelease ||= T.let( begin current_version = dependency.numeric_version - current_version&.prerelease? - end + !current_version&.prerelease?.nil? + end, + T.nilable(T::Boolean) + ) end + sig { returns(T::Array[Dependabot::Requirement]) } def ignore_requirements ignored_versions.flat_map { |req| requirement_class.requirements_array(req) } end + sig { returns(T.class_of(Dependabot::Requirement)) } def requirement_class dependency.requirement_class end + sig { returns(T.class_of(Dependabot::Version)) } def version_class dependency.version_class end diff --git a/go_modules/lib/dependabot/go_modules/version.rb b/go_modules/lib/dependabot/go_modules/version.rb index 59297b768bd..494ea732146 100644 --- a/go_modules/lib/dependabot/go_modules/version.rb +++ b/go_modules/lib/dependabot/go_modules/version.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true # Go pre-release versions use 1.0.1-rc1 syntax, which Gem::Version @@ -6,6 +6,8 @@ # alteration. # Best docs are at https://github.com/Masterminds/semver +require "sorbet-runtime" + require "dependabot/version" require "dependabot/utils" @@ -19,41 +21,48 @@ class Version < Dependabot::Version '(\+incompatible)?' ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ + sig { override.params(version: VersionParameter).returns(T::Boolean) } def self.correct?(version) version = version.gsub(/^v/, "") if version.is_a?(String) version = version.to_s.split("+").first if version.to_s.include?("+") - super(version) + super end + sig { override.params(version: VersionParameter).void } def initialize(version) - @version_string = version.to_s.gsub(/^v/, "") + @version_string = T.let(version.to_s.gsub(/^v/, ""), String) version = version.gsub(/^v/, "") if version.is_a?(String) version = version.to_s.split("+").first if version.to_s.include?("+") + @prerelease = T.let(nil, T.nilable(String)) version, @prerelease = version.to_s.split("-", 2) if version.to_s.include?("-") super end + sig { returns(String) } def inspect # :nodoc: "#<#{self.class} #{@version_string.inspect}>" end + sig { returns(String) } def to_s @version_string end + sig { params(other: Object).returns(T.nilable(Integer)) } def <=>(other) - result = super(other) + result = super return if result.nil? return result unless result.zero? - other = self.class.new(other) unless other.is_a?(Version) + other = self.class.new(other.to_s) unless other.is_a?(Version) compare_prerelease(@prerelease || "", T.unsafe(other).prerelease || "") end protected + sig { returns(T.nilable(String)) } attr_reader :prerelease private @@ -62,6 +71,7 @@ def <=>(other) # see https://github.com/golang/mod/blob/fa1ba4269bda724bb9f01ec381fbbaf031e45833/semver/semver.go#L333 # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity + sig { params(left: T.untyped, right: T.untyped).returns(Integer) } def compare_prerelease(left, right) return 0 if left == right return 1 if left == "" @@ -98,12 +108,14 @@ def compare_prerelease(left, right) # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/PerceivedComplexity + sig { params(data: String).returns(T.untyped) } def next_ident(data) i = 0 i += 1 while i < data.length && data[i] != "." [data[0..i], data[i..-1]] end + sig { params(data: T.untyped).returns(T::Boolean) } def num?(data) i = 0 i += 1 while i < data.length && data[i] >= "0" && data[i] <= "9" diff --git a/go_modules/script/ci-test b/go_modules/script/ci-test index a9cf3203e11..811461cbb03 100755 --- a/go_modules/script/ci-test +++ b/go_modules/script/ci-test @@ -2,5 +2,8 @@ set -e +pushd helpers +go test ./... +popd bundle install bundle exec turbo_tests --verbose diff --git a/go_modules/spec/dependabot/go_modules/update_checker/latest_version_finder_spec.rb b/go_modules/spec/dependabot/go_modules/update_checker/latest_version_finder_spec.rb index babb5a484d5..caf509d0dfd 100644 --- a/go_modules/spec/dependabot/go_modules/update_checker/latest_version_finder_spec.rb +++ b/go_modules/spec/dependabot/go_modules/update_checker/latest_version_finder_spec.rb @@ -267,13 +267,8 @@ module foobar let(:dependency_name) { "github.com/dependabot-fixtures/future-go" } let(:dependency_version) { "0.0.0-1" } - it "raises a DependencyFileNotResolvable error" do - error_class = Dependabot::DependencyFileNotResolvable - expect { finder.latest_version } - .to raise_error(error_class) do |error| - expect(error.message).to include("github.com/dependabot-fixtures/future-go") - expect(error.message).to include("requires go >= 99.21.3") - end + it "returns the correct release number" do + expect(finder.latest_version).to eq(Dependabot::GoModules::Version.new("1.0.0")) end end diff --git a/go_modules/spec/dependabot/go_modules/version_spec.rb b/go_modules/spec/dependabot/go_modules/version_spec.rb index 4159ac35903..ad5eeb3e3f1 100644 --- a/go_modules/spec/dependabot/go_modules/version_spec.rb +++ b/go_modules/spec/dependabot/go_modules/version_spec.rb @@ -148,46 +148,8 @@ expect(described_class.new("v1.0.1-0.20231231120000-abcdefabcdef")).to be < described_class.new("v1.0.1") end - # Tested against the following Go program: - # package main - # - # import ( - # "golang.org/x/mod/semver" - # "log" - # "reflect" - # ) - # - # func main() { - # expected := []string{ - # "v1.0.0", - # "v1.0.1-1", - # "v1.0.1-2", - # "v1.0.1", - # "v1.1.0-rc.6", - # "v1.1.0-rc5", - # "v1.1.0-rc6", - # "v1.1.0", - # } - # actual := make([]string, len(expected)) - # copy(actual, expected) - # semver.Sort(actual) - # if !reflect.DeepEqual(actual, expected) { - # log.Fatalf("got %v", actual) - # } - # } - sorted_versions = [ - "v1.0.0", - "v1.0.1-1", - "v1.0.1-2", - "v1.0.1", - "v1.1.0-rc.6", - "v1.1.0-rc0", - "v1.1.0-rc5", - "v1.1.0-rc6", - "v1.1.0", - "v1.34.2-20220907172603-9a877cf260e1.1", - "v1.34.2-20220907172603-9a877cf260e1.2" - ] + # See also the companion Go program that verifies the version order matches. + sorted_versions = JSON.parse(fixture("ordered_versions.json")) sorted_versions.combination(2).each do |lhs, rhs| it "'#{lhs}' < '#{rhs}'" do expect(described_class.new(lhs)).to be < rhs diff --git a/go_modules/spec/fixtures/ordered_versions.json b/go_modules/spec/fixtures/ordered_versions.json new file mode 100644 index 00000000000..7eeed7ad7e9 --- /dev/null +++ b/go_modules/spec/fixtures/ordered_versions.json @@ -0,0 +1,12 @@ +[ + "v1.0.0", + "v1.0.1-1", + "v1.0.1-2", + "v1.0.1", + "v1.1.0-rc.6", + "v1.1.0-rc5", + "v1.1.0-rc6", + "v1.1.0", + "v1.34.2-20220907172603-9a877cf260e1.1", + "v1.34.2-20220907172603-9a877cf260e1.2" +] diff --git a/gradle/.rubocop.yml b/gradle/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/gradle/.rubocop.yml +++ b/gradle/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/gradle/lib/dependabot/gradle/file_fetcher.rb b/gradle/lib/dependabot/gradle/file_fetcher.rb index e2d70a91d2c..abbd1dc2ef3 100644 --- a/gradle/lib/dependabot/gradle/file_fetcher.rb +++ b/gradle/lib/dependabot/gradle/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "sorbet-runtime" @@ -16,21 +16,39 @@ class FileFetcher < Dependabot::FileFetchers::Base require_relative "file_fetcher/settings_file_parser" SUPPORTED_BUILD_FILE_NAMES = - %w(build.gradle build.gradle.kts).freeze + T.let(%w(build.gradle build.gradle.kts).freeze, T::Array[String]) SUPPORTED_SETTINGS_FILE_NAMES = - %w(settings.gradle settings.gradle.kts).freeze + T.let(%w(settings.gradle settings.gradle.kts).freeze, T::Array[String]) # For now Gradle only supports library .toml files in the main gradle folder SUPPORTED_VERSION_CATALOG_FILE_PATH = - %w(/gradle/libs.versions.toml).freeze + T.let(%w(/gradle/libs.versions.toml).freeze, T::Array[String]) + sig do + override + .params( + source: Dependabot::Source, + credentials: T::Array[Dependabot::Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[String, String] + ) + .void + end + def initialize(source:, credentials:, repo_contents_path: nil, options: {}) + super + + @buildfile_name = T.let(nil, T.nilable(String)) + end + + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) filenames.any? do |filename| SUPPORTED_BUILD_FILE_NAMES.any? { |supported| filename.end_with?(supported) } end end + sig { override.returns(String) } def self.required_files_message "Repo must contain a build.gradle / build.gradle.kts file." end @@ -42,6 +60,7 @@ def fetch_files private + sig { params(root_dir: String).returns(T::Array[DependencyFile]) } def all_buildfiles_in_build(root_dir) files = [buildfile(root_dir), settings_file(root_dir), version_catalog_file(root_dir)].compact files += subproject_buildfiles(root_dir) @@ -50,6 +69,7 @@ def all_buildfiles_in_build(root_dir) .flat_map { |dir| all_buildfiles_in_build(dir) } end + sig { params(root_dir: String).returns(T::Array[String]) } def included_builds(root_dir) builds = [] @@ -61,7 +81,7 @@ def included_builds(root_dir) return builds unless settings_file(root_dir) builds += SettingsFileParser - .new(settings_file: settings_file(root_dir)) + .new(settings_file: T.must(settings_file(root_dir))) .included_build_paths .map { |p| clean_join([root_dir, p]) } @@ -73,17 +93,19 @@ def clean_join(parts) Pathname.new(File.join(parts)).cleanpath.to_path end + sig { params(root_dir: String).returns(T::Array[DependencyFile]) } def subproject_buildfiles(root_dir) return [] unless settings_file(root_dir) subproject_paths = SettingsFileParser - .new(settings_file: settings_file(root_dir)) + .new(settings_file: T.must(settings_file(root_dir))) .subproject_paths subproject_paths.filter_map do |path| if @buildfile_name - fetch_file_from_host(File.join(root_dir, path, @buildfile_name)) + buildfile_path = File.join(root_dir, path, @buildfile_name) + fetch_file_from_host(buildfile_path) else buildfile(File.join(root_dir, path)) end @@ -93,6 +115,7 @@ def subproject_buildfiles(root_dir) end end + sig { params(root_dir: String).returns(T.nilable(DependencyFile)) } def version_catalog_file(root_dir) return nil unless root_dir == "." @@ -100,6 +123,7 @@ def version_catalog_file(root_dir) end # rubocop:disable Metrics/PerceivedComplexity + sig { params(root_dir: String).returns(T::Array[DependencyFile]) } def dependency_script_plugins(root_dir) return [] unless buildfile(root_dir) @@ -123,6 +147,7 @@ def dependency_script_plugins(root_dir) end # rubocop:enable Metrics/PerceivedComplexity + sig { params(path: T.any(Pathname, String)).returns(T::Boolean) } def file_exists_in_submodule?(path) fetch_file_from_host(path, fetch_submodules: true) true @@ -130,20 +155,24 @@ def file_exists_in_submodule?(path) false end + sig { params(dir: String).returns(T.nilable(DependencyFile)) } def buildfile(dir) file = find_first(dir, SUPPORTED_BUILD_FILE_NAMES) || return @buildfile_name ||= File.basename(file.name) file end + sig { params(dir: String).returns(T.nilable(DependencyFile)) } def gradle_toml_file(dir) find_first(dir, SUPPORTED_VERSION_CATALOG_FILE_PATH) end + sig { params(dir: String).returns(T.nilable(DependencyFile)) } def settings_file(dir) find_first(dir, SUPPORTED_SETTINGS_FILE_NAMES) end + sig { params(dir: String, supported_names: T::Array[String]).returns(T.nilable(DependencyFile)) } def find_first(dir, supported_names) paths = supported_names .map { |name| clean_join([dir, name]) } @@ -153,10 +182,12 @@ def find_first(dir, supported_names) fetch_first_if_present(paths) end + sig { returns(T::Hash[String, DependencyFile]) } def cached_files - @cached_files ||= {} + @cached_files ||= T.let({}, T.nilable(T::Hash[String, DependencyFile])) end + sig { params(paths: T::Array[String]).returns(T.nilable(DependencyFile)) } def fetch_first_if_present(paths) paths.each do |path| file = fetch_file_if_present(path) || next diff --git a/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb b/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb index 25f25e639d0..72f1b905fb8 100644 --- a/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb +++ b/gradle/lib/dependabot/gradle/file_fetcher/settings_file_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "sorbet-runtime" @@ -11,53 +11,61 @@ class FileFetcher class SettingsFileParser extend T::Sig + sig { params(settings_file: Dependabot::DependencyFile).void } def initialize(settings_file:) @settings_file = settings_file end + sig { returns(T::Array[String]) } def included_build_paths paths = [] - comment_free_content.scan(function_regex("includeBuild")) do + comment_free_content&.scan(function_regex("includeBuild")) do arg = T.must(Regexp.last_match).named_captures.fetch("args") paths << T.must(arg).gsub(/["']/, "").strip end paths.uniq end + sig { returns(T::Array[T.nilable(String)]) } def subproject_paths subprojects = T.let([], T::Array[String]) + process_include_functions(subprojects) + subprojects.uniq.map { |name| process_subproject_name(name) } + end + + private - comment_free_content.scan(function_regex("include")) do + sig { params(subprojects: T::Array[String]).void } + def process_include_functions(subprojects) + comment_free_content&.scan(function_regex("include")) do args = T.must(Regexp.last_match).named_captures.fetch("args") args = T.must(args).split(",") args = args.filter_map { |p| p.gsub(/["']/, "").strip } - subprojects += args + subprojects.concat(args) end + end - subprojects = subprojects.uniq - - subproject_dirs = subprojects.map do |proj| - if comment_free_content.match?(project_dir_regex(proj)) - comment_free_content.match(project_dir_regex(proj)) - .named_captures.fetch("path").sub(%r{^/}, "") - else - proj.tr(":", "/").sub(%r{^/}, "") - end + sig { params(proj: String).returns(T.nilable(String)) } + def process_subproject_name(proj) + if comment_free_content&.match?(project_dir_regex(proj)) + comment_free_content&.match(project_dir_regex(proj)) + &.named_captures&.fetch("path")&.sub(%r{^/}, "") + else + proj.tr(":", "/").sub(%r{^/}, "") end - - subproject_dirs.uniq end - private - + sig { returns(Dependabot::DependencyFile) } attr_reader :settings_file + sig { returns(T.nilable(String)) } def comment_free_content settings_file.content - .gsub(%r{(?<=^|\s)//.*$}, "\n") - .gsub(%r{(?<=^|\s)/\*.*?\*/}m, "") + &.gsub(%r{(?<=^|\s)//.*$}, "\n") + &.gsub(%r{(?<=^|\s)/\*.*?\*/}m, "") end + sig { params(function_name: T.any(String, Symbol)).returns(Regexp) } def function_regex(function_name) / (?:^|\s)#{Regexp.quote(function_name)}(?:\s*\(|\s) @@ -65,6 +73,7 @@ def function_regex(function_name) /mx end + sig { params(proj: String).returns(Regexp) } def project_dir_regex(proj) prefixed_proj = Regexp.quote(":#{proj.gsub(/^:/, '')}") /['"]#{prefixed_proj}['"].*dir\s*=.*['"](?.*?)['"]/i diff --git a/gradle/lib/dependabot/gradle/file_updater.rb b/gradle/lib/dependabot/gradle/file_updater.rb index a9b9c198a6c..ca626a98836 100644 --- a/gradle/lib/dependabot/gradle/file_updater.rb +++ b/gradle/lib/dependabot/gradle/file_updater.rb @@ -56,7 +56,6 @@ def original_file def update_buildfiles_for_dependency(buildfiles:, dependency:) files = buildfiles.dup - # The UpdateChecker ensures the order of requirements is preserved # when updating, so we can zip them together in new/old pairs. reqs = dependency.requirements.zip(dependency.previous_requirements) @@ -69,6 +68,13 @@ def update_buildfiles_for_dependency(buildfiles:, dependency:) buildfile = files.find { |f| f.name == new_req.fetch(:file) } + # Exception raised to handle issue that arises when buildfiles function (see this file) + # removes the build file that contains the dependency itself. So no build file exists to + # update dependency, This behaviour is evident for extremely small number of users + # that have added separate repos as sub-modules in parent projects + + raise DependencyFileNotResolvable, "No build file found to update the dependency" if buildfile.nil? + if new_req.dig(:metadata, :property_name) files = update_files_for_property_change(files, old_req, new_req) elsif new_req.dig(:metadata, :dependency_set) diff --git a/gradle/lib/dependabot/gradle/version.rb b/gradle/lib/dependabot/gradle/version.rb index c74c043e581..cf0cbdfa070 100644 --- a/gradle/lib/dependabot/gradle/version.rb +++ b/gradle/lib/dependabot/gradle/version.rb @@ -22,7 +22,7 @@ class Version < Dependabot::Version "a" => 1, "alpha" => 1, "b" => 2, "beta" => 2, "m" => 3, "milestone" => 3, - "rc" => 4, "cr" => 4, "pr" => 4, + "rc" => 4, "cr" => 4, "pr" => 4, "pre" => 4, "snapshot" => 5, "dev" => 5, "ga" => 6, "" => 6, "final" => 6, "sp" => 7 diff --git a/gradle/spec/dependabot/gradle/file_updater_spec.rb b/gradle/spec/dependabot/gradle/file_updater_spec.rb index 6fca4714f34..d32207d8fee 100644 --- a/gradle/spec/dependabot/gradle/file_updater_spec.rb +++ b/gradle/spec/dependabot/gradle/file_updater_spec.rb @@ -309,6 +309,59 @@ end end + context "with multiple sub module buildfiles" do + let(:dependency_files) { [buildfile, subproject_buildfile] } + let(:subproject_buildfile) do + Dependabot::DependencyFile.new( + name: "submodule/build.gradle", + content: fixture("buildfiles", buildfile_fixture_name) + ) + end + + context "when trying to update buildfiles" do + let(:dependency) do + Dependabot::Dependency.new( + name: "co.aikar:acf-paper", + version: "0.5.0-SNAPSHOT", + requirements: [{ + file: "build.gradle", + requirement: "0.6.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }, { + file: "app/build.gradle", + requirement: "0.6.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }], + previous_requirements: [{ + file: "build.gradle", + requirement: "0.5.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }, { + file: "app/build.gradle", + requirement: "0.5.0-SNAPSHOT", + groups: [], + source: nil, + metadata: nil + }], + package_manager: "gradle" + ) + end + + describe "updates the submodule/build.gradle file" do + it "raises a DependencyFileNotResolvable error" do + expect { updated_files.find { |f| f.name == "submodule/build.gradle" } } + .to raise_error(Dependabot::DependencyFileNotResolvable) + end + end + end + end + context "with a dependency name defined by a property" do let(:buildfile_fixture_name) { "name_property.gradle" } diff --git a/gradle/spec/dependabot/gradle/version_spec.rb b/gradle/spec/dependabot/gradle/version_spec.rb index 6f6207de112..8dcf33d6132 100644 --- a/gradle/spec/dependabot/gradle/version_spec.rb +++ b/gradle/spec/dependabot/gradle/version_spec.rb @@ -92,12 +92,18 @@ it { is_expected.to be(false) } end - context "with a pre-release" do + context "with a 'pr' pre-release separated with a ." do let(:version_string) { "2.10.0.pr3" } it { is_expected.to be(true) } end + context "with a 'pre' pre-release separated with a -" do + let(:version_string) { "2.10.0-pre0" } + + it { is_expected.to be(true) } + end + context "with a release" do let(:version_string) { "1.0.0" } diff --git a/hex/.rubocop.yml b/hex/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/hex/.rubocop.yml +++ b/hex/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/hex/helpers/mix.lock b/hex/helpers/mix.lock index ddb949c9954..87cf2f98782 100644 --- a/hex/helpers/mix.lock +++ b/hex/helpers/mix.lock @@ -1,3 +1,3 @@ %{ - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, + "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, } diff --git a/hex/lib/dependabot/hex/file_fetcher.rb b/hex/lib/dependabot/hex/file_fetcher.rb index 4e4e9021373..d639bac6c48 100644 --- a/hex/lib/dependabot/hex/file_fetcher.rb +++ b/hex/lib/dependabot/hex/file_fetcher.rb @@ -42,7 +42,6 @@ def fetch_files sig { returns(T.nilable(DependencyFile)) } def mixfile @mixfile ||= T.let(fetch_file_from_host("mix.exs"), T.nilable(Dependabot::DependencyFile)) - fetch_file_from_host("mix.exs") end sig { returns(T.nilable(Dependabot::DependencyFile)) } diff --git a/hex/lib/dependabot/hex/version.rb b/hex/lib/dependabot/hex/version.rb index bc3c926a911..ae6cd219670 100644 --- a/hex/lib/dependabot/hex/version.rb +++ b/hex/lib/dependabot/hex/version.rb @@ -43,7 +43,7 @@ def inspect # :nodoc: end def <=>(other) - version_comparison = super(other) + version_comparison = super return version_comparison unless version_comparison&.zero? return build_info.nil? ? 0 : 1 unless other.is_a?(Hex::Version) diff --git a/maven/.rubocop.yml b/maven/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/maven/.rubocop.yml +++ b/maven/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/maven/lib/dependabot/maven/file_parser.rb b/maven/lib/dependabot/maven/file_parser.rb index 6e64d028272..ee3ce5128bb 100644 --- a/maven/lib/dependabot/maven/file_parser.rb +++ b/maven/lib/dependabot/maven/file_parser.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "nokogiri" @@ -15,6 +15,7 @@ module Dependabot module Maven class FileParser < Dependabot::FileParsers::Base + extend T::Sig require "dependabot/file_parsers/base/dependency_set" require_relative "file_parser/property_value_finder" @@ -35,6 +36,7 @@ class FileParser < Dependabot::FileParsers::Base # Regex to get the property name from a declaration that uses a property PROPERTY_REGEX = /\$\{(?.*?)\}/ + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse dependency_set = DependencySet.new pomfiles.each { |pom| dependency_set += pomfile_dependencies(pom) } @@ -44,6 +46,7 @@ def parse private + sig { params(pom: Dependabot::DependencyFile).returns(DependencySet) } def pomfile_dependencies(pom) dependency_set = DependencySet.new @@ -70,6 +73,7 @@ def pomfile_dependencies(pom) dependency_set end + sig { params(extension: Dependabot::DependencyFile).returns(DependencySet) } def extensionfile_dependencies(extension) dependency_set = DependencySet.new @@ -89,6 +93,10 @@ def extensionfile_dependencies(extension) dependency_set end + sig do + params(pom: Dependabot::DependencyFile, + dependency_node: Nokogiri::XML::Element).returns(T.nilable(Dependabot::Dependency)) + end def dependency_from_dependency_node(pom, dependency_node) return unless (name = dependency_name(dependency_node, pom)) return if internal_dependency_names.include?(name) @@ -96,6 +104,10 @@ def dependency_from_dependency_node(pom, dependency_node) build_dependency(pom, dependency_node, name) end + sig do + params(pom: Dependabot::DependencyFile, + dependency_node: Nokogiri::XML::Element).returns(T.nilable(Dependabot::Dependency)) + end def dependency_from_plugin_node(pom, dependency_node) return unless (name = plugin_name(dependency_node, pom)) return if internal_dependency_names.include?(name) @@ -103,6 +115,10 @@ def dependency_from_plugin_node(pom, dependency_node) build_dependency(pom, dependency_node, name) end + sig do + params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element, + name: String).returns(T.nilable(Dependabot::Dependency)) + end def build_dependency(pom, dependency_node, name) property_details = { @@ -127,6 +143,10 @@ def build_dependency(pom, dependency_node, name) ) end + sig do + params(dependency_node: Nokogiri::XML::Element, + pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def dependency_name(dependency_node, pom) return unless dependency_node.at_xpath("./groupId") return unless dependency_node.at_xpath("./artifactId") @@ -143,6 +163,9 @@ def dependency_name(dependency_node, pom) ].join(":") end + sig do + params(dependency_node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def dependency_classifier(dependency_node, pom) return unless dependency_node.at_xpath("./classifier") @@ -152,6 +175,9 @@ def dependency_classifier(dependency_node, pom) ) end + sig do + params(dependency_node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def plugin_name(dependency_node, pom) return unless plugin_group_id(pom, dependency_node) return unless dependency_node.at_xpath("./artifactId") @@ -165,6 +191,7 @@ def plugin_name(dependency_node, pom) ].join(":") end + sig { params(pom: Dependabot::DependencyFile, node: Nokogiri::XML::Element).returns(T.nilable(String)) } def plugin_group_id(pom, node) return "org.apache.maven.plugins" unless node.at_xpath("./groupId") @@ -174,6 +201,9 @@ def plugin_group_id(pom, node) ) end + sig do + params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(T.nilable(String)) + end def dependency_version(pom, dependency_node) requirement = dependency_requirement(pom, dependency_node) return nil unless requirement @@ -185,6 +215,9 @@ def dependency_version(pom, dependency_node) requirement.gsub(/[\(\)\[\]]/, "").strip end + sig do + params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(T.nilable(String)) + end def dependency_requirement(pom, dependency_node) return unless dependency_node.at_xpath("./version") @@ -194,10 +227,12 @@ def dependency_requirement(pom, dependency_node) version_content.empty? ? nil : version_content end + sig { params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(T::Array[String]) } def dependency_groups(pom, dependency_node) dependency_scope(pom, dependency_node) == "test" ? ["test"] : [] end + sig { params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(String) } def dependency_scope(pom, dependency_node) return "compile" unless dependency_node.at_xpath("./scope") @@ -207,6 +242,7 @@ def dependency_scope(pom, dependency_node) scope_content.empty? ? "compile" : scope_content end + sig { params(pom: Dependabot::DependencyFile, dependency_node: Nokogiri::XML::Element).returns(String) } def packaging_type(pom, dependency_node) return "pom" if dependency_node.node_name == "parent" return "jar" unless dependency_node.at_xpath("./type") @@ -217,6 +253,7 @@ def packaging_type(pom, dependency_node) evaluated_value(packaging_type_content, pom) end + sig { params(dependency_node: Nokogiri::XML::Element).returns(T.nilable(String)) } def version_property_name(dependency_node) return unless dependency_node.at_xpath("./version") @@ -228,17 +265,21 @@ def version_property_name(dependency_node) .named_captures.fetch("property") end + sig { params(value: String, pom: Dependabot::DependencyFile).returns(String) } def evaluated_value(value, pom) return value unless value.match?(PROPERTY_REGEX) - property_name = value.match(PROPERTY_REGEX) - .named_captures.fetch("property") - property_value = value_for_property(property_name, pom) + property_name = T.must(value.match(PROPERTY_REGEX)) + .named_captures.fetch("property") + property_value = value_for_property(T.must(property_name), pom) new_value = value.gsub(value.match(PROPERTY_REGEX).to_s, property_value) evaluated_value(new_value, pom) end + sig do + params(dependency_node: Nokogiri::XML::Element, pom: Dependabot::DependencyFile).returns(T.nilable(String)) + end def property_source(dependency_node, pom) property_name = version_property_name(dependency_node) return unless property_name @@ -254,6 +295,7 @@ def property_source(dependency_node, pom) raise DependencyFileNotEvaluatable, msg end + sig { params(property_name: String, pom: Dependabot::DependencyFile).returns(String) } def value_for_property(property_name, pom) value = property_value_finder @@ -268,25 +310,35 @@ def value_for_property(property_name, pom) # Cached, since this can makes calls to the registry (to get property # values from parent POMs) + sig { returns(Dependabot::Maven::FileParser::PropertyValueFinder) } def property_value_finder - @property_value_finder ||= - PropertyValueFinder.new(dependency_files: dependency_files, credentials: credentials) + @property_value_finder ||= T.let( + PropertyValueFinder.new(dependency_files: dependency_files, credentials: credentials.map(&:to_s)), + T.nilable(Dependabot::Maven::FileParser::PropertyValueFinder) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def pomfiles - @pomfiles ||= + @pomfiles ||= T.let( dependency_files.select do |f| f.name.end_with?(".xml") && !f.name.end_with?("extensions.xml") - end + end, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def extensionfiles - @extensionfiles ||= - dependency_files.select { |f| f.name.end_with?("extensions.xml") } + @extensionfiles ||= T.let( + dependency_files.select { |f| f.name.end_with?("extensions.xml") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[String]) } def internal_dependency_names - @internal_dependency_names ||= + @internal_dependency_names ||= T.let( dependency_files.filter_map do |pom| doc = Nokogiri::XML(pom.content) group_id = doc.at_css("project > groupId") || @@ -296,9 +348,12 @@ def internal_dependency_names next unless group_id && artifact_id [group_id.content.strip, artifact_id.content.strip].join(":") - end + end, + T.nilable(T::Array[String]) + ) end + sig { override.void } def check_required_files raise "No pom.xml!" unless get_original_file("pom.xml") end diff --git a/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb b/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb index fad30682805..e707edc5b7a 100644 --- a/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb +++ b/maven/lib/dependabot/maven/file_parser/pom_fetcher.rb @@ -1,6 +1,7 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" require "nokogiri" require "dependabot/dependency_file" @@ -11,15 +12,19 @@ module Dependabot module Maven class FileParser class PomFetcher + extend T::Sig + + sig { params(dependency_files: T::Array[DependencyFile]).void } def initialize(dependency_files:) @dependency_files = dependency_files - @poms = {} + @poms = T.let({}, T::Hash[String, DependencyFile]) end + sig { returns(T::Hash[String, DependencyFile]) } def internal_dependency_poms return @internal_dependency_poms if @internal_dependency_poms - @internal_dependency_poms = {} + @internal_dependency_poms = T.let({}, T.nilable(T::Hash[String, DependencyFile])) dependency_files.each do |pom| doc = Nokogiri::XML(pom.content) group_id = doc.at_css("project > groupId") || @@ -33,12 +38,20 @@ def internal_dependency_poms artifact_id.content.strip ].join(":") - @internal_dependency_poms[dependency_name] = pom + T.must(@internal_dependency_poms)[dependency_name] = pom end - @internal_dependency_poms + T.must(@internal_dependency_poms) end + sig do + params( + group_id: String, + artifact_id: String, + version: String, + urls_to_try: T::Array[String] + ).returns(T.nilable(DependencyFile)) # Fix: Added closing parenthesis + end def fetch_remote_parent_pom(group_id, artifact_id, version, urls_to_try) pom_id = "#{group_id}:#{artifact_id}:#{version}" return @poms[pom_id] if @poms.key?(pom_id) @@ -74,24 +87,33 @@ def fetch_remote_parent_pom(group_id, artifact_id, version, urls_to_try) private + sig { params(group_id: String, artifact_id: String, version: String, base_repo_url: String).returns(String) } def remote_pom_url(group_id, artifact_id, version, base_repo_url) "#{base_repo_url}/" \ "#{group_id.tr('.', '/')}/#{artifact_id}/#{version}/" \ "#{artifact_id}-#{version}.pom" end + sig do + params(group_id: String, artifact_id: String, version: String, snapshot_version: String, + base_repo_url: String).returns(String) + end def remote_pom_snapshot_url(group_id, artifact_id, version, snapshot_version, base_repo_url) "#{base_repo_url}/" \ "#{group_id.tr('.', '/')}/#{artifact_id}/#{version}/" \ "#{artifact_id}-#{snapshot_version}.pom" end + sig { params(group_id: String, artifact_id: String, version: String, base_repo_url: String).returns(String) } def remote_pom_snapshot_metadata_url(group_id, artifact_id, version, base_repo_url) "#{base_repo_url}/" \ "#{group_id.tr('.', '/')}/#{artifact_id}/#{version}/" \ "maven-metadata.xml" end + sig do + params(group_id: String, artifact_id: String, version: String, base_url: String).returns(T.nilable(String)) + end def fetch_snapshot_pom_url(group_id, artifact_id, version, base_url) url = remote_pom_snapshot_metadata_url(group_id, artifact_id, version, base_url) response = fetch(url) @@ -107,15 +129,18 @@ def fetch_snapshot_pom_url(group_id, artifact_id, version, base_url) remote_pom_snapshot_url(group_id, artifact_id, version, snapshot, base_url) end + sig { params(url: String).returns(Excon::Response) } def fetch(url) - @maven_responses ||= {} + @maven_responses ||= T.let({}, T.nilable(T::Hash[String, Excon::Response])) @maven_responses[url] ||= Dependabot::RegistryClient.get(url: url, options: { retry_limit: 1 }) end + sig { params(content: String).returns(T::Boolean) } def pom?(content) !Nokogiri::XML(content).at_css("project > artifactId").nil? end + sig { returns(T::Array[DependencyFile]) } attr_reader :dependency_files end end diff --git a/maven/lib/dependabot/maven/file_parser/property_value_finder.rb b/maven/lib/dependabot/maven/file_parser/property_value_finder.rb index 1928201c933..ad818ab1764 100644 --- a/maven/lib/dependabot/maven/file_parser/property_value_finder.rb +++ b/maven/lib/dependabot/maven/file_parser/property_value_finder.rb @@ -1,8 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "nokogiri" - +require "sorbet-runtime" require "dependabot/dependency_file" require "dependabot/maven/file_parser" require "dependabot/registry_client" @@ -14,17 +14,24 @@ module Dependabot module Maven class FileParser class PropertyValueFinder + extend T::Sig + require_relative "repositories_finder" require_relative "pom_fetcher" DOT_SEPARATOR_REGEX = %r{\.(?!\d+([.\/_\-]|$)+)} + sig { params(dependency_files: T::Array[DependencyFile], credentials: T::Array[String]).void } def initialize(dependency_files:, credentials: []) @dependency_files = dependency_files @credentials = credentials - @pom_fetcher = PomFetcher.new(dependency_files: dependency_files) + @pom_fetcher = T.let(PomFetcher.new(dependency_files: dependency_files), + Dependabot::Maven::FileParser::PomFetcher) end + sig do + params(property_name: String, callsite_pom: DependencyFile).returns(T.nilable(T::Hash[Symbol, T.untyped])) + end def property_details(property_name:, callsite_pom:) pom = callsite_pom doc = Nokogiri::XML(pom.content) @@ -71,8 +78,17 @@ def property_details(property_name:, callsite_pom:) private + sig { returns(T::Array[DependencyFile]) } attr_reader :dependency_files + sig do + params( + expression: String, + property_name: String, + callsite_pom: DependencyFile + ) + .returns(T.nilable(T::Hash[Symbol, String])) + end def extract_value_from_expression(expression:, property_name:, callsite_pom:) # and the expression is pointing to self then raise the error if expression.eql?("${#{property_name}}") @@ -83,14 +99,16 @@ def extract_value_from_expression(expression:, property_name:, callsite_pom:) end # and the expression is pointing to another tag, then get the value of that tag - property_details(property_name: expression.slice(2..-2), callsite_pom: callsite_pom) + property_details(property_name: T.must(expression.slice(2..-2)), callsite_pom: callsite_pom) end + sig { params(property_name: String).returns(String) } def sanitize_property_name(property_name) property_name.sub(/^pom\./, "").sub(/^project\./, "") end # rubocop:disable Metrics/PerceivedComplexity + sig { params(pom: DependencyFile).returns(T.nilable(DependencyFile)) } def parent_pom(pom) doc = Nokogiri::XML(pom.content) doc.remove_namespaces! @@ -111,6 +129,7 @@ def parent_pom(pom) end # rubocop:enable Metrics/PerceivedComplexity + sig { params(pom: DependencyFile).returns(T::Array[String]) } def parent_repository_urls(pom) repositories_finder.repository_urls( pom: pom, @@ -119,14 +138,17 @@ def parent_repository_urls(pom) ) end + sig { returns(RepositoriesFinder) } def repositories_finder - @repositories_finder ||= - RepositoriesFinder.new( + @repositories_finder ||= T.let( + Dependabot::Maven::FileParser::RepositoriesFinder.new( pom_fetcher: @pom_fetcher, dependency_files: dependency_files, credentials: @credentials, evaluate_properties: false - ) + ), + T.nilable(Dependabot::Maven::FileParser::RepositoriesFinder) + ) end end end diff --git a/maven/lib/dependabot/maven/file_updater/property_value_updater.rb b/maven/lib/dependabot/maven/file_updater/property_value_updater.rb index a902c438641..970a409f093 100644 --- a/maven/lib/dependabot/maven/file_updater/property_value_updater.rb +++ b/maven/lib/dependabot/maven/file_updater/property_value_updater.rb @@ -1,6 +1,7 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" require "nokogiri" require "dependabot/dependency_file" @@ -11,54 +12,72 @@ module Dependabot module Maven class FileUpdater class PropertyValueUpdater + extend T::Sig + + sig { params(dependency_files: T::Array[DependencyFile]).void } def initialize(dependency_files:) @dependency_files = dependency_files end + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/PerceivedComplexity + sig do + params( + property_name: String, + callsite_pom: DependencyFile, + updated_value: String + ).returns(T::Array[DependencyFile]) + end def update_pomfiles_for_property_change(property_name:, callsite_pom:, updated_value:) declaration_details = property_value_finder.property_details( property_name: property_name, callsite_pom: callsite_pom ) - node = declaration_details.fetch(:node) - filename = declaration_details.fetch(:file) + node = declaration_details&.fetch(:node) + filename = declaration_details&.fetch(:file) pom_to_update = dependency_files.find { |f| f.name == filename } property_re = %r{<#{Regexp.quote(node.name)}> \s*#{Regexp.quote(node.content)}\s* }xm property_text = node.to_s - if pom_to_update.content&.match?(property_re) - updated_content = pom_to_update.content.sub( + if pom_to_update&.content&.match?(property_re) + updated_content = pom_to_update&.content&.sub( property_re, "<#{node.name}>#{updated_value}" ) - elsif pom_to_update.content.include? property_text + elsif pom_to_update&.content&.include? property_text node.content = updated_value - updated_content = pom_to_update.content.sub( + updated_content = pom_to_update&.content&.sub( property_text, node.to_s ) end updated_pomfiles = dependency_files.dup - updated_pomfiles[updated_pomfiles.index(pom_to_update)] = - update_file(file: pom_to_update, content: updated_content) + updated_pomfiles[T.must(updated_pomfiles.index(pom_to_update))] = + update_file(file: T.must(pom_to_update), content: T.must(updated_content)) updated_pomfiles end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize private + sig { returns T::Array[Dependabot::DependencyFile] } attr_reader :dependency_files + sig { returns Maven::FileParser::PropertyValueFinder } def property_value_finder - @property_value_finder ||= - Maven::FileParser::PropertyValueFinder - .new(dependency_files: dependency_files) + @property_value_finder ||= T.let( + Maven::FileParser::PropertyValueFinder.new(dependency_files: dependency_files), + T.nilable(Dependabot::Maven::FileParser::PropertyValueFinder) + ) end + sig { params(file: DependencyFile, content: String).returns(DependencyFile) } def update_file(file:, content:) updated_file = file.dup updated_file.content = content diff --git a/maven/lib/dependabot/maven/update_checker.rb b/maven/lib/dependabot/maven/update_checker.rb index ebef09ca70d..3898599975a 100644 --- a/maven/lib/dependabot/maven/update_checker.rb +++ b/maven/lib/dependabot/maven/update_checker.rb @@ -138,7 +138,7 @@ def property_updater def property_value_finder @property_value_finder ||= Maven::FileParser::PropertyValueFinder - .new(dependency_files: dependency_files, credentials: credentials) + .new(dependency_files: dependency_files, credentials: credentials.map(&:to_s)) end def version_comes_from_multi_dependency_property? diff --git a/maven/lib/dependabot/maven/version.rb b/maven/lib/dependabot/maven/version.rb index 20056941e51..0cd26fe8f81 100644 --- a/maven/lib/dependabot/maven/version.rb +++ b/maven/lib/dependabot/maven/version.rb @@ -22,7 +22,7 @@ class Version < Dependabot::Version "a" => 1, "alpha" => 1, "b" => 2, "beta" => 2, "m" => 3, "milestone" => 3, - "rc" => 4, "cr" => 4, "pr" => 4, + "rc" => 4, "cr" => 4, "pr" => 4, "pre" => 4, "snapshot" => 5, "dev" => 5, "ga" => 6, "" => 6, "final" => 6, "sp" => 7 diff --git a/maven/spec/dependabot/maven/version_spec.rb b/maven/spec/dependabot/maven/version_spec.rb index 5e3308340c3..934c90779ea 100644 --- a/maven/spec/dependabot/maven/version_spec.rb +++ b/maven/spec/dependabot/maven/version_spec.rb @@ -116,12 +116,18 @@ it { is_expected.to be(false) } end - context "with a pre-release" do + context "with a 'pr' pre-release separated with a ." do let(:version_string) { "2.10.0.pr3" } it { is_expected.to be(true) } end + context "with a 'pre' pre-release separated with a -" do + let(:version_string) { "2.10.0-pre0" } + + it { is_expected.to be(true) } + end + context "with a dev token" do let(:version_string) { "1.2.1-dev-65" } diff --git a/npm_and_yarn/.rubocop.yml b/npm_and_yarn/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/npm_and_yarn/.rubocop.yml +++ b/npm_and_yarn/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/npm_and_yarn/helpers/package-lock.json b/npm_and_yarn/helpers/package-lock.json index e8a941a861e..4dd10e2bb6c 100644 --- a/npm_and_yarn/helpers/package-lock.json +++ b/npm_and_yarn/helpers/package-lock.json @@ -8,9 +8,9 @@ "hasInstallScript": true, "dependencies": { "@dependabot/yarn-lib": "^1.22.22", - "@npmcli/arborist": "^7.5.3", - "@pnpm/dependency-path": "^4.0.0", - "@pnpm/lockfile-file": "^9.0.5", + "@npmcli/arborist": "^7.5.4", + "@pnpm/dependency-path": "^5.1.1", + "@pnpm/lockfile-file": "^9.1.2", "detect-indent": "^6.1.0", "nock": "^13.5.4", "npm": "6.14.18", @@ -21,10 +21,10 @@ "helper": "run.js" }, "devDependencies": { - "eslint": "^9.2.0", + "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", - "prettier": "^3.2.5" + "prettier": "^3.3.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -731,10 +731,47 @@ "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/@eslint/eslintrc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz", - "integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -761,9 +798,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -796,51 +833,23 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.2.0.tgz", - "integrity": "sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -854,16 +863,10 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, "node_modules/@humanwhocodes/retry": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.2.3.tgz", - "integrity": "sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true, "engines": { "node": ">=18.18" @@ -1959,9 +1962,9 @@ } }, "node_modules/@npmcli/arborist": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.3.tgz", - "integrity": "sha512-7gbMdDNSYUzi0j2mpb6FoXRg3BxXWplMQZH1MZlvNjSdWFObaUz2Ssvo0Nlh2xmWks1OPo+gpsE6qxpT/5M7lQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.4.tgz", + "integrity": "sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g==", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^3.1.1", @@ -2397,11 +2400,11 @@ } }, "node_modules/@pnpm/core-loggers": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.0.tgz", - "integrity": "sha512-nf6DWO+75llaOxZ4Wb5xIzC86jb9PEeD8y7E4bbkLCJUvv/vRVgaPO3+Fo2GFTw5ZY7cip60rTF6dUzbP9dOVw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.3.tgz", + "integrity": "sha512-G038bkMTuvmgG3XtuajnfoBS/u2CoeywRzJZb3qxvcj1XpLFTDAhHyUv/2Rr+yh6KDOVAuTWqdk+WNfeNf6yrw==", "dependencies": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "11.0.0" }, "engines": { "node": ">=18.12" @@ -2428,13 +2431,13 @@ } }, "node_modules/@pnpm/dependency-path": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-4.0.0.tgz", - "integrity": "sha512-d2tTvjnWJtqVjREPZa1h81i7wfQSeg7YkMc7BZAr8QJ4he5KlHY1Zmfa4LpyXVQJSV3trGfy/dmxhV2A5lo34g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-5.1.2.tgz", + "integrity": "sha512-223YCb6SiCi2+112wHPiG+fWsnSpGINNYZKVwlNwZugheSRuda68SjpUbjc7JIkmceRUD8gbBguk8ynv8IS4TA==", "dependencies": { "@pnpm/crypto.base32-hash": "3.0.0", - "@pnpm/types": "10.0.0", - "semver": "^7.6.0" + "@pnpm/types": "11.0.0", + "semver": "^7.6.2" }, "engines": { "node": ">=18.12" @@ -2458,14 +2461,14 @@ } }, "node_modules/@pnpm/fetch": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.0.tgz", - "integrity": "sha512-V9khLYMUmadH45A5zZnrt1nUsZ0NokWkw0QjjgSdiBCgRyQnf1SvFjVcj4sVWxK0ZaijZQnIhIcKvlV3/zB0Ig==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.3.tgz", + "integrity": "sha512-yUeoVCc/pPicpdU3s+2Vzl7VfLWDUblizRbglQaaXhAawLWOAYu5a/jMoIclN2dJzh5juRPhYowMX82oTG9Y0Q==", "dependencies": { - "@pnpm/core-loggers": "10.0.0", + "@pnpm/core-loggers": "10.0.3", "@pnpm/fetching-types": "6.0.0", - "@pnpm/network.agent": "^1.0.1", - "@pnpm/types": "10.0.0", + "@pnpm/network.agent": "^2.0.0", + "@pnpm/types": "11.0.0", "@zkochan/retry": "^0.2.0", "node-fetch": "npm:@pnpm/node-fetch@1.0.0" }, @@ -2495,15 +2498,15 @@ } }, "node_modules/@pnpm/git-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.1.tgz", - "integrity": "sha512-B1FtKwEEUm8130XqmX7eqgMhqdBxJ5gPrWssOLnpIlp/rvmJFsfD2P//80OjORPNFWnpfqdfBF34c/+ZCzAxZg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.4.tgz", + "integrity": "sha512-k6jglET3h66oLwqoUBslfRWmL6ULXXIHjQoc1uLS0it+m1cI5toHWkrKJOwbI/9K3KQ88EhhulFP4tQQpS+1fg==", "dependencies": { - "@pnpm/fetch": "8.0.0", - "@pnpm/resolver-base": "12.0.0", - "graceful-git": "^3.1.2", + "@pnpm/fetch": "8.0.3", + "@pnpm/resolver-base": "13.0.0", + "graceful-git": "^4.0.0", "hosted-git-info": "npm:@pnpm/hosted-git-info@1.0.0", - "semver": "^7.6.0" + "semver": "^7.6.2" }, "engines": { "node": ">=18.12" @@ -2586,26 +2589,26 @@ } }, "node_modules/@pnpm/lockfile-file": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.0.5.tgz", - "integrity": "sha512-QQFYohFy39FkAQbtDEtqVIzNu5XZhA9aonh/AM/vwvptNcfnajeBuNKfAJepdjWPg/xSBDuU96So29pkPMK8+Q==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.1.2.tgz", + "integrity": "sha512-kQxQOTCTt8edqj1EOGzoGO+ef8iZCKN5GHY+KdZ54Mt8LThXVIu9LYTEuNXpaRdu9kH1wpfla5TbUDK0vMEvwg==", "dependencies": { "@pnpm/constants": "8.0.0", - "@pnpm/dependency-path": "4.0.0", + "@pnpm/dependency-path": "5.1.2", "@pnpm/error": "6.0.1", - "@pnpm/git-resolver": "9.0.1", + "@pnpm/git-resolver": "9.0.4", "@pnpm/git-utils": "2.0.0", - "@pnpm/lockfile-types": "6.0.0", - "@pnpm/lockfile-utils": "10.1.1", - "@pnpm/merge-lockfile-changes": "6.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/lockfile-types": "7.1.2", + "@pnpm/lockfile-utils": "11.0.3", + "@pnpm/merge-lockfile-changes": "6.0.4", + "@pnpm/types": "11.0.0", "@pnpm/util.lex-comparator": "3.0.0", - "@zkochan/rimraf": "^2.1.3", + "@zkochan/rimraf": "^3.0.2", "comver-to-semver": "^1.0.0", "js-yaml": "npm:@zkochan/js-yaml@0.0.7", "normalize-path": "^3.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0", + "semver": "^7.6.2", "sort-keys": "^4.2.0", "strip-bom": "^4.0.0", "write-file-atomic": "^5.0.1" @@ -2668,11 +2671,11 @@ } }, "node_modules/@pnpm/lockfile-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-6.0.0.tgz", - "integrity": "sha512-a4/ULIPLZIIq8Qmi2HEoFgRTtEouGU5RNhuGDxnSmkxu1BjlNMNjLJeEI5jzMZCGOjBoML+AirY/XOO3bcEQ/w==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-7.1.2.tgz", + "integrity": "sha512-+64KoK8gtTS5lxslW8ATtwwEbikW4e9i/OV5eaR+X+//5SeUA796uCN96sKu6q6OzpZi3/aVU4VgVe15MT9XKA==", "dependencies": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "11.0.0" }, "engines": { "node": ">=18.12" @@ -2682,15 +2685,15 @@ } }, "node_modules/@pnpm/lockfile-utils": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-10.1.1.tgz", - "integrity": "sha512-Zl5S1WW3Fk8SFjzjuV8jog7VYtPC+RMcsLpvmgFUDyMy/IRG1x2vQ7m3BY1SpmfRLf4XqxACRwKBlbjlRrVY4Q==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-11.0.3.tgz", + "integrity": "sha512-HQ3TjUd7TCRovi6wSJ8wcSe1BxXJVs3Hf1msHSZ3Ng1Bwd8rj2mQBNu022u3279Oe1kz35APN0yYciynWWlWkA==", "dependencies": { - "@pnpm/dependency-path": "4.0.0", - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/dependency-path": "5.1.2", + "@pnpm/lockfile-types": "7.1.2", "@pnpm/pick-fetcher": "3.0.0", - "@pnpm/resolver-base": "12.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/resolver-base": "13.0.0", + "@pnpm/types": "11.0.0", "get-npm-tarball-url": "^2.1.0", "ramda": "npm:@pnpm/ramda@0.28.1" }, @@ -2715,14 +2718,15 @@ } }, "node_modules/@pnpm/merge-lockfile-changes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.0.tgz", - "integrity": "sha512-K9ARTZ+o/EZ10RPZY4dftlSnvPgJrVeOG0QwZLNTb9Z9q8D6EqSVwEh7CxDobGFe5FAj2lkDK6DY7EgPI4hhdw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.4.tgz", + "integrity": "sha512-S15nSd/LPZKLArnMfHpQLgK7MvNYvSs9meb839Eh29pqp2wSPHLKOroK4Upbod6SOrGtihmgjmpLaFNAYschpg==", "dependencies": { - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/lockfile-types": "7.1.2", + "@pnpm/types": "11.0.0", "comver-to-semver": "^1.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0" + "semver": "^7.6.2" }, "engines": { "node": ">=18.12" @@ -2732,17 +2736,17 @@ } }, "node_modules/@pnpm/network.agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-1.0.1.tgz", - "integrity": "sha512-yRm8MzpZvst5IYF5IUgK7q5SvcncCUWOVBqpl527Pz6BafmDlcxAYyFy7lV4AiQr+VZ9VWudQsaHQeaYikyDGw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-2.0.0.tgz", + "integrity": "sha512-CqONDs5W6vaAdgQEHyFSr4vj25Pv8eVzwI+oUvId/FBHOcTCgHndLIJGON39JnyQS40+yT9kpEj21la3rcJK2w==", "dependencies": { - "@pnpm/network.config": "1.0.1", - "@pnpm/network.proxy-agent": "1.0.1", + "@pnpm/network.config": "2.0.0", + "@pnpm/network.proxy-agent": "2.0.0", "agentkeepalive": "4.2.1", "lru-cache": "7.10.1" }, "engines": { - "node": ">=12.22.0" + "node": ">=18.12" } }, "node_modules/@pnpm/network.agent/node_modules/lru-cache": { @@ -2754,54 +2758,29 @@ } }, "node_modules/@pnpm/network.config": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-1.0.1.tgz", - "integrity": "sha512-ZmTsSFxd4QT5+IZvwHtQjzSlkB7OXAty6MfSenRyHOvR1f8j3l1VDWVXJiNaiLrKeidiZH6ADfsMTr2N0CGDeA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-2.0.0.tgz", + "integrity": "sha512-DpTQTz4KBUgR0NNo/+/WXFlE4dy4+vgINhR9Eb+qo/Kb9RzGbhTN0ypv3sRYa6YG4UO5ft47rvEtHJ9i6VBwzA==", "dependencies": { "nerf-dart": "^1.0.0" }, "engines": { - "node": ">=12.22.0" + "node": ">=18.12" } }, "node_modules/@pnpm/network.proxy-agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-1.0.1.tgz", - "integrity": "sha512-0q9Btpw43aTPzEJJmQY1TNBrwNlPINRae8EpO7VpqbmFflBRO6u6qady6XFfbi+wwPxpcpVOYr6rCDBzALXYHA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-2.0.0.tgz", + "integrity": "sha512-gCShibUggQS1vveAzr84PhDvwoChR4HrHHdvTB8CqXHQu12eoXO8R01awalZWERrHL3fDkUQcqLqCospm2O/QQ==", "dependencies": { - "@pnpm/error": "^4.0.0", + "@pnpm/error": "^6.0.0", "http-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.1", "lru-cache": "7.10.1", "socks-proxy-agent": "6.1.1" }, "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.proxy-agent/node_modules/@pnpm/constants": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@pnpm/constants/-/constants-6.2.0.tgz", - "integrity": "sha512-GlDVUkeTR2WK0oZAM+wtDY6RBMLw6b0Z/5qKgBbDszx4e+R7CHyfG7JofyypogRCfeWXeAXp2C2FkFTh+sNgIg==", - "engines": { - "node": ">=14.6" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/network.proxy-agent/node_modules/@pnpm/error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/error/-/error-4.0.1.tgz", - "integrity": "sha512-6UFakGqUDhnZVzYCfN+QaG1epxtBVS1M9mb9RzoBuvWxcimBYTT04fdYuyk1Nay8y/TvAVl3AVB/lCziWG0+2w==", - "dependencies": { - "@pnpm/constants": "6.2.0" - }, - "engines": { - "node": ">=14.6" - }, - "funding": { - "url": "https://opencollective.com/pnpm" + "node": ">=18.12" } }, "node_modules/@pnpm/network.proxy-agent/node_modules/lru-cache": { @@ -2824,11 +2803,11 @@ } }, "node_modules/@pnpm/resolver-base": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-12.0.0.tgz", - "integrity": "sha512-R5FmojIoHRIC8hZDyr6a9SM6TkpAQXQXgq5QrycUwknRvGjTnrOFD5JaTzMZohcfFg6TWdA3sp3B0w/mhj98Rg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-13.0.0.tgz", + "integrity": "sha512-hUAn2OqHEBB3MRLlbvtczI0KdNM9CJgd0hDRuLDrcaVrhZrhHDwgLywls+hWbgNvUpcdMR7k+uEIo+07Vu/Qvg==", "dependencies": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "11.0.0" }, "engines": { "node": ">=18.12" @@ -2838,9 +2817,9 @@ } }, "node_modules/@pnpm/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-10.0.0.tgz", - "integrity": "sha512-P608MRTOExt5BkIN2hsrb/ycEchwaPW/x80ujJUAqxKZSXNVAOrlEu3KJ+2+jTCunyWmo/EcE01ZdwCw8jgVrQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-11.0.0.tgz", + "integrity": "sha512-BSdk9nlYLHHHLrTFNpmdrXrXVc+1sY/E1Fs1zqR8pY/KjpjVhxkruLZuXitPRPxbk4jSqm7UnG5WCz008iiaig==", "engines": { "node": ">=18.12" }, @@ -3126,14 +3105,11 @@ } }, "node_modules/@zkochan/rimraf": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@zkochan/rimraf/-/rimraf-2.1.3.tgz", - "integrity": "sha512-mCfR3gylCzPC+iqdxEA6z5SxJeOgzgbwmyxanKriIne5qZLswDe/M43aD3p5MNzwzXRhbZg/OX+MpES6Zk1a6A==", - "dependencies": { - "rimraf": "^3.0.2" - }, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@zkochan/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-GBf4ua7ogWTr7fATnzk/JLowZDBnBJMm8RkMaC/KcvxZ9gxbMWix0/jImd815LmqKyIHZ7h7lADRddGMdGBuCA==", "engines": { - "node": ">=12.10" + "node": ">=18.12" } }, "node_modules/@zkochan/which": { @@ -3159,9 +3135,9 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3191,9 +3167,9 @@ } }, "node_modules/agent-base/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -3225,9 +3201,9 @@ } }, "node_modules/agentkeepalive/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -3647,9 +3623,9 @@ } }, "node_modules/bole": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.3.tgz", - "integrity": "sha512-4o8wk9dlpU0e69sXhIsPIaFfXgOvj6en2GgZkG8hadkqNEqYKcz9Y70ijg7Kjq9hz2prJkWXljca5OBJZ451xg==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.13.tgz", + "integrity": "sha512-JQ3xWh2nYsVUuJx7ZN4fzU3vHpzceWb7CC06LUXWwdY++Hzd7Wola7zN3Ud5XgmOVoH/6KzrdMmJokol/xtejw==", "peer": true, "dependencies": { "fast-safe-stringify": "^2.0.7", @@ -4558,18 +4534,18 @@ } }, "node_modules/eslint": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.2.0.tgz", - "integrity": "sha512-0n/I88vZpCOzO+PQpt0lbsqmn9AsnsJAQseIqhZFI8ibQT0U1AkEKRxA3EVMos0BoHSXDQvCXY25TUjB5tr8Og==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "9.2.0", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint/config-array": "^0.17.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.2.3", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", @@ -4578,8 +4554,8 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", - "esquery": "^1.4.2", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -4605,7 +4581,7 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-config-prettier": { @@ -4824,12 +4800,12 @@ } }, "node_modules/espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "dependencies": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -4865,9 +4841,9 @@ } }, "node_modules/esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -5473,15 +5449,23 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "node_modules/graceful-git": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/graceful-git/-/graceful-git-3.1.2.tgz", - "integrity": "sha512-Xyh9Y43yA23/KQ16mpwO4zkzVGUAXyzuSVZQxw9ddQklssIYIY0el24VYfJBFhyCWGriZPRAB2nCgsDizqna9g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/graceful-git/-/graceful-git-4.0.0.tgz", + "integrity": "sha512-zK/rCH/I0DMKpPBLCElXGI7za3EnXeQFdiK6CTP02Tt1N1L+bMLghZY7cXozlx9M2bx4Q0zrY9ADYP3eI8haIw==", "dependencies": { - "retry": "^0.12.0", - "safe-execa": "^0.1.0" + "retry": "^0.13.1", + "safe-execa": "^0.1.1" }, "engines": { - "node": ">=10" + "node": ">=18.12" + } + }, + "node_modules/graceful-git/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "engines": { + "node": ">= 4" } }, "node_modules/gunzip-maybe": { @@ -5630,9 +5614,9 @@ } }, "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -5677,9 +5661,9 @@ } }, "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -14083,9 +14067,9 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -14636,20 +14620,6 @@ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==" }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rsvp": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", @@ -14822,9 +14792,9 @@ } }, "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dependencies": { "ms": "2.1.2" }, @@ -16444,10 +16414,38 @@ "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", "dev": true }, + "@eslint/config-array": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz", + "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==", + "dev": true, + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@eslint/eslintrc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz", - "integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -16468,9 +16466,9 @@ "dev": true }, "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "dev": true, "requires": { "ms": "2.1.2" @@ -16494,38 +16492,16 @@ } }, "@eslint/js": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.2.0.tgz", - "integrity": "sha512-ESiIudvhoYni+MdsI8oD7skpprZ89qKocwRM2KEvhhBJ9nl5MRh7BXU5GTod7Mdygq+AUl+QzId6iWJKR/wABA==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz", + "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==", "dev": true }, - "@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true }, "@humanwhocodes/module-importer": { "version": "1.0.1", @@ -16533,16 +16509,10 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true }, - "@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true - }, "@humanwhocodes/retry": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.2.3.tgz", - "integrity": "sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", "dev": true }, "@isaacs/cliui": { @@ -17350,9 +17320,9 @@ } }, "@npmcli/arborist": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.3.tgz", - "integrity": "sha512-7gbMdDNSYUzi0j2mpb6FoXRg3BxXWplMQZH1MZlvNjSdWFObaUz2Ssvo0Nlh2xmWks1OPo+gpsE6qxpT/5M7lQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@npmcli/arborist/-/arborist-7.5.4.tgz", + "integrity": "sha512-nWtIc6QwwoUORCRNzKx4ypHqCk3drI+5aeYdMTQQiRCcn4lOOgfQh7WyZobGYTxXPSq1VwV53lkpN/BRlRk08g==", "requires": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/fs": "^3.1.1", @@ -17675,11 +17645,11 @@ "integrity": "sha512-yQosGUvYPpAjb1jOFcdbwekRjZRVxN6C0hHzfRCZrMKbxGjt/E0g0RcFlEDNVZ95tm4oMMcr7nEPa7H7LX3emw==" }, "@pnpm/core-loggers": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.0.tgz", - "integrity": "sha512-nf6DWO+75llaOxZ4Wb5xIzC86jb9PEeD8y7E4bbkLCJUvv/vRVgaPO3+Fo2GFTw5ZY7cip60rTF6dUzbP9dOVw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-10.0.3.tgz", + "integrity": "sha512-G038bkMTuvmgG3XtuajnfoBS/u2CoeywRzJZb3qxvcj1XpLFTDAhHyUv/2Rr+yh6KDOVAuTWqdk+WNfeNf6yrw==", "requires": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "11.0.0" } }, "@pnpm/crypto.base32-hash": { @@ -17691,13 +17661,13 @@ } }, "@pnpm/dependency-path": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-4.0.0.tgz", - "integrity": "sha512-d2tTvjnWJtqVjREPZa1h81i7wfQSeg7YkMc7BZAr8QJ4he5KlHY1Zmfa4LpyXVQJSV3trGfy/dmxhV2A5lo34g==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@pnpm/dependency-path/-/dependency-path-5.1.2.tgz", + "integrity": "sha512-223YCb6SiCi2+112wHPiG+fWsnSpGINNYZKVwlNwZugheSRuda68SjpUbjc7JIkmceRUD8gbBguk8ynv8IS4TA==", "requires": { "@pnpm/crypto.base32-hash": "3.0.0", - "@pnpm/types": "10.0.0", - "semver": "^7.6.0" + "@pnpm/types": "11.0.0", + "semver": "^7.6.2" } }, "@pnpm/error": { @@ -17709,14 +17679,14 @@ } }, "@pnpm/fetch": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.0.tgz", - "integrity": "sha512-V9khLYMUmadH45A5zZnrt1nUsZ0NokWkw0QjjgSdiBCgRyQnf1SvFjVcj4sVWxK0ZaijZQnIhIcKvlV3/zB0Ig==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/fetch/-/fetch-8.0.3.tgz", + "integrity": "sha512-yUeoVCc/pPicpdU3s+2Vzl7VfLWDUblizRbglQaaXhAawLWOAYu5a/jMoIclN2dJzh5juRPhYowMX82oTG9Y0Q==", "requires": { - "@pnpm/core-loggers": "10.0.0", + "@pnpm/core-loggers": "10.0.3", "@pnpm/fetching-types": "6.0.0", - "@pnpm/network.agent": "^1.0.1", - "@pnpm/types": "10.0.0", + "@pnpm/network.agent": "^2.0.0", + "@pnpm/types": "11.0.0", "@zkochan/retry": "^0.2.0", "node-fetch": "npm:@pnpm/node-fetch@1.0.0" } @@ -17731,15 +17701,15 @@ } }, "@pnpm/git-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.1.tgz", - "integrity": "sha512-B1FtKwEEUm8130XqmX7eqgMhqdBxJ5gPrWssOLnpIlp/rvmJFsfD2P//80OjORPNFWnpfqdfBF34c/+ZCzAxZg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@pnpm/git-resolver/-/git-resolver-9.0.4.tgz", + "integrity": "sha512-k6jglET3h66oLwqoUBslfRWmL6ULXXIHjQoc1uLS0it+m1cI5toHWkrKJOwbI/9K3KQ88EhhulFP4tQQpS+1fg==", "requires": { - "@pnpm/fetch": "8.0.0", - "@pnpm/resolver-base": "12.0.0", - "graceful-git": "^3.1.2", + "@pnpm/fetch": "8.0.3", + "@pnpm/resolver-base": "13.0.0", + "graceful-git": "^4.0.0", "hosted-git-info": "npm:@pnpm/hosted-git-info@1.0.0", - "semver": "^7.6.0" + "semver": "^7.6.2" }, "dependencies": { "hosted-git-info": { @@ -17799,26 +17769,26 @@ } }, "@pnpm/lockfile-file": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.0.5.tgz", - "integrity": "sha512-QQFYohFy39FkAQbtDEtqVIzNu5XZhA9aonh/AM/vwvptNcfnajeBuNKfAJepdjWPg/xSBDuU96So29pkPMK8+Q==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-file/-/lockfile-file-9.1.2.tgz", + "integrity": "sha512-kQxQOTCTt8edqj1EOGzoGO+ef8iZCKN5GHY+KdZ54Mt8LThXVIu9LYTEuNXpaRdu9kH1wpfla5TbUDK0vMEvwg==", "requires": { "@pnpm/constants": "8.0.0", - "@pnpm/dependency-path": "4.0.0", + "@pnpm/dependency-path": "5.1.2", "@pnpm/error": "6.0.1", - "@pnpm/git-resolver": "9.0.1", + "@pnpm/git-resolver": "9.0.4", "@pnpm/git-utils": "2.0.0", - "@pnpm/lockfile-types": "6.0.0", - "@pnpm/lockfile-utils": "10.1.1", - "@pnpm/merge-lockfile-changes": "6.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/lockfile-types": "7.1.2", + "@pnpm/lockfile-utils": "11.0.3", + "@pnpm/merge-lockfile-changes": "6.0.4", + "@pnpm/types": "11.0.0", "@pnpm/util.lex-comparator": "3.0.0", - "@zkochan/rimraf": "^2.1.3", + "@zkochan/rimraf": "^3.0.2", "comver-to-semver": "^1.0.0", "js-yaml": "npm:@zkochan/js-yaml@0.0.7", "normalize-path": "^3.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0", + "semver": "^7.6.2", "sort-keys": "^4.2.0", "strip-bom": "^4.0.0", "write-file-atomic": "^5.0.1" @@ -17858,23 +17828,23 @@ } }, "@pnpm/lockfile-types": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-6.0.0.tgz", - "integrity": "sha512-a4/ULIPLZIIq8Qmi2HEoFgRTtEouGU5RNhuGDxnSmkxu1BjlNMNjLJeEI5jzMZCGOjBoML+AirY/XOO3bcEQ/w==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-types/-/lockfile-types-7.1.2.tgz", + "integrity": "sha512-+64KoK8gtTS5lxslW8ATtwwEbikW4e9i/OV5eaR+X+//5SeUA796uCN96sKu6q6OzpZi3/aVU4VgVe15MT9XKA==", "requires": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "11.0.0" } }, "@pnpm/lockfile-utils": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-10.1.1.tgz", - "integrity": "sha512-Zl5S1WW3Fk8SFjzjuV8jog7VYtPC+RMcsLpvmgFUDyMy/IRG1x2vQ7m3BY1SpmfRLf4XqxACRwKBlbjlRrVY4Q==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@pnpm/lockfile-utils/-/lockfile-utils-11.0.3.tgz", + "integrity": "sha512-HQ3TjUd7TCRovi6wSJ8wcSe1BxXJVs3Hf1msHSZ3Ng1Bwd8rj2mQBNu022u3279Oe1kz35APN0yYciynWWlWkA==", "requires": { - "@pnpm/dependency-path": "4.0.0", - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/dependency-path": "5.1.2", + "@pnpm/lockfile-types": "7.1.2", "@pnpm/pick-fetcher": "3.0.0", - "@pnpm/resolver-base": "12.0.0", - "@pnpm/types": "10.0.0", + "@pnpm/resolver-base": "13.0.0", + "@pnpm/types": "11.0.0", "get-npm-tarball-url": "^2.1.0", "ramda": "npm:@pnpm/ramda@0.28.1" } @@ -17890,23 +17860,24 @@ } }, "@pnpm/merge-lockfile-changes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.0.tgz", - "integrity": "sha512-K9ARTZ+o/EZ10RPZY4dftlSnvPgJrVeOG0QwZLNTb9Z9q8D6EqSVwEh7CxDobGFe5FAj2lkDK6DY7EgPI4hhdw==", + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@pnpm/merge-lockfile-changes/-/merge-lockfile-changes-6.0.4.tgz", + "integrity": "sha512-S15nSd/LPZKLArnMfHpQLgK7MvNYvSs9meb839Eh29pqp2wSPHLKOroK4Upbod6SOrGtihmgjmpLaFNAYschpg==", "requires": { - "@pnpm/lockfile-types": "6.0.0", + "@pnpm/lockfile-types": "7.1.2", + "@pnpm/types": "11.0.0", "comver-to-semver": "^1.0.0", "ramda": "npm:@pnpm/ramda@0.28.1", - "semver": "^7.6.0" + "semver": "^7.6.2" } }, "@pnpm/network.agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-1.0.1.tgz", - "integrity": "sha512-yRm8MzpZvst5IYF5IUgK7q5SvcncCUWOVBqpl527Pz6BafmDlcxAYyFy7lV4AiQr+VZ9VWudQsaHQeaYikyDGw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.agent/-/network.agent-2.0.0.tgz", + "integrity": "sha512-CqONDs5W6vaAdgQEHyFSr4vj25Pv8eVzwI+oUvId/FBHOcTCgHndLIJGON39JnyQS40+yT9kpEj21la3rcJK2w==", "requires": { - "@pnpm/network.config": "1.0.1", - "@pnpm/network.proxy-agent": "1.0.1", + "@pnpm/network.config": "2.0.0", + "@pnpm/network.proxy-agent": "2.0.0", "agentkeepalive": "4.2.1", "lru-cache": "7.10.1" }, @@ -17919,38 +17890,25 @@ } }, "@pnpm/network.config": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-1.0.1.tgz", - "integrity": "sha512-ZmTsSFxd4QT5+IZvwHtQjzSlkB7OXAty6MfSenRyHOvR1f8j3l1VDWVXJiNaiLrKeidiZH6ADfsMTr2N0CGDeA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.config/-/network.config-2.0.0.tgz", + "integrity": "sha512-DpTQTz4KBUgR0NNo/+/WXFlE4dy4+vgINhR9Eb+qo/Kb9RzGbhTN0ypv3sRYa6YG4UO5ft47rvEtHJ9i6VBwzA==", "requires": { "nerf-dart": "^1.0.0" } }, "@pnpm/network.proxy-agent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-1.0.1.tgz", - "integrity": "sha512-0q9Btpw43aTPzEJJmQY1TNBrwNlPINRae8EpO7VpqbmFflBRO6u6qady6XFfbi+wwPxpcpVOYr6rCDBzALXYHA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/network.proxy-agent/-/network.proxy-agent-2.0.0.tgz", + "integrity": "sha512-gCShibUggQS1vveAzr84PhDvwoChR4HrHHdvTB8CqXHQu12eoXO8R01awalZWERrHL3fDkUQcqLqCospm2O/QQ==", "requires": { - "@pnpm/error": "^4.0.0", + "@pnpm/error": "^6.0.0", "http-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.1", "lru-cache": "7.10.1", "socks-proxy-agent": "6.1.1" }, "dependencies": { - "@pnpm/constants": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/@pnpm/constants/-/constants-6.2.0.tgz", - "integrity": "sha512-GlDVUkeTR2WK0oZAM+wtDY6RBMLw6b0Z/5qKgBbDszx4e+R7CHyfG7JofyypogRCfeWXeAXp2C2FkFTh+sNgIg==" - }, - "@pnpm/error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/error/-/error-4.0.1.tgz", - "integrity": "sha512-6UFakGqUDhnZVzYCfN+QaG1epxtBVS1M9mb9RzoBuvWxcimBYTT04fdYuyk1Nay8y/TvAVl3AVB/lCziWG0+2w==", - "requires": { - "@pnpm/constants": "6.2.0" - } - }, "lru-cache": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", @@ -17964,17 +17922,17 @@ "integrity": "sha512-2eisylRAU/jeuxFEPnS1gjLZKJGbYc4QEtEW6MVUYjO4Xi+2ttkSm7825S0J5IPpUIvln8HYPCUS0eQWSfpOaQ==" }, "@pnpm/resolver-base": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-12.0.0.tgz", - "integrity": "sha512-R5FmojIoHRIC8hZDyr6a9SM6TkpAQXQXgq5QrycUwknRvGjTnrOFD5JaTzMZohcfFg6TWdA3sp3B0w/mhj98Rg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-13.0.0.tgz", + "integrity": "sha512-hUAn2OqHEBB3MRLlbvtczI0KdNM9CJgd0hDRuLDrcaVrhZrhHDwgLywls+hWbgNvUpcdMR7k+uEIo+07Vu/Qvg==", "requires": { - "@pnpm/types": "10.0.0" + "@pnpm/types": "11.0.0" } }, "@pnpm/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-10.0.0.tgz", - "integrity": "sha512-P608MRTOExt5BkIN2hsrb/ycEchwaPW/x80ujJUAqxKZSXNVAOrlEu3KJ+2+jTCunyWmo/EcE01ZdwCw8jgVrQ==" + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-11.0.0.tgz", + "integrity": "sha512-BSdk9nlYLHHHLrTFNpmdrXrXVc+1sY/E1Fs1zqR8pY/KjpjVhxkruLZuXitPRPxbk4jSqm7UnG5WCz008iiaig==" }, "@pnpm/util.lex-comparator": { "version": "3.0.0", @@ -18214,12 +18172,9 @@ "integrity": "sha512-WhB+2B/ZPlW2Xy/kMJBrMbqecWXcbDDgn0K0wKBAgO2OlBTz1iLJrRWduo+DGGn0Akvz1Lu4Xvls7dJojximWw==" }, "@zkochan/rimraf": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@zkochan/rimraf/-/rimraf-2.1.3.tgz", - "integrity": "sha512-mCfR3gylCzPC+iqdxEA6z5SxJeOgzgbwmyxanKriIne5qZLswDe/M43aD3p5MNzwzXRhbZg/OX+MpES6Zk1a6A==", - "requires": { - "rimraf": "^3.0.2" - } + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@zkochan/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-GBf4ua7ogWTr7fATnzk/JLowZDBnBJMm8RkMaC/KcvxZ9gxbMWix0/jImd815LmqKyIHZ7h7lADRddGMdGBuCA==" }, "@zkochan/which": { "version": "2.0.3", @@ -18235,9 +18190,9 @@ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==" }, "acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", "dev": true }, "acorn-jsx": { @@ -18256,9 +18211,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -18281,9 +18236,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -18607,9 +18562,9 @@ } }, "bole": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.3.tgz", - "integrity": "sha512-4o8wk9dlpU0e69sXhIsPIaFfXgOvj6en2GgZkG8hadkqNEqYKcz9Y70ijg7Kjq9hz2prJkWXljca5OBJZ451xg==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.13.tgz", + "integrity": "sha512-JQ3xWh2nYsVUuJx7ZN4fzU3vHpzceWb7CC06LUXWwdY++Hzd7Wola7zN3Ud5XgmOVoH/6KzrdMmJokol/xtejw==", "peer": true, "requires": { "fast-safe-stringify": "^2.0.7", @@ -19302,18 +19257,18 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.2.0.tgz", - "integrity": "sha512-0n/I88vZpCOzO+PQpt0lbsqmn9AsnsJAQseIqhZFI8ibQT0U1AkEKRxA3EVMos0BoHSXDQvCXY25TUjB5tr8Og==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz", + "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^3.0.2", - "@eslint/js": "9.2.0", - "@humanwhocodes/config-array": "^0.13.0", + "@eslint/config-array": "^0.17.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.6.0", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.2.3", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.12.4", "chalk": "^4.0.0", @@ -19322,8 +19277,8 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.0.1", "eslint-visitor-keys": "^4.0.0", - "espree": "^10.0.1", - "esquery": "^1.4.2", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -19482,12 +19437,12 @@ "dev": true }, "espree": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", - "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, "requires": { - "acorn": "^8.11.3", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.0.0" }, @@ -19506,9 +19461,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.2.tgz", - "integrity": "sha512-JVSoLdTlTDkmjFmab7H/9SL9qGSyjElT3myyKp7krqjVFQCDLmj1QFaCLRFBszBKI0XVZaiiXvuPIX3ZwHe1Ng==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -19963,12 +19918,19 @@ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "graceful-git": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/graceful-git/-/graceful-git-3.1.2.tgz", - "integrity": "sha512-Xyh9Y43yA23/KQ16mpwO4zkzVGUAXyzuSVZQxw9ddQklssIYIY0el24VYfJBFhyCWGriZPRAB2nCgsDizqna9g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/graceful-git/-/graceful-git-4.0.0.tgz", + "integrity": "sha512-zK/rCH/I0DMKpPBLCElXGI7za3EnXeQFdiK6CTP02Tt1N1L+bMLghZY7cXozlx9M2bx4Q0zrY9ADYP3eI8haIw==", "requires": { - "retry": "^0.12.0", - "safe-execa": "^0.1.0" + "retry": "^0.13.1", + "safe-execa": "^0.1.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==" + } } }, "gunzip-maybe": { @@ -20091,9 +20053,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -20125,9 +20087,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } @@ -26371,9 +26333,9 @@ "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" }, "prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", + "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", "dev": true }, "pretty-format": { @@ -26777,14 +26739,6 @@ "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==" }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, "rsvp": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", @@ -26905,9 +26859,9 @@ }, "dependencies": { "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", "requires": { "ms": "2.1.2" } diff --git a/npm_and_yarn/helpers/package.json b/npm_and_yarn/helpers/package.json index 3e978949edb..3bf1b8c3c1d 100644 --- a/npm_and_yarn/helpers/package.json +++ b/npm_and_yarn/helpers/package.json @@ -11,19 +11,19 @@ }, "dependencies": { "@dependabot/yarn-lib": "^1.22.22", - "@npmcli/arborist": "^7.5.3", + "@npmcli/arborist": "^7.5.4", "detect-indent": "^6.1.0", "nock": "^13.5.4", "npm": "6.14.18", - "@pnpm/lockfile-file": "^9.0.5", - "@pnpm/dependency-path": "^4.0.0", + "@pnpm/lockfile-file": "^9.1.2", + "@pnpm/dependency-path": "^5.1.1", "semver": "^7.6.2", "patch-package": "^8.0.0" }, "devDependencies": { - "eslint": "^9.2.0", + "eslint": "^9.6.0", "eslint-config-prettier": "^9.1.0", "jest": "^29.7.0", - "prettier": "^3.2.5" + "prettier": "^3.3.2" } } diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb index bc107f293be..614c423026f 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater.rb @@ -1,6 +1,8 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + require "dependabot/errors" require "dependabot/logger" require "dependabot/npm_and_yarn/file_parser" @@ -15,9 +17,20 @@ module Dependabot module NpmAndYarn class FileUpdater < Dependabot::FileUpdaters::Base class NpmLockfileUpdater + extend T::Sig + require_relative "npmrc_builder" require_relative "package_json_updater" + sig do + params( + lockfile: Dependabot::DependencyFile, + dependencies: T::Array[Dependabot::Dependency], + dependency_files: T::Array[Dependabot::DependencyFile], + credentials: T::Array[Credential] + ) + .void + end def initialize(lockfile:, dependencies:, dependency_files:, credentials:) @lockfile = lockfile @dependencies = dependencies @@ -25,17 +38,30 @@ def initialize(lockfile:, dependencies:, dependency_files:, credentials:) @credentials = credentials end + sig { returns(Dependabot::DependencyFile) } def updated_lockfile updated_file = lockfile.dup updated_file.content = updated_lockfile_content updated_file end + sig { params(response: Exception).returns(T.noreturn) } + def updated_lockfile_reponse(response) + handle_npm_updater_error(response) + end + private + sig { returns(Dependabot::DependencyFile) } attr_reader :lockfile + + sig { returns(T::Array[Dependabot::Dependency]) } attr_reader :dependencies + + sig { returns(T::Array[Dependabot::DependencyFile]) } attr_reader :dependency_files + + sig { returns(T::Array[Credential]) } attr_reader :credentials UNREACHABLE_GIT = /fatal: repository '(?.*)' not found/ @@ -45,6 +71,9 @@ def updated_lockfile -\sGET\shttps?://(?[^/]+)/(?[^/\s]+)}x MISSING_PACKAGE = %r{(?[^/]+) - Not found} INVALID_PACKAGE = /Can't install (?.*): Missing/ + SOCKET_HANG_UP = /request to (?.*) failed, reason: socket hang up/ + UNABLE_TO_AUTH_NPMRC = /Unable to authenticate, need: Basic, Bearer/ + UNABLE_TO_AUTH_REGISTRY = /Unable to authenticate, need: *.*(Basic|BASIC) *.*realm="(?.*)"/ # TODO: look into fixing this in npm, seems like a bug in the git # downloader introduced in npm 7 @@ -54,44 +83,54 @@ def updated_lockfile NPM8_MISSING_GIT_REF = /already exists and is not an empty directory/ NPM6_MISSING_GIT_REF = /did not match any file\(s\) known to git/ + sig { returns(T.nilable(String)) } def updated_lockfile_content return lockfile.content if npmrc_disables_lockfile? return lockfile.content unless updatable_dependencies.any? - @updated_lockfile_content ||= + @updated_lockfile_content ||= T.let( SharedHelpers.in_a_temporary_directory do write_temporary_dependency_files updated_files = Dir.chdir(lockfile_directory) { run_current_npm_update } updated_lockfile_content = updated_files.fetch(lockfile_basename) post_process_npm_lockfile(updated_lockfile_content) - end + end, + T.nilable(String) + ) rescue SharedHelpers::HelperSubprocessFailed => e handle_npm_updater_error(e) end + sig { returns(T::Array[Dependabot::Dependency]) } def top_level_dependencies dependencies.select(&:top_level?) end + sig { returns(T::Array[Dependabot::Dependency]) } def sub_dependencies dependencies.reject(&:top_level?) end + sig { returns(T::Array[Dependabot::Dependency]) } def updatable_dependencies dependencies.reject do |dependency| dependency_up_to_date?(dependency) || top_level_dependency_update_not_required?(dependency) end end + sig { returns(T::Array[Dependabot::Dependency]) } def lockfile_dependencies - @lockfile_dependencies ||= + @lockfile_dependencies ||= T.let( NpmAndYarn::FileParser.new( dependency_files: [lockfile, *package_files], source: nil, credentials: credentials - ).parse + ).parse, + T.nilable(T::Array[Dependabot::Dependency]) + ) end + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def dependency_up_to_date?(dependency) existing_dep = lockfile_dependencies.find { |dep| dep.name == dependency.name } @@ -101,7 +140,7 @@ def dependency_up_to_date?(dependency) # (likely it is no longer required) return !dependency.top_level? if existing_dep.nil? - existing_dep&.version == dependency.version + existing_dep.version == dependency.version end # NOTE: Prevent changes to npm 6 lockfiles when the dependency has been @@ -109,16 +148,19 @@ def dependency_up_to_date?(dependency) # proj). npm 7 introduces workspace support so we explicitly want to # update the root lockfile and check if the dependency is in the # lockfile + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def top_level_dependency_update_not_required?(dependency) dependency.top_level? && !dependency_in_package_json?(dependency) && !dependency_in_lockfile?(dependency) end + sig { returns(T::Hash[String, String]) } def run_current_npm_update run_npm_updater(top_level_dependencies: top_level_dependencies, sub_dependencies: sub_dependencies) end + sig { returns(T::Hash[String, String]) } def run_previous_npm_update previous_top_level_dependencies = top_level_dependencies.map do |d| Dependabot::Dependency.new( @@ -126,7 +168,7 @@ def run_previous_npm_update package_manager: d.package_manager, version: d.previous_version, previous_version: d.previous_version, - requirements: d.previous_requirements, + requirements: T.must(d.previous_requirements), previous_requirements: d.previous_requirements ) end @@ -146,35 +188,47 @@ def run_previous_npm_update sub_dependencies: previous_sub_dependencies) end + sig do + params( + top_level_dependencies: T::Array[Dependabot::Dependency], + sub_dependencies: T::Array[Dependabot::Dependency] + ) + .returns(T::Hash[String, String]) + end def run_npm_updater(top_level_dependencies:, sub_dependencies:) SharedHelpers.with_git_configured(credentials: credentials) do - updated_files = {} + updated_files = T.let({}, T::Hash[String, String]) if top_level_dependencies.any? updated_files.merge!(run_npm_top_level_updater(top_level_dependencies: top_level_dependencies)) end if sub_dependencies.any? - updated_files.merge!(run_npm_subdependency_updater(sub_dependencies: sub_dependencies)) + updated_files.merge!(T.must(run_npm_subdependency_updater(sub_dependencies: sub_dependencies))) end updated_files end end + sig { params(top_level_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) } def run_npm_top_level_updater(top_level_dependencies:) if npm8? run_npm8_top_level_updater(top_level_dependencies: top_level_dependencies) else - SharedHelpers.run_helper_subprocess( - command: NativeHelpers.helper_path, - function: "npm6:update", - args: [ - Dir.pwd, - lockfile_basename, - top_level_dependencies.map(&:to_h) - ] + T.cast( + SharedHelpers.run_helper_subprocess( + command: NativeHelpers.helper_path, + function: "npm6:update", + args: [ + Dir.pwd, + lockfile_basename, + top_level_dependencies.map(&:to_h) + ] + ), + T::Hash[String, String] ) end end + sig { params(top_level_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) } def run_npm8_top_level_updater(top_level_dependencies:) dependencies_in_current_package_json = top_level_dependencies.any? do |dependency| dependency_in_package_json?(dependency) @@ -186,7 +240,7 @@ def run_npm8_top_level_updater(top_level_dependencies:) # lockfile. To overcome this, we save the content before the update, # and then re-run `npm install` after the update against the previous # content to remove that - previous_package_json = File.read(package_json.name) + previous_package_json = File.read(T.must(package_json).name) end # TODO: Update the npm 6 updater to use these args as we currently @@ -194,10 +248,10 @@ def run_npm8_top_level_updater(top_level_dependencies:) # the npm 7 rollout install_args = top_level_dependencies.map { |dependency| npm_install_args(dependency) } - run_npm_install_lockfile_only(*install_args) + run_npm_install_lockfile_only(install_args) unless dependencies_in_current_package_json - File.write(package_json.name, previous_package_json) + File.write(T.must(package_json).name, previous_package_json) run_npm_install_lockfile_only end @@ -205,24 +259,32 @@ def run_npm8_top_level_updater(top_level_dependencies:) { lockfile_basename => File.read(lockfile_basename) } end + sig do + params(sub_dependencies: T::Array[Dependabot::Dependency]).returns(T.nilable(T::Hash[String, String])) + end def run_npm_subdependency_updater(sub_dependencies:) if npm8? run_npm8_subdependency_updater(sub_dependencies: sub_dependencies) else - SharedHelpers.run_helper_subprocess( - command: NativeHelpers.helper_path, - function: "npm6:updateSubdependency", - args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)] + T.cast( + SharedHelpers.run_helper_subprocess( + command: NativeHelpers.helper_path, + function: "npm6:updateSubdependency", + args: [Dir.pwd, lockfile_basename, sub_dependencies.map(&:to_h)] + ), + T.nilable(T::Hash[String, String]) ) end end + sig { params(sub_dependencies: T::Array[Dependabot::Dependency]).returns(T::Hash[String, String]) } def run_npm8_subdependency_updater(sub_dependencies:) dependency_names = sub_dependencies.map(&:name) NativeHelpers.run_npm8_subdependency_update_command(dependency_names) { lockfile_basename => File.read(lockfile_basename) } end + sig { params(dependency: Dependabot::Dependency).returns(T.nilable(String)) } def updated_version_requirement_for_dependency(dependency) flattenend_manifest_dependencies[dependency.name] end @@ -231,13 +293,14 @@ def updated_version_requirement_for_dependency(dependency) # instead of fishing it out of the updated package json, we need to do # this because we don't store the same requirement in # Dependency#requirements for git dependencies - see PackageJsonUpdater + sig { returns(T::Hash[String, String]) } def flattenend_manifest_dependencies - return @flattenend_manifest_dependencies if defined?(@flattenend_manifest_dependencies) - - @flattenend_manifest_dependencies = + @flattenend_manifest_dependencies ||= T.let( NpmAndYarn::FileParser::DEPENDENCY_TYPES.inject({}) do |deps, type| deps.merge(parsed_package_json[type] || {}) - end + end, + T.nilable(T::Hash[String, String]) + ) end # Runs `npm install` with `--package-lock-only` flag to update the @@ -249,7 +312,8 @@ def flattenend_manifest_dependencies # to work around an issue in npm 6, we don't want that here # - `--ignore-scripts` disables prepare and prepack scripts which are # run when installing git dependencies - def run_npm_install_lockfile_only(*install_args) + sig { params(install_args: T::Array[String]).returns(String) } + def run_npm_install_lockfile_only(install_args = []) command = [ "install", *install_args, @@ -273,6 +337,7 @@ def run_npm_install_lockfile_only(*install_args) Helpers.run_npm_command(command, fingerprint: fingerprint) end + sig { params(dependency: Dependabot::Dependency).returns(String) } def npm_install_args(dependency) git_requirement = dependency.requirements.find { |req| req[:source] && req[:source][:type] == "git" } @@ -302,12 +367,14 @@ def npm_install_args(dependency) end end + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def dependency_in_package_json?(dependency) dependency.requirements.any? do |req| - req[:file] == package_json.name + req[:file] == T.must(package_json).name end end + sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def dependency_in_lockfile?(dependency) lockfile_dependencies.any? do |dep| dep.name == dependency.name @@ -318,13 +385,14 @@ def dependency_in_lockfile?(dependency) # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity # rubocop:disable Metrics/MethodLength + sig { params(error: Exception).returns(T.noreturn) } def handle_npm_updater_error(error) error_message = error.message if error_message.match?(MISSING_PACKAGE) - package_name = error_message.match(MISSING_PACKAGE) - .named_captures["package_req"] - sanitized_name = sanitize_package_name(package_name) - sanitized_error = error_message.gsub(package_name, sanitized_name) + package_name = T.must(error_message.match(MISSING_PACKAGE)) + .named_captures["package_req"] + sanitized_name = sanitize_package_name(T.must(package_name)) + sanitized_error = error_message.gsub(T.must(package_name), sanitized_name) handle_missing_package(sanitized_name, sanitized_error) end @@ -370,26 +438,26 @@ def handle_npm_updater_error(error) end if error_message.match?(FORBIDDEN_PACKAGE) - package_name = error_message.match(FORBIDDEN_PACKAGE) - .named_captures["package_req"] - sanitized_name = sanitize_package_name(package_name) - sanitized_error = error_message.gsub(package_name, sanitized_name) + package_name = T.must(error_message.match(FORBIDDEN_PACKAGE)) + .named_captures["package_req"] + sanitized_name = sanitize_package_name(T.must(package_name)) + sanitized_error = error_message.gsub(T.must(package_name), sanitized_name) handle_missing_package(sanitized_name, sanitized_error) end # Some private registries return a 403 when the user is readonly if error_message.match?(FORBIDDEN_PACKAGE_403) - package_name = error_message.match(FORBIDDEN_PACKAGE_403) - .named_captures["package_req"] - sanitized_name = sanitize_package_name(package_name) - sanitized_error = error_message.gsub(package_name, sanitized_name) + package_name = T.must(error_message.match(FORBIDDEN_PACKAGE_403)) + .named_captures["package_req"] + sanitized_name = sanitize_package_name(T.must(package_name)) + sanitized_error = error_message.gsub(T.must(package_name), sanitized_name) handle_missing_package(sanitized_name, sanitized_error) end if (git_error = error_message.match(UNREACHABLE_GIT) || error_message.match(FORBIDDEN_GIT)) dependency_url = git_error.named_captures.fetch("url") - raise Dependabot::GitDependenciesNotReachable, dependency_url + raise Dependabot::GitDependenciesNotReachable, T.must(dependency_url) end # This error happens when the lockfile has been messed up and some @@ -426,6 +494,23 @@ def handle_npm_updater_error(error) raise Dependabot::DependencyFileNotResolvable, msg end + if (git_source = error_message.match(SOCKET_HANG_UP)) + msg = git_source.named_captures.fetch("url") + raise Dependabot::PrivateSourceTimedOut, T.must(msg) + end + + # Error handled when no authentication info ( _auth = user:pass ) + # is provided in config file (.npmrc) to access private registry + if error_message.match?(UNABLE_TO_AUTH_NPMRC) + msg = "check .npmrc config file" + raise Dependabot::PrivateSourceAuthenticationFailure, msg + end + + if (registry_source = error_message.match(UNABLE_TO_AUTH_REGISTRY)) + msg = registry_source.named_captures.fetch("url") + raise Dependabot::PrivateSourceAuthenticationFailure, msg + end + raise error end # rubocop:enable Metrics/AbcSize @@ -433,6 +518,7 @@ def handle_npm_updater_error(error) # rubocop:enable Metrics/PerceivedComplexity # rubocop:enable Metrics/MethodLength + sig { params(error_message: String).returns(T.noreturn) } def raise_resolvability_error(error_message) dependency_names = dependencies.map(&:name).join(", ") msg = "Error whilst updating #{dependency_names} in " \ @@ -440,6 +526,7 @@ def raise_resolvability_error(error_message) raise Dependabot::DependencyFileNotResolvable, msg end + sig { params(error_message: String).returns(T.noreturn) } def raise_missing_lockfile_version_resolvability_error(error_message) modules_path = File.join(lockfile_directory, "node_modules") # NOTE: don't include the dependency names to prevent opening @@ -456,6 +543,7 @@ def raise_missing_lockfile_version_resolvability_error(error_message) raise Dependabot::DependencyFileNotResolvable, msg end + sig { params(package_name: String, error_message: String).void } def handle_missing_package(package_name, error_message) missing_dep = lockfile_dependencies.find { |dep| dep.name == package_name } @@ -474,10 +562,9 @@ def handle_missing_package(package_name, error_message) raise Dependabot::PrivateSourceAuthenticationFailure, reg end + sig { returns(T::Boolean) } def resolvable_before_update? - return @resolvable_before_update if defined?(@resolvable_before_update) - - @resolvable_before_update = + @resolvable_before_update ||= T.let( begin SharedHelpers.in_a_temporary_directory do write_temporary_dependency_files(update_package_json: false) @@ -487,18 +574,22 @@ def resolvable_before_update? true rescue SharedHelpers::HelperSubprocessFailed false - end + end, + T.nilable(T::Boolean) + ) end + sig { params(error_message: String).returns(T::Boolean) } def dependencies_in_error_message?(error_message) names = dependencies.map { |dep| dep.name.split("/").first } # Example format: No matching version found for # @dependabot/dummy-pkg-b@^1.3.0 names.any? do |name| - error_message.match?(%r{#{Regexp.quote(name)}[\/@]}) + error_message.match?(%r{#{Regexp.quote(T.must(name))}[\/@]}) end end + sig { params(update_package_json: T::Boolean).void } def write_temporary_dependency_files(update_package_json: true) write_lockfiles @@ -508,12 +599,13 @@ def write_temporary_dependency_files(update_package_json: true) path = file.name FileUtils.mkdir_p(Pathname.new(path).dirname) - updated_content = + updated_content = T.must( if update_package_json && top_level_dependencies.any? updated_package_json_content(file) else file.content end + ) package_json_preparer = package_json_preparer(updated_content) @@ -532,6 +624,7 @@ def write_temporary_dependency_files(update_package_json: true) end end + sig { void } def write_lockfiles excluded_lock = case lockfile.name @@ -549,8 +642,9 @@ def write_lockfiles # Takes a JSON string and detects if it is spaces or tabs and how many # levels deep it is indented. + sig { params(json: String).returns(String) } def detect_indentation(json) - indentation = json.scan(/^[[:blank:]]+/).min_by(&:length) + indentation = T.cast(json.scan(/^[[:blank:]]+/).min_by(&:length), T.nilable(String)) return "" if indentation.nil? # let npm set the default if we can't detect any indentation indentation_size = indentation.length @@ -559,6 +653,7 @@ def detect_indentation(json) indentation_type * indentation_size end + sig { params(content: String).returns(String) } def lock_git_deps(content) return content if git_dependencies_to_lock.empty? @@ -574,33 +669,35 @@ def lock_git_deps(content) JSON.pretty_generate(json, indent: indent) end + sig { returns(T::Hash[String, T.untyped]) } def git_dependencies_to_lock return {} unless package_locks.any? return @git_dependencies_to_lock if @git_dependencies_to_lock - @git_dependencies_to_lock = {} + @git_dependencies_to_lock = T.let({}, T.nilable(T::Hash[String, T.untyped])) dependency_names = dependencies.map(&:name) package_locks.each do |package_lock| - parsed_lockfile = JSON.parse(package_lock.content) + parsed_lockfile = JSON.parse(T.must(package_lock.content)) parsed_lockfile.fetch("dependencies", {}).each do |nm, details| next if dependency_names.include?(nm) next unless details["version"] next unless details["version"].start_with?("git") - @git_dependencies_to_lock[nm] = { + T.must(@git_dependencies_to_lock)[nm] = { version: details["version"], from: details["from"] } end end - @git_dependencies_to_lock + T.must(@git_dependencies_to_lock) end # When a package.json version requirement is set to `latest`, npm will # always try to update these dependencies when doing an `npm install`, # regardless of lockfile version. Prevent any unrelated updates by # changing the version requirement to `*` while updating the lockfile. + sig { params(content: String).returns(String) } def lock_deps_with_latest_reqs(content) json = JSON.parse(content) @@ -614,14 +711,17 @@ def lock_deps_with_latest_reqs(content) JSON.pretty_generate(json, indent: indent) end + sig { returns(T::Array[String]) } def git_ssh_requirements_to_swap - return @git_ssh_requirements_to_swap if @git_ssh_requirements_to_swap - - @git_ssh_requirements_to_swap = package_files.flat_map do |file| - package_json_preparer(file.content).swapped_ssh_requirements - end + @git_ssh_requirements_to_swap ||= T.let( + package_files.flat_map do |file| + package_json_preparer(T.must(file.content)).swapped_ssh_requirements + end, + T.nilable(T::Array[String]) + ) end + sig { params(updated_lockfile_content: String).returns(String) } def post_process_npm_lockfile(updated_lockfile_content) # Switch SSH requirements back for git dependencies updated_lockfile_content = replace_swapped_git_ssh_requirements(updated_lockfile_content) @@ -648,6 +748,13 @@ def post_process_npm_lockfile(updated_lockfile_content) replace_tarball_urls(updated_lockfile_content) end + sig do + params( + updated_lockfile_content: String, + parsed_updated_lockfile_content: T::Hash[String, T.untyped] + ) + .returns(String) + end def replace_project_name(updated_lockfile_content, parsed_updated_lockfile_content) current_name = parsed_updated_lockfile_content["name"] original_name = parsed_lockfile["name"] @@ -659,6 +766,13 @@ def replace_project_name(updated_lockfile_content, parsed_updated_lockfile_conte updated_lockfile_content end + sig do + params( + updated_lockfile_content: String, + parsed_updated_lockfile_content: T::Hash[String, T.untyped] + ) + .returns(String) + end def restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_content) return updated_lockfile_content unless npm8? @@ -684,6 +798,14 @@ def restore_packages_name(updated_lockfile_content, parsed_updated_lockfile_cont updated_lockfile_content end + sig do + params( + current_name: String, + original_name: String, + updated_lockfile_content: String + ) + .returns(String) + end def replace_lockfile_name_attribute(current_name, original_name, updated_lockfile_content) updated_lockfile_content.sub( /"name":\s"#{current_name}"/, @@ -691,6 +813,14 @@ def replace_lockfile_name_attribute(current_name, original_name, updated_lockfil ) end + sig do + params( + current_name: String, + original_name: String, + updated_lockfile_content: String + ) + .returns(String) + end def replace_lockfile_packages_name_attribute(current_name, original_name, updated_lockfile_content) packages_key_line = '"": {' updated_lockfile_content.sub( @@ -699,6 +829,7 @@ def replace_lockfile_packages_name_attribute(current_name, original_name, update ) end + sig { params(current_name: String, updated_lockfile_content: String).returns(String) } def remove_lockfile_packages_name_attribute(current_name, updated_lockfile_content) packages_key_line = '"": {' updated_lockfile_content.gsub(/(#{packages_key_line})[\n\s]+"name":\s"#{current_name}",/, '\1') @@ -713,6 +844,13 @@ def remove_lockfile_packages_name_attribute(current_name, updated_lockfile_conte # `package.json` requirement for eslint at `^1.0.0`, in which case we # need to copy this from the manifest to the lockfile after the update # has finished. + sig do + params( + updated_lockfile_content: String, + parsed_updated_lockfile_content: T::Hash[String, T.untyped] + ) + .returns(String) + end def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated_lockfile_content) return updated_lockfile_content unless npm8? @@ -732,6 +870,7 @@ def restore_locked_package_dependencies(updated_lockfile_content, parsed_updated updated_lockfile_content end + sig { params(updated_lockfile_content: String).returns(String) } def replace_swapped_git_ssh_requirements(updated_lockfile_content) git_ssh_requirements_to_swap.each do |req| new_r = req.gsub(%r{git\+ssh://git@(.*?)[:/]}, 'git+https://\1/') @@ -742,6 +881,7 @@ def replace_swapped_git_ssh_requirements(updated_lockfile_content) updated_lockfile_content end + sig { params(updated_lockfile_content: String).returns(String) } def replace_locked_git_dependencies(updated_lockfile_content) # Switch from details back for git dependencies (they will have # changed because we locked them) @@ -766,6 +906,7 @@ def replace_locked_git_dependencies(updated_lockfile_content) updated_lockfile_content end + sig { params(updated_lockfile_content: String).returns(String) } def replace_tarball_urls(updated_lockfile_content) tarball_urls.each do |url| trimmed_url = url.gsub(/(\d+\.)*tgz$/, "") @@ -783,9 +924,10 @@ def replace_tarball_urls(updated_lockfile_content) updated_lockfile_content end + sig { returns(T::Array[String]) } def tarball_urls all_urls = [*package_locks, *shrinkwraps].flat_map do |file| - file.content.scan(/"resolved":\s+"(.*)\"/).flatten + T.must(file.content).scan(/"resolved":\s+"(.*)\"/).flatten end all_urls.uniq! { |url| url.gsub(/(\d+\.)*tgz$/, "") } @@ -800,6 +942,7 @@ def tarball_urls end end + sig { returns(String) } def npmrc_content NpmrcBuilder.new( credentials: credentials, @@ -807,73 +950,106 @@ def npmrc_content ).npmrc_content end + sig { params(file: Dependabot::DependencyFile).returns(T.nilable(String)) } def updated_package_json_content(file) - @updated_package_json_content ||= {} - @updated_package_json_content[file.name] ||= + @updated_package_json_content ||= T.let( + {}, + T.nilable(T::Hash[String, T.nilable(String)]) + ) + @updated_package_json_content[file.name] ||= T.let( PackageJsonUpdater.new( package_json: file, dependencies: top_level_dependencies - ).updated_package_json.content + ).updated_package_json.content, + T.nilable(String) + ) end + sig { params(content: String).returns(Dependabot::NpmAndYarn::FileUpdater::PackageJsonPreparer) } def package_json_preparer(content) - @package_json_preparer ||= {} + @package_json_preparer ||= T.let( + {}, + T.nilable(T::Hash[String, Dependabot::NpmAndYarn::FileUpdater::PackageJsonPreparer]) + ) @package_json_preparer[content] ||= PackageJsonPreparer.new( package_json_content: content ) end + sig { returns(T::Boolean) } def npmrc_disables_lockfile? npmrc_content.match?(/^package-lock\s*=\s*false/) end + sig { returns(T::Boolean) } def npm8? - return @npm8 if defined?(@npm8) + return T.must(@npm8) if defined?(@npm8) - @npm8 = Dependabot::NpmAndYarn::Helpers.npm8?(lockfile) + @npm8 ||= T.let( + Dependabot::NpmAndYarn::Helpers.npm8?(lockfile), + T.nilable(T::Boolean) + ) end + sig { params(package_name: String).returns(String) } def sanitize_package_name(package_name) package_name.gsub("%2f", "/").gsub("%2F", "/") end + sig { returns(String) } def lockfile_directory Pathname.new(lockfile.name).dirname.to_s end + sig { returns(String) } def lockfile_basename Pathname.new(lockfile.name).basename.to_s end + sig { returns(T::Hash[String, T.untyped]) } def parsed_lockfile - @parsed_lockfile ||= JSON.parse(lockfile.content) + @parsed_lockfile ||= T.let( + JSON.parse(T.must(lockfile.content)), + T.nilable(T::Hash[String, T.untyped]) + ) end + sig { returns(T::Hash[String, T.untyped]) } def parsed_package_json return {} unless package_json - return @parsed_package_json if defined?(@parsed_package_json) - @parsed_package_json = JSON.parse(updated_package_json_content(package_json)) + @parsed_package_json ||= T.let( + JSON.parse(T.must(updated_package_json_content(T.must(package_json)))), + T.nilable(T::Hash[String, T.untyped]) + ) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def package_json package_name = lockfile.name.sub(lockfile_basename, "package.json") package_files.find { |f| f.name == package_name } end + sig { returns(T::Array[Dependabot::DependencyFile]) } def package_locks - @package_locks ||= + @package_locks ||= T.let( dependency_files - .select { |f| f.name.end_with?("package-lock.json") } + .select { |f| f.name.end_with?("package-lock.json") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def shrinkwraps - @shrinkwraps ||= + @shrinkwraps ||= T.let( dependency_files - .select { |f| f.name.end_with?("npm-shrinkwrap.json") } + .select { |f| f.name.end_with?("npm-shrinkwrap.json") }, + T.nilable(T::Array[Dependabot::DependencyFile]) + ) end + sig { returns(T::Array[Dependabot::DependencyFile]) } def package_files dependency_files.select { |f| f.name.end_with?("package.json") } end diff --git a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb index 43f11d386c9..6161d906d32 100644 --- a/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb +++ b/npm_and_yarn/lib/dependabot/npm_and_yarn/file_updater/npmrc_builder.rb @@ -214,8 +214,11 @@ def dependency_urls sig { returns(String) } def complete_npmrc_from_credentials + # removes attribute timeout to allow for job update, + # having a timeout=xxxxx value is causing some jobs to fail initial_content = T.must(T.must(npmrc_file).content) - .gsub(/^.*\$\{.*\}.*/, "").strip + "\n" + .gsub(/^.*\$\{.*\}.*/, "").strip.gsub(/^timeout.*/, "").strip + "\n" + return initial_content unless yarn_lock || package_lock return initial_content unless global_registry diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb index f8fa1f9df35..82e38c06ba9 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npm_lockfile_updater_spec.rb @@ -298,6 +298,17 @@ end end + context "with a registry that times out" do + registry_source = "https://registry.npm.com" + let(:files) { project_dependency_files("npm/simple_with_registry_that_times_out") } + let(:error) { Dependabot::PrivateSourceTimedOut.new(registry_source) } + + it "raises a helpful error" do + expect(error.source).to eq(registry_source) + expect(error.message).to eq("The following source timed out: " + registry_source) + end + end + %w(npm6 npm8).each do |npm_version| describe "#{npm_version} updates" do let(:files) { project_dependency_files("#{npm_version}/simple") } @@ -606,6 +617,19 @@ end end + context "with a private registry that is inaccessible due to auth" do + let(:files) { project_dependency_files("npm/simple_with_registry_with_auth") } + let(:npmrc_content) do + { registry: "https://pkgs.dev.azure.com/example/npm/registry/" } + end + let(:error) { Dependabot::PrivateSourceAuthenticationFailure.new(npmrc_content[:registry]) } + + it "raises a helpful error" do + expect(error.source).to eq(npmrc_content[:registry]) + expect(error.to_s).to include(npmrc_content[:registry]) + end + end + context "when updating a git source dependency that is not pinned to a hash" do subject(:parsed_lock_file) { JSON.parse(updated_npm_lock_content) } @@ -647,4 +671,46 @@ .to eq("github:dependabot-fixtures/npm6-dependency") end end + + context "with a private registry that is inaccessible due to missing or invalid auth" do + subject(:updated_npm_lock) { updater.updated_lockfile_reponse(exception) } + + let(:files) { project_dependency_files("npm/simple_with_registry_with_auth") } + let(:exception) { Exception.new(response) } + + context "with a private registry that is missing .npmrc auth info" do + let(:response) { "Unable to authenticate, need: Basic, Bearer" } + + it "raises a helpful error" do + expect { updated_npm_lock }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) + end + end + + context "with a private registry that is inaccessible due to missing Basic auth info" do + let(:response) { "Unable to authenticate, need: Basic realm=\"https://example.pkgs.visualstudio.com/\"" } + + it "raises a helpful error" do + expect { updated_npm_lock }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) + end + end + + context "with a private registry that is inaccessible due to changed auth info" do + let(:response) do + "Unable to authenticate, need: Bearer authorization_uri=https://login.windows.net/....," \ + "Basic realm=\"https://exs.app.pkg1.visualstudio.com/\", TFS-Federated" + end + + it "raises a helpful error" do + expect { updated_npm_lock }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) + end + end + + context "with a private registry that is inaccessible due to missing auth info" do + let(:response) { "Unable to authenticate, need: BASIC realm=\"Repository Manager\"" } + + it "raises a helpful error" do + expect { updated_npm_lock }.to raise_error(Dependabot::PrivateSourceAuthenticationFailure) + end + end + end end diff --git a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npmrc_builder_spec.rb b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npmrc_builder_spec.rb index 5a179a82d19..b972ecd9b18 100644 --- a/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npmrc_builder_spec.rb +++ b/npm_and_yarn/spec/dependabot/npm_and_yarn/file_updater/npmrc_builder_spec.rb @@ -724,6 +724,57 @@ end end end + + context "when dealing with an npmrc file with timeout" do + let(:dependency_files) { project_dependency_files("npm6/npmrc_env_timeout") } + + it "populates the already existing npmrc" do + expect(npmrc_content) + .to eq("legacy-peer-deps=true\n" \ + "loglevel=verbose\n\n" \ + "fetch-retries=3\n" \ + "fetch-retry-maxtimeout=4\n" \ + "fetch-retry-mintimeout=3\n" \ + "fetch-timeout=400000\n\n" \ + "always-auth = true\n" \ + "strict-ssl = true\n" \ + "//npm.fury.io/dependabot/:_authToken=secret_token\n" \ + "registry = https://npm.fury.io/dependabot\n" \ + "//npm.fury.io/dependabot/:_authToken=my_token\n" \ + "always-auth = true\n") + end + + context "with basic auth credentials" do + let(:credentials) do + [Dependabot::Credential.new({ + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "token" + }), Dependabot::Credential.new({ + "type" => "npm_registry", + "registry" => "npm.fury.io/dependabot", + "token" => "secret:token" + })] + end + + it "populates the already existing npmrc" do + expect(npmrc_content) + .to eq("legacy-peer-deps=true\n" \ + "loglevel=verbose\n\n" \ + "fetch-retries=3\n" \ + "fetch-retry-maxtimeout=4\n" \ + "fetch-retry-mintimeout=3\n" \ + "fetch-timeout=400000\n\n" \ + "always-auth = true\n" \ + "strict-ssl = true\n" \ + "//npm.fury.io/dependabot/:_authToken=secret_token\n" \ + "registry = https://npm.fury.io/dependabot\n" \ + "//npm.fury.io/dependabot/:_auth=c2VjcmV0OnRva2Vu\n" \ + "always-auth = true\n") + end + end + end end end diff --git a/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/.npmrc b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/.npmrc new file mode 100644 index 00000000000..aa7e5bb58a1 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/.npmrc @@ -0,0 +1 @@ +registry=https://repo.example.com/repository/npm-js \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/package-lock.json b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/package-lock.json new file mode 100644 index 00000000000..35e8333c504 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "example", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + } + } +} \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/package.json b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/package.json new file mode 100644 index 00000000000..8b08d70ae2e --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_that_times_out/package.json @@ -0,0 +1,18 @@ +{ + "name": "dl-interactive-landscape", + "version": "1.0.0", + "description": "Example", + "engines": { + "npm": ">=3", + "node": ">= 10.5" + }, + "author": "XYZ", + "dependencies": { + "ejs": "3.0.1", + "ejs-loader": "0.3.5" + }, + "repository": { + "type": "git", + "url": "https://git.example.com/abc/xyz" + } +} \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/.npmrc b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/.npmrc new file mode 100644 index 00000000000..7eceb0c5ac4 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/.npmrc @@ -0,0 +1,5 @@ +registry=https://pkgs.dev.azure.com/example/_packaging/PPM@Local/npm/registry/ +always-auth=true +; begin auth token + +; end auth token \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/package-lock.json b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/package-lock.json new file mode 100644 index 00000000000..35e8333c504 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "example", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + } + } +} \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/package.json b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/package.json new file mode 100644 index 00000000000..7253ca47b24 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm/simple_with_registry_with_auth/package.json @@ -0,0 +1,18 @@ +{ + "name": "name", + "version": "1.0.0", + "description": "Example", + "engines": { + "npm": ">=3", + "node": ">= 10.5" + }, + "author": "XYZ", + "dependencies": { + "ejs": "3.0.1", + "ejs-loader": "0.3.5" + }, + "repository": { + "type": "git", + "url": "https://git.example.com/abc/xyz" + } +} \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/.npmrc b/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/.npmrc new file mode 100644 index 00000000000..1a4c9252f11 --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/.npmrc @@ -0,0 +1,11 @@ +legacy-peer-deps=true +loglevel=verbose +timeout=90000 +fetch-retries=3 +fetch-retry-maxtimeout=4 +fetch-retry-mintimeout=3 +fetch-timeout=400000 +_auth = ${NPM_AUTH} +always-auth = true +strict-ssl = true +//npm.fury.io/dependabot/:_authToken=secret_token diff --git a/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/package-lock.json b/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/package-lock.json new file mode 100644 index 00000000000..517187dc86f --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "test", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "encoding": { + "version": "0.1.12", + "resolved": "https://npm.fury.io/dependabot/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "0.4.19" + } + }, + "es6-promise": { + "version": "3.3.1", + "resolved": "https://npm.fury.io/dependabot/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://npm.fury.io/dependabot/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "fetch-factory": { + "version": "0.0.1", + "resolved": "https://npm.fury.io/dependabot/fetch-factory/-/fetch-factory-0.0.1.tgz", + "integrity": "sha1-4AdgWb2zHjFHx1s7jAQTO6jH4HE=", + "requires": { + "es6-promise": "3.3.1", + "isomorphic-fetch": "2.2.1", + "lodash": "3.10.1" + } + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": false, + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://npm.fury.io/dependabot/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "isomorphic-fetch": { + "version": "2.2.1", + "resolved": "https://npm.fury.io/dependabot/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", + "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", + "requires": { + "node-fetch": "1.7.3", + "whatwg-fetch": "2.0.3" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "https://npm.fury.io/dependabot/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + }, + "node-fetch": { + "version": "1.7.3", + "resolved": "https://npm.fury.io/dependabot/node-fetch/-/node-fetch-1.7.3.tgz", + "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "requires": { + "encoding": "0.1.12", + "is-stream": "1.1.0" + } + }, + "whatwg-fetch": { + "version": "2.0.3", + "resolved": "https://npm.fury.io/dependabot/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + } + } + } + \ No newline at end of file diff --git a/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/package.json b/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/package.json new file mode 100644 index 00000000000..9bf13f32a4d --- /dev/null +++ b/npm_and_yarn/spec/fixtures/projects/npm6/npmrc_env_timeout/package.json @@ -0,0 +1,25 @@ +{ + "name": "{{ name }}", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no\\ test\ specified\" && exit 1", + "prettify": "prettier --write \"{{packages/*/src,examples,cypress,scripts}/**/,}*.{js,jsx,ts,tsx,css,md}\"" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/waltfy/PROTO_TEST.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/waltfy/PROTO_TEST/issues" + }, + "homepage": "https://github.com/waltfy/PROTO_TEST#readme", + "dependencies": { + "fetch-factory": "^0.0.1" + }, + "devDependencies": { + "etag" : "^1.0.0" + }} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs new file mode 100644 index 00000000000..44db5bb03dd --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Analyze.cs @@ -0,0 +1,169 @@ +using System.Text; +using System.Xml.Linq; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Test; +using NuGetUpdater.Core.Test.Analyze; +using NuGetUpdater.Core.Test.Update; + +using Xunit; + +namespace NuGetUpdater.Cli.Test; + +using TestFile = (string Path, string Content); + +public partial class EntryPointTests +{ + public class Analyze : AnalyzeWorkerTestBase + { + [Fact] + public async Task FindsUpdatedPackageAndReturnsTheCorrectData() + { + var repositoryXml = XElement.Parse(""""""); + await RunAsync(path => + [ + "analyze", + "--repo-root", + path, + "--discovery-file-path", + Path.Join(path, "discovery.json"), + "--dependency-file-path", + Path.Join(path, "Some.Package.json"), + "--analysis-folder-path", + Path.Join(path, AnalyzeWorker.AnalysisDirectoryName), + "--verbose", + ], + packages: [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0", additionalMetadata: [repositoryXml]), + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.1", "net8.0", additionalMetadata: [repositoryXml]), + ], + dependencyName: "Some.Package", + initialFiles: + [ + ("discovery.json", """ + { + "Path": "", + "IsSuccess": true, + "Projects": [ + { + "FilePath": "project.csproj", + "Dependencies": [ + { + "Name": "Microsoft.NET.Sdk", + "Version": null, + "Type": "MSBuildSdk", + "EvaluationResult": null, + "TargetFrameworks": null, + "IsDevDependency": false, + "IsDirect": false, + "IsTransitive": false, + "IsOverride": false, + "IsUpdate": false + }, + { + "Name": "Some.Package", + "Version": "1.0.0", + "Type": "PackageReference", + "EvaluationResult": { + "ResultType": "Success", + "OriginalValue": "1.0.0", + "EvaluatedValue": "1.0.0", + "RootPropertyName": null, + "ErrorMessage": null + }, + "TargetFrameworks": [ + "net8.0" + ], + "IsDevDependency": false, + "IsDirect": true, + "IsTransitive": false, + "IsOverride": false, + "IsUpdate": false + } + ], + "IsSuccess": true, + "Properties": [ + { + "Name": "TargetFramework", + "Value": "net8.0", + "SourceFilePath": "project.csproj" + } + ], + "TargetFrameworks": [ + "net8.0" + ], + "ReferencedProjectPaths": [] + } + ], + "DirectoryPackagesProps": null, + "GlobalJson": null, + "DotNetToolsJson": null + } + """), + ("Some.Package.json", """ + { + "Name": "Some.Package", + "Version": "1.0.0", + "IsVulnerable": false, + "IgnoredVersions": [], + "Vulnerabilities": [] + } + """), + ("project.csproj", """ + + + net8.0 + + + + + + """), + ], + expectedResult: new() + { + UpdatedVersion = "1.0.1", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = + [ + new Dependency("Some.Package", "1.0.1", DependencyType.Unknown, TargetFrameworks: ["net8.0"], InfoUrl: "https://nuget.example.com/some.package") + ], + } + ); + } + + private static async Task RunAsync(Func getArgs, string dependencyName, TestFile[] initialFiles, ExpectedAnalysisResult expectedResult, MockNuGetPackage[]? packages = null) + { + var actualResult = await RunAnalyzerAsync(dependencyName, initialFiles, async path => + { + var sb = new StringBuilder(); + var writer = new StringWriter(sb); + + var originalOut = Console.Out; + var originalErr = Console.Error; + Console.SetOut(writer); + Console.SetError(writer); + + try + { + await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, path); + var args = getArgs(path); + var result = await Program.Main(args); + if (result != 0) + { + throw new Exception($"Program exited with code {result}.\nOutput:\n\n{sb}"); + } + } + finally + { + Console.SetOut(originalOut); + Console.SetError(originalErr); + } + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs index d51d8178d9f..8ce89b75001 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Discover.cs @@ -1,6 +1,7 @@ using System.Text; using NuGetUpdater.Core; +using NuGetUpdater.Core.Discover; using NuGetUpdater.Core.Test; using NuGetUpdater.Core.Test.Discover; using NuGetUpdater.Core.Test.Update; @@ -25,6 +26,8 @@ await RunAsync(path => path, "--workspace", "path/to/some directory with spaces", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [], initialFiles: @@ -42,7 +45,7 @@ await RunAsync(path => ], expectedResult: new() { - FilePath = "path/to/some directory with spaces", + Path = "path/to/some directory with spaces", Projects = [ new() { @@ -72,6 +75,8 @@ await RunAsync(path => path, "--workspace", "/", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [ @@ -129,7 +134,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -159,6 +164,8 @@ await RunAsync(path => path, "--workspace", "path/to", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [ @@ -193,7 +200,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = "path/to", + Path = "path/to", Projects = [ new() { @@ -224,6 +231,8 @@ await RunAsync(path => path, "--workspace", workspacePath, + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName), ], packages: [ @@ -258,7 +267,7 @@ await RunAsync(path => }, expectedResult: new() { - FilePath = workspacePath, + Path = workspacePath, Projects = [ new() { @@ -282,69 +291,72 @@ await RunAsync(path => public async Task WithDuplicateDependenciesOfDifferentTypes() { await RunAsync(path => - [ - "discover", - "--repo-root", - path, - "--workspace", - "path/to", - ], - new[] - { - ("path/to/my.csproj", """ - - - net8.0 - - - - - - - """), - ("path/Directory.Build.props", """ - - - - - - - - - """) - }, - expectedResult: new() - { - FilePath = "path/to", - Projects = [ - new() - { - FilePath = "my.csproj", - TargetFrameworks = ["net8.0"], - ReferencedProjectPaths = [], - ExpectedDependencyCount = 2, - Dependencies = [ - new("Newtonsoft.Json", "7.0.1", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), - // $(ManagePackageVersionsCentrally) evaluates false by default, we only get a PackageReference - new("System.Text.Json", "8.0.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"]) - ], - Properties = [ - new("TargetFramework", "net8.0", "path/to/my.csproj"), - ], - }, - new() - { - FilePath = "../Directory.Build.props", - ReferencedProjectPaths = [], - ExpectedDependencyCount = 2, - Dependencies = [ - new("System.Text.Json", "8.0.3", DependencyType.PackageReference, IsDirect: true), - new("System.Text.Json", "8.0.3", DependencyType.GlobalPackageReference, IsDirect: true) - ], - Properties = [], - } - ] - }); + [ + "discover", + "--repo-root", + path, + "--workspace", + "path/to", + "--output", + Path.Combine(path, DiscoveryWorker.DiscoveryResultFileName) + ], + new[] + { + ("path/to/my.csproj", """ + + + net8.0 + + + + + + + """), + ("path/Directory.Build.props", """ + + + + + + + + + """) + }, + expectedResult: new() + { + Path = "path/to", + Projects = [ + new() + { + FilePath = "my.csproj", + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, + Dependencies = [ + new("Newtonsoft.Json", "7.0.1", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + // $(ManagePackageVersionsCentrally) evaluates false by default, we only get a PackageReference + new("System.Text.Json", "8.0.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"]) + ], + Properties = [ + new("TargetFramework", "net8.0", "path/to/my.csproj"), + ], + }, + new() + { + FilePath = "../Directory.Build.props", + ReferencedProjectPaths = [], + ExpectedDependencyCount = 2, + Dependencies = [ + new("System.Text.Json", "8.0.3", DependencyType.PackageReference, IsDirect: true), + new("System.Text.Json", "8.0.3", DependencyType.GlobalPackageReference, IsDirect: true) + ], + Properties = [], + } + ] + } + ); } private static async Task RunAsync( diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs index 0bc2f50d8df..280affb9801 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.FrameworkCheck.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - using Xunit; namespace NuGetUpdater.Cli.Test; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs index 4f4a3ee2ce0..a9a5f6c9581 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli.Test/EntryPointTests.Update.cs @@ -44,19 +44,19 @@ await Run(path => Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "my", "my.csproj", "{782E0C0A-10D3-444D-9640-263D03D2B20C}" EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution + GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {782E0C0A-10D3-444D-9640-263D03D2B20C}.Debug|Any CPU.Build.0 = Debug|Any CPU {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.ActiveCfg = Release|Any CPU {782E0C0A-10D3-444D-9640-263D03D2B20C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE - EndGlobalSection + EndGlobalSection EndGlobal """), ("path/to/my.csproj", """ @@ -82,7 +82,7 @@ await Run(path => """) - ], + ], expectedFiles: [ ("path/to/my.csproj", """ @@ -253,7 +253,7 @@ await Run(path => """), ("other-dir/Directory.Build.props", """ - + @@ -271,8 +271,7 @@ await Run(path => """), - ("some-dir/project1/project.csproj", - """ + ("some-dir/project1/project.csproj", """ Exe @@ -300,7 +299,7 @@ await Run(path => """), ("other-dir/Directory.Build.props", """ - + diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs new file mode 100644 index 00000000000..1b227c9aa0d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/AnalyzeCommand.cs @@ -0,0 +1,37 @@ +using System.CommandLine; + +using NuGetUpdater.Core; +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Cli.Commands; + +internal static class AnalyzeCommand +{ + internal static readonly Option RepoRootOption = new("--repo-root") { IsRequired = true }; + internal static readonly Option DependencyFilePathOption = new("--dependency-file-path") { IsRequired = true }; + internal static readonly Option DiscoveryFilePathOption = new("--discovery-file-path") { IsRequired = true }; + internal static readonly Option AnalysisFolderOption = new("--analysis-folder-path") { IsRequired = true }; + internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); + + internal static Command GetCommand(Action setExitCode) + { + Command command = new("analyze", "Determines how to update a dependency based on the workspace discovery information.") + { + RepoRootOption, + DependencyFilePathOption, + DiscoveryFilePathOption, + AnalysisFolderOption, + VerboseOption + }; + + command.TreatUnmatchedTokensAsErrors = true; + + command.SetHandler(async (repoRoot, discoveryPath, dependencyPath, analysisDirectory, verbose) => + { + var worker = new AnalyzeWorker(new Logger(verbose)); + await worker.RunAsync(repoRoot.FullName, discoveryPath.FullName, dependencyPath.FullName, analysisDirectory.FullName); + }, RepoRootOption, DiscoveryFilePathOption, DependencyFilePathOption, AnalysisFolderOption, VerboseOption); + + return command; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs index a08e2f589bb..6bd29c4bba6 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Commands/DiscoverCommand.cs @@ -7,9 +7,9 @@ namespace NuGetUpdater.Cli.Commands; internal static class DiscoverCommand { - internal static readonly Option RepoRootOption = new("--repo-root", () => new DirectoryInfo(Environment.CurrentDirectory)) { IsRequired = false }; + internal static readonly Option RepoRootOption = new("--repo-root") { IsRequired = true }; internal static readonly Option WorkspaceOption = new("--workspace") { IsRequired = true }; - internal static readonly Option OutputOption = new("--output", () => DiscoveryWorker.DiscoveryResultFileName) { IsRequired = false }; + internal static readonly Option OutputOption = new("--output") { IsRequired = true }; internal static readonly Option VerboseOption = new("--verbose", getDefaultValue: () => false); internal static Command GetCommand(Action setExitCode) @@ -27,7 +27,7 @@ internal static Command GetCommand(Action setExitCode) command.SetHandler(async (repoRoot, workspace, outputPath, verbose) => { var worker = new DiscoveryWorker(new Logger(verbose)); - await worker.RunAsync(repoRoot.FullName, workspace, outputPath); + await worker.RunAsync(repoRoot.FullName, workspace, outputPath.FullName); }, RepoRootOption, WorkspaceOption, OutputOption, VerboseOption); return command; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs index 772fb9bf683..ff0235509b4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Cli/Program.cs @@ -15,6 +15,7 @@ internal static async Task Main(string[] args) { FrameworkCheckCommand.GetCommand(setExitCode), DiscoverCommand.GetCommand(setExitCode), + AnalyzeCommand.GetCommand(setExitCode), UpdateCommand.GetCommand(setExitCode), }; command.TreatUnmatchedTokensAsErrors = true; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs new file mode 100644 index 00000000000..bace499ff65 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTestBase.cs @@ -0,0 +1,90 @@ +using System.Collections.Immutable; +using System.Text.Json; + +using NuGetUpdater.Core.Analyze; +using NuGetUpdater.Core.Discover; +using NuGetUpdater.Core.Test.Update; +using NuGetUpdater.Core.Test.Utilities; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +using TestFile = (string Path, string Content); + +public class AnalyzeWorkerTestBase +{ + protected static async Task TestAnalyzeAsync( + WorkspaceDiscoveryResult discovery, + DependencyInfo dependencyInfo, + ExpectedAnalysisResult expectedResult, + MockNuGetPackage[]? packages = null) + { + var relativeDependencyPath = $"./dependabot/dependency/{dependencyInfo.Name}.json"; + + TestFile[] files = [ + (DiscoveryWorker.DiscoveryResultFileName, JsonSerializer.Serialize(discovery, AnalyzeWorker.SerializerOptions)), + (relativeDependencyPath, JsonSerializer.Serialize(dependencyInfo, AnalyzeWorker.SerializerOptions)), + ]; + + var actualResult = await RunAnalyzerAsync(dependencyInfo.Name, files, async directoryPath => + { + await UpdateWorkerTestBase.MockNuGetPackagesInDirectory(packages, directoryPath); + + var discoveryPath = Path.GetFullPath(DiscoveryWorker.DiscoveryResultFileName, directoryPath); + var dependencyPath = Path.GetFullPath(relativeDependencyPath, directoryPath); + var analysisPath = Path.GetFullPath(AnalyzeWorker.AnalysisDirectoryName, directoryPath); + + var worker = new AnalyzeWorker(new Logger(verbose: true)); + await worker.RunAsync(directoryPath, discoveryPath, dependencyPath, analysisPath); + }); + + ValidateAnalysisResult(expectedResult, actualResult); + } + + protected static void ValidateAnalysisResult(ExpectedAnalysisResult expectedResult, AnalysisResult actualResult) + { + Assert.NotNull(actualResult); + Assert.Equal(expectedResult.UpdatedVersion, actualResult.UpdatedVersion); + Assert.Equal(expectedResult.CanUpdate, actualResult.CanUpdate); + Assert.Equal(expectedResult.VersionComesFromMultiDependencyProperty, actualResult.VersionComesFromMultiDependencyProperty); + ValidateDependencies(expectedResult.UpdatedDependencies, actualResult.UpdatedDependencies); + Assert.Equal(expectedResult.ExpectedUpdatedDependenciesCount ?? expectedResult.UpdatedDependencies.Length, actualResult.UpdatedDependencies.Length); + + return; + + void ValidateDependencies(ImmutableArray expectedDependencies, ImmutableArray actualDependencies) + { + if (expectedDependencies.IsDefault) + { + return; + } + + foreach (var expectedDependency in expectedDependencies) + { + var actualDependency = actualDependencies.Single(d => d.Name == expectedDependency.Name); + Assert.Equal(expectedDependency.Name, actualDependency.Name); + Assert.Equal(expectedDependency.Version, actualDependency.Version); + Assert.Equal(expectedDependency.Type, actualDependency.Type); + AssertEx.Equal(expectedDependency.TargetFrameworks, actualDependency.TargetFrameworks); + Assert.Equal(expectedDependency.IsDirect, actualDependency.IsDirect); + Assert.Equal(expectedDependency.IsTransitive, actualDependency.IsTransitive); + Assert.Equal(expectedDependency.InfoUrl, actualDependency.InfoUrl); + } + } + } + + protected static async Task RunAnalyzerAsync(string dependencyName, TestFile[] files, Func action) + { + // write initial files + using var temporaryDirectory = await TemporaryDirectory.CreateWithContentsAsync(files); + + // run discovery + await action(temporaryDirectory.DirectoryPath); + + // gather results + var resultPath = Path.Join(temporaryDirectory.DirectoryPath, AnalyzeWorker.AnalysisDirectoryName, $"{dependencyName}.json"); + var resultJson = await File.ReadAllTextAsync(resultPath); + return JsonSerializer.Deserialize(resultJson, DiscoveryWorker.SerializerOptions)!; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs new file mode 100644 index 00000000000..7fab8c46aca --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/AnalyzeWorkerTests.cs @@ -0,0 +1,304 @@ +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public partial class AnalyzeWorkerTests : AnalyzeWorkerTestBase +{ + [Fact] + public async Task FindsUpdatedVersion() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.0.0", "net8.0"), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.1.0", "net8.0"), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "1.2.0", "net8.0"), // `IgnoredVersions` should prevent this from being selected + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "1.0.0", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "1.0.0", + IgnoredVersions = [Requirement.Parse("> 1.1.0")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "1.1.0", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Some.Package", "1.1.0", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FindsUpdatedPeerDependencies() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "4.0.1", DependencyType.PackageReference), + new("Some.Transitive.Dependency", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [ + new("Some.Package", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Some.Transitive.Dependency", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task DeterminesMultiPropertyVersion() + { + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(SomePackageVersion)", "4.0.1", "SomePackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Transitive.Dependency", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Package", "4.0.1", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Transitive.Dependency", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.9.2", + CanUpdate = true, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [ + new("Some.Package", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + new("Some.Transitive.Dependency", "4.9.2", DependencyType.Unknown, TargetFrameworks: ["net8.0"]), + ], + } + ); + } + + [Fact] + public async Task FailsToUpdateMultiPropertyVersion() + { + // Package.A and Package.B happen to share some versions but would fail to update in sync with each other. + var evaluationResult = new EvaluationResult(EvaluationResultType.Success, "$(TestPackageVersion)", "4.5.0", "TestPackageVersion", ErrorMessage: null); + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Package.A", "4.5.0", "net8.0"), // initial package versions match, purely by accident + MockNuGetPackage.CreateSimplePackage("Package.A", "4.9.2", "net8.0"), // subsequent versions do not match + MockNuGetPackage.CreateSimplePackage("Package.A", "4.9.3", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Package.B", "4.5.0", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Package.B", "4.5.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Package.B", "4.5.2", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Package.A", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + new() + { + FilePath = "./project2.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Package.B", "4.5.0", DependencyType.PackageReference, EvaluationResult: evaluationResult, TargetFrameworks: ["net8.0"]), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Package.A", + Version = "4.5.0", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.5.0", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = true, + UpdatedDependencies = [], + } + ); + } + + + [Fact] + public async Task ReturnsUpToDate_ForMissingVersionProperty() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Transitive.Dependency", "$(MissingPackageVersion)", DependencyType.PackageReference, EvaluationResult: new EvaluationResult(EvaluationResultType.PropertyNotFound, "$(MissingPackageVersion)", "$(MissingPackageVersion)", "$(MissingPackageVersion)", ErrorMessage: null)), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "$(MissingPackageVersion)", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "$(MissingPackageVersion)", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } + + [Fact] + public async Task ReturnsUpToDate_ForMissingDependency() + { + await TestAnalyzeAsync( + packages: + [ + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.0.1", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.0.1]")])]), // initially this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.2", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.2]")])]), // should update to this + MockNuGetPackage.CreateSimplePackage("Some.Package", "4.9.3", "net8.0", [(null, [("Some.Transitive.Dependency", "[4.9.3]")])]), // will not update this far + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.0.1", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.2", "net8.0"), + MockNuGetPackage.CreateSimplePackage("Some.Transitive.Dependency", "4.9.3", "net8.0"), + ], + discovery: new() + { + Path = "/", + Projects = [ + new() + { + FilePath = "./project.csproj", + TargetFrameworks = ["net8.0"], + Dependencies = [ + new("Some.Transitive.Dependency", "4.0.1", DependencyType.PackageReference), + ], + }, + ], + }, + dependencyInfo: new() + { + Name = "Some.Package", + Version = "4.0.1", + IgnoredVersions = [Requirement.Parse("> 4.9.2")], + IsVulnerable = false, + Vulnerabilities = [], + }, + expectedResult: new() + { + UpdatedVersion = "4.0.1", + CanUpdate = false, + VersionComesFromMultiDependencyProperty = false, + UpdatedDependencies = [], + } + ); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs new file mode 100644 index 00000000000..8ad02a925b1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/CompatibilityCheckerTests.cs @@ -0,0 +1,145 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class CompatibilityCheckerTests +{ + [Fact] + public void PerformCheck_CompatiblePackage_IsCompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_IncompatiblePackage_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithPackageFrameworks_IsChecked() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("net462"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_DevDependencyWithoutPackageFrameworks_IsCompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = true; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.True(result); + } + + [Fact] + public void PerformCheck_WithoutPackageFrameworks_IsIncompatibile() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = [ + NuGetFramework.Parse("net6.0"), + NuGetFramework.Parse("netstandard2.0"), + ]; + var isDevDependency = false; + ImmutableArray packageFrameworks = []; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } + + [Fact] + public void PerformCheck_WithoutProjectFrameworks_IsIncompatible() + { + var package = new PackageIdentity("Dependency", NuGetVersion.Parse("1.0.0")); + ImmutableArray projectFrameworks = []; + var isDevDependency = true; + ImmutableArray packageFrameworks = [ + NuGetFramework.Parse("netstandard1.3"), + ]; + + var result = CompatibilityChecker.PerformCheck( + package, + projectFrameworks, + isDevDependency, + packageFrameworks, + new Logger(verbose: false)); + + Assert.False(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs new file mode 100644 index 00000000000..d6302fc70cf --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/ExpectedAnalysisResult.cs @@ -0,0 +1,8 @@ +using NuGetUpdater.Core.Analyze; + +namespace NuGetUpdater.Core.Test.Analyze; + +public record ExpectedAnalysisResult : AnalysisResult +{ + public int? ExpectedUpdatedDependenciesCount { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs new file mode 100644 index 00000000000..f2150ae11f0 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/RequirementTests.cs @@ -0,0 +1,69 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class RequirementTests +{ + // Supported OPs (=, !=, >, <, >=, <=, ~>) + [Theory] + [InlineData("1.0.0", "1.0.0", true)] + [InlineData("1.0.0-alpha", "1.0.0", false)] + [InlineData("1.0.0", "= 1.0.0", true)] + [InlineData("1.0.0-alpha", "= 1.0.0", false)] + [InlineData("1.0.0", "!=1.0.1", true)] + [InlineData("1.0.0", "!= 1.0.0", false)] + [InlineData("1.0.1", "> 1.0.0", true)] + [InlineData("1.0.0-alpha", "> 1.0.0", false)] + [InlineData("1.0.0", "< 1.0.1", true)] + [InlineData("1.0.0", "<1.0.0-alpha", false)] + [InlineData("1.0.0", ">= 1.0.0", true)] + [InlineData("1.0.1", ">= 1.0.0", true)] + [InlineData("1.0.0-alpha", ">= 1.0.0", false)] + [InlineData("1.0.0", "<= 1.0.0", true)] + [InlineData("1.0.0-alpha", "<= 1.0.0", true)] + [InlineData("1.0.1", "<= 1.0.0", false)] + [InlineData("1.0.1", "~>1.0.0", true)] + [InlineData("1.1.0", "~> 1.0.0", false)] + [InlineData("1.1", "~> 1.0", true)] + [InlineData("2.0", "~> 1.0", false)] + [InlineData("1", "~> 1", true)] + [InlineData("2", "~> 1", false)] + public void IsSatisfiedBy(string versionString, string requirementString, bool expected) + { + var version = NuGetVersion.Parse(versionString); + var requirement = Requirement.Parse(requirementString); + + var actual = requirement.IsSatisfiedBy(version); + + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData("> = 1.0.0")] // Invalid format + [InlineData("<>= 1.0.0")] // Invalid Operator + [InlineData(">")] // Missing version + public void Parse_ThrowsForInvalid(string requirementString) + { + Assert.Throws(() => Requirement.Parse(requirementString)); + } + + [Theory] + [InlineData("1.0.0-alpha", "1.1.0.0")] + [InlineData("1.0.0.0", "1.0.1.0")] + [InlineData("1.0.0", "1.1.0.0")] + [InlineData("1.0", "2.0.0.0")] + [InlineData("1", "2.0.0.0")] + public void Bump(string versionString, string expectedString) + { + var version = NuGetVersion.Parse(versionString); + var expected = Version.Parse(expectedString); + + var actual = Requirement.Bump(version); + + Assert.Equal(expected, actual); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs new file mode 100644 index 00000000000..1460e910f6e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/SecurityVulnerabilityExtensionsTests.cs @@ -0,0 +1,78 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class SecurityVulnerabilityExtensionsTests +{ + [Fact] + public void VersionInSafeVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionInVulnerableVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } + + [Fact] + public void VersionNotInVulnerableVersions_IsNotVulnerable() + { + var version = NuGetVersion.Parse("1.0.1"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("<= 1.0.0")], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.False(result); + } + + [Fact] + public void VersionNotInSafeVersions_IsVulnerable() + { + var version = NuGetVersion.Parse("1.0.0"); + var vulnerability = new SecurityVulnerability + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [Requirement.Parse("> 1.0.0")], + VulnerableVersions = [], + }; + + var result = vulnerability.IsVulnerable(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs new file mode 100644 index 00000000000..a1617961929 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Analyze/VersionFinderTests.cs @@ -0,0 +1,193 @@ +using NuGet.Versioning; + +using NuGetUpdater.Core.Analyze; + +using Xunit; + +namespace NuGetUpdater.Core.Test.Analyze; + +public class VersionFinderTests +{ + [Fact] + public void VersionFilter_VersionInIgnoredVersions_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInIgnoredVersions_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [Requirement.Parse("< 1.0.0")], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionInVulnerabilities_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionNotInVulnerabilities_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "0.8.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [new() + { + DependencyName = "Dependency", + PackageManager = "PackageManager", + SafeVersions = [], + VulnerableVersions = [Requirement.Parse("< 1.0.0")], + }], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_VersionLessThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("0.9.0"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_VersionHigherThanCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_PreviewVersionDifferentThanCurrentVersion_ReturnsFalse() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.1-beta"); + + var result = filter(version); + + Assert.False(result); + } + + [Fact] + public void VersionFilter_PreviewVersionSameAsCurrentVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "1.0.0-alpha", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } + + [Fact] + public void VersionFilter_WildcardPreviewVersion_ReturnsTrue() + { + var dependencyInfo = new DependencyInfo + { + Name = "Dependency", + Version = "*-*", + IsVulnerable = false, + IgnoredVersions = [], + Vulnerabilities = [], + }; + var filter = VersionFinder.CreateVersionFilter(dependencyInfo, VersionRange.Parse(dependencyInfo.Version)); + var version = NuGetVersion.Parse("1.0.0-beta"); + + var result = filter(version); + + Assert.True(result); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs index 0f811bf28ac..4e482315ad1 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTestBase.cs @@ -7,7 +7,6 @@ using NuGetUpdater.Core.Test.Utilities; using Xunit; -using Xunit.Sdk; namespace NuGetUpdater.Core.Test.Discover; @@ -35,7 +34,7 @@ protected static async Task TestDiscoveryAsync( protected static void ValidateWorkspaceResult(ExpectedWorkspaceDiscoveryResult expectedResult, WorkspaceDiscoveryResult actualResult) { Assert.NotNull(actualResult); - Assert.Equal(expectedResult.FilePath.NormalizePathToUnix(), actualResult.FilePath.NormalizePathToUnix()); + Assert.Equal(expectedResult.Path.NormalizePathToUnix(), actualResult.Path.NormalizePathToUnix()); ValidateDirectoryPackagesProps(expectedResult.DirectoryPackagesProps, actualResult.DirectoryPackagesProps); ValidateResultWithDependencies(expectedResult.GlobalJson, actualResult.GlobalJson); ValidateResultWithDependencies(expectedResult.DotNetToolsJson, actualResult.DotNetToolsJson); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs index b2dafa70e61..e7804d091b8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.DotNetToolsJson.cs @@ -36,7 +36,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", @@ -80,7 +80,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", DotNetToolsJson = new() { FilePath = ".config/dotnet-tools.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs index fafb99d7ec2..35c252d2943 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.GlobalJson.cs @@ -26,7 +26,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", @@ -60,7 +60,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", GlobalJson = new() { FilePath = "global.json", diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs index e6adca58bee..cebdae1a624 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.PackagesConfig.cs @@ -34,7 +34,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs index 7922a61a531..c4739edf353 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Proj.cs @@ -54,7 +54,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "dependabot", + Path = "dependabot", Projects = [ new() diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs index 07e31184a86..6833e4ff5ad 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.Project.cs @@ -37,7 +37,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -108,7 +108,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 2, Projects = [ new() @@ -189,7 +189,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -299,7 +299,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 5, Projects = [ new() @@ -365,7 +365,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -386,6 +386,59 @@ await TestDiscoveryAsync( ); } + [Fact] + public async Task TargetFrameworkCanBeResolvedFromImplicitlyImportedFile() + { + await TestDiscoveryAsync( + packages: [], + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + $(SomeTfm) + + + + + + """), + ("Directory.Build.props", """ + + + net8.0 + + + """) + ], + expectedResult: new() + { + Path = "", + Projects = [ + new() + { + FilePath = "Directory.Build.props", + Dependencies = [], + }, + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 2, + Dependencies = [ + new("Package.A", "1.2.3", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + ], + Properties = [ + new("SomeTfm", "net8.0", "Directory.Build.props"), + new("TargetFramework", "$(SomeTfm)", "myproj.csproj"), + ], + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + } + ] + } + ); + } + [Fact] public async Task NoDependenciesReturnedIfNoTargetFrameworkCanBeResolved() @@ -407,12 +460,52 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [] } ); } + [Fact] + public async Task PropertyWithWildcardVersionIsRetained() + { + await TestDiscoveryAsync( + packages: [], + workspacePath: "", + files: [ + ("myproj.csproj", """ + + + net8.0 + + + + + + """) + ], + expectedResult: new() + { + Path = "", + Projects = [ + new() + { + FilePath = "myproj.csproj", + ExpectedDependencyCount = 2, + Dependencies = [ + new("Some.Package", "1.*", DependencyType.PackageReference, TargetFrameworks: ["net8.0"], IsDirect: true), + ], + Properties = [ + new("TargetFramework", "net8.0", "myproj.csproj"), + ], + TargetFrameworks = ["net8.0"], + ReferencedProjectPaths = [], + } + ] + } + ); + } + [Fact] public async Task DiscoverReportsTransitivePackageVersionsWithFourPartsForMultipleTargetFrameworks() { @@ -438,7 +531,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "", + Path = "", Projects = [ new() { @@ -493,7 +586,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "test", + Path = "test", Projects = [ new() { @@ -569,7 +662,7 @@ await TestDiscoveryAsync( ], expectedResult: new() { - FilePath = "solutions", + Path = "solutions", Projects = [ new() { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs index bc1c8139056..f35cf224561 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/DiscoveryWorkerTests.cs @@ -33,7 +33,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "src", + Path = "src", Projects = [ new() { @@ -92,7 +92,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "src", + Path = "src", Projects = [ new() { @@ -151,7 +151,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "src", + Path = "src", ExpectedProjectCount = 2, Projects = [ new() @@ -276,7 +276,7 @@ await TestDiscoveryAsync( }, expectedResult: new() { - FilePath = "", + Path = "", ExpectedProjectCount = 2, Projects = [ new() diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs index 4fd2094d7b2..e5d65ad09dd 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Discover/ExpectedDiscoveryResults.cs @@ -4,9 +4,9 @@ namespace NuGetUpdater.Core.Test.Discover; -public record ExpectedWorkspaceDiscoveryResult : IDiscoveryResult +public record ExpectedWorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public int? ExpectedProjectCount { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs index d5dce346346..f1dc2cae175 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/MockNuGetPackage.cs @@ -71,12 +71,18 @@ public void WriteToDirectory(string localPackageSourcePath) /// Creates a mock NuGet package with a single assembly in the appropriate `lib/` directory. The assembly will /// be empty. /// - public static MockNuGetPackage CreateSimplePackage(string id, string version, string targetFramework, (string? TargetFramework, (string Id, string Version)[] Packages)[]? dependencyGroups = null) + public static MockNuGetPackage CreateSimplePackage( + string id, + string version, + string targetFramework, + (string? TargetFramework, (string Id, string Version)[] Packages)[]? dependencyGroups = null, + XElement[]? additionalMetadata = null + ) { return new( id, version, - AdditionalMetadata: null, + AdditionalMetadata: additionalMetadata, DependencyGroups: dependencyGroups, Files: [ diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs index 9397fb8a999..d03c978fdd5 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs @@ -246,12 +246,13 @@ public static async Task MockNuGetPackagesInDirectory(MockNuGetPackage[]? packag } // ensure only the test feed is used + string relativeLocalFeedPath = Path.GetRelativePath(temporaryDirectory, localFeedPath); await File.WriteAllTextAsync(Path.Join(temporaryDirectory, "NuGet.Config"), $""" - + """ diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs index 74c79c1c013..10c3c7a2c61 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs @@ -383,7 +383,7 @@ [new Dependency("Some.Package", "4.5.11", DependencyType.Unknown)] } [Fact] - public async Task AllPackageDependenciesCanBeFoundWithNuGetConfig() + public async Task LocalPackageSourcesAreHonored() { var nugetPackagesDirectory = Environment.GetEnvironmentVariable("NUGET_PACKAGES"); var nugetHttpCacheDirectory = Environment.GetEnvironmentVariable("NUGET_HTTP_CACHE_PATH"); @@ -399,20 +399,21 @@ public async Task AllPackageDependenciesCanBeFoundWithNuGetConfig() Environment.SetEnvironmentVariable("NUGET_HTTP_CACHE_PATH", tempNuGetHttpCacheDirectory); // create two local package sources with different packages available in each - string localSource1 = Path.Combine(temp.DirectoryPath, "localSource1"); + string localSource1 = Path.Combine(temp.DirectoryPath, "local", "source1"); Directory.CreateDirectory(localSource1); - string localSource2 = Path.Combine(temp.DirectoryPath, "localSource2"); + string localSource2 = Path.Combine(temp.DirectoryPath, "local", "source2"); Directory.CreateDirectory(localSource2); - // `Package.A` will only live in `localSource1` and will have a dependency on `Package.B` which is only - // available in `localSource2` + // `Package.A` will only live in `local\source1` and uses Windows-style directory separators and will have + // a dependency on `Package.B` which is only available in `local/source2` and uses Unix-style directory + // separators. MockNuGetPackage.CreateSimplePackage("Package.A", "1.0.0", "net8.0", [(null, [("Package.B", "2.0.0")])]).WriteToDirectory(localSource1); MockNuGetPackage.CreateSimplePackage("Package.B", "2.0.0", "net8.0").WriteToDirectory(localSource2); await File.WriteAllTextAsync(Path.Join(temp.DirectoryPath, "NuGet.Config"), """ - - + + """); diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs new file mode 100644 index 00000000000..5c62a0944ab --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalysisResult.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public record AnalysisResult +{ + public required string UpdatedVersion { get; init; } + public bool CanUpdate { get; init; } + public bool VersionComesFromMultiDependencyProperty { get; init; } + public required ImmutableArray UpdatedDependencies { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs new file mode 100644 index 00000000000..f5e60227c0e --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/AnalyzeWorker.cs @@ -0,0 +1,441 @@ +using System.Collections.Immutable; +using System.Text.Json; +using System.Text.Json.Serialization; + +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core.Discover; + +namespace NuGetUpdater.Core.Analyze; + +using MultiDependency = (string PropertyName, ImmutableArray TargetFrameworks, ImmutableHashSet DependencyNames); + +public partial class AnalyzeWorker +{ + public const string AnalysisDirectoryName = "./.dependabot/analysis"; + + private readonly Logger _logger; + + internal static readonly JsonSerializerOptions SerializerOptions = new() + { + WriteIndented = true, + Converters = { new JsonStringEnumConverter(), new RequirementConverter() }, + }; + + public AnalyzeWorker(Logger logger) + { + _logger = logger; + } + + public async Task RunAsync(string repoRoot, string discoveryPath, string dependencyPath, string analysisDirectory) + { + var discovery = await DeserializeJsonFileAsync(discoveryPath, nameof(WorkspaceDiscoveryResult)); + var dependencyInfo = await DeserializeJsonFileAsync(dependencyPath, nameof(DependencyInfo)); + var startingDirectory = PathHelper.JoinPath(repoRoot, discovery.Path); + + _logger.Log($"Starting analysis of {dependencyInfo.Name}..."); + + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase))) + .ToImmutableArray(); + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + var propertyBasedDependencies = discovery.Projects.SelectMany(p + => p.Dependencies.Where(d => !d.IsTransitive && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + + bool usesMultiDependencyProperty = false; + NuGetVersion? updatedVersion = null; + ImmutableArray updatedDependencies = []; + + bool isUpdateNecessary = IsUpdateNecessary(dependencyInfo, projectsWithDependency); + if (isUpdateNecessary) + { + var nugetContext = new NuGetContext(startingDirectory); + if (!Directory.Exists(nugetContext.TempPackageDirectory)) + { + Directory.CreateDirectory(nugetContext.TempPackageDirectory); + } + + _logger.Log($" Determining multi-dependency property."); + var multiDependencies = DetermineMultiDependencyDetails( + discovery, + dependencyInfo.Name, + propertyBasedDependencies); + + usesMultiDependencyProperty = multiDependencies.Any(md => md.DependencyNames.Count > 1); + var dependenciesToUpdate = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.DependencyNames) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + : [dependencyInfo.Name]; + var applicableTargetFrameworks = usesMultiDependencyProperty + ? multiDependencies + .SelectMany(md => md.TargetFrameworks) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(NuGetFramework.Parse) + .ToImmutableArray() + : projectFrameworks; + + _logger.Log($" Finding updated version."); + updatedVersion = await FindUpdatedVersionAsync( + startingDirectory, + dependencyInfo, + dependenciesToUpdate, + applicableTargetFrameworks, + nugetContext, + _logger, + CancellationToken.None); + + _logger.Log($" Finding updated peer dependencies."); + updatedDependencies = updatedVersion is not null + ? await FindUpdatedDependenciesAsync( + repoRoot, + discovery, + dependenciesToUpdate, + updatedVersion, + nugetContext, + _logger, + CancellationToken.None) + : []; + + //TODO: At this point we should add the peer dependencies to a queue where + // we will analyze them one by one to see if they themselves are part of a + // multi-dependency property. Basically looping this if-body until we have + // emptied the queue and have a complete list of updated dependencies. We + // should track the dependenciesToUpdate as they have already been analyzed. + } + + var result = new AnalysisResult + { + UpdatedVersion = updatedVersion?.ToNormalizedString() ?? dependencyInfo.Version, + CanUpdate = updatedVersion is not null, + VersionComesFromMultiDependencyProperty = usesMultiDependencyProperty, + UpdatedDependencies = updatedDependencies, + }; + + await WriteResultsAsync(analysisDirectory, dependencyInfo.Name, result, _logger); + + _logger.Log($"Analysis complete."); + } + + private static bool IsUpdateNecessary(DependencyInfo dependencyInfo, ImmutableArray projectsWithDependency) + { + if (projectsWithDependency.Length == 0) + { + return false; + } + + // We will even attempt to update transitive dependencies if the dependency is vulnerable. + if (dependencyInfo.IsVulnerable) + { + return true; + } + + // Since the dependency is not vulnerable, we only need to update if it is not transitive. + return projectsWithDependency.Any(p => + p.Dependencies.Any(d => + d.Name.Equals(dependencyInfo.Name, StringComparison.OrdinalIgnoreCase) && + !d.IsTransitive)); + } + + internal static async Task DeserializeJsonFileAsync(string path, string fileType) + { + var json = File.Exists(path) + ? await File.ReadAllTextAsync(path) + : throw new FileNotFoundException($"{fileType} file not found.", path); + + return JsonSerializer.Deserialize(json, SerializerOptions) + ?? throw new InvalidOperationException($"{fileType} file is empty."); + } + + internal static async Task FindUpdatedVersionAsync( + string startingDirectory, + DependencyInfo dependencyInfo, + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + dependencyInfo, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + dependencyInfo.Version, + versionResult, + projectFrameworks, + findLowestVersion: dependencyInfo.IsVulnerable, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + ImmutableArray projectFrameworks, + NuGetVersion version, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionResult = await VersionFinder.GetVersionsAsync( + packageIds.First(), + version, + nugetContext, + logger, + cancellationToken); + + return await FindUpdatedVersionAsync( + packageIds, + version.ToNormalizedString(), + versionResult, + projectFrameworks, + findLowestVersion, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindUpdatedVersionAsync( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + ImmutableArray projectFrameworks, + bool findLowestVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versions = versionResult.GetVersions(); + var orderedVersions = findLowestVersion + ? versions.OrderBy(v => v) // If we are fixing a vulnerability, then we want the lowest version that is safe. + : versions.OrderByDescending(v => v); // If we are just updating versions, then we want the highest version possible. + + return await FindFirstCompatibleVersion( + packageIds, + versionString, + versionResult, + orderedVersions, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + } + + internal static async Task FindFirstCompatibleVersion( + ImmutableHashSet packageIds, + string versionString, + VersionResult versionResult, + IEnumerable orderedVersions, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + if (NuGetVersion.TryParse(versionString, out var currentVersion)) + { + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + currentVersion, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + + if (!isCompatible) + { + // If the current package is incompatible, then don't check for compatibility. + return orderedVersions.First(); + } + } + + foreach (var version in orderedVersions) + { + var existsForAll = await VersionFinder.DoVersionsExistAsync(packageIds, version, nugetContext, logger, cancellationToken); + if (!existsForAll) + { + continue; + } + + var isCompatible = await AreAllPackagesCompatibleAsync( + packageIds, + version, + projectFrameworks, + nugetContext, + logger, + cancellationToken); + + if (isCompatible) + { + return version; + } + } + + // Could not find a compatible version + return null; + } + + internal static async Task AreAllPackagesCompatibleAsync( + ImmutableHashSet packageIds, + NuGetVersion currentVersion, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + var isCompatible = await CompatibilityChecker.CheckAsync( + new(packageId, currentVersion), + projectFrameworks, + nugetContext, + logger, + cancellationToken); + if (!isCompatible) + { + return false; + } + } + + return true; + } + + internal static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + Dependency package, + Logger logger) + { + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + [package], + logger); + result.Add(framework, [.. dependencies]); + } + return result.ToImmutable(); + } + + internal static async Task> FindUpdatedDependenciesAsync( + string repoRoot, + WorkspaceDiscoveryResult discovery, + ImmutableHashSet packageIds, + NuGetVersion updatedVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + // We need to find all projects which have the given dependency. Even in cases that they + // have it transitively may require that peer dependencies be updated in the project. + var projectsWithDependency = discovery.Projects + .Where(p => p.Dependencies.Any(d => packageIds.Contains(d.Name))) + .ToImmutableArray(); + if (projectsWithDependency.Length == 0) + { + return []; + } + + var projectFrameworks = projectsWithDependency + .SelectMany(p => p.TargetFrameworks) + .Distinct() + .Select(NuGetFramework.Parse) + .ToImmutableArray(); + + // When updating peer dependencies, we only need to consider top-level dependencies. + var projectDependencyNames = projectsWithDependency + .SelectMany(p => p.Dependencies) + .Where(d => !d.IsTransitive) + .Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + // Determine updated peer dependencies + var workspacePath = PathHelper.JoinPath(repoRoot, discovery.Path); + // We need any project path so the dependency finder can locate the nuget.config + var projectPath = Path.Combine(workspacePath, projectsWithDependency.First().FilePath); + + // Create distinct list of dependencies taking the highest version of each + var dependencyResult = await DependencyFinder.GetDependenciesAsync( + workspacePath, + projectPath, + projectFrameworks, + packageIds, + updatedVersion, + nugetContext, + logger, + cancellationToken); + + // Filter dependencies by whether any project references them + var dependencies = dependencyResult.GetDependencies() + .Where(d => projectDependencyNames.Contains(d.Name)) + .ToImmutableArray(); + + return dependencies; + } + + internal static ImmutableArray DetermineMultiDependencyDetails( + WorkspaceDiscoveryResult discovery, + string packageId, + ImmutableArray propertyBasedDependencies) + { + var packageDeclarationsUsingProperty = discovery.Projects + .SelectMany(p => + p.Dependencies.Where(d => !d.IsTransitive && + d.Name.Equals(packageId, StringComparison.OrdinalIgnoreCase) && + d.EvaluationResult?.RootPropertyName is not null) + ).ToImmutableArray(); + + return packageDeclarationsUsingProperty + .Select(d => d.EvaluationResult!.RootPropertyName!) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase) + .Select(property => + { + // Find all dependencies that use the same property + var packages = propertyBasedDependencies + .Where(d => property.Equals(d.EvaluationResult?.RootPropertyName, StringComparison.OrdinalIgnoreCase)); + + // Combine all their target frameworks + var tfms = packages.SelectMany(d => d.TargetFrameworks ?? []) + .Distinct() + .ToImmutableArray(); + + var packageIds = packages.Select(d => d.Name) + .ToImmutableHashSet(StringComparer.OrdinalIgnoreCase); + + return (property, tfms, packageIds); + }).ToImmutableArray(); + } + + internal static async Task WriteResultsAsync(string analysisDirectory, string dependencyName, AnalysisResult result, Logger logger) + { + if (!Directory.Exists(analysisDirectory)) + { + Directory.CreateDirectory(analysisDirectory); + } + + var resultPath = Path.Combine(analysisDirectory, $"{dependencyName}.json"); + + logger.Log($" Writing analysis result to [{resultPath}]."); + + var resultJson = JsonSerializer.Serialize(result, SerializerOptions); + await File.WriteAllTextAsync(path: resultPath, resultJson); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs new file mode 100644 index 00000000000..29aa2fa88ba --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/CompatabilityChecker.cs @@ -0,0 +1,177 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +using NuGetUpdater.Core.FrameworkChecker; + +namespace NuGetUpdater.Core.Analyze; + +using PackageInfo = (bool IsDevDependency, ImmutableArray Frameworks); +using PackageReaders = (IAsyncPackageCoreReader CoreReader, IAsyncPackageContentReader ContentReader); + +internal static class CompatibilityChecker +{ + public static async Task CheckAsync( + PackageIdentity package, + ImmutableArray projectFrameworks, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var (isDevDependency, packageFrameworks) = await GetPackageInfoAsync( + package, + nugetContext, + cancellationToken); + + return PerformCheck(package, projectFrameworks, isDevDependency, packageFrameworks, logger); + } + + internal static bool PerformCheck( + PackageIdentity package, + ImmutableArray projectFrameworks, + bool isDevDependency, + ImmutableArray packageFrameworks, + Logger logger) + { + // development dependencies are packages such as analyzers which need to be compatible with the compiler not the + // project itself, but some packages that report themselves as development dependencies still contain target + // framework dependencies and should be checked for compatibility through the regular means + if (isDevDependency && packageFrameworks.Length == 0) + { + return true; + } + + if (packageFrameworks.Length == 0 || projectFrameworks.Length == 0) + { + return false; + } + + var compatibilityService = new FrameworkCompatibilityService(); + var compatibleFrameworks = compatibilityService.GetCompatibleFrameworks(packageFrameworks); + + var incompatibleFrameworks = projectFrameworks.Where(f => !compatibleFrameworks.Contains(f)).ToArray(); + if (incompatibleFrameworks.Length > 0) + { + logger.Log($"The package {package} is not compatible. Incompatible project frameworks: {string.Join(", ", incompatibleFrameworks.Select(f => f.GetShortFolderName()))}"); + return false; + } + + return true; + } + + internal static async Task GetPackageInfoAsync( + PackageIdentity package, + NuGetContext nugetContext, + CancellationToken cancellationToken) + { + var tempPackagePath = GetTempPackagePath(package, nugetContext); + var readers = File.Exists(tempPackagePath) + ? ReadPackage(tempPackagePath) + : await DownloadPackageAsync(package, nugetContext, cancellationToken); + + var nuspecStream = await readers.CoreReader.GetNuspecAsync(cancellationToken); + var reader = new NuspecReader(nuspecStream); + + var isDevDependency = reader.GetDevelopmentDependency(); + + var tfms = reader.GetDependencyGroups() + .Select(d => d.TargetFramework) + .ToImmutableArray(); + if (tfms.Length == 0) + { + // If the nuspec doesn't have any dependency groups, + // try to get the TargetFramework from files in the lib folder. + var libItems = (await readers.ContentReader.GetLibItemsAsync(cancellationToken)).ToList(); + if (libItems.Count == 0) + { + // If there is no lib folder in this package, then assume it is a dev dependency. + isDevDependency = true; + } + + tfms = libItems.Select(item => item.TargetFramework) + .Distinct() + .ToImmutableArray(); + } + + // The interfaces we given are not disposable but the underlying type can be. + // This will ensure we dispose of any resources that need to be cleaned up. + (readers.CoreReader as IDisposable)?.Dispose(); + (readers.ContentReader as IDisposable)?.Dispose(); + + return (isDevDependency, tfms); + } + + internal static PackageReaders ReadPackage(string tempPackagePath) + { + var stream = new FileStream( + tempPackagePath, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + bufferSize: 4096); + PackageArchiveReader archiveReader = new(stream); + return (archiveReader, archiveReader); + } + + internal static async Task DownloadPackageAsync( + PackageIdentity package, + NuGetContext context, + CancellationToken cancellationToken) + { + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(context.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(package.Id).ToHashSet(); + var sources = packageSources.Count == 0 + ? context.PackageSources + : context.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + throw new NotSupportedException($"Failed to get FindPackageByIdResource for {source.SourceUri}"); + } + + var exists = await feed.DoesPackageExistAsync( + package.Id, + package.Version, + context.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + + if (!exists) + { + continue; + } + + var downloader = await feed.GetPackageDownloaderAsync( + package, + context.SourceCacheContext, + context.Logger, + cancellationToken); + + var tempPackagePath = GetTempPackagePath(package, context); + var isDownloaded = await downloader.CopyNupkgFileToAsync(tempPackagePath, cancellationToken); + if (!isDownloaded) + { + throw new Exception($"Failed to download package [{package.Id}/{package.Version}] from [${source.SourceUri}]"); + } + + return (downloader.CoreReader, downloader.ContentReader); + } + + throw new Exception($"Package [{package.Id}/{package.Version}] does not exist in any of the configured sources."); + } + + internal static string GetTempPackagePath(PackageIdentity package, NuGetContext context) + => Path.Combine(context.TempPackageDirectory, package.Id + "." + package.Version + ".nupkg"); +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs new file mode 100644 index 00000000000..87fb53778f3 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyFinder.cs @@ -0,0 +1,47 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static class DependencyFinder +{ + public static async Task>> GetDependenciesAsync( + string workspacePath, + string projectPath, + IEnumerable frameworks, + ImmutableHashSet packageIds, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionString = version.ToNormalizedString(); + var packages = packageIds + .Select(id => new Dependency(id, versionString, DependencyType.Unknown)) + .ToImmutableArray(); + + var result = ImmutableDictionary.CreateBuilder>(); + foreach (var framework in frameworks) + { + var dependencies = await MSBuildHelper.GetAllPackageDependenciesAsync( + workspacePath, + projectPath, + framework.ToString(), + packages, + logger); + var updatedDependencies = new List(); + foreach (var dependency in dependencies) + { + var infoUrl = await nugetContext.GetPackageInfoUrlAsync(dependency.Name, dependency.Version!, cancellationToken); + var updatedDependency = dependency with { IsTransitive = false, InfoUrl = infoUrl }; + updatedDependencies.Add(updatedDependency); + } + + result.Add(framework, updatedDependencies.ToImmutableArray()); + } + + return result.ToImmutable(); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs new file mode 100644 index 00000000000..e04b96e17f1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/DependencyInfo.cs @@ -0,0 +1,12 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record DependencyInfo +{ + public required string Name { get; init; } + public required string Version { get; init; } + public required bool IsVulnerable { get; init; } + public ImmutableArray IgnoredVersions { get; init; } + public ImmutableArray Vulnerabilities { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs new file mode 100644 index 00000000000..8089fd5e78f --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Extensions.cs @@ -0,0 +1,36 @@ +using System.Collections.Immutable; + +using NuGet.Frameworks; +using NuGet.Versioning; + +using NuGetUpdater.Core; + +internal static class Extensions +{ + public static ImmutableArray GetDependencies(this ImmutableDictionary> dependenciesByTfm) + { + Dictionary dependencies = []; + foreach (var (_framework, dependenciesForTfm) in dependenciesByTfm) + { + foreach (var dependency in dependenciesForTfm) + { + if (dependencies.TryGetValue(dependency.Name, out Dependency? value)) + { + if (NuGetVersion.Parse(value.Version!) < NuGetVersion.Parse(dependency.Version!)) + { + dependencies[dependency.Name] = dependency with + { + TargetFrameworks = [.. value.TargetFrameworks ?? [], .. dependency.TargetFrameworks ?? []] + }; + } + } + else + { + dependencies.Add(dependency.Name, dependency); + } + } + } + + return [.. dependencies.Values]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs new file mode 100644 index 00000000000..0d87b417203 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/NuGetContext.cs @@ -0,0 +1,128 @@ +using System.Collections.Immutable; +using System.Text; + +using NuGet.CommandLine; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal record NuGetContext : IDisposable +{ + public SourceCacheContext SourceCacheContext { get; } + public PackageDownloadContext PackageDownloadContext { get; } + public string CurrentDirectory { get; } + public ISettings Settings { get; } + public IMachineWideSettings MachineWideSettings { get; } + public ImmutableArray PackageSources { get; } + public ILogger Logger { get; } + public string TempPackageDirectory { get; } + + public NuGetContext(string? currentDirectory = null, ILogger? logger = null) + { + SourceCacheContext = new SourceCacheContext(); + PackageDownloadContext = new PackageDownloadContext(SourceCacheContext); + CurrentDirectory = currentDirectory ?? Environment.CurrentDirectory; + MachineWideSettings = new CommandLineMachineWideSettings(); + Settings = NuGet.Configuration.Settings.LoadDefaultSettings( + CurrentDirectory, + configFileName: null, + MachineWideSettings); + var sourceProvider = new PackageSourceProvider(Settings); + PackageSources = sourceProvider.LoadPackageSources() + .Where(p => p.IsEnabled) + .ToImmutableArray(); + Logger = logger ?? NullLogger.Instance; + TempPackageDirectory = Path.Combine(Path.GetTempPath(), ".dependabot", "packages"); + } + + public void Dispose() + { + SourceCacheContext.Dispose(); + } + + private readonly Dictionary _packageInfoUrlCache = new(); + + public async Task GetPackageInfoUrlAsync(string packageId, string packageVersion, CancellationToken cancellationToken) + { + var packageIdentity = new PackageIdentity(packageId, NuGetVersion.Parse(packageVersion)); + if (_packageInfoUrlCache.TryGetValue(packageIdentity, out var cachedUrl)) + { + return cachedUrl; + } + + var infoUrl = await FindPackageInfoUrlAsync(packageIdentity, cancellationToken); + _packageInfoUrlCache[packageIdentity] = infoUrl; + + return infoUrl; + } + + private async Task FindPackageInfoUrlAsync(PackageIdentity packageIdentity, CancellationToken cancellationToken) + { + var globalPackagesFolder = SettingsUtility.GetGlobalPackagesFolder(Settings); + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageIdentity.Id).ToHashSet(); + var sources = packageSources.Count == 0 + ? PackageSources + : PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + var message = new StringBuilder(); + message.AppendLine($"finding info url for {packageIdentity}, using package sources: {string.Join(", ", sources.Select(s => s.Name))}"); + + foreach (var source in sources) + { + message.AppendLine($" checking {source.Name}"); + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(cancellationToken); + if (feed is null) + { + message.AppendLine($" feed for {source.Name} was null"); + continue; + } + + var existsInFeed = await feed.Exists( + packageIdentity, + includeUnlisted: false, + SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (!existsInFeed) + { + message.AppendLine($" package {packageIdentity} does not exist in {source.Name}"); + continue; + } + + var downloadResource = await sourceRepository.GetResourceAsync(cancellationToken); + using var downloadResult = await downloadResource.GetDownloadResourceResultAsync(packageIdentity, PackageDownloadContext, globalPackagesFolder, Logger, cancellationToken); + if (downloadResult.Status == DownloadResourceResultStatus.Available) + { + var repositoryMetadata = downloadResult.PackageReader.NuspecReader.GetRepositoryMetadata(); + message.AppendLine($" repometadata: type=[{repositoryMetadata.Type}], url=[{repositoryMetadata.Url}], branch=[{repositoryMetadata.Branch}], commit=[{repositoryMetadata.Commit}]"); + if (!string.IsNullOrEmpty(repositoryMetadata.Url)) + { + return repositoryMetadata.Url; + } + } + else + { + message.AppendLine($" download result status: {downloadResult.Status}"); + } + + var metadataResource = await sourceRepository.GetResourceAsync(cancellationToken); + var metadata = await metadataResource.GetMetadataAsync(packageIdentity, SourceCacheContext, Logger, cancellationToken); + var url = metadata.ProjectUrl ?? metadata.LicenseUrl; + if (url is not null) + { + return url.ToString(); + } + } + + return null; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs new file mode 100644 index 00000000000..63695e787c3 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/Requirement.cs @@ -0,0 +1,105 @@ +using System.Collections.Immutable; + +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +/// +/// A Requirement is a set of one or more version restrictions. It supports a +/// few (=, !=, >, <, >=, <=, ~>) different restriction operators. +/// +/// +/// See Gem::Version for a description on how versions and requirements work +/// together in RubyGems. +/// +public class Requirement +{ + private static readonly ImmutableDictionary> Operators = new Dictionary>() + { + ["="] = (v, r) => v == r, + ["!="] = (v, r) => v != r, + [">"] = (v, r) => v > r, + ["<"] = (v, r) => v < r, + [">="] = (v, r) => v >= r, + ["<="] = (v, r) => v <= r, + ["~>"] = (v, r) => v >= r && v.Version < Bump(r), + }.ToImmutableDictionary(); + + public static Requirement Parse(string requirement) + { + var splitIndex = requirement.LastIndexOfAny(['=', '>', '<']); + + // Throw if the requirement is all operator and no version. + if (splitIndex == requirement.Length - 1) + { + throw new ArgumentException($"`{requirement}` is a invalid requirement string", nameof(requirement)); + } + + string[] parts = splitIndex == -1 + ? [requirement.Trim()] + : [requirement[..(splitIndex + 1)].Trim(), requirement[(splitIndex + 1)..].Trim()]; + + var op = parts.Length == 1 ? "=" : parts[0]; + var version = NuGetVersion.Parse(parts[^1]); + + return new Requirement(op, version); + } + + public string Operator { get; } + public NuGetVersion Version { get; } + + public Requirement(string op, NuGetVersion version) + { + if (!Operators.ContainsKey(op)) + { + throw new ArgumentException("Invalid operator", nameof(op)); + } + + Operator = op; + Version = version; + } + + public override string ToString() + { + return $"{Operator} {Version}"; + } + + public bool IsSatisfiedBy(NuGetVersion version) + { + return Operators[Operator](version, Version); + } + + private static readonly Dictionary BumpMap = []; + /// + /// Return a new version object where the next to the last revision + /// number is one greater (e.g., 5.3.1 => 5.4). + /// + /// + /// This logic intended to be similar to RubyGems Gem::Version#bump + /// + public static Version Bump(NuGetVersion version) + { + if (BumpMap.TryGetValue(version.OriginalVersion!, out var bumpedVersion)) + { + return bumpedVersion; + } + + var versionParts = version.OriginalVersion! // Get the original string this version was created from + .Split('-')[0] // Get the version part without pre-release + .Split('.') // Split into Major.Minor.Patch.Revision if present + .Select(int.Parse) + .ToArray(); + + if (versionParts.Length > 1) + { + versionParts = versionParts[..^1]; // Remove the last part + } + + versionParts[^1]++; // Increment the new last part + + bumpedVersion = NuGetVersion.Parse(string.Join('.', versionParts)).Version; + BumpMap[version.OriginalVersion!] = bumpedVersion; + + return bumpedVersion; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs new file mode 100644 index 00000000000..361643f67fa --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/RequirementConverter.cs @@ -0,0 +1,17 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace NuGetUpdater.Core.Analyze; + +public class RequirementConverter : JsonConverter +{ + public override Requirement? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return Requirement.Parse(reader.GetString()!); + } + + public override void Write(Utf8JsonWriter writer, Requirement value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs new file mode 100644 index 00000000000..e2edb0541c1 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerability.cs @@ -0,0 +1,11 @@ +using System.Collections.Immutable; + +namespace NuGetUpdater.Core.Analyze; + +public sealed record SecurityVulnerability +{ + public required string DependencyName { get; init; } + public required string PackageManager { get; init; } + public required ImmutableArray VulnerableVersions { get; init; } + public required ImmutableArray SafeVersions { get; init; } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs new file mode 100644 index 00000000000..3123e5ac55d --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/SecurityVulnerabilityExtensions.cs @@ -0,0 +1,36 @@ +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +public static class SecurityVulnerabilityExtensions +{ + // This logic taken from Dependabot security_advisory.rb + public static bool IsVulnerable(this SecurityVulnerability vulnerability, NuGetVersion version) + { + var inSafeRange = vulnerability.SafeVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inSafeRange) + { + // If version is known safe for this advisory, it's not vulnerable + return false; + } + + var inVulnerableRange = vulnerability.VulnerableVersions + .Any(r => r.IsSatisfiedBy(version)); + if (inVulnerableRange) + { + // If in the vulnerable range and not known safe, it's vulnerable + return true; + } + + if (vulnerability.VulnerableVersions.Length > 0) + { + // If a vulnerable range present but not met, it's not vulnerable + return false; + } + + // Finally, if no vulnerable range provided, but a safe range provided, + // and this versions isn't included (checked earlier), it's vulnerable + return vulnerability.SafeVersions.Length > 0; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs new file mode 100644 index 00000000000..2ad8a4fb07b --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionFinder.cs @@ -0,0 +1,179 @@ +using System.Collections.Immutable; + +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal static class VersionFinder +{ + public static Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var versionFilter = CreateVersionFilter(currentVersion); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static Task GetVersionsAsync( + DependencyInfo dependencyInfo, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var packageId = dependencyInfo.Name; + var versionRange = VersionRange.Parse(dependencyInfo.Version); + var currentVersion = versionRange.MinVersion!; + var versionFilter = CreateVersionFilter(dependencyInfo, versionRange); + + return GetVersionsAsync(packageId, currentVersion, versionFilter, nugetContext, logger, cancellationToken); + } + + public static async Task GetVersionsAsync( + string packageId, + NuGetVersion currentVersion, + Func versionFilter, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = currentVersion.IsPrerelease; + VersionResult result = new(currentVersion); + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + packageId, + includePrerelease, + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (!existsInFeed) + { + continue; + } + + var feedVersions = (await feed.GetVersions( + packageId, + includePrerelease, + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + CancellationToken.None)).ToHashSet(); + + if (feedVersions.Contains(currentVersion)) + { + result.AddCurrentVersionSource(source); + } + + result.AddRange(source, feedVersions.Where(versionFilter)); + } + + return result; + } + + internal static Func CreateVersionFilter(DependencyInfo dependencyInfo, VersionRange versionRange) + { + // If we are floating to the absolute latest version, we should not filter pre-release versions at all. + var currentVersion = versionRange.Float?.FloatBehavior != NuGetVersionFloatBehavior.AbsoluteLatest + ? versionRange.MinVersion + : null; + + return version => (currentVersion is null || version > currentVersion) + && versionRange.Satisfies(version) + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version) + && !dependencyInfo.IgnoredVersions.Any(r => r.IsSatisfiedBy(version)) + && !dependencyInfo.Vulnerabilities.Any(v => v.IsVulnerable(version)); + } + + internal static Func CreateVersionFilter(NuGetVersion currentVersion) + { + return version => version > currentVersion + && (currentVersion is null || !currentVersion.IsPrerelease || !version.IsPrerelease || version.Version == currentVersion.Version); + } + + public static async Task DoVersionsExistAsync( + IEnumerable packageIds, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + foreach (var packageId in packageIds) + { + if (!await DoesVersionExistAsync(packageId, version, nugetContext, logger, cancellationToken)) + { + return false; + } + } + + return true; + } + + public static async Task DoesVersionExistAsync( + string packageId, + NuGetVersion version, + NuGetContext nugetContext, + Logger logger, + CancellationToken cancellationToken) + { + var includePrerelease = version.IsPrerelease; + + var sourceMapping = PackageSourceMapping.GetPackageSourceMapping(nugetContext.Settings); + var packageSources = sourceMapping.GetConfiguredPackageSources(packageId).ToHashSet(); + var sources = packageSources.Count == 0 + ? nugetContext.PackageSources + : nugetContext.PackageSources + .Where(p => packageSources.Contains(p.Name)) + .ToImmutableArray(); + + foreach (var source in sources) + { + var sourceRepository = Repository.Factory.GetCoreV3(source); + var feed = await sourceRepository.GetResourceAsync(); + if (feed is null) + { + logger.Log($"Failed to get MetadataResource for [{source.Source}]"); + continue; + } + + var existsInFeed = await feed.Exists( + new PackageIdentity(packageId, version), + includeUnlisted: false, + nugetContext.SourceCacheContext, + NullLogger.Instance, + cancellationToken); + if (existsInFeed) + { + return true; + } + } + + return false; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs new file mode 100644 index 00000000000..2c7d304ec28 --- /dev/null +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Analyze/VersionResult.cs @@ -0,0 +1,54 @@ +using System.Collections.Immutable; + +using NuGet.Configuration; +using NuGet.Versioning; + +namespace NuGetUpdater.Core.Analyze; + +internal class VersionResult +{ + private readonly Dictionary> _versions = []; + private readonly List _currentVersionSources = []; + + public NuGetVersion CurrentVersion { get; } + + public VersionResult(NuGetVersion currentVersion) + { + CurrentVersion = currentVersion; + } + + public void AddCurrentVersionSource(PackageSource source) + { + _currentVersionSources.Add(source); + } + + public void AddRange(PackageSource source, IEnumerable versions) + { + foreach (var version in versions) + { + if (_versions.ContainsKey(version)) + { + _versions[version].Add(source); + } + else + { + _versions.Add(version, [source]); + } + } + } + + public ImmutableArray GetPackageSources(NuGetVersion version) + { + if (version == CurrentVersion) + { + return [.. _currentVersionSources]; + } + + return [.. _versions[version]]; + } + + public ImmutableArray GetVersions() + { + return [.. _versions.Keys]; + } +} diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs index 9fcbf8072c4..aff773c6db4 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Dependency.cs @@ -14,7 +14,8 @@ public sealed record Dependency( bool IsDirect = false, bool IsTransitive = false, bool IsOverride = false, - bool IsUpdate = false) : IEquatable + bool IsUpdate = false, + string? InfoUrl = null) : IEquatable { public bool Equals(Dependency? other) { @@ -37,7 +38,8 @@ public bool Equals(Dependency? other) IsDirect == other.IsDirect && IsTransitive == other.IsTransitive && IsOverride == other.IsOverride && - IsUpdate == other.IsUpdate; + IsUpdate == other.IsUpdate && + InfoUrl == other.InfoUrl; } public override int GetHashCode() @@ -53,6 +55,7 @@ public override int GetHashCode() hash.Add(IsTransitive); hash.Add(IsOverride); hash.Add(IsUpdate); + hash.Add(InfoUrl); return hash.ToHashCode(); } } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs index 32177af1cab..521b70f293b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/DiscoveryWorker.cs @@ -39,6 +39,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out workspacePath = workspacePath[1..]; } + string initialWorkspacePath = workspacePath; workspacePath = Path.Combine(repoRootPath, workspacePath); DotNetToolsJsonDiscoveryResult? dotNetToolsJsonDiscovery = null; @@ -75,7 +76,7 @@ public async Task RunAsync(string repoRootPath, string workspacePath, string out var result = new WorkspaceDiscoveryResult { - FilePath = repoRootPath != workspacePath ? Path.GetRelativePath(repoRootPath, workspacePath) : string.Empty, + Path = initialWorkspacePath, DotNetToolsJson = dotNetToolsJsonDiscovery, GlobalJson = globalJsonDiscovery, DirectoryPackagesProps = directoryPackagesPropsDiscovery, diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs index cae5259bb4c..9f38552b529 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Discover/WorkspaceDiscoveryResult.cs @@ -2,9 +2,9 @@ namespace NuGetUpdater.Core.Discover; -public sealed record WorkspaceDiscoveryResult : IDiscoveryResult +public sealed record WorkspaceDiscoveryResult { - public required string FilePath { get; init; } + public required string Path { get; init; } public bool IsSuccess { get; init; } = true; public ImmutableArray Projects { get; init; } public DirectoryPackagesPropsDiscoveryResult? DirectoryPackagesProps { get; init; } diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs index e49f4c9faf3..7457bd20346 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/CompatabilityChecker.cs @@ -1,5 +1,3 @@ -using System.Linq; - using NuGet.Frameworks; namespace NuGetUpdater.Core.FrameworkChecker; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs index e3e6642e159..349df7b7e6e 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/FrameworkCompatibilityService.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using NuGetGallery.Frameworks; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs index 9ded2bf4f2b..429fa041c07 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/FrameworkChecker/SupportedFrameworks.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Collections.Generic; - using NuGet.Frameworks; using static NuGet.Frameworks.FrameworkConstants; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs index 81fde97a042..892d506ef8f 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectManager.cs @@ -1,10 +1,5 @@ extern alias CoreV2; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using System.Xml.Linq; using CoreV2::NuGet.Runtime; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs index 79c57646861..29bdf75dbe2 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/BindingRedirectResolver.cs @@ -1,10 +1,6 @@ extern alias CoreV2; -using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; using System.Reflection; using System.Text.RegularExpressions; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs index 5b8fc4c89d9..3ac4c8c769b 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs @@ -78,8 +78,10 @@ private static async Task DoesDependencyRequireUpdateAsync( tfm, topLevelDependencies, logger); - foreach (var (packageName, packageVersion, _, _, _, _, _, _, _, _) in dependencies) + foreach (var dependency in dependencies) { + var packageName = dependency.Name; + var packageVersion = dependency.Version; if (packageVersion is null) { continue; @@ -263,8 +265,10 @@ private static async Task AddTransitiveDependencyAsync(string projectPath, strin var packagesAndVersions = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var (_, dependencies) in tfmsAndDependencies) { - foreach (var (packageName, packageVersion, _, _, _, _, _, _, _, _) in dependencies) + foreach (var dependency in dependencies) { + var packageName = dependency.Name; + var packageVersion = dependency.Version; if (packagesAndVersions.TryGetValue(packageName, out var existingVersion) && existingVersion != packageVersion) { diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs index 8468cdb8c56..5d7cce01a8c 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/WebApplicationTargetsConditionPatcher.cs @@ -1,7 +1,3 @@ -using System; -using System.IO; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs index 5b9af19eda4..39423fb04c7 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/XmlFilePreAndPostProcessor.cs @@ -1,5 +1,3 @@ -using System; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core.Updater diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs index 41e8051a030..7dc0d277fca 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/HashSetExtensions.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; - namespace NuGetUpdater.Core.Utilities; public static class HashSetExtensions diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs index ec139486e19..ec1f10aef84 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/JsonHelper.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.Encodings.Web; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs index a658bbdfe6c..f4f8eddf3f8 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/Logger.cs @@ -1,6 +1,3 @@ -using System; -using System.IO; - namespace NuGetUpdater.Core; public sealed class Logger diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs index 84366d1d183..f952aaefc3d 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs @@ -231,11 +231,7 @@ public static IEnumerable GetTopLevelPackageDependencyInfos(Immutabl ? evaluationResult.EvaluatedValue.TrimStart('[', '(').TrimEnd(']', ')') : evaluationResult.EvaluatedValue; - // We don't know the version for range requirements or wildcard - // requirements, so return "" for these. - yield return packageVersion.Contains(',') || packageVersion.Contains('*') - ? new Dependency(name, string.Empty, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate) - : new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate); + yield return new Dependency(name, packageVersion, dependencyType, EvaluationResult: evaluationResult, IsUpdate: isUpdate); } } @@ -483,11 +479,13 @@ internal static async Task CreateTempProjectAsync( // if the source is relative to the original location, copy it to the temp directory if (PathHelper.IsSubdirectoryOf(nugetConfigDir!, localSource.Source)) { - string sourceRelativePath = Path.GetRelativePath(nugetConfigDir!, localSource.Source); + // normalize the directory separators and copy the contents + string localSourcePath = localSource.Source.Replace("\\", "/"); + string sourceRelativePath = Path.GetRelativePath(nugetConfigDir!, localSourcePath); string destPath = Path.Join(tempDir.FullName, sourceRelativePath); - if (Directory.Exists(localSource.Source)) + if (Directory.Exists(localSourcePath)) { - PathHelper.CopyDirectory(localSource.Source, destPath); + PathHelper.CopyDirectory(localSourcePath, destPath); } } } @@ -542,6 +540,7 @@ await File.WriteAllTextAsync( true + false diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs index e6ee282cfb9..3810795c3ad 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; - namespace NuGetUpdater.Core; internal static class PathHelper diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs index c7b5da87580..7fd5172e18a 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/ProcessExtensions.cs @@ -1,8 +1,5 @@ -using System; using System.Diagnostics; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace NuGetUpdater.Core; diff --git a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs index 1a38affa7d7..a747bf54a57 100644 --- a/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs +++ b/nuget/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/XmlExtensions.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; - using Microsoft.Language.Xml; namespace NuGetUpdater.Core; diff --git a/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb new file mode 100644 index 00000000000..99b08cffb30 --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/analysis_json_reader.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/dependency" +require "dependabot/nuget/analysis/dependency_analysis" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "json" +require "sorbet-runtime" + +module Dependabot + module Nuget + class AnalysisJsonReader + extend T::Sig + + sig { returns(String) } + def self.temp_directory + File.join(NativeDiscoveryJsonReader.temp_directory, "analysis") + end + + sig { params(dependency_name: String).returns(String) } + def self.analysis_file_path(dependency_name:) + File.join(temp_directory, "#{dependency_name}.json") + end + + sig { params(dependency_name: String).returns(T.nilable(DependencyFile)) } + def self.analysis_json(dependency_name:) + file_path = analysis_file_path(dependency_name: dependency_name) + return unless File.exist?(file_path) + + DependencyFile.new( + name: Pathname.new(file_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(file_path) + ) + end + + sig { params(analysis_json: DependencyFile).void } + def initialize(analysis_json:) + @analysis_json = analysis_json + end + + sig { returns(DependencyAnalysis) } + def dependency_analysis + @dependency_analysis ||= T.let(begin + raise Dependabot::DependencyFileNotParseable, analysis_json.path unless analysis_json.content + + Dependabot.logger.info("#{File.basename(analysis_json.path)} analysis content: #{analysis_json.content}") + + parsed_json = T.let(JSON.parse(T.must(analysis_json.content)), T::Hash[String, T.untyped]) + DependencyAnalysis.from_json(parsed_json) + end, T.nilable(DependencyAnalysis)) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, analysis_json.path + end + + private + + sig { returns(DependencyFile) } + attr_reader :analysis_json + end + end +end diff --git a/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb new file mode 100644 index 00000000000..abb13d946c6 --- /dev/null +++ b/nuget/lib/dependabot/nuget/analysis/dependency_analysis.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/version" +require "sorbet-runtime" + +module Dependabot + module Nuget + class DependencyAnalysis + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(DependencyAnalysis) } + def self.from_json(json) + updated_version = T.let(json.fetch("UpdatedVersion"), String) + can_update = T.let(json.fetch("CanUpdate"), T::Boolean) + version_comes_from_multi_dependency_property = T.let(json.fetch("VersionComesFromMultiDependencyProperty"), + T::Boolean) + updated_dependencies = T.let(json.fetch("UpdatedDependencies"), + T::Array[T::Hash[String, T.untyped]]).map do |dep| + NativeDependencyDetails.from_json(dep) + end + + DependencyAnalysis.new( + updated_version: updated_version, + can_update: can_update, + version_comes_from_multi_dependency_property: version_comes_from_multi_dependency_property, + updated_dependencies: updated_dependencies + ) + end + + sig do + params(updated_version: String, + can_update: T::Boolean, + version_comes_from_multi_dependency_property: T::Boolean, + updated_dependencies: T::Array[NativeDependencyDetails]).void + end + def initialize(updated_version:, can_update:, version_comes_from_multi_dependency_property:, + updated_dependencies:) + @updated_version = updated_version + @can_update = can_update + @version_comes_from_multi_dependency_property = version_comes_from_multi_dependency_property + @updated_dependencies = updated_dependencies + end + + sig { returns(String) } + attr_reader :updated_version + + sig { returns(T::Boolean) } + attr_reader :can_update + + sig { returns(T::Boolean) } + attr_reader :version_comes_from_multi_dependency_property + + sig { returns(T::Array[NativeDependencyDetails]) } + attr_reader :updated_dependencies + + sig { returns(Dependabot::Nuget::Version) } + def numeric_updated_version + @numeric_updated_version ||= T.let(Version.new(updated_version), T.nilable(Dependabot::Nuget::Version)) + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/file_fetcher.rb b/nuget/lib/dependabot/nuget/file_fetcher.rb index 289a0514b0c..f3e0ab40a84 100644 --- a/nuget/lib/dependabot/nuget/file_fetcher.rb +++ b/nuget/lib/dependabot/nuget/file_fetcher.rb @@ -34,15 +34,16 @@ def self.required_files_message end sig do - params( - source: Dependabot::Source, - credentials: T::Array[Credential], - repo_contents_path: T.nilable(String), - options: T::Hash[String, String] - ).void + override + .params( + source: Dependabot::Source, + credentials: T::Array[Credential], + repo_contents_path: T.nilable(String), + options: T::Hash[String, String] + ).void end def initialize(source:, credentials:, repo_contents_path: nil, options: {}) - super(source: source, credentials: credentials, repo_contents_path: repo_contents_path, options: options) + super @sln_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile])) @sln_project_files = T.let(nil, T.nilable(T::Array[Dependabot::DependencyFile])) diff --git a/nuget/lib/dependabot/nuget/file_parser.rb b/nuget/lib/dependabot/nuget/file_parser.rb index e602f458bc3..6b02c39e7ba 100644 --- a/nuget/lib/dependabot/nuget/file_parser.rb +++ b/nuget/lib/dependabot/nuget/file_parser.rb @@ -4,7 +4,7 @@ require "dependabot/dependency" require "dependabot/file_parsers" require "dependabot/file_parsers/base" -require "dependabot/nuget/discovery/discovery_json_reader" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" require "dependabot/nuget/native_helpers" require "sorbet-runtime" @@ -18,39 +18,46 @@ class FileParser < Dependabot::FileParsers::Base require "dependabot/file_parsers/base/dependency_set" require_relative "cache_manager" + sig { returns(T::Hash[String, T::Array[Dependabot::Dependency]]) } + def self.file_dependency_cache + T.let(CacheManager.cache("file_parser.parse"), T::Hash[String, T::Array[Dependabot::Dependency]]) + end + sig { override.returns(T::Array[Dependabot::Dependency]) } def parse return [] unless repo_contents_path - cache = T.let(CacheManager.cache("file_parser.parse"), T::Hash[String, T::Array[Dependabot::Dependency]]) - # key the cache on the dependency files, excluding the content - key = dependency_files.map { |d| d.to_h.except("content") }.to_s - cache[key] ||= begin + key = NativeDiscoveryJsonReader.create_cache_key(dependency_files) + workspace_path = source&.directory || "/" + self.class.file_dependency_cache[key] ||= begin # run discovery for the repo + discovery_json_path = NativeDiscoveryJsonReader.create_discovery_file_path_from_dependency_files( + dependency_files + ) NativeHelpers.run_nuget_discover_tool(repo_root: T.must(repo_contents_path), - workspace_path: source&.directory || "/", - output_path: DiscoveryJsonReader.discovery_file_path, + workspace_path: workspace_path, + output_path: discovery_json_path, credentials: credentials) - discovered_dependencies.dependencies - end - T.must(cache[key]) - end + discovery_json = NativeDiscoveryJsonReader.discovery_json_from_path(discovery_json_path) + return [] unless discovery_json - private + Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") + discovery_json_reader = NativeDiscoveryJsonReader.new( + discovery_json: discovery_json + ) - sig { returns(Dependabot::FileParsers::Base::DependencySet) } - def discovered_dependencies - discovery_json = DiscoveryJsonReader.discovery_json - return DependencySet.new unless discovery_json - - Dependabot.logger.info("Discovery JSON content: #{discovery_json.content}") + # cache discovery results + NativeDiscoveryJsonReader.set_discovery_from_dependency_files(dependency_files: dependency_files, + discovery: discovery_json_reader) + discovery_json_reader.dependency_set.dependencies + end - DiscoveryJsonReader.new( - discovery_json: discovery_json - ).dependency_set + T.must(self.class.file_dependency_cache[key]) end + private + sig { returns(T::Array[Dependabot::DependencyFile]) } def proj_files projfile = /\.proj$/ diff --git a/nuget/lib/dependabot/nuget/file_updater.rb b/nuget/lib/dependabot/nuget/file_updater.rb index b9a40f0e844..10b5d758cea 100644 --- a/nuget/lib/dependabot/nuget/file_updater.rb +++ b/nuget/lib/dependabot/nuget/file_updater.rb @@ -4,9 +4,9 @@ require "dependabot/dependency_file" require "dependabot/file_updaters" require "dependabot/file_updaters/base" -require "dependabot/nuget/discovery/dependency_details" -require "dependabot/nuget/discovery/discovery_json_reader" -require "dependabot/nuget/discovery/workspace_discovery" +require "dependabot/nuget/native_discovery/native_dependency_details" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/native_discovery/native_workspace_discovery" require "dependabot/nuget/native_helpers" require "dependabot/shared_helpers" require "sorbet-runtime" @@ -31,7 +31,7 @@ def self.updated_files_regex sig { override.returns(T::Array[Dependabot::DependencyFile]) } def updated_dependency_files - base_dir = T.must(dependency_files.first).directory + base_dir = "/" SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do dependencies.each do |dependency| try_update_projects(dependency) || try_update_json(dependency) @@ -111,7 +111,7 @@ def call_nuget_updater_tool(dependency, proj_path) # Ideally we should find a way to not run this code in prod # (or a better way to track calls made to NativeHelpers) @update_tooling_calls ||= T.let({}, T.nilable(T::Hash[String, Integer])) - key = proj_path + dependency.name + key = "#{proj_path.delete_prefix(T.must(repo_contents_path))}+#{dependency.name}" @update_tooling_calls[key] = if @update_tooling_calls[key] T.must(@update_tooling_calls[key]) + 1 @@ -126,18 +126,10 @@ def testonly_update_tooling_calls @update_tooling_calls end - sig { returns(T.nilable(WorkspaceDiscovery)) } + sig { returns(T.nilable(NativeWorkspaceDiscovery)) } def workspace - @workspace ||= T.let(begin - discovery_json = DiscoveryJsonReader.discovery_json - if discovery_json - workspace = DiscoveryJsonReader.new( - discovery_json: discovery_json - ).workspace_discovery - end - - workspace - end, T.nilable(WorkspaceDiscovery)) + discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files) + discovery_json_reader.workspace_discovery end sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[String]) } @@ -145,17 +137,20 @@ def referenced_project_paths(project_file) workspace&.projects&.find { |p| p.file_path == project_file.name }&.referenced_project_paths || [] end - sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[DependencyDetails]) } + sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[NativeDependencyDetails]) } def project_dependencies(project_file) - workspace&.projects&.find { |p| p.file_path == project_file.name }&.dependencies || [] + workspace&.projects&.find do |p| + full_project_file_path = File.join(project_file.directory, project_file.name) + p.file_path == full_project_file_path + end&.dependencies || [] end - sig { returns(T::Array[DependencyDetails]) } + sig { returns(T::Array[NativeDependencyDetails]) } def global_json_dependencies workspace&.global_json&.dependencies || [] end - sig { returns(T::Array[DependencyDetails]) } + sig { returns(T::Array[NativeDependencyDetails]) } def dotnet_tools_json_dependencies workspace&.dotnet_tools_json&.dependencies || [] end @@ -164,13 +159,15 @@ def dotnet_tools_json_dependencies sig { params(dependency_file: Dependabot::DependencyFile, updated_content: String).returns(String) } def normalize_content(dependency_file, updated_content) # Fix up line endings - if dependency_file.content&.include?("\r\n") && updated_content.match?(/(? "application/json" } - ) - return unless response.status == 200 + source_url = dependency_source_url + return Source.from_url(source_url) if source_url - # Extract the query url e.g. https://nuget.pkg.github.com/ORG/query - search_base = extract_search_url(response.body) - return unless search_base - - response = Dependabot::RegistryClient.get( - url: search_base + "?q=#{dependency.name.downcase}&prerelease=true&semVerLevel=2.0.0", - headers: { **auth_header, "Accept" => "application/json" } - ) - return unless response.status == 200 - - # Find a projectUrl or licenseUrl that look like a source URL - extract_source_repo(response.body) - rescue JSON::ParserError - # Ignored, this is expected for some registries that don't handle these request. - end - - sig { params(body: String).returns(T.nilable(String)) } - def extract_search_url(body) - JSON.parse(body) - .fetch("resources", []) - .find { |r| r.fetch("@type") == "SearchQueryService" } - &.fetch("@id") - end - - sig { params(body: String).returns(T.nilable(Dependabot::Source)) } - def extract_source_repo(body) - JSON.parse(body).fetch("data", []).each do |search_result| - next unless search_result["id"].casecmp(dependency.name).zero? - - if search_result.key?("projectUrl") - source = Source.from_url(search_result.fetch("projectUrl")) - return source if source - end - if search_result.key?("licenseUrl") - source = Source.from_url(search_result.fetch("licenseUrl")) - return source if source - end - end - # failed to find a source URL nil end - sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(Dependabot::Source)) } - def look_up_source_in_nuspec(nuspec) - potential_source_urls = [ - nuspec.at_css("package > metadata > repository") - &.attribute("url")&.value, - nuspec.at_css("package > metadata > repository > url")&.content, - nuspec.at_css("package > metadata > projectUrl")&.content, - nuspec.at_css("package > metadata > licenseUrl")&.content - ].compact - - source_url = potential_source_urls.find { |url| Source.from_url(url) } - source_url ||= source_from_anywhere_in_nuspec(nuspec) - - Source.from_url(source_url) - end - - sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(String)) } - def source_from_anywhere_in_nuspec(nuspec) - github_urls = [] - nuspec.to_s.force_encoding(Encoding::UTF_8) - .scan(Source::SOURCE_REGEX) do - github_urls << Regexp.last_match.to_s - end - - github_urls.find do |url| - repo = T.must(Source.from_url(url)).repo - repo.downcase.end_with?(dependency.name.downcase) - end - end - - sig { returns(T.nilable(Nokogiri::XML::Document)) } - def dependency_nuspec_file - return @dependency_nuspec_file unless @dependency_nuspec_file.nil? - - return if dependency_nuspec_url.nil? - - response = Dependabot::RegistryClient.get( - url: T.must(dependency_nuspec_url), - headers: auth_header - ) - - @dependency_nuspec_file = Nokogiri::XML(response.body) - end - - sig { returns(T.nilable(String)) } - def dependency_nuspec_url - source = dependency.requirements - .find { |r| r.fetch(:source) }&.fetch(:source) - - source.fetch(:nuspec_url) if source&.key?(:nuspec_url) - end - sig { returns(T.nilable(String)) } def dependency_source_url source = dependency.requirements @@ -164,32 +32,6 @@ def dependency_source_url source.fetch("source_url") end - - # rubocop:disable Metrics/PerceivedComplexity - sig { returns(T::Hash[String, String]) } - def auth_header - source = dependency.requirements - .find { |r| r.fetch(:source) }&.fetch(:source) - url = source&.fetch(:url, nil) || source&.fetch("url") - - token = credentials - .select { |cred| cred["type"] == "nuget_feed" } - .find { |cred| cred["url"] == url } - &.fetch("token", nil) - - return {} unless token - - if token.include?(":") - encoded_token = Base64.encode64(token).delete("\n") - { "Authorization" => "Basic #{encoded_token}" } - elsif Base64.decode64(token).ascii_only? && - Base64.decode64(token).include?(":") - { "Authorization" => "Basic #{token.delete("\n")}" } - else - { "Authorization" => "Bearer #{token}" } - end - end - # rubocop:enable Metrics/PerceivedComplexity end end end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb new file mode 100644 index 00000000000..94cdecf0dab --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_details.rb @@ -0,0 +1,102 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_evaluation_details" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeDependencyDetails + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(NativeDependencyDetails) } + def self.from_json(json) + name = T.let(json.fetch("Name"), String) + version = T.let(json.fetch("Version"), T.nilable(String)) + type = T.let(json.fetch("Type"), String) + evaluation = NativeEvaluationDetails + .from_json(T.let(json.fetch("EvaluationResult"), T.nilable(T::Hash[String, T.untyped]))) + target_frameworks = T.let(json.fetch("TargetFrameworks"), T.nilable(T::Array[String])) + is_dev_dependency = T.let(json.fetch("IsDevDependency"), T::Boolean) + is_direct = T.let(json.fetch("IsDirect"), T::Boolean) + is_transitive = T.let(json.fetch("IsTransitive"), T::Boolean) + is_override = T.let(json.fetch("IsOverride"), T::Boolean) + is_update = T.let(json.fetch("IsUpdate"), T::Boolean) + info_url = T.let(json.fetch("InfoUrl"), T.nilable(String)) + + NativeDependencyDetails.new(name: name, + version: version, + type: type, + evaluation: evaluation, + target_frameworks: target_frameworks, + is_dev_dependency: is_dev_dependency, + is_direct: is_direct, + is_transitive: is_transitive, + is_override: is_override, + is_update: is_update, + info_url: info_url) + end + + sig do + params(name: String, + version: T.nilable(String), + type: String, + evaluation: T.nilable(NativeEvaluationDetails), + target_frameworks: T.nilable(T::Array[String]), + is_dev_dependency: T::Boolean, + is_direct: T::Boolean, + is_transitive: T::Boolean, + is_override: T::Boolean, + is_update: T::Boolean, + info_url: T.nilable(String)).void + end + def initialize(name:, version:, type:, evaluation:, target_frameworks:, is_dev_dependency:, is_direct:, + is_transitive:, is_override:, is_update:, info_url:) + @name = name + @version = version + @type = type + @evaluation = evaluation + @target_frameworks = target_frameworks + @is_dev_dependency = is_dev_dependency + @is_direct = is_direct + @is_transitive = is_transitive + @is_override = is_override + @is_update = is_update + @info_url = info_url + end + + sig { returns(String) } + attr_reader :name + + sig { returns(T.nilable(String)) } + attr_reader :version + + sig { returns(String) } + attr_reader :type + + sig { returns(T.nilable(NativeEvaluationDetails)) } + attr_reader :evaluation + + sig { returns(T.nilable(T::Array[String])) } + attr_reader :target_frameworks + + sig { returns(T::Boolean) } + attr_reader :is_dev_dependency + + sig { returns(T::Boolean) } + attr_reader :is_direct + + sig { returns(T::Boolean) } + attr_reader :is_transitive + + sig { returns(T::Boolean) } + attr_reader :is_override + + sig { returns(T::Boolean) } + attr_reader :is_update + + sig { returns(T.nilable(String)) } + attr_reader :info_url + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb new file mode 100644 index 00000000000..5e4fe8e25a9 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_dependency_file_discovery.rb @@ -0,0 +1,129 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_dependency_details" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeDependencyFileDiscovery + extend T::Sig + + sig do + params(json: T.nilable(T::Hash[String, T.untyped]), + directory: String).returns(T.nilable(NativeDependencyFileDiscovery)) + end + def self.from_json(json, directory) + return nil if json.nil? + + file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| + NativeDependencyDetails.from_json(dep) + end + + NativeDependencyFileDiscovery.new(file_path: file_path, + dependencies: dependencies) + end + + sig do + params(file_path: String, + dependencies: T::Array[NativeDependencyDetails]).void + end + def initialize(file_path:, dependencies:) + @file_path = file_path + @dependencies = dependencies + end + + sig { returns(String) } + attr_reader :file_path + + sig { returns(T::Array[NativeDependencyDetails]) } + attr_reader :dependencies + + sig { overridable.returns(Dependabot::FileParsers::Base::DependencySet) } + def dependency_set # rubocop:disable Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize + dependency_set = Dependabot::FileParsers::Base::DependencySet.new + + file_name = Pathname.new(file_path).cleanpath.to_path + dependencies.each do |dependency| + next if dependency.name.casecmp("Microsoft.NET.Sdk")&.zero? + + # If the version string was evaluated it must have been successfully resolved + if dependency.evaluation && dependency.evaluation&.result_type != "Success" + logger.warn "Dependency '#{dependency.name}' excluded due to unparsable version: #{dependency.version}" + next + end + + # Exclude any dependencies using version ranges or wildcards + next if dependency.version&.include?(",") || + dependency.version&.include?("*") + + # Exclude any dependencies specified using interpolation + next if dependency.name.include?("%(") || + dependency.version&.include?("%(") + + # Exclude any dependencies which reference an item type + next if dependency.name.include?("@(") + + dependency_file_name = file_name + if dependency.type == "PackagesConfig" + dir_name = File.dirname(file_name) + dependency_file_name = "packages.config" + dependency_file_name = File.join(dir_name, "packages.config") unless dir_name == "." + end + + dependency_set << build_dependency(dependency_file_name, dependency) + end + + dependency_set + end + + private + + sig { returns(::Logger) } + def logger + Dependabot.logger + end + + sig { params(file_name: String, dependency_details: NativeDependencyDetails).returns(Dependabot::Dependency) } + def build_dependency(file_name, dependency_details) + requirement = build_requirement(file_name, dependency_details) + requirements = requirement.nil? ? [] : [requirement] + + version = dependency_details.version&.gsub(/[\(\)\[\]]/, "")&.strip + version = nil if version&.empty? + + Dependency.new( + name: dependency_details.name, + version: version, + package_manager: "nuget", + requirements: requirements + ) + end + + sig do + params(file_name: String, dependency_details: NativeDependencyDetails) + .returns(T.nilable(T::Hash[Symbol, T.untyped])) + end + def build_requirement(file_name, dependency_details) + return if dependency_details.is_transitive + + version = dependency_details.version + version = nil if version&.empty? + + requirement = { + requirement: version, + file: file_name, + groups: [dependency_details.is_dev_dependency ? "devDependencies" : "dependencies"], + source: nil + } + + property_name = dependency_details.evaluation&.root_property_name + return requirement unless property_name + + requirement[:metadata] = { property_name: property_name } + requirement + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_directory_packages_props_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_directory_packages_props_discovery.rb new file mode 100644 index 00000000000..8e66873225e --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_directory_packages_props_discovery.rb @@ -0,0 +1,44 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_dependency_details" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeDirectoryPackagesPropsDiscovery < NativeDependencyFileDiscovery + extend T::Sig + + sig do + override.params(json: T.nilable(T::Hash[String, T.untyped]), + directory: String).returns(T.nilable(NativeDirectoryPackagesPropsDiscovery)) + end + def self.from_json(json, directory) + return nil if json.nil? + + file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) + is_transitive_pinning_enabled = T.let(json.fetch("IsTransitivePinningEnabled"), T::Boolean) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).map do |dep| + NativeDependencyDetails.from_json(dep) + end + + NativeDirectoryPackagesPropsDiscovery.new(file_path: file_path, + is_transitive_pinning_enabled: is_transitive_pinning_enabled, + dependencies: dependencies) + end + + sig do + params(file_path: String, + is_transitive_pinning_enabled: T::Boolean, + dependencies: T::Array[NativeDependencyDetails]).void + end + def initialize(file_path:, is_transitive_pinning_enabled:, dependencies:) + super(file_path: file_path, dependencies: dependencies) + @is_transitive_pinning_enabled = is_transitive_pinning_enabled + end + + sig { returns(T::Boolean) } + attr_reader :is_transitive_pinning_enabled + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb b/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb new file mode 100644 index 00000000000..26f53fbaff9 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_discovery_json_reader.rb @@ -0,0 +1,174 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/dependency" +require "dependabot/nuget/native_discovery/native_workspace_discovery" +require "json" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeDiscoveryJsonReader + extend T::Sig + + sig { returns(T::Hash[String, NativeDiscoveryJsonReader]) } + def self.discovery_result_cache + T.let(CacheManager.cache("discovery_json_cache"), T::Hash[String, NativeDiscoveryJsonReader]) + end + + sig { returns(T::Hash[String, String]) } + def self.discovery_path_cache + T.let(CacheManager.cache("discovery_path_cache"), T::Hash[String, String]) + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(NativeDiscoveryJsonReader) + end + def self.get_discovery_from_dependency_files(dependency_files) + key = create_cache_key(dependency_files) + discovery_json = discovery_result_cache[key] + raise "No discovery result for specified dependency files: #{key}" unless discovery_json + + discovery_json + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile], + discovery: NativeDiscoveryJsonReader + ).void + end + def self.set_discovery_from_dependency_files(dependency_files:, discovery:) + key = create_cache_key(dependency_files) + discovery_result_cache[key] = discovery + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(String) + end + def self.get_discovery_file_path_from_dependency_files(dependency_files) + key = create_cache_key(dependency_files) + discovery_path = discovery_path_cache[key] + raise "No discovery path found for specified dependency files: #{key}" unless discovery_path + + discovery_path + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(String) + end + def self.create_discovery_file_path_from_dependency_files(dependency_files) + discovery_key = create_cache_key(dependency_files) + if discovery_path_cache[discovery_key] + raise "Discovery file path already exists for the given dependency files: #{discovery_key}" + end + + discovery_counter_cache = T.let(CacheManager.cache("discovery_counter_cache"), T::Hash[String, Integer]) + counter_key = "counter" + current_counter = discovery_counter_cache[counter_key] || 0 + current_counter += 1 + discovery_counter_cache[counter_key] = current_counter + incremeted_discovery_file_path = File.join(temp_directory, "discovery.#{current_counter}.json") + discovery_path_cache[discovery_key] = incremeted_discovery_file_path + incremeted_discovery_file_path + end + + # this is a test-only method + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).void + end + def self.clear_discovery_file_path_from_cache(dependency_files) + key = create_cache_key(dependency_files) + discovery_file_path = discovery_path_cache[key] + File.delete(discovery_file_path) if discovery_file_path && File.exist?(discovery_file_path) + discovery_path_cache.delete(key) + end + + sig do + params( + dependency_files: T::Array[Dependabot::DependencyFile] + ).returns(String) + end + def self.create_cache_key(dependency_files) + dependency_files.map { |d| d.to_h.except("content") }.to_s + end + + sig { returns(String) } + def self.temp_directory + File.join(Dir.tmpdir, ".dependabot") + end + + sig do + params( + discovery_json_path: String + ).returns(T.nilable(DependencyFile)) + end + def self.discovery_json_from_path(discovery_json_path) + return unless File.exist?(discovery_json_path) + + DependencyFile.new( + name: Pathname.new(discovery_json_path).cleanpath.to_path, + directory: temp_directory, + type: "file", + content: File.read(discovery_json_path) + ) + end + + sig { returns(T.nilable(NativeWorkspaceDiscovery)) } + attr_reader :workspace_discovery + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + attr_reader :dependency_set + + sig { params(discovery_json: DependencyFile).void } + def initialize(discovery_json:) + @discovery_json = discovery_json + @workspace_discovery = T.let(read_workspace_discovery, T.nilable(Dependabot::Nuget::NativeWorkspaceDiscovery)) + @dependency_set = T.let(read_dependency_set, Dependabot::FileParsers::Base::DependencySet) + end + + private + + sig { returns(DependencyFile) } + attr_reader :discovery_json + + sig { returns(T.nilable(NativeWorkspaceDiscovery)) } + def read_workspace_discovery + return nil unless discovery_json.content + + parsed_json = T.let(JSON.parse(T.must(discovery_json.content)), T::Hash[String, T.untyped]) + NativeWorkspaceDiscovery.from_json(parsed_json) + rescue JSON::ParserError + raise Dependabot::DependencyFileNotParseable, discovery_json.path + end + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def read_dependency_set + dependency_set = Dependabot::FileParsers::Base::DependencySet.new + return dependency_set unless workspace_discovery + + workspace_result = T.must(workspace_discovery) + workspace_result.projects.each do |project| + dependency_set += project.dependency_set + end + if workspace_result.directory_packages_props + dependency_set += T.must(workspace_result.directory_packages_props).dependency_set + end + if workspace_result.dotnet_tools_json + dependency_set += T.must(workspace_result.dotnet_tools_json).dependency_set + end + dependency_set += T.must(workspace_result.global_json).dependency_set if workspace_result.global_json + + dependency_set + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb b/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb new file mode 100644 index 00000000000..37e13d67ad5 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_evaluation_details.rb @@ -0,0 +1,63 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeEvaluationDetails + extend T::Sig + + sig { params(json: T.nilable(T::Hash[String, T.untyped])).returns(T.nilable(NativeEvaluationDetails)) } + def self.from_json(json) + return nil if json.nil? + + result_type = T.let(json.fetch("ResultType"), String) + original_value = T.let(json.fetch("OriginalValue"), String) + evaluated_value = T.let(json.fetch("EvaluatedValue"), String) + root_property_name = T.let(json.fetch("RootPropertyName", nil), T.nilable(String)) + error_message = T.let(json.fetch("ErrorMessage", nil), T.nilable(String)) + + NativeEvaluationDetails.new(result_type: result_type, + original_value: original_value, + evaluated_value: evaluated_value, + root_property_name: root_property_name, + error_message: error_message) + end + + sig do + params(result_type: String, + original_value: String, + evaluated_value: String, + root_property_name: T.nilable(String), + error_message: T.nilable(String)).void + end + def initialize(result_type:, + original_value:, + evaluated_value:, + root_property_name:, + error_message:) + @result_type = result_type + @original_value = original_value + @evaluated_value = evaluated_value + @root_property_name = root_property_name + @error_message = error_message + end + + sig { returns(String) } + attr_reader :result_type + + sig { returns(String) } + attr_reader :original_value + + sig { returns(String) } + attr_reader :evaluated_value + + sig { returns(T.nilable(String)) } + attr_reader :root_property_name + + sig { returns(T.nilable(String)) } + attr_reader :error_message + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb new file mode 100644 index 00000000000..fc92becc047 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_project_discovery.rb @@ -0,0 +1,82 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_dependency_details" +require "dependabot/nuget/native_discovery/native_property_details" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeProjectDiscovery < NativeDependencyFileDiscovery + extend T::Sig + + sig do + override.params(json: T.nilable(T::Hash[String, T.untyped]), + directory: String).returns(T.nilable(NativeProjectDiscovery)) + end + def self.from_json(json, directory) + return nil if json.nil? + + file_path = File.join(directory, T.let(json.fetch("FilePath"), String)) + properties = T.let(json.fetch("Properties"), T::Array[T::Hash[String, T.untyped]]).map do |prop| + NativePropertyDetails.from_json(prop) + end + target_frameworks = T.let(json.fetch("TargetFrameworks"), T::Array[String]) + referenced_project_paths = T.let(json.fetch("ReferencedProjectPaths"), T::Array[String]) + dependencies = T.let(json.fetch("Dependencies"), T::Array[T::Hash[String, T.untyped]]).filter_map do |dep| + details = NativeDependencyDetails.from_json(dep) + next unless details.version # can't do anything without a version + + version = T.must(details.version) + next unless version.length.positive? # can't do anything with an empty version + + next if version.include? "," # can't do anything with a range + + next if version.include? "*" # can't do anything with a wildcard + + details + end + + NativeProjectDiscovery.new(file_path: file_path, + properties: properties, + target_frameworks: target_frameworks, + referenced_project_paths: referenced_project_paths, + dependencies: dependencies) + end + + sig do + params(file_path: String, + properties: T::Array[NativePropertyDetails], + target_frameworks: T::Array[String], + referenced_project_paths: T::Array[String], + dependencies: T::Array[NativeDependencyDetails]).void + end + def initialize(file_path:, properties:, target_frameworks:, referenced_project_paths:, dependencies:) + super(file_path: file_path, dependencies: dependencies) + @properties = properties + @target_frameworks = target_frameworks + @referenced_project_paths = referenced_project_paths + end + + sig { returns(T::Array[NativePropertyDetails]) } + attr_reader :properties + + sig { returns(T::Array[String]) } + attr_reader :target_frameworks + + sig { returns(T::Array[String]) } + attr_reader :referenced_project_paths + + sig { override.returns(Dependabot::FileParsers::Base::DependencySet) } + def dependency_set + if target_frameworks.empty? && file_path.end_with?("proj") + Dependabot.logger.warn("Excluding project file '#{file_path}' due to unresolvable target framework") + dependency_set = Dependabot::FileParsers::Base::DependencySet.new + return dependency_set + end + + super + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb b/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb new file mode 100644 index 00000000000..aa29f5c48ea --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_property_details.rb @@ -0,0 +1,43 @@ +# typed: strong +# frozen_string_literal: true + +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativePropertyDetails + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(NativePropertyDetails) } + def self.from_json(json) + name = T.let(json.fetch("Name"), String) + value = T.let(json.fetch("Value"), String) + source_file_path = T.let(json.fetch("SourceFilePath"), String) + + NativePropertyDetails.new(name: name, + value: value, + source_file_path: source_file_path) + end + + sig do + params(name: String, + value: String, + source_file_path: String).void + end + def initialize(name:, value:, source_file_path:) + @name = name + @value = value + @source_file_path = source_file_path + end + + sig { returns(String) } + attr_reader :name + + sig { returns(String) } + attr_reader :value + + sig { returns(String) } + attr_reader :source_file_path + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb b/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb new file mode 100644 index 00000000000..b16ec1ee447 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_discovery/native_workspace_discovery.rb @@ -0,0 +1,68 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/native_discovery/native_dependency_file_discovery" +require "dependabot/nuget/native_discovery/native_directory_packages_props_discovery" +require "dependabot/nuget/native_discovery/native_project_discovery" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeWorkspaceDiscovery + extend T::Sig + + sig { params(json: T::Hash[String, T.untyped]).returns(NativeWorkspaceDiscovery) } + def self.from_json(json) + path = T.let(json.fetch("Path"), String) + path = "/" + path unless path.start_with?("/") + projects = T.let(json.fetch("Projects"), T::Array[T::Hash[String, T.untyped]]).filter_map do |project| + NativeProjectDiscovery.from_json(project, path) + end + directory_packages_props = NativeDirectoryPackagesPropsDiscovery + .from_json(T.let(json.fetch("DirectoryPackagesProps"), + T.nilable(T::Hash[String, T.untyped])), path) + global_json = NativeDependencyFileDiscovery + .from_json(T.let(json.fetch("GlobalJson"), T.nilable(T::Hash[String, T.untyped])), path) + dotnet_tools_json = NativeDependencyFileDiscovery + .from_json(T.let(json.fetch("DotNetToolsJson"), + T.nilable(T::Hash[String, T.untyped])), path) + + NativeWorkspaceDiscovery.new(path: path, + projects: projects, + directory_packages_props: directory_packages_props, + global_json: global_json, + dotnet_tools_json: dotnet_tools_json) + end + + sig do + params(path: String, + projects: T::Array[NativeProjectDiscovery], + directory_packages_props: T.nilable(NativeDirectoryPackagesPropsDiscovery), + global_json: T.nilable(NativeDependencyFileDiscovery), + dotnet_tools_json: T.nilable(NativeDependencyFileDiscovery)).void + end + def initialize(path:, projects:, directory_packages_props:, global_json:, dotnet_tools_json:) + @path = path + @projects = projects + @directory_packages_props = directory_packages_props + @global_json = global_json + @dotnet_tools_json = dotnet_tools_json + end + + sig { returns(String) } + attr_reader :path + + sig { returns(T::Array[NativeProjectDiscovery]) } + attr_reader :projects + + sig { returns(T.nilable(NativeDirectoryPackagesPropsDiscovery)) } + attr_reader :directory_packages_props + + sig { returns(T.nilable(NativeDependencyFileDiscovery)) } + attr_reader :global_json + + sig { returns(T.nilable(NativeDependencyFileDiscovery)) } + attr_reader :dotnet_tools_json + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_helpers.rb b/nuget/lib/dependabot/nuget/native_helpers.rb index 4047feb226d..f75ac88db27 100644 --- a/nuget/lib/dependabot/nuget/native_helpers.rb +++ b/nuget/lib/dependabot/nuget/native_helpers.rb @@ -110,6 +110,65 @@ def self.run_nuget_discover_tool(repo_root:, workspace_path:, output_path:, cred end end + sig do + params(repo_root: String, discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String).returns([String, String]) + end + def self.get_nuget_analyze_tool_command(repo_root:, discovery_file_path:, dependency_file_path:, + analysis_folder_path:) + exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli") + command_parts = [ + exe_path, + "analyze", + "--repo-root", + repo_root, + "--discovery-file-path", + discovery_file_path, + "--dependency-file-path", + dependency_file_path, + "--analysis-folder-path", + analysis_folder_path, + "--verbose" + ].compact + + command = Shellwords.join(command_parts) + + fingerprint = [ + exe_path, + "analyze", + "--discovery-file-path", + "", + "--dependency-file-path", + "", + "--analysis-folder-path", + "", + "--verbose" + ].compact.join(" ") + + [command, fingerprint] + end + + sig do + params( + repo_root: String, discovery_file_path: String, dependency_file_path: String, + analysis_folder_path: String, credentials: T::Array[Dependabot::Credential] + ).void + end + def self.run_nuget_analyze_tool(repo_root:, discovery_file_path:, dependency_file_path:, + analysis_folder_path:, credentials:) + (command, fingerprint) = get_nuget_analyze_tool_command(repo_root: repo_root, + discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path) + + puts "running NuGet analyze:\n" + command + + NuGetConfigCredentialHelpers.patch_nuget_config_for_action(credentials) do + output = SharedHelpers.run_shell_command(command, allow_unsafe_shell_command: true, fingerprint: fingerprint) + puts output + end + end + sig do params(repo_root: String, proj_path: String, dependency: Dependency, is_transitive: T::Boolean).returns([String, String]) diff --git a/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb b/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb new file mode 100644 index 00000000000..ac8264e9c50 --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_update_checker/native_requirements_updater.rb @@ -0,0 +1,105 @@ +# typed: strict +# frozen_string_literal: true + +####################################################################### +# For more details on Dotnet version constraints, see: # +# https://docs.microsoft.com/en-us/nuget/reference/package-versioning # +####################################################################### + +require "sorbet-runtime" + +require "dependabot/update_checkers/base" +require "dependabot/nuget/native_discovery/native_dependency_details" +require "dependabot/nuget/version" + +module Dependabot + module Nuget + class NativeUpdateChecker < Dependabot::UpdateCheckers::Base + class NativeRequirementsUpdater + extend T::Sig + + sig do + params( + requirements: T::Array[T::Hash[Symbol, T.untyped]], + dependency_details: T.nilable(Dependabot::Nuget::NativeDependencyDetails) + ) + .void + end + def initialize(requirements:, dependency_details:) + @requirements = requirements + @dependency_details = dependency_details + end + + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def updated_requirements + return requirements unless clean_version + + # NOTE: Order is important here. The FileUpdater needs the updated + # requirement at index `i` to correspond to the previous requirement + # at the same index. + requirements.map do |req| + next req if req.fetch(:requirement).nil? + next req if req.fetch(:requirement).include?(",") + + new_req = + if req.fetch(:requirement).include?("*") + update_wildcard_requirement(req.fetch(:requirement)) + else + # Since range requirements are excluded by the line above we can + # replace anything that looks like a version with the new + # version + req[:requirement].sub( + /#{Nuget::Version::VERSION_PATTERN}/o, + clean_version.to_s + ) + end + + next req if new_req == req.fetch(:requirement) + + new_source = req[:source]&.dup + unless @dependency_details.nil? + new_source = { + type: "nuget_repo", + source_url: @dependency_details.info_url + } + end + + req.merge({ requirement: new_req, source: new_source }) + end + end + + private + + sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) } + attr_reader :requirements + + sig { returns(T.class_of(Dependabot::Nuget::Version)) } + def version_class + Dependabot::Nuget::Version + end + + sig { returns(T.nilable(Dependabot::Nuget::Version)) } + def clean_version + return unless @dependency_details&.version + + version_class.new(@dependency_details.version) + end + + sig { params(req_string: String).returns(String) } + def update_wildcard_requirement(req_string) + return req_string if req_string == "*-*" + + return req_string if req_string == "*" + + precision = T.must(req_string.split("*").first).split(/\.|\-/).count + wildcard_section = req_string.partition(/(?=[.\-]\*)/).last + + version_parts = T.must(clean_version).segments.first(precision) + version = version_parts.join(".") + + version + wildcard_section + end + end + end + end +end diff --git a/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb b/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb new file mode 100644 index 00000000000..de3e07aabcf --- /dev/null +++ b/nuget/lib/dependabot/nuget/native_update_checker/native_update_checker.rb @@ -0,0 +1,200 @@ +# typed: strong +# frozen_string_literal: true + +require "dependabot/nuget/analysis/analysis_json_reader" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/update_checkers" +require "dependabot/update_checkers/base" +require "sorbet-runtime" + +module Dependabot + module Nuget + class NativeUpdateChecker < Dependabot::UpdateCheckers::Base + extend T::Sig + + require_relative "native_requirements_updater" + + sig { override.returns(T.nilable(String)) } + def latest_version + # No need to find latest version for transitive dependencies unless they have a vulnerability. + return dependency.version if !dependency.top_level? && !vulnerable? + + # if no update sources have the requisite package, then we can only assume that the current version is correct + @latest_version = T.let( + update_analysis.dependency_analysis.updated_version, + T.nilable(String) + ) + end + + sig { override.returns(T.nilable(T.any(String, Gem::Version))) } + def latest_resolvable_version + # We always want a full unlock since any package update could update peer dependencies as well. + # To force a full unlock instead of an own unlock, we return nil. + nil + end + + sig { override.returns(Dependabot::Nuget::Version) } + def lowest_security_fix_version + update_analysis.dependency_analysis.numeric_updated_version + end + + sig { override.returns(T.nilable(Dependabot::Nuget::Version)) } + def lowest_resolvable_security_fix_version + return nil if version_comes_from_multi_dependency_property? + + update_analysis.dependency_analysis.numeric_updated_version + end + + sig { override.returns(NilClass) } + def latest_resolvable_version_with_no_unlock + # Irrelevant, since Nuget has a single dependency file + nil + end + + sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } + def updated_requirements + dep_details = updated_dependency_details.find { |d| d.name.casecmp?(dependency.name) } + NativeRequirementsUpdater.new( + requirements: dependency.requirements, + dependency_details: dep_details + ).updated_requirements + end + + sig { returns(T::Boolean) } + def up_to_date? + !update_analysis.dependency_analysis.can_update + end + + sig { returns(T::Boolean) } + def requirements_unlocked_or_can_be? + update_analysis.dependency_analysis.can_update + end + + sig { returns(T::Boolean) } + def public_latest_version_resolvable_with_full_unlock? + latest_version_resolvable_with_full_unlock? + end + + sig { returns(T::Array[Dependabot::Dependency]) } + def public_updated_dependencies_after_full_unlock + updated_dependencies_after_full_unlock + end + + private + + sig { returns(AnalysisJsonReader) } + def update_analysis + @update_analysis ||= T.let(request_analysis, T.nilable(AnalysisJsonReader)) + end + + sig { returns(String) } + def dependency_file_path + File.join(NativeDiscoveryJsonReader.temp_directory, "dependency", "#{dependency.name}.json") + end + + sig { returns(AnalysisJsonReader) } + def request_analysis + discovery_file_path = NativeDiscoveryJsonReader.get_discovery_file_path_from_dependency_files(dependency_files) + analysis_folder_path = AnalysisJsonReader.temp_directory + + write_dependency_info + + NativeHelpers.run_nuget_analyze_tool(repo_root: T.must(repo_contents_path), + discovery_file_path: discovery_file_path, + dependency_file_path: dependency_file_path, + analysis_folder_path: analysis_folder_path, + credentials: credentials) + + analysis_json = AnalysisJsonReader.analysis_json(dependency_name: dependency.name) + + AnalysisJsonReader.new(analysis_json: T.must(analysis_json)) + end + + sig { void } + def write_dependency_info + dependency_info = { + Name: dependency.name, + Version: dependency.version.to_s, + IsVulnerable: vulnerable?, + IgnoredVersions: ignored_versions, + Vulnerabilities: security_advisories.map do |vulnerability| + { + DependencyName: vulnerability.dependency_name, + PackageManager: vulnerability.package_manager, + VulnerableVersions: vulnerability.vulnerable_versions.map(&:to_s), + SafeVersions: vulnerability.safe_versions.map(&:to_s) + } + end + }.to_json + dependency_directory = File.dirname(dependency_file_path) + + begin + Dir.mkdir(dependency_directory) + rescue StandardError + nil? + end + + File.write(dependency_file_path, dependency_info) + end + + sig { returns(Dependabot::FileParsers::Base::DependencySet) } + def discovered_dependencies + discovery_json_reader = NativeDiscoveryJsonReader.get_discovery_from_dependency_files(dependency_files) + discovery_json_reader.dependency_set + end + + sig { override.returns(T::Boolean) } + def latest_version_resolvable_with_full_unlock? + # We always want a full unlock since any package update could update peer dependencies as well. + true + end + + sig { override.returns(T::Array[Dependabot::Dependency]) } + def updated_dependencies_after_full_unlock + dependencies = discovered_dependencies.dependencies + updated_dependency_details.filter_map do |dependency_details| + dep = dependencies.find { |d| d.name.casecmp(dependency_details.name)&.zero? } + next unless dep + + metadata = {} + # For peer dependencies, instruct updater to not directly update this dependency + metadata = { information_only: true } unless dependency.name.casecmp(dependency_details.name)&.zero? + + # rebuild the new requirements with the updated dependency details + updated_reqs = dep.requirements.map do |r| + r = r.clone + r[:requirement] = dependency_details.version + r[:source] = { + type: "nuget_repo", + source_url: dependency_details.info_url + } + r + end + + Dependency.new( + name: dep.name, + version: dependency_details.version, + requirements: updated_reqs, + previous_version: dep.version, + previous_requirements: dep.requirements, + package_manager: dep.package_manager, + metadata: metadata + ) + end + end + + sig { returns(T::Array[Dependabot::Nuget::NativeDependencyDetails]) } + def updated_dependency_details + @updated_dependency_details ||= T.let(update_analysis.dependency_analysis.updated_dependencies, + T.nilable(T::Array[Dependabot::Nuget::NativeDependencyDetails])) + end + + sig { returns(T::Boolean) } + def version_comes_from_multi_dependency_property? + update_analysis.dependency_analysis.version_comes_from_multi_dependency_property + end + end + end +end + +Dependabot::UpdateCheckers.register("nuget", Dependabot::Nuget::UpdateChecker) diff --git a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb index c1ac11f2a02..ad8eee731b2 100644 --- a/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb +++ b/nuget/lib/dependabot/nuget/nuget_config_credential_helpers.rb @@ -69,12 +69,13 @@ def self.patch_nuget_config_for_action(credentials, &_block) begin yield rescue StandardError => e - Dependabot.logger.error( + log_message = <<~LOG_MESSAGE Block argument of NuGetConfigCredentialHelpers::patch_nuget_config_for_action causes an exception #{e}: #{e.message} LOG_MESSAGE - ) + Dependabot.logger.error(log_message) + puts log_message ensure restore_user_nuget_config end diff --git a/nuget/lib/dependabot/nuget/update_checker.rb b/nuget/lib/dependabot/nuget/update_checker.rb index 8eaa943b28d..2e7dde12866 100644 --- a/nuget/lib/dependabot/nuget/update_checker.rb +++ b/nuget/lib/dependabot/nuget/update_checker.rb @@ -16,10 +16,19 @@ class UpdateChecker < Dependabot::UpdateCheckers::Base require_relative "update_checker/requirements_updater" require_relative "update_checker/dependency_finder" + require_relative "native_update_checker/native_update_checker" + PROPERTY_REGEX = /\$\((?.*?)\)/ + sig { returns(T::Boolean) } + def self.native_analysis_enabled? + Dependabot::Experiments.enabled?(:nuget_native_analysis) + end + sig { override.returns(T.nilable(String)) } def latest_version + return native_update_checker.latest_version if UpdateChecker.native_analysis_enabled? + # No need to find latest version for transitive dependencies unless they have a vulnerability. return dependency.version if !dependency.top_level? && !vulnerable? @@ -32,6 +41,8 @@ def latest_version sig { override.returns(T.nilable(T.any(String, Gem::Version))) } def latest_resolvable_version + return native_update_checker.latest_resolvable_version if UpdateChecker.native_analysis_enabled? + # We always want a full unlock since any package update could update peer dependencies as well. # To force a full unlock instead of an own unlock, we return nil. nil @@ -39,6 +50,8 @@ def latest_resolvable_version sig { override.returns(Dependabot::Nuget::Version) } def lowest_security_fix_version + return native_update_checker.lowest_security_fix_version if UpdateChecker.native_analysis_enabled? + lowest_security_fix_version_details&.fetch(:version) end @@ -51,12 +64,16 @@ def lowest_resolvable_security_fix_version sig { override.returns(NilClass) } def latest_resolvable_version_with_no_unlock + return native_update_checker.latest_resolvable_version_with_no_unlock if UpdateChecker.native_analysis_enabled? + # Irrelevant, since Nuget has a single dependency file nil end sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) } def updated_requirements + return native_update_checker.updated_requirements if UpdateChecker.native_analysis_enabled? + RequirementsUpdater.new( requirements: dependency.requirements, latest_version: preferred_resolvable_version_details&.fetch(:version, nil)&.to_s, @@ -66,6 +83,8 @@ def updated_requirements sig { returns(T::Boolean) } def up_to_date? + return native_update_checker.up_to_date? if UpdateChecker.native_analysis_enabled? + # No need to update transitive dependencies unless they have a vulnerability. return true if !dependency.top_level? && !vulnerable? @@ -89,6 +108,26 @@ def requirements_unlocked_or_can_be? private + sig { returns(Dependabot::Nuget::NativeUpdateChecker) } + def native_update_checker + @native_update_checker ||= + T.let( + Dependabot::Nuget::NativeUpdateChecker.new( + dependency: dependency, + dependency_files: dependency_files, + credentials: credentials, + repo_contents_path: repo_contents_path, + ignored_versions: ignored_versions, + raise_on_ignored: raise_on_ignored, + security_advisories: security_advisories, + requirements_update_strategy: requirements_update_strategy, + dependency_group: dependency_group, + options: options + ), + T.nilable(Dependabot::Nuget::NativeUpdateChecker) + ) + end + sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } def preferred_resolvable_version_details # If this dependency is vulnerable, prefer trying to update to the @@ -101,6 +140,10 @@ def preferred_resolvable_version_details sig { override.returns(T::Boolean) } def latest_version_resolvable_with_full_unlock? + if UpdateChecker.native_analysis_enabled? + return native_update_checker.public_latest_version_resolvable_with_full_unlock? + end + # We always want a full unlock since any package update could update peer dependencies as well. return true unless version_comes_from_multi_dependency_property? @@ -109,6 +152,10 @@ def latest_version_resolvable_with_full_unlock? sig { override.returns(T::Array[Dependabot::Dependency]) } def updated_dependencies_after_full_unlock + if UpdateChecker.native_analysis_enabled? + return native_update_checker.public_updated_dependencies_after_full_unlock + end + return property_updater.updated_dependencies if version_comes_from_multi_dependency_property? puts "Finding updated dependencies for #{dependency.name}." diff --git a/nuget/spec/dependabot/nuget/file_parser_spec.rb b/nuget/spec/dependabot/nuget/file_parser_spec.rb index c8b965d0ca8..276d4178f9e 100644 --- a/nuget/spec/dependabot/nuget/file_parser_spec.rb +++ b/nuget/spec/dependabot/nuget/file_parser_spec.rb @@ -14,6 +14,17 @@ config.include(NuGetSearchStubs) end + let(:stub_native_tools) { true } # set to `false` to allow invoking the native tools during tests + let(:report_stub_debug_information) { false } # set to `true` to write native tool stubbing information to the screen + + let(:dependency_files) { [csproj_file] + additional_files } + let(:additional_files) { [] } + let(:csproj_file) do + Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) + end + let(:csproj_body) { fixture("csproj", "basic.csproj") } + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:directory) { "/" } let(:source) do Dependabot::Source.new( provider: "github", @@ -21,71 +32,111 @@ directory: directory ) end - let(:directory) { "/" } - let(:parser) do - described_class.new(dependency_files: files, - source: source, - repo_contents_path: repo_contents_path) - end - let(:repo_contents_path) { write_tmp_repo(files) } - let(:csproj_body) { fixture("csproj", "basic.csproj") } - let(:csproj_file) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:additional_files) { [] } let(:files) { [csproj_file] + additional_files } it_behaves_like "a dependency file parser" - describe "parse" do - subject(:top_level_dependencies) { dependencies.select(&:top_level?) } + def run_parser_test(&_block) + # caching is explicitly required for these tests + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" - let(:dependencies) { parser.parse } + # don't allow a previous test to pollute the file parser cache + Dependabot::Nuget::FileParser.file_dependency_cache.clear - context "with a single project file" do - before do - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1", "1.1.1"]) - stub_search_results_with_versions_v3("microsoft.aspnetcore.app", []) - stub_search_results_with_versions_v3("microsoft.net.test.sdk", []) - stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", ["1.1.0"]) - stub_search_results_with_versions_v3("system.collections.specialized", ["4.3.0"]) - end + # create the parser... + parser = Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path) - its(:length) { is_expected.to eq(5) } - - describe "the Microsoft.Extensions.DependencyModel dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } + # ...and invoke the actual test + yield parser + ensure + Dependabot::Nuget::NativeDiscoveryJsonReader.clear_discovery_file_path_from_cache(dependency_files) + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" + end - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) + def intercept_native_tools(discovery_content_hash:) + return unless stub_native_tools + + # don't allow `FileParser#parse` to call into the native tool; just fake it + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_discover_tool) + .and_wrap_original do |_original_method, *args, &_block| + discovery_json_path = args[0][:output_path] + FileUtils.mkdir_p(File.dirname(discovery_json_path)) + if report_stub_debug_information + puts "stubbing call to `run_nuget_discover_tool` with args #{args}; writing prefabricated discovery " \ + "response to discovery.json to #{discovery_json_path}" end + discovery_json_content = discovery_content_hash.to_json + File.write(discovery_json_path, discovery_json_content) + end + end + + describe "parse" do + context "with a single project file" do + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "System.Collections.Specialized", + Version: "4.3.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFrameworks", + Value: "net462", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net462"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - describe "the System.Collections.Specialized dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "System.Collections.Specialized" } } + it "is returns the expected set of dependencies" do + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(2) - it "has the right details" do + dependency = dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.Collections.Specialized") - expect(dependency.version).to eq("4.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "4.3.0", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) + expect(dependency.version).to eq("1.1.1") + expect(dependency.requirements).to eq([{ + requirement: "1.1.1", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }]) end end end @@ -100,50 +151,80 @@ end before do - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.1", "1.1.1"]) - stub_search_results_with_versions_v3("microsoft.aspnetcore.app", []) - stub_search_results_with_versions_v3("microsoft.net.test.sdk", []) - stub_search_results_with_versions_v3("microsoft.extensions.platformabstractions", ["1.1.0"]) - stub_search_results_with_versions_v3("system.collections.specialized", ["4.3.0"]) - stub_search_results_with_versions_v3("serilog", ["2.3.0"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFrameworks", + Value: "net462", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net462"], + ReferencedProjectPaths: [] + }, { + FilePath: "my.vbproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net462"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFrameworks", + Value: "net462", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net462"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(6) } - - describe "the Microsoft.Extensions.DependencyModel dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } } - - it "has the right details" do + it "reports the correct dependency information" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.Extensions.DependencyModel" } expect(dependency).to be_a(Dependabot::Dependency) expect(dependency.name).to eq("Microsoft.Extensions.DependencyModel") expect(dependency.version).to eq("1.0.1") expect(dependency.requirements).to eq( [{ requirement: "1.1.1", - file: "my.csproj", + file: "/my.csproj", groups: ["dependencies"], source: nil }, { requirement: "1.0.1", - file: "my.vbproj", - groups: ["dependencies"], - source: nil - }] - ) - end - end - - describe "the Serilog dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Serilog") - expect(dependency.version).to eq("2.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "2.3.0", - file: "my.vbproj", + file: "/my.vbproj", groups: ["dependencies"], source: nil }] @@ -172,24 +253,52 @@ XML end - its(:length) { is_expected.to eq(9) } - - describe "the Microsoft.CodeDom.Providers.DotNetCompilerPlatform dependency" do - subject(:dependency) do - dependencies.find do |d| - d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" - end - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.CodeDom.Providers.DotNetCompilerPlatform", + Version: "1.0.0", + Type: "PackagesConfig", + EvaluationResult: nil, + TargetFrameworks: ["netstandard2.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard2.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard2.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end - it "has the right details" do + it "reports the correct dependencies" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.CodeDom.Providers.DotNetCompilerPlatform") expect(dependency.version).to eq("1.0.0") expect(dependency.requirements).to eq( [{ requirement: "1.0.0", - file: "packages.config", + file: "/packages.config", groups: ["dependencies"], source: nil }] @@ -197,26 +306,7 @@ end end - describe "the Microsoft.Net.Compilers dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Net.Compilers" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.Net.Compilers") - expect(dependency.version).to eq("1.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.1", - file: "packages.config", - groups: ["devDependencies"], - source: nil - }] - ) - end - end - - context "when the dependency is nested" do + context "when it is nested" do let(:directory) { "/dir" } let(:packages_config) do Dependabot::DependencyFile.new( @@ -228,49 +318,58 @@ Dependabot::DependencyFile.new(name: "dir/my.csproj", content: csproj_body) end - its(:length) { is_expected.to eq(9) } - - describe "the Microsoft.CodeDom.Providers.DotNetCompilerPlatform dependency" do - subject(:dependency) do - dependencies.find do |d| - d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" - end - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "dir", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.CodeDom.Providers.DotNetCompilerPlatform", + Version: "1.0.0", + Type: "PackagesConfig", + EvaluationResult: nil, + TargetFrameworks: ["netstandard2.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard2.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard2.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end - it "has the right details" do + it "reports the correct results" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.CodeDom.Providers.DotNetCompilerPlatform" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.CodeDom.Providers.DotNetCompilerPlatform") expect(dependency.version).to eq("1.0.0") expect(dependency.requirements).to eq( [{ requirement: "1.0.0", - file: "packages.config", + file: "/dir/packages.config", groups: ["dependencies"], source: nil }] ) end end - - describe "the Microsoft.Net.Compilers dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Net.Compilers" } } - - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name) - .to eq("Microsoft.Net.Compilers") - expect(dependency.version).to eq("1.0.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.0.1", - file: "packages.config", - groups: ["devDependencies"], - source: nil - }] - ) - end - end end end @@ -283,19 +382,45 @@ ) end - its(:length) { is_expected.to eq(6) } - - describe "the Microsoft.Build.Traversal dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Microsoft.Build.Traversal" } } + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [], # not relevant for this test + DirectoryPackagesProps: nil, + GlobalJson: { + FilePath: "global.json", + IsSuccess: true, + Dependencies: [{ + Name: "Microsoft.Build.Traversal", + Version: "1.0.45", + Type: "MSBuildSdk", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + }, + DotNetToolsJson: nil + } + ) + end - it "has the right details" do + it "reports the expected results" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Microsoft.Build.Traversal" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Microsoft.Build.Traversal") expect(dependency.version).to eq("1.0.45") expect(dependency.requirements).to eq( [{ requirement: "1.0.45", - file: "global.json", + file: "/global.json", groups: ["dependencies"], source: nil }] @@ -313,19 +438,45 @@ ) end - its(:length) { is_expected.to eq(7) } - - describe "the dotnetsay dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "dotnetsay" } } + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [], # not relevant for this test + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: { + FilePath: ".config/dotnet-tools.json", + IsSuccess: true, + Dependencies: [{ + Name: "dotnetsay", + Version: "1.0.0", + Type: "DotNetTool", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + } + ) + end - it "has the right details" do + it "has the right details" do + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "dotnetsay" } expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("dotnetsay") expect(dependency.version).to eq("1.0.0") expect(dependency.requirements).to eq( [{ requirement: "1.0.0", - file: ".config/dotnet-tools.json", + file: "/.config/dotnet-tools.json", groups: ["dependencies"], source: nil }] @@ -355,31 +506,82 @@ end before do - stub_search_results_with_versions_v3("serilog", ["2.3.0"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "commonprops.props", + Dependencies: [{ + Name: "Serilog", + Version: "2.3.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [], + TargetFrameworks: [], + ReferencedProjectPaths: [] + }, { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Serilog", + Version: "2.3.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["netstandard1.6"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard1.6", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard1.6"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(1) } - describe "the Serilog dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Serilog" } } - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Serilog") - expect(dependency.version).to eq("2.3.0") - expect(dependency.requirements).to eq( - [{ - requirement: "2.3.0", - file: "commonprops.props", - groups: ["dependencies"], - source: nil - }, { - requirement: "2.3.0", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }] - ) + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "Serilog" } + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.name).to eq("Serilog") + expect(dependency.version).to eq("2.3.0") + expect(dependency.requirements).to eq( + [{ + requirement: "2.3.0", + file: "/commonprops.props", + groups: ["dependencies"], + source: nil + }, { + requirement: "2.3.0", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }] + ) + end end end end @@ -405,35 +607,81 @@ end before do - stub_search_results_with_versions_v3("microsoft.sourcelink.github", ["1.0.0-beta2-19367-01"]) - stub_search_results_with_versions_v3("system.lycos", ["3.23.3"]) - stub_search_results_with_versions_v3("system.askjeeves", ["2.2.2"]) - stub_search_results_with_versions_v3("system.google", ["0.1.0-beta.3"]) - stub_search_results_with_versions_v3("system.webcrawler", ["1.1.1"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["netstandard1.6"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: true, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard1.6", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard1.6"], + ReferencedProjectPaths: [] + }, { + FilePath: "packages.props", + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: true, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [], + TargetFrameworks: [], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(5) } - describe "the System.WebCrawler dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "System.WebCrawler" } } - it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.WebCrawler") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }, { - requirement: "1.1.1", - file: "packages.props", - groups: ["dependencies"], - source: nil - }] - ) + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "System.WebCrawler" } + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.version).to eq("1.1.1") + expect(dependency.requirements).to eq( + [{ + requirement: "1.1.1", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }, { + requirement: "1.1.1", + file: "/packages.props", + groups: ["dependencies"], + source: nil + }] + ) + end end end end @@ -464,40 +712,87 @@ end before do - stub_search_results_with_versions_v3("system.lycos", ["3.23.3"]) - stub_search_results_with_versions_v3("system.askjeeves", ["2.2.2"]) - stub_search_results_with_versions_v3("system.google", ["0.1.0-beta.3"]) - stub_search_results_with_versions_v3("system.webcrawler", ["1.1.1"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["netstandard1.6"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "netstandard1.6", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["netstandard1.6"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: { + FilePath: "Directory.Packages.props", + IsSuccess: true, + IsTransitivePinningEnabled: false, + Dependencies: [{ + Name: "System.WebCrawler", + Version: "1.1.1", + Type: "PackageVersion", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + }, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(4) } - describe "the System.WebCrawler dependency" do subject(:dependency) { dependencies.find { |d| d.name == "System.WebCrawler" } } it "has the right details" do - expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("System.WebCrawler") - expect(dependency.version).to eq("1.1.1") - expect(dependency.requirements).to eq( - [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }, { - requirement: "1.1.1", - file: "Directory.Packages.props", - groups: ["dependencies"], - source: nil - }] - ) + run_parser_test do |parser| + dependencies = parser.parse + dependency = dependencies.find { |d| d.name == "System.WebCrawler" } + expect(dependency).to be_a(Dependabot::Dependency) + expect(dependency.version).to eq("1.1.1") + expect(dependency.requirements).to eq( + [{ + requirement: "1.1.1", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }, { + requirement: "1.1.1", + file: "/Directory.Packages.props", + groups: ["dependencies"], + source: nil + }] + ) + end end end end context "with only directory.packages.props file" do - let(:files) { [packages_file] } + let(:dependency_files) { [packages_file] } let(:packages_file) do Dependabot::DependencyFile.new( name: "directory.packages.props", @@ -505,8 +800,12 @@ ) end - it do - expect { dependencies }.to raise_error(Dependabot::DependencyFileNotFound) + it "fails in the initializer" do + expect do + run_parser_test do |parser| + _dependencies = parser.parse + end + end.to raise_error(Dependabot::DependencyFileNotFound) end end @@ -531,96 +830,28 @@ before do allow(Dependabot.logger).to receive(:info) - stub_search_results_with_versions_v3("some.package", ["1.2.3"]) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/some.package/1.2.3/some.package.nuspec") - .to_return( - status: 200, - body: - <<~XML - - - Some.Package - 1.2.3 - - - - - - - XML - ) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end it "reports the relevant information" do - expect(dependencies.length).to eq(1) # this line is really just to force evaluation so we can see the infos - expect(Dependabot.logger).to have_received(:info).with( - <<~INFO - Discovery JSON content: { - "FilePath": "", - "IsSuccess": true, - "Projects": [ - { - "FilePath": "my.csproj", - "Dependencies": [ - { - "Name": "Microsoft.NET.Sdk", - "Version": null, - "Type": "MSBuildSdk", - "EvaluationResult": null, - "TargetFrameworks": null, - "IsDevDependency": false, - "IsDirect": false, - "IsTransitive": false, - "IsOverride": false, - "IsUpdate": false - }, - { - "Name": "Some.Package", - "Version": "1.2.3", - "Type": "PackageReference", - "EvaluationResult": { - "ResultType": "Success", - "OriginalValue": "$(SomePackageVersion)", - "EvaluatedValue": "1.2.3", - "RootPropertyName": "SomePackageVersion", - "ErrorMessage": null - }, - "TargetFrameworks": [ - "net8.0" - ], - "IsDevDependency": false, - "IsDirect": true, - "IsTransitive": false, - "IsOverride": false, - "IsUpdate": false - } - ], - "IsSuccess": true, - "Properties": [ - { - "Name": "SomePackageVersion", - "Value": "1.2.3", - "SourceFilePath": "my.csproj" - }, - { - "Name": "TargetFramework", - "Value": "net8.0", - "SourceFilePath": "my.csproj" - } - ], - "TargetFrameworks": [ - "net8.0" - ], - "ReferencedProjectPaths": [] - } - ], - "DirectoryPackagesProps": null, - "GlobalJson": null, - "DotNetToolsJson": null - } - INFO - .chomp - ) + run_parser_test do |parser| + _dependencies = parser.parse # the result doesn't matter, but it forces discovery to run + expect(Dependabot.logger).to have_received(:info).with( + <<~INFO + Discovery JSON content: {"Path":"","IsSuccess":true,"Projects":[],"DirectoryPackagesProps":null,"GlobalJson":null,"DotNetToolsJson":null} + INFO + .chomp + ) + end end end @@ -645,35 +876,71 @@ before do allow(Dependabot.logger).to receive(:warn) - stub_search_results_with_versions_v3("package.a", ["1.2.3"]) - stub_search_results_with_versions_v3("package.b", ["4.5.6"]) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/package.a/1.2.3/package.a.nuspec") - .to_return( - status: 200, - body: - <<~XML - - - Package.A - 1.2.3 - - - - - - - XML - ) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Package.A", + Version: "1.2.3", + Type: "PackageReference", + EvaluationResult: { + ResultType: "Success", + OriginalValue: "1.2.3", + EvaluatedValue: "1.2.3", + RootPropertyName: nil, + ErrorMessage: nil + }, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.B", + Version: "$(ThisPropertyCannotBeResolved)", + Type: "PackageReference", + EvaluationResult: { + ResultType: "PropertyNotFound", + OriginalValue: "$(ThisPropertyCannotBeResolved)", + EvaluatedValue: "$(ThisPropertyCannotBeResolved)", + RootPropertyName: "ThisPropertyCannotBeResolved", + ErrorMessage: "Property 'ThisPropertyCannotBeResolved' was not found." + }, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end - its(:length) { is_expected.to eq(1) } - - describe "the Package.A dependency" do - subject(:dependency) { dependencies.find { |d| d.name == "Package.A" } } - - it "has the right details" do + it "has the right details" do + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(1) + dependency = dependencies[0] expect(dependency).to be_a(Dependabot::Dependency) - expect(dependency.name).to eq("Package.A") expect(dependency.version).to eq("1.2.3") expect(Dependabot.logger).to have_received(:warn).with( "Dependency 'Package.B' excluded due to unparsable version: $(ThisPropertyCannotBeResolved)" @@ -682,34 +949,7 @@ end end - context "with a property that can't be evaluated" do - let(:csproj_file) do - Dependabot::DependencyFile.new( - name: "my.csproj", - content: - <<~XML - - - $(SomeCommonTfmThatCannotBeResolved) - - - - - - XML - ) - end - - before do - allow(Dependabot.logger).to receive(:warn) - end - - it "does not return the `.csproj` with an unresolvable TFM" do - expect(dependencies.length).to eq(0) - end - end - - context "when packages are referenced in implicitly included `.targets` file" do + context "when packages referenced in implicitly included `.targets` file are reported" do let(:additional_files) { [directory_build_targets] } let(:csproj_file) do Dependabot::DependencyFile.new( @@ -742,19 +982,83 @@ end before do - stub_search_results_with_versions_v3("package.a", ["1.2.3"]) - stub_search_results_with_versions_v3("package.b", ["4.5.6"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "Directory.Build.targets", + Dependencies: [{ + Name: "Package.B", + Version: "4.5.6", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [], + TargetFrameworks: [], + ReferencedProjectPaths: [] + }, { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Package.A", + Version: "1.2.3", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.B", + Version: "4.5.6", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end it "returns the correct dependency set" do - expect(dependencies.length).to eq(2) - expect(dependencies.map(&:name)).to match_array(%w(Package.A Package.B)) - expect(dependencies.map(&:version)).to match_array(%w(1.2.3 4.5.6)) + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(2) + expect(dependencies.map(&:name)).to match_array(%w(Package.A Package.B)) + expect(dependencies.map(&:version)).to match_array(%w(1.2.3 4.5.6)) + end end end - context "when the project element can be resolved from implicitly imported file" do - let(:additional_files) { [directory_build_props] } + context "when non-concrete version numbers are reported" do let(:csproj_file) do Dependabot::DependencyFile.new( name: "my.csproj", @@ -762,7 +1066,7 @@ <<~XML - $(SomeTfm) + net8.0 @@ -771,28 +1075,97 @@ XML ) end - let(:directory_build_props) do - Dependabot::DependencyFile.new( - name: "Directory.Build.props", - content: - <<~XML - - - net8.0 - - - XML - ) - end before do - stub_search_results_with_versions_v3("package.a", ["1.2.3"]) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [{ + FilePath: "my.csproj", + Dependencies: [{ + Name: "Package.A", + Version: nil, # not reported without version + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.B", + Version: "", # not reported with empty version + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.C", + Version: "[1.0,2.0)", # not reported with range + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.D", + Version: "1.*", # not reported with wildcard + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Package.E", + Version: "1.2.3", # regular version _is_ reported + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: false, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + }], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + }], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) end it "returns the correct dependency set" do - expect(dependencies.length).to eq(1) - expect(dependencies[0].name).to eq("Package.A") - expect(dependencies[0].version).to eq("1.2.3") + run_parser_test do |parser| + dependencies = parser.parse + expect(dependencies.length).to eq(1) + expect(dependencies[0].name).to eq("Package.E") + end end end end diff --git a/nuget/spec/dependabot/nuget/file_updater_spec.rb b/nuget/spec/dependabot/nuget/file_updater_spec.rb index 0d8904e4128..6c0bf3d5dbc 100644 --- a/nuget/spec/dependabot/nuget/file_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/file_updater_spec.rb @@ -16,16 +16,21 @@ config.include(NuGetSearchStubs) end - let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } - let(:previous_requirements) do - [{ file: "dirs.proj", requirement: "1.0.0", groups: [], source: nil }] - end - let(:requirements) do - [{ file: "dirs.proj", requirement: "1.1.1", groups: [], source: nil }] + let(:stub_native_tools) { true } # set to `false` to allow invoking the native tools during tests + let(:report_stub_debug_information) { false } # set to `true` to write native tool stubbing information to the screen + + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) end - let(:dependency_previous_version) { "1.0.0" } - let(:dependency_version) { "1.1.1" } - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } + let(:dependencies) { [dependency] } + let(:project_name) { "file_updater_dirsproj" } + let(:directory) { "/" } + # project_dependency files comes back with directory files first, we need the closest project at the top + let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } let(:dependency) do Dependabot::Dependency.new( name: dependency_name, @@ -36,23 +41,41 @@ package_manager: "nuget" ) end - # project_dependency files comes back with directory files first, we need the closest project at the top - let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } - let(:directory) { "/" } - let(:project_name) { "file_updater_dirsproj" } - let(:dependencies) { [dependency] } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) + let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } + let(:dependency_version) { "1.1.1" } + let(:dependency_previous_version) { "1.0.0" } + let(:requirements) do + [{ file: "dirs.proj", requirement: "1.1.1", groups: [], source: nil }] + end + let(:previous_requirements) do + [{ file: "dirs.proj", requirement: "1.0.0", groups: [], source: nil }] end - let(:file_updater_instance) do + let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } + + before do + stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.0", "1.1.1"]) + stub_request(:get, "https://api.nuget.org/v3-flatcontainer/" \ + "microsoft.extensions.dependencymodel/1.0.0/" \ + "microsoft.extensions.dependencymodel.nuspec") + .to_return(status: 200, body: fixture("nuspecs", "Microsoft.Extensions.DependencyModel.1.0.0.nuspec")) + end + + it_behaves_like "a dependency file updater" + + def run_update_test(&_block) + # caching is explicitly required for these tests + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" + + # don't allow a previous test to pollute the file parser cache + Dependabot::Nuget::FileParser.file_dependency_cache.clear + + # calling `#parse` is necessary to force `discover` which is stubbed below Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, source: source, repo_contents_path: repo_contents_path).parse - described_class.new( + + # create the file updater... + updater = described_class.new( dependency_files: dependency_files, dependencies: dependencies, credentials: [{ @@ -61,31 +84,82 @@ }], repo_contents_path: repo_contents_path ) - end - before do - stub_search_results_with_versions_v3("microsoft.extensions.dependencymodel", ["1.0.0", "1.1.1"]) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.0.0/" \ - "microsoft.extensions.dependencymodel.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "Microsoft.Extensions.DependencyModel.1.0.0.nuspec")) + # ...and invoke the actual test + yield updater + ensure + Dependabot::Nuget::NativeDiscoveryJsonReader.clear_discovery_file_path_from_cache(dependency_files) + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" end - it_behaves_like "a dependency file updater" + def intercept_native_tools(discovery_content_hash:) + return unless stub_native_tools + + # don't allow `FileParser#parse` to call into the native tool; just fake it + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_discover_tool) + .and_wrap_original do |_original_method, *args, &_block| + discovery_json_path = args[0][:output_path] + FileUtils.mkdir_p(File.dirname(discovery_json_path)) + if report_stub_debug_information + puts "stubbing call to `run_nuget_discover_tool` with args #{args}; writing prefabricated discovery " \ + "response to discovery.json to #{discovery_json_path}" + end + discovery_json_content = discovery_content_hash.to_json + File.write(discovery_json_path, discovery_json_content) + end + end describe "#updated_dependency_files" do - subject(:updated_files) { file_updater_instance.updated_dependency_files } + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "Proj1/Proj1/Proj1.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net461"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net461", + SourceFilePath: "Proj1/Proj1/Proj1.csproj" + }], + TargetFrameworks: ["net461"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end context "with a dirs.proj" do it "does not repeatedly update the same project" do - puts dependency_files.map(&:name) - expect(updated_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj") + run_update_test do |updater| + expect(updater.updated_dependency_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj") - expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( - { - "#{repo_contents_path}/Proj1/Proj1/Proj1.csprojMicrosoft.Extensions.DependencyModel" => 1 - } - ) + expect(updater.send(:testonly_update_tooling_calls)).to eq( + { + "/Proj1/Proj1/Proj1.csproj+Microsoft.Extensions.DependencyModel" => 1 + } + ) + end end context "when the file has only deleted lines" do @@ -98,30 +172,94 @@ end it "does not update the project" do - expect(updated_files.map(&:name)).to be_empty + run_update_test do |updater| + expect(updater.updated_dependency_files.map(&:name)).to be_empty + end end end end end describe "#updated_dependency_files_with_wildcard" do - subject(:updated_files) { file_updater_instance.updated_dependency_files } - let(:project_name) { "file_updater_dirsproj_wildcards" } let(:dependency_files) { nuget_project_dependency_files(project_name, directory: directory).reverse } let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } let(:dependency_version) { "1.1.1" } let(:dependency_previous_version) { "1.0.0" } - it "updates the wildcard project" do - expect(updated_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj", "Proj2/Proj2.csproj") - - expect(file_updater_instance.send(:testonly_update_tooling_calls)).to eq( - { - "#{repo_contents_path}/Proj1/Proj1/Proj1.csprojMicrosoft.Extensions.DependencyModel" => 1, - "#{repo_contents_path}/Proj2/Proj2.csprojMicrosoft.Extensions.DependencyModel" => 1 + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "Proj1/Proj1/Proj1.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net461"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net461", + SourceFilePath: "Proj1/Proj1/Proj1.csproj" + }], + TargetFrameworks: ["net461"], + ReferencedProjectPaths: [] + }, { + FilePath: "Proj2/Proj2.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.0.0", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net461"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [{ + Name: "TargetFramework", + Value: "net461", + SourceFilePath: "Proj2/Proj2.csproj" + }], + TargetFrameworks: ["net461"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil } ) end + + it "updates the wildcard project" do + run_update_test do |updater| + expect(updater.updated_dependency_files.map(&:name)).to contain_exactly("Proj1/Proj1/Proj1.csproj", + "Proj2/Proj2.csproj") + + expect(updater.send(:testonly_update_tooling_calls)).to eq( + { + "/Proj1/Proj1/Proj1.csproj+Microsoft.Extensions.DependencyModel" => 1, + "/Proj2/Proj2.csproj+Microsoft.Extensions.DependencyModel" => 1 + } + ) + end + end end end diff --git a/nuget/spec/dependabot/nuget/metadata_finder_spec.rb b/nuget/spec/dependabot/nuget/metadata_finder_spec.rb index 75e3194e9c6..a99298e4399 100644 --- a/nuget/spec/dependabot/nuget/metadata_finder_spec.rb +++ b/nuget/spec/dependabot/nuget/metadata_finder_spec.rb @@ -26,212 +26,76 @@ Dependabot::Dependency.new( name: dependency_name, version: dependency_version, - requirements: [{ - file: "my.csproj", - requirement: dependency_version, - groups: ["dependencies"], - source: source - }], + requirements: requirements, package_manager: "nuget" ) end - it_behaves_like "a dependency metadata finder" - - describe "#source_url" do - subject(:source_url) { finder.source_url } + let(:requirements) do + [{ + file: "my.csproj", + requirement: dependency_version, + groups: ["dependencies"], + source: source + }] + end - let(:nuget_url) do - "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/2.1.0/" \ - "microsoft.extensions.dependencymodel.nuspec" - end - let(:nuget_response) do - fixture( - "nuspecs", - "Microsoft.Extensions.DependencyModel.nuspec" - ) - end + it_behaves_like "a dependency metadata finder" - before do - stub_request(:get, nuget_url).to_return(status: 200, body: nuget_response) - stub_request(:get, "https://example.com/status").to_return( - status: 200, - body: "Not GHES", - headers: {} - ) - end + describe "#dependency_source_url" do + subject(:look_up_source) { finder.send(:dependency_source_url) } - context "with a source" do + context "with a source object with symbol keys" do let(:source) do { - type: "nuget_repo", - url: "https://www.myget.org/F/exceptionless/api/v3/index.json", - source_url: nil, - nuspec_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/2.1.0/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package", + type: "nuget_repo" } end - let(:nuget_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/2.1.0/" \ - "microsoft.extensions.dependencymodel.nuspec" - end - - it { is_expected.to eq("https://github.com/dotnet/core-setup") } - - it "caches the call to nuget" do - 2.times { source_url } - expect(WebMock).to have_requested(:get, nuget_url).once - end - - context "when nuget repo has a source_url only" do - let(:source) do - { - type: "nuget_repo", - url: "https://www.myget.org/F/exceptionless/api/v3/index.json", - source_url: "https://github.com/my/repo", - nuspec_url: nil - } - end - - it { is_expected.to eq("https://github.com/my/repo") } - end - - context "when the nuget repo has neither a source_url nor a nuspec_url" do - let(:source) do - { - type: "nuget_repo", - url: "https://www.myget.org/F/exceptionless/api/v3/index.json", - source_url: nil, - nuspec_url: nil - } - end - - it { is_expected.to be_nil } - end - - context "with details in the credentials (but no token)" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json" - }] - end - - it { is_expected.to eq("https://github.com/dotnet/core-setup") } - end - - context "when the registry requires authentication" do - before do - stub_request(:get, nuget_url).to_return(status: 404) - stub_request(:get, nuget_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_response) - end - - it { is_expected.to be_nil } - - context "with details in the credentials" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - "token" => "my:passw0rd" - }] - end - - it { is_expected.to eq("https://github.com/dotnet/core-setup") } - end - end - - context "when the registry doesn't support .nuspec routes" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback begins by getting the search URL from the index - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: fixture("nuspecs", "index.json")) - # next query for the package at the search URL returned - stub_request(:get, "https://azuresearch-usnc.nuget.org/query?prerelease=true&q=microsoft.extensions.dependencymodel&semVerLevel=2.0.0") - .to_return(status: 200, body: fixture("nuspecs", "microsoft.extensions.dependencymodel-results.json")) - end + it { is_expected.to eq("https://nuget.example.com/some.package") } + end - # data was extracted from the projectUrl in the search results - it { is_expected.to eq "https://github.com/dotnet/core-setup" } + context "with a source object with string keys" do + let(:source) do + { + "source_url" => "https://nuget.example.com/some.package", + "type" => "nuget_repo" + } end - context "when the index returns XML" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback tries to get the index, but gets a 200 with XML - # This might be due to artifactory not supporting index? - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: 'world') - end + it { is_expected.to eq("https://nuget.example.com/some.package") } + end - # no exceptions - it { is_expected.to be_nil } - end + context "with a nil source object" do + let(:source) { nil } - context "when the search results do not contain a projectUrl" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback begins by getting the search URL from the index - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: fixture("nuspecs", "index.json")) - # the search results have a blank projectUrl field AND missing the licenseUrl field entirely - stub_request(:get, "https://azuresearch-usnc.nuget.org/query?prerelease=true&q=microsoft.extensions.dependencymodel&semVerLevel=2.0.0") - .to_return(status: 200, body: '{"data":[{"id":"Microsoft.Extensions.DependencyModel","projectUrl":""}]}') - end + it { is_expected.to be_nil } + end - # no exceptions - it { is_expected.to be_nil } + context "with multiple requirements" do + let(:requirements) do + [{ + file: "project.csproj", + requirement: dependency_version, + groups: ["dependencies"], + source: nil + }, { + file: "my.csproj", + requirement: dependency_version, + groups: ["dependencies"], + source: source + }] end - context "when the source url fails to get the index.json" do - before do - # registry is in a bad state - stub_request(:get, nuget_url).to_return(status: 500) - # it falls back to get search URL from the index, but it fails too - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 500, body: "internal server error") - end - - it { is_expected.to be_nil } + let(:source) do + { + source_url: "https://nuget.example.com/some.package", + type: "nuget_repo" + } end - context "when it fails to get the search results" do - before do - # registry doesn't support .nuspec route, so returns 404 - stub_request(:get, nuget_url).to_return(status: 404) - # fallback begins by getting the search URL from the index - stub_request(:get, "https://www.myget.org/F/exceptionless/api/v3/index.json") - .to_return(status: 200, body: fixture("nuspecs", "index.json")) - # oops, we're a little overloaded - stub_request(:get, "https://azuresearch-usnc.nuget.org/query?prerelease=true&q=microsoft.extensions.dependencymodel&semVerLevel=2.0.0") - .to_return(status: 503, body: "") - end - - it { is_expected.to be_nil } - end + it { is_expected.to eq("https://nuget.example.com/some.package") } end end end diff --git a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb b/nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb similarity index 68% rename from nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb rename to nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb index 2273dbdae17..573c886d5cd 100644 --- a/nuget/spec/dependabot/nuget/update_checker/requirements_updater_spec.rb +++ b/nuget/spec/dependabot/nuget/native_update_checker/native_requirements_updater_spec.rb @@ -2,14 +2,13 @@ # frozen_string_literal: true require "spec_helper" -require "dependabot/nuget/update_checker/requirements_updater" +require "dependabot/nuget/native_update_checker/native_requirements_updater" -RSpec.describe Dependabot::Nuget::UpdateChecker::RequirementsUpdater do +RSpec.describe Dependabot::Nuget::NativeUpdateChecker::NativeRequirementsUpdater do let(:updater) do described_class.new( requirements: requirements, - latest_version: latest_version, - source_details: source_details + dependency_details: dependency_details ) end @@ -23,20 +22,25 @@ } end let(:csproj_req_string) { "23.3-jre" } - let(:latest_version) { version_class.new("23.6-jre") } - let(:source_details) do - { - source_url: nil, - repo_url: "https://api.nuget.org/v3/index.json", - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" - } + let(:latest_version) { "23.6-jre" } + let(:info_url) { "https://nuget.example.com/some.package" } + let(:dependency_details) do + Dependabot::Nuget::NativeDependencyDetails.from_json(JSON.parse({ + Name: "unused", + Version: latest_version, + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: nil, + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: info_url + }.to_json)) end - let(:version_class) { Dependabot::Nuget::Version } - - describe "#updated_requirements" do + describe "#updated_requirements.version" do subject { updater.updated_requirements.first } specify { expect(updater.updated_requirements.count).to eq(1) } @@ -48,7 +52,7 @@ end context "when there is a latest version" do - let(:latest_version) { version_class.new("23.6-jre") } + let(:latest_version) { "23.6-jre" } context "when no requirement was previously specified" do let(:csproj_req_string) { nil } @@ -69,9 +73,7 @@ end context "when a suffixed requirement was previously specified" do - let(:latest_version) do - version_class.new("3.0.0-beta4.20210.2+38fe3493") - end + let(:latest_version) { "3.0.0-beta4.20210.2+38fe3493" } let(:csproj_req_string) { "3.0.0-beta4.20207.4+07df2f07" } its([:requirement]) do @@ -124,11 +126,7 @@ groups: ["dependencies"], source: { type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package" } }, { file: "another/my.csproj", @@ -136,11 +134,7 @@ groups: ["dependencies"], source: { type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package" } }) end @@ -155,11 +149,7 @@ groups: ["dependencies"], source: { type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.2.3/" \ - "microsoft.extensions.dependencymodel.nuspec" + source_url: "https://nuget.example.com/some.package" } }, { file: "another/my.csproj", diff --git a/nuget/spec/dependabot/nuget/nuget_client_spec.rb b/nuget/spec/dependabot/nuget/nuget_client_spec.rb deleted file mode 100644 index 1c9256904ac..00000000000 --- a/nuget/spec/dependabot/nuget/nuget_client_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/nuget/nuget_client" - -RSpec.describe Dependabot::Nuget::NugetClient do - describe "#get_package_versions" do - subject(:package_versions) do - described_class.get_package_versions(dependency_name, repository_details) - end - - let(:dependency_name) { "Some.Dependency" } - - context "when retrieving the package versions from local" do - let(:repository_details) do - nuget_dir = File.join(File.dirname(__FILE__), "..", "..", "fixtures", "nuget_responses", "local_repo") - base_url = File.expand_path(nuget_dir) - - { - base_url: base_url, - repository_type: "local" - } - end - - it "expects to crawl the directory" do - expect(package_versions).to eq(Set["1.0.0", "1.1.0"]) - end - end - - context "when the package versions _might_ have the `listed` flag" do - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, # nuget.org provides this flag and it should be honored - version: "0.1.0" - } - }, - { - catalogEntry: { - listed: false, # if this is ever false, the package should not be included - version: "0.1.1" - } - }, - { - catalogEntry: { - # e.g., github doesn't have the `listed` flag, but should still be returned - version: "0.1.2" - } - } - ] - ] - }.to_json - ) - end - - let(:repository_details) do - { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/#{dependency_name.downcase}/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "#{dependency_name.downcase}/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=#{dependency_name.downcase}&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["0.1.0", "0.1.2"]) - end - end - - context "when the versions can be retrieved from v2 apis" do - before do - stub_request(:get, "https://www.nuget.org/api/v2/FindPackagesById()?id=%27Some.Dependency%27") - .to_return( - status: 200, - body: - <<~XML - - - - - Some.Dependency - - - 1.0.0.0 - - - - - Some.Dependency - - 1.1.0.0 - - - - - Some.Dependency.But.The.Wrong.One - - 1.2.0.0 - - - - - XML - ) - end - - let(:repository_details) do - { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: "https://www.nuget.org/api/v2/FindPackagesById()?id='#{dependency_name}'", - auth_header: {}, - repository_type: "v2" - } - end - - it "returns the correct version information" do - expect(package_versions).to eq(Set["1.0.0.0", "1.1.0.0"]) - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb deleted file mode 100644 index 9a923e3eccc..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/compatibility_checker_spec.rb +++ /dev/null @@ -1,260 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/compatibility_checker" -require "dependabot/nuget/update_checker/repository_finder" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::CompatibilityChecker do - subject(:checker) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency_urls: dependency_urls, - dependency: dependency - ) - end - - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency_urls) do - Dependabot::Nuget::RepositoryFinder.new( - dependency: dependency, - credentials: credentials, - config_files: [] - ).dependency_urls - end - - let(:credentials) do - [{ - "type" => "nuget_feed", - "url" => "https://api.nuget.org/v3/index.json", - "token" => "my:passw0rd" - }] - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_name) { "Microsoft.AppCenter.Crashes" } - let(:dependency_version) { "5.0.2" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "5.0.2", groups: ["dependencies"], source: nil }] - end - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) do - <<~XML - - - uap10.0.16299 - - - - - - XML - end - - describe "#compatible?" do - subject(:compatible) { checker.compatible?(version) } - - before do - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/microsoft.appcenter.crashes/index.json") - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - listed: true, - version: "5.0.2" - } - }, - { - catalogEntry: { - listed: true, - version: "5.0.3" - } - } - ] - ] - }.to_json - ) - end - - context "when the `.nuspec` reports itself as a development dependency, but still has regular dependencies" do - let(:csproj_body) do - <<~XML - - - net6.0 - - - - - - XML - end - - before do - nuspec502 = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - true - - - - - - - XML - nuspec503 = nuspec502.gsub("5.0.2", "5.0.3") - nuspec601 = nuspec502.gsub("5.0.2", "6.0.1").gsub("net6.0", "net8.0") - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec502 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec503 - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/6.0.1/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec601 - ) - end - - context "with a targetFramework compatible version" do - let(:version) { "5.0.3" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "with a targetFramework non-compatible version" do - let(:version) { "6.0.1" } - - it "returns the correct data" do - expect(compatible).to be_falsey - end - end - end - - context "when the `.nuspec` has groups without a `targetFramework` attribute" do - let(:version) { "5.0.3" } - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.3/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: fixture("nuspecs", "Microsoft.AppCenter.Crashes_faked.nuspec") - ) - end - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - - context "when the `.nupkg` zip object contains an empty `lib/` entry" do - def create_nupkg_with_lib_contents(package_name, nuspec_contents, lib_subdirectories) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{package_name}.nuspec") - zio.write(nuspec_contents) - - zio.put_next_entry("lib/") # some zip files have an empty directory object entry - - lib_subdirectories.each do |lib| - zio.put_next_entry("lib/#{lib}/_._") - zio.write("fake contents") - end - end - content.rewind - content.sysread - end - - let(:csproj_body) do - <<~XML - - - net481 - - - - - - XML - end - - before do - nuspec_xml = - <<~XML - - - Microsoft.AppCenter.Crashes - 5.0.2 - - - - - - - XML - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.nuspec") - .to_return( - status: 200, - body: nuspec_xml - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/microsoft.appcenter.crashes/5.0.2/microsoft.appcenter.crashes.5.0.2.nupkg") - .to_return( - status: 200, - body: create_nupkg_with_lib_contents(dependency_name, nuspec_xml, ["net45"]) - ) - end - - context "when checking the `.nupkg` contents" do - let(:version) { "5.0.2" } - - it "returns the correct data" do - expect(compatible).to be_truthy - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb deleted file mode 100644 index bd6b7a44203..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/dependency_finder_spec.rb +++ /dev/null @@ -1,101 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/dependency_finder" - -RSpec.describe Dependabot::Nuget::UpdateChecker::DependencyFinder do - subject(:finder) do - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - ignored_versions: [], - credentials: credentials, - repo_contents_path: "test/repo" - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - - # Can get transitive dependencies - describe "#transitive_dependencies", :vcr do - subject(:transitive_dependencies) { finder.transitive_dependencies } - - its(:length) { is_expected.to eq(34) } - end - - context "when the api.nuget.org is not hit due to absence in the NuGet.Config" do - subject(:transitive_dependencies) { finder.transitive_dependencies } - - let(:dependency_version) { "42.42.42" } - let(:nuget_config_body) { fixture("configs", "example.com_nuget.config") } - let(:nuget_config) { Dependabot::DependencyFile.new(name: "NuGet.Config", content: nuget_config_body) } - let(:dependency_files) { [csproj, nuget_config] } - - before do - disallowed_urls = %w( - https://api.nuget.org/v3/index.json - https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.nuspec - https://api.nuget.org/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.nuspec - ) - - disallowed_urls.each do |url| - stub_request(:get, url) - .to_raise(StandardError.new("Not allowed to query `#{url}`")) - end - - stub_request(:get, "https://nuget.example.com/v3/index.json") - .to_return(status: 200, body: fixture("nuget_responses", "example_index.json")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.extensions.dependencymodel/42.42.42/microsoft.extensions.dependencymodel.42.42.42.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.Extensions.DependencyModel", - "Microsoft.Extensions.DependencyModel_42.42.42_faked.nuspec")) - stub_request(:get, "https://api.example.com/v3-flatcontainer/microsoft.netcore.platforms/43.43.43/microsoft.netcore.platforms.43.43.43.nupkg") - .to_return(status: 200, body: create_nupkg("Microsoft.NETCore.Platforms", - "Microsoft.NETCore.Platforms_43.43.43_faked.nuspec")) - end - - def create_nupkg(nuspec_name, nuspec_fixture_path) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(fixture("nuspecs", nuspec_fixture_path)) - end - content.rewind - content.sysread - end - - # this test doesn't really care about the dependency count, we just need to ensure that `api.nuget.org` wasn't hit - its(:length) do - is_expected.to eq(1) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb deleted file mode 100644 index e99e71d0880..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nupkg_fetcher_spec.rb +++ /dev/null @@ -1,259 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nupkg_fetcher" -require "dependabot/nuget/update_checker/repository_finder" - -RSpec.describe Dependabot::Nuget::NupkgFetcher do - describe "#fetch_nupkg_url_from_repository" do - subject(:nupkg_url) do - described_class.fetch_nupkg_url_from_repository(repository_details, package_name, package_version) - end - - let(:dependency) { Dependabot::Dependency.new(name: package_name, requirements: [], package_manager: "nuget") } - let(:package_name) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:credentials) { [] } - let(:config_files) { [nuget_config] } - let(:nuget_config) do - Dependabot::DependencyFile.new( - name: "NuGet.config", - content: nuget_config_content - ) - end - let(:nuget_config_content) do - <<~XML - - - - - - - - XML - end - let(:repository_finder) do - Dependabot::Nuget::RepositoryFinder.new(dependency: dependency, credentials: credentials, - config_files: config_files) - end - let(:repository_details) { repository_finder.dependency_urls.first } - - context "with a nuget feed url" do - let(:feed_url) { "https://api.nuget.org/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "nuget.index.json") - ) - end - - it { is_expected.to eq("https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with an azure feed url" do - let(:feed_url) { "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public/nuget/v3/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "dotnet-public.index.json") - ) - end - - it { is_expected.to eq("https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/45bacae2-5efb-47c8-91e5-8ec20c22b4f8/nuget/v3/flat2/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a github feed url" do - let(:feed_url) { "https://nuget.pkg.github.com/some-namespace/index.json" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it { is_expected.to eq("https://nuget.pkg.github.com/some-namespace/download/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") } - end - - context "with a v2 feed url" do - let(:feed_url) { "https://www.nuget.org/api/v2" } - - before do - stub_request(:get, feed_url) - .to_return( - status: 200, - body: - +<<~XML - - - - Packages - - - - XML - ) - stub_request(:get, "https://www.nuget.org/api/v2/Packages(Id='Newtonsoft.Json',Version='13.0.1')") - .to_return( - status: 200, - body: - <<~XML - - - - - XML - ) - end - - it { is_expected.to eq("https://www.nuget.org/api/v2/Download/Newtonsoft.Json/13.0.1") } - end - - context "when the v3 feed doesn't specify `PackageBaseAddress`" do - let(:feed_url) { "https://nuget.example.com/v3-without-package-base/index.json" } - - before do - # initial `index.json` response; only provides `SearchQueryService` and not `PackageBaseAddress` - stub_request(:get, feed_url) - .to_return( - status: 200, - body: { - version: "3.0.0", - resources: [ - { - "@id" => "https://nuget.example.com/query", - "@type" => "SearchQueryService" - } - ] - }.to_json - ) - # SearchQueryService - stub_request(:get, "https://nuget.example.com/query?q=newtonsoft.json&prerelease=true&semVerLevel=2.0.0") - .to_return( - status: 200, - body: { - totalHits: 2, - data: [ - # this is a false match - { - registration: "not-used", - version: "42.42.42", - versions: [ - { - version: "1.0.0", - "@id" => "not-used" - }, - { - version: "42.42.42", - "@id" => "not-used" - } - ], - id: "Newtonsoft.Json.False.Match" - }, - # this is the real one - { - registration: "not-used", - version: "13.0.1", - versions: [ - { - version: "12.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/12.0.1.json" - }, - { - version: "13.0.1", - "@id" => "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json" - } - ], - id: "Newtonsoft.Json" - } - ] - }.to_json - ) - # registration content - stub_request(:get, "https://nuget.example.com/registration/newtonsoft.json/13.0.1.json") - .to_return( - status: 200, - body: { - listed: true, - packageContent: "https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg", - registration: "not-used", - "@id" => "not-used" - }.to_json - ) - end - - it { is_expected.to eq("https://nuget.example.com/nuget-local/Download/newtonsoft.json.13.0.1.nupkg") } - end - end - - describe "#fetch_nupkg_buffer" do - subject(:nupkg_buffer) do - described_class.fetch_nupkg_buffer(dependency_urls, package_id, package_version) - end - - let(:package_id) { "Newtonsoft.Json" } - let(:package_version) { "13.0.1" } - let(:repository_details) { Dependabot::Nuget::RepositoryFinder.get_default_repository_details(package_id) } - let(:dependency_urls) { [repository_details] } - - before do - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.1/newtonsoft.json.13.0.1.nupkg") - .to_return( - status: 301, - headers: { - "Location" => "https://api.nuget.org/redirect-on-301" - }, - body: "redirecting on 301" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-301") - .to_return( - status: 302, - headers: { - "Location" => "https://api.nuget.org/redirect-on-302" - }, - body: "redirecting on 302" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-302") - .to_return( - status: 303, - headers: { - "Location" => "https://api.nuget.org/redirect-on-303" - }, - body: "redirecting on 303" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-303") - .to_return( - status: 307, - headers: { - "Location" => "https://api.nuget.org/redirect-on-307" - }, - body: "redirecting on 307" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-307") - .to_return( - status: 308, - headers: { - "Location" => "https://api.nuget.org/redirect-on-308" - }, - body: "redirecting on 308" - ) - stub_request(:get, "https://api.nuget.org/redirect-on-308") - .to_return( - status: 200, - body: "the final contents" - ) - end - - it "fetches the nupkg after multiple redirects" do - expect(nupkg_buffer.to_s).to eq("the final contents") - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb b/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb deleted file mode 100644 index 8752837f118..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/nuspec_fetcher_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/nuspec_fetcher" - -RSpec.describe Dependabot::Nuget::NuspecFetcher do - describe "#feed_supports_nuspec_download?" do - context "when checking with a azure feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://pkgs.dev.azure.com/dependabot/dependabot-test/_packaging/dependabot-feed/nuget/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with a azure feed url (no project)" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://pkgs.dev.azure.com/dependabot/_packaging/dependabot-feed/nuget/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with a visual studio feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://dynamicscrm.pkgs.visualstudio.com/_packaging/CRM.Engineering/nuget/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with the nuget.org feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://api.nuget.org/v3/index.json" } - - it { is_expected.to be_truthy } - end - - context "when checking with github feed url" do - subject(:result) { described_class.feed_supports_nuspec_download?(url) } - - let(:url) { "https://nuget.pkg.github.com/some_namespace/index.json" } - - it { is_expected.to be_falsy } - end - end - - describe "remove_invalid_characters" do - context "when a utf-16 bom is present" do - subject(:result) { described_class.remove_invalid_characters(response_body) } - - let(:response_body) { "\xFE\xFF" } - - it { is_expected.to eq("") } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb deleted file mode 100644 index aec08e163c6..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/repository_finder_spec.rb +++ /dev/null @@ -1,963 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/update_checker/repository_finder" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::RepositoryFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - subject(:finder) do - described_class.new( - dependency: dependency, - credentials: credentials, - config_files: [config_file].compact - ) - end - - let(:config_file) { nil } - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:dependency) do - Dependabot::Dependency.new( - name: "Microsoft.Extensions.DependencyModel", - version: "1.1.1", - requirements: [{ - requirement: "1.1.1", - file: "my.csproj", - groups: ["dependencies"], - source: nil - }], - package_manager: "nuget" - ) - end - - describe "local path in NuGet.Config" do - subject(:known_repositories) { finder.known_repositories } - - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content, - directory: "some/directory" - ) - end - - it "finds all local paths" do - urls = known_repositories.map { |r| r[:url] } - expected = [ - "/some/directory/SomePath", - "/some/directory/RelativePath", - "/AbsolutePath", - "https://nuget.example.com/index.json" - ] - expect(urls).to match_array(expected) - end - end - - describe "environment variables in NuGet.Config" do - subject(:known_repositories) { finder.known_repositories } - - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - context "when expanded" do - before do - allow(Dependabot.logger).to receive(:warn) - ENV["FEED_URL"] = "https://nuget.example.com/index.json" - ENV["THIS_VARIBLE_EXISTS"] = "replacement-text" - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - - after do - ENV.delete("THIS_VARIBLE_EXISTS") - ENV.delete("THIS_VARIABLE_DOES_NOT") - end - - it "contains the expected values and warns on unavailable" do - repo = known_repositories[0] - expect(repo[:url]).to eq("https://nuget.example.com/index.json") - expect(repo[:token]).to eq("user:(head)replacement-text(mid)%THIS_VARIABLE_DOES_NOT%(tail)") - expect(Dependabot.logger).to have_received(:warn).with( - <<~WARN - The variable '%THIS_VARIABLE_DOES_NOT%' could not be expanded in NuGet.Config - WARN - ) - end - end - end - - describe "dependency_urls" do - subject(:dependency_urls) { finder.dependency_urls } - - it "gets the right URL without making any requests" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - - context "with a URL passed as a credential" do - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - - context "when the PackageBaseAddress is not returned" do - let(:custom_repo_url) { "http://localhost:8082/artifactory/api/nuget/v3/nuget-local" } - - before do - stub_request(:get, custom_repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: nil, - registration_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/registration/microsoft.extensions.dependencymodel/index.json", - repository_url: custom_repo_url, - search_url: "http://localhost:8081/artifactory/api/nuget/v3/" \ - "dependabot-nuget-local/query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "when URLs need to be escaped" do - let(:custom_repo_url) { "https://www.myget.org/F/exceptionless/api with spaces/v3/index.json" } - - before do - stub_request(:get, "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json") - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api%20with%20spaces/v3/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }] - ) - end - end - - context "when a request returns a 404 response" do - before { stub_request(:get, custom_repo_url).to_return(status: 404) } - - # TODO: Might want to raise here instead? - it { is_expected.to eq([]) } - end - - context "when a request returns a 403 response" do - before { stub_request(:get, custom_repo_url).to_return(status: 403) } - - it "raises a useful error" do - error_class = Dependabot::PrivateSourceAuthenticationFailure - expect { finder.dependency_urls } - .to raise_error do |error| - expect(error).to be_a(error_class) - expect(error.source).to eq(custom_repo_url) - end - end - end - - context "without a token" do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url - }] - end - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: nil) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URL" do - expect(dependency_urls).to eq( - [{ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - end - - context "with a URL included in the nuget.config" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", config_file_fixture_name) - ) - end - let(:config_file_fixture_name) { "nuget.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url).to_return(status: 404) - stub_request(:get, repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - # skipped - # it "gets the right URLs" do - # expect(dependency_urls).to match_array( - # [{ - # repository_url: "https://api.nuget.org/v3/index.json", - # versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - # "microsoft.extensions.dependencymodel/index.json", - # search_url: "https://azuresearch-usnc.nuget.org/query" \ - # "?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: {}, - # repository_type: "v3" - # }, { - # repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "index.json", - # versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "flatcontainer/microsoft.extensions." \ - # "dependencymodel/index.json", - # search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - # "query?q=microsoft.extensions.dependencymodel" \ - # "&prerelease=true&semVerLevel=2.0.0", - # auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - # repository_type: "v3" - # }] - # ) - # end - - context "when including the default repository" do - let(:config_file_fixture_name) { "include_default_disable_ext_sources.config" } - - it "with disable external source" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }, { - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when the spec overrides the default package sources" do - let(:config_file_fixture_name) { "override_def_source_with_same_key.config" } - let(:config_file_fixture_name) { "override_def_source_with_same_key_default.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "when the default api key of default registry is provided without clear" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - - it "when the default api key of default registry is provided with clear" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when not including the default repository" do - let(:config_file_fixture_name) { "excludes_default.config" } - - it "still includes the default repository (as it wasn't cleared)" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query" \ - "?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - - context "when spec clears default repo info" do - let(:config_file_fixture_name) { "clears_default.config" } - - it "still excludes the default repository" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - end - - context "when the spec has disabled package sources" do - let(:config_file_fixture_name) { "disabled_sources.config" } - - it "when only including the enabled package sources" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - end - - context "when the spec has disabled default package sources" do - let(:config_file_fixture_name) { "disabled_default_sources.config" } - - it "only includes the enable package sources" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: { "Authorization" => "Basic bXk6cGFzc3cwcmQ=" }, - repository_type: "v3" - }) - end - end - end - - context "when the spec has a numeric key" do - let(:config_file_fixture_name) { "numeric_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when only providing versioned `SearchQueryService`` entries" do - let(:config_file_fixture_name) { "versioned_search.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when including repositories in the `trustedSigners` section" do - let(:config_file_fixture_name) { "with_trustedSigners.config" } - - before do - repo_url = "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "versioned_SearchQueryService.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://api.nuget.org/v3-flatcontainer/", - registration_url: "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://api.nuget.org/v3/index.json", - versions_url: "https://api.nuget.org/v3-flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: "https://azuresearch-usnc.nuget.org/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, - { - base_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/", - registration_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/registrations2/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-libraries/nuget/v3/index.json", - versions_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/flat2/microsoft.extensions.dependencymodel/index.json", - search_url: "https://pkgs.dev.azure.com/dnceng/9ee6d478-d288-47f7-aacc-f6e6d082ae6d/_packaging/516521bf-6417-457e-9a9c-0a4bdfde03e7/nuget/v3/query2/?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "with GitHub packages url" do - let(:config_file_fixture_name) { "github.nuget.config" } - - before do - repo_url = "https://nuget.pkg.github.com/some-namespace/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "index.json", "github.index.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to eq( - [{ - base_url: "https://nuget.pkg.github.com/some-namespace/download", - registration_url: "https://nuget.pkg.github.com/some-namespace/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.pkg.github.com/some-namespace/index.json", - versions_url: "https://nuget.pkg.github.com/some-namespace/download/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.pkg.github.com/some-namespace/query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }] - ) - end - end - - context "when the repo URL has a non-ascii key" do - let(:config_file_fixture_name) { "non_ascii_key.config" } - - before do - repo_url = "https://www.myget.org/F/exceptionless/api/v3/index.json" - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "index.json", - versions_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions." \ - "dependencymodel/index.json", - search_url: "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel" \ - "&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when the repo URL uses the v2 API alongside the v3 API" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2", - repository_url: "https://www.nuget.org/api/v2", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }) - end - end - - context "when the repo URL has no base url in v2 API response" do - let(:config_file_fixture_name) { "with_v2_endpoints.config" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_no_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - end - - it "gets the right URLs" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/", - registration_url: "https://www.myget.org/F/exceptionless/api/v3/registration1/" \ - "microsoft.extensions.dependencymodel/index.json", - repository_url: - "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json", - versions_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "flatcontainer/microsoft.extensions.dependencymodel/index.json", - search_url: - "https://www.myget.org/F/exceptionless/api/v3/" \ - "query?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://www.nuget.org/api/v2/", - repository_url: "https://www.nuget.org/api/v2/", - versions_url: - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice/api/v2", - repository_url: "https://www.myget.org/F/azure-appservice/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/azure-appservice-staging/api/v2", - repository_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2", - versions_url: - "https://www.myget.org/F/azure-appservice-staging/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - repository_url: "https://www.myget.org/F/fusemandistfeed/api/v2", - versions_url: - "https://www.myget.org/F/fusemandistfeed/api/v2/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }, { - base_url: "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - repository_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/", - versions_url: - "https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/" \ - "FindPackagesById()?id=" \ - "'Microsoft.Extensions.DependencyModel'", - auth_header: {}, - repository_type: "v2" - }) - end - end - - context "when matching `packageSourceMapping` entries are honored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # `source1` and `source3` should never be queried - stub_index_json("https://nuget.example.com/source2/index.json") - end - - it "matches on the best pattern" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - - context "when non-matching `packageSourceMapping` entries are ignored" do - let(:config_file) do - nuget_config_content = <<~XML - - - - - - - - - - - - - - - - - - - - XML - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: nuget_config_content - ) - end - - before do - # all sources will need to be queried - stub_index_json("https://nuget.example.com/source1/index.json") - stub_index_json("https://nuget.example.com/source2/index.json") - stub_index_json("https://nuget.example.com/source3/index.json") - end - - it "returns all sources" do - expect(dependency_urls).to contain_exactly({ - base_url: "https://nuget.example.com/source1/PackageBaseAddress", - registration_url: "https://nuget.example.com/source1/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source1/index.json", - versions_url: "https://nuget.example.com/source1/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source1/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source2/PackageBaseAddress", - registration_url: "https://nuget.example.com/source2/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source2/index.json", - versions_url: "https://nuget.example.com/source2/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source2/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }, { - base_url: "https://nuget.example.com/source3/PackageBaseAddress", - registration_url: "https://nuget.example.com/source3/RegistrationsBaseUrl/microsoft.extensions.dependencymodel/index.json", - repository_url: "https://nuget.example.com/source3/index.json", - versions_url: "https://nuget.example.com/source3/PackageBaseAddress/microsoft.extensions.dependencymodel/index.json", - search_url: "https://nuget.example.com/source3/SearchQueryService?q=microsoft.extensions.dependencymodel&prerelease=true&semVerLevel=2.0.0", - auth_header: {}, - repository_type: "v3" - }) - end - end - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb deleted file mode 100644 index 36d1e103b13..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/tfm_finder_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/tfm_finder" - -RSpec.describe Dependabot::Nuget::TfmFinder do - subject(:frameworks) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.frameworks(dependency) - end - - let(:project_name) { "tfm_finder" } - let(:dependency_files) { nuget_project_dependency_files(project_name, directory: "/").reverse } - let(:repo_contents_path) { nuget_build_tmp_repo(project_name) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - describe "#frameworks" do - context "when checking for a transitive dependency" do - let(:dependency_requirements) { [] } - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - its(:length) { is_expected.to eq(2) } - end - - context "when checking for a top-level dependency" do - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "2.3.0", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Serilog" } - let(:dependency_version) { "2.3.0" } - - its(:length) { is_expected.to eq(1) } - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb b/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb deleted file mode 100644 index 7746d776565..00000000000 --- a/nuget/spec/dependabot/nuget/update_checker/version_finder_spec.rb +++ /dev/null @@ -1,720 +0,0 @@ -# typed: false -# frozen_string_literal: true - -require "spec_helper" -require "dependabot/dependency" -require "dependabot/dependency_file" -require "dependabot/nuget/file_parser" -require "dependabot/nuget/update_checker/version_finder" -require "dependabot/nuget/update_checker/tfm_comparer" -require_relative "../nuget_search_stubs" - -RSpec.describe Dependabot::Nuget::UpdateChecker::VersionFinder do - RSpec.configure do |config| - config.include(NuGetSearchStubs) - end - - let(:repo_contents_path) { write_tmp_repo(dependency_files) } - let(:source) do - Dependabot::Source.new( - provider: "github", - repo: "gocardless/bump", - directory: "/" - ) - end - - let(:finder) do - Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, - source: source, - repo_contents_path: repo_contents_path).parse - described_class.new( - dependency: dependency, - dependency_files: dependency_files, - credentials: credentials, - ignored_versions: ignored_versions, - raise_on_ignored: raise_on_ignored, - security_advisories: security_advisories, - repo_contents_path: repo_contents_path - ) - end - - let(:dependency) do - Dependabot::Dependency.new( - name: dependency_name, - version: dependency_version, - requirements: dependency_requirements, - package_manager: "nuget" - ) - end - - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "1.1.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "Microsoft.Extensions.DependencyModel" } - let(:dependency_version) { "1.1.1" } - - let(:dependency_files) { [csproj] } - let(:csproj) do - Dependabot::DependencyFile.new(name: "my.csproj", content: csproj_body) - end - let(:csproj_body) { fixture("csproj", "basic.csproj") } - - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }] - end - let(:ignored_versions) { [] } - let(:raise_on_ignored) { false } - let(:security_advisories) { [] } - - let(:nuget_versions_url) do - "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:nuget_search_url) do - "https://api.nuget.org/v3/registration5-gz-semver2/" \ - "microsoft.extensions.dependencymodel/index.json" - end - let(:version_class) { Dependabot::Nuget::Version } - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - let(:nuspec) do - fixture("nuspecs", "#{dependency_name}.#{dependency_version}.nuspec") - end - - let(:nuspec_url) do - "https://api.nuget.org/v3-flatcontainer/#{dependency_name.downcase}/#{dependency_version}/#{dependency_name.downcase}.nuspec" - end - - let(:version_instance) do - version_class.new(dependency_version) - end - - let(:expected_version_instance) do - version_class.new(expected_version) - end - - before do - stub_request(:get, nuget_versions_url) - .to_return(status: 200, body: nuget_versions) - stub_request(:get, nuget_search_url) - .to_return(status: 200, body: nuget_search_results) - end - - describe "#latest_version_details" do - subject(:latest_version_details) { finder.latest_version_details } - - let(:expected_version) { "2.1.0" } - let(:current_compatible) { true } - let(:expected_compatible) { true } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(current_compatible) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(expected_compatible) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - - context "when the returned versions is prefixed with a zero-width char" do - let(:nuget_search_results) do - fixture("nuget_responses", "search_results_zero_width.json") - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when the user wants a pre-release" do - let(:dependency_version) { "2.2.0-preview1-26216-03" } - let(:expected_version) { "2.2.0-preview2-26406-04" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - - context "when dealing with a previous version" do - let(:dependency_version) { "2.1.0-preview1-26216-03" } - let(:expected_version) { "2.1.0" } - - its([:version]) do - is_expected.to eq(expected_version_instance) - end - end - end - - context "when the user wants a pre-release with wildcard" do - let(:dependency_version) { "*-*" } - let(:current_compatible) { false } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "*-*", groups: ["dependencies"], source: nil }] - end - - its([:version]) do - is_expected.to eq(version_class.new("2.2.0-preview2-26406-04")) - end - end - - context "when the user is using an unfound property" do - let(:dependency_version) { "$PackageVersion_LibGit2SharpNativeBinaries" } - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "when raise_on_ignored is enabled and later versions are allowed" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - - context "when the user is on the latest version" do - let(:dependency_version) { "2.1.0" } - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - end - - context "when the current version isn't known" do - let(:dependency_version) { nil } - let(:current_compatible) { false } - let(:expected_version) { nil } - let(:expected_compatible) { false } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - end - - context "when the dependency is a git dependency" do - let(:dependency_version) { "a1b78a929dac93a52f08db4f2847d76d6cfe39bd" } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "doesn't raise an error" do - expect { latest_version_details }.not_to raise_error - end - end - end - - context "when the user is ignoring all later versions" do - let(:ignored_versions) { ["> 1.1.1"] } - - its([:version]) { is_expected.to eq(version_class.new("1.1.1")) } - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "raises an error" do - expect { latest_version_details }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when the user is ignoring the latest version" do - let(:ignored_versions) { ["[2.a,3.0.0)"] } - let(:expected_version) { "1.1.2" } - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "when a version range is specified using Ruby syntax" do - let(:ignored_versions) { [">= 2.a"] } - let(:expected_version) { "1.1.2" } - - its([:version]) { is_expected.to eq(version_class.new("1.1.2")) } - end - - context "when the user has ignored all versions" do - let(:ignored_versions) { ["[0,)"] } - - it "returns nil" do - expect(latest_version_details).to be_nil - end - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "raises an error" do - expect { latest_version_details }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when an open version range is specified using Ruby syntax" do - let(:ignored_versions) { ["> 0"] } - - it "returns nil" do - expect(latest_version_details).to be_nil - end - - context "when raise_on_ignored is enabled" do - let(:raise_on_ignored) { true } - - it "raises an error" do - expect { latest_version_details }.to raise_error(Dependabot::AllVersionsIgnored) - end - end - end - - context "when the user is ignoring all versions but a very specific one" do - let(:ignored_versions) { ["< 1.1.1, > 1.1.1"] } - let(:expected_version) { "1.1.1" } - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - - context "with a custom repo in a nuget.config file" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "nuget.config") - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - # skipped - # its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "when the repo uses the v2 API" do - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: fixture("configs", "with_v2_endpoints.config") - ) - end - - let(:custom_v3_nuget_versions_url) do - "https://www.myget.org/F/exceptionless/api/v3/flatcontainer/" \ - "#{dependency_name}/index.json" - end - - let(:expected_version) { "4.8.1" } - - before do - v2_repo_urls = %w( - https://www.nuget.org/api/v2/ - https://www.myget.org/F/azure-appservice/api/v2 - https://www.myget.org/F/azure-appservice-staging/api/v2 - https://www.myget.org/F/fusemandistfeed/api/v2 - https://www.myget.org/F/30de4ee06dd54956a82013fa17a3accb/ - ) - - v2_repo_urls.each do |repo_url| - stub_request(:get, repo_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_base.xml") - ) - end - - url = "https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" - stub_request(:get, url) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_v3_nuget_versions_url) - .to_return(status: 404) - - custom_v2_nuget_versions_url = - "https://www.nuget.org/api/v2/FindPackagesById()?id=" \ - "'#{dependency_name}'" - stub_request(:get, custom_v2_nuget_versions_url) - .to_return( - status: 200, - body: fixture("nuget_responses", "v2_versions.xml") - ) - end - - its([:version]) { is_expected.to eq(expected_version_instance) } - end - end - - context "with a package that returns paginated api results when using the v2 nuget api", :vcr do - let(:dependency_files) { project_dependency_files("paginated_package_v2_api") } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "4.7.1", groups: ["dependencies"], source: nil }] - end - let(:dependency_name) { "FakeItEasy" } - let(:dependency_version) { "4.7.1" } - let(:expected_version) { "7.3.0" } - - it "returns the expected version" do - expect(latest_version_details[:version]).to eq(expected_version_instance) - end - end - - context "with a custom repo in the credentials", :vcr do - let(:credentials) do - [{ - "type" => "git_source", - "host" => "github.com", - "username" => "x-access-token", - "password" => "token" - }, { - "type" => "nuget_feed", - "url" => custom_repo_url, - "token" => "my:passw0rd" - }] - end - - let(:nuget_versions) { fixture("nuget_responses", "versions.json") } - - let(:nuget_search_results) do - fixture("nuget_responses", "search_results.json") - end - - let(:custom_repo_url) do - "https://www.myget.org/F/exceptionless/api/v3/index.json" - end - let(:custom_nuget_search_url) do - "https://www.myget.org/F/exceptionless/api/v3/" \ - "registration1/microsoft.extensions.dependencymodel/index.json" - end - - before do - stub_request(:get, nuget_versions_url).to_return(status: 404) - stub_request(:get, nuget_search_url).to_return(status: 404) - - stub_request(:get, custom_repo_url).to_return(status: 404) - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(my passw0rd)) - .to_return( - status: 200, - body: fixture("nuget_responses", "myget_base.json") - ) - - stub_request(:get, custom_nuget_search_url).to_return(status: 404) - stub_request(:get, custom_nuget_search_url) - .with(basic_auth: %w(my passw0rd)) - .to_return(status: 200, body: nuget_search_results) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - - context "when the URL does not return PackageBaseAddress" do - let(:custom_repo_url) { "http://www.myget.org/artifactory/api/nuget/v3/dependabot-nuget-local" } - - before do - stub_request(:get, custom_repo_url) - .with(basic_auth: %w(admin password)) - .to_return( - status: 200, - body: fixture("nuget_responses", "artifactory_base.json") - ) - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - end - - context "with a version range specified" do - let(:dependency_files) { project_dependency_files("version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0, 3.0.0)", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with an open upper version range specified" do - let(:dependency_files) { project_dependency_files("open_upper_version_range") } - let(:dependency_version) { "1.1.0" } - let(:dependency_requirements) do - [{ file: "my.csproj", requirement: "[1.1.0-alpha,", groups: ["dependencies"], source: nil }] - end - - its([:version]) { is_expected.to eq(version_class.new("2.1.0")) } - end - - context "with a package that is implicitly referenced", :vcr do - let(:dependency_files) { project_dependency_files("implicit_reference") } - let(:dependency_requirements) do - [{ file: "implicitReference.csproj", requirement: "1.1.2-beta1.22511.2", groups: ["dependencies"], - source: nil }] - end - let(:dependency_name) { "NuGet.Protocol" } - let(:dependency_version) { "6.3.0" } - - it "returns the expected version" do - skip "This test was commented out and does not work at the moment" - expect(latest_version_details[:version]).to eq(version_class.new("6.5.0")) - end - end - - context "when the package can't be meaningfully sorted by just version" do - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - reported_versions = [ - "2.6.1", - "2.7.1", - "3.4.0", - "3.14.0", - "4.0.1" - ] - stub_request(:get, "https://api.nuget.org/v3/registration5-gz-semver2/nunit/index.json") - .to_return( - status: 200, - body: { - items: [ - items: reported_versions.map { |v| { catalogEntry: { listed: true, version: v } } } - ] - }.to_json - ) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/3.14.0/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.3.14.0_faked.nuspec")) - stub_request(:get, "https://api.nuget.org/v3-flatcontainer/nunit/4.0.1/nunit.nuspec") - .to_return(status: 200, body: fixture("nuspecs", "nunit.4.0.1_faked.nuspec")) - end - - let(:csproj_body) do - <<~XML - - - netcoreapp3.1 - - - - - - XML - end - let(:expected_version) { version_class.new("3.14.0") } - let(:dependency_version) { "3.14.0" } - let(:dependency) do - Dependabot::Dependency.new( - name: "nunit", - version: dependency_version, - requirements: [{ file: "my.csproj", requirement: "3.14.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - - it "returns the expected version" do - expect(latest_version_details[:version]).to eq(version_class.new("3.14.0")) - end - end - - context "when `packageSourceMapping`s are specified" do - let(:csproj_body) do - <<~XML - - - net8.0 - - - - - - XML - end - let(:config_file) do - Dependabot::DependencyFile.new( - name: "NuGet.Config", - content: - <<~XML - - - - - - - - - - - - - - - - - - XML - ) - end - let(:dependency_files) { [csproj, config_file] } - let(:dependency) do - Dependabot::Dependency.new( - name: "Some.Package", - version: "1.0.0", - requirements: [{ file: "my.csproj", requirement: "1.0.0", groups: ["dependencies"], source: nil }], - package_manager: "nuget" - ) - end - let(:expected_version) { version_class.new("1.1.0") } - - def create_nupkg(nuspec_name, nuspec_content) - content = Zip::OutputStream.write_buffer do |zio| - zio.put_next_entry("#{nuspec_name}.nuspec") - zio.write(nuspec_content) - end - content.rewind - content.sysread - end - - before do - allow(finder).to receive(:str_version_compatible?).and_call_original - - # stub source 1 - stub_index_json("https://nuget.example.com/source1/index.json") - stub_request(:get, "https://nuget.example.com/source1/RegistrationsBaseUrl/some.package/index.json") - .to_return( - status: 200, - body: { - count: 1, - items: [ - { - count: 2, - items: [ - { - catalogEntry: { - id: "Some.Package", - version: "1.0.0" # this is what's currently installed - } - }, - { - catalogEntry: { - id: "Some.Package", - version: "1.1.0" # this is what we'd like to upgrade to - } - } - ] - } - ] - }.to_json - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.0.0/some.package.1.0.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - stub_request(:get, "https://nuget.example.com/source1/PackageBaseAddress/some.package/1.1.0/some.package.1.1.0.nupkg") - .to_return( - status: 200, - body: create_nupkg( - "Some.Package", - <<~XML - - - - - - - - XML - ) - ) - # none of the `source2` URLs should be called - end - - it "returns the expected version honoring the package source mapping" do - expect(latest_version_details[:version]).to eq(version_class.new("1.1.0")) - end - end - end - - describe "#lowest_security_fix_version_details" do - subject(:lowest_security_fix_version_details) do - finder.lowest_security_fix_version_details - end - - let(:dependency_version) { "1.1.1" } - let(:security_advisories) do - [ - Dependabot::SecurityAdvisory.new( - dependency_name: "rails", - package_manager: "nuget", - vulnerable_versions: ["< 2.0.0"] - ) - ] - end - - let(:expected_version) { "2.0.0" } - - before do - allow(finder).to receive(:str_version_compatible?).with(dependency_version.to_s).and_return(true) - allow(finder).to receive(:str_version_compatible?).with(expected_version.to_s).and_return(true) - end - - its([:version]) { is_expected.to eq(version_class.new("2.0.0")) } - - context "when the user is ignoring the lowest version" do - let(:ignored_versions) { ["<= 2.0.0"] } - let(:expected_version) { "2.0.3" } - - its([:version]) { is_expected.to eq(version_class.new("2.0.3")) } - end - end - - describe "#versions" do - subject(:versions) { finder.versions } - - it "includes the correct versions" do - expect(versions.count).to eq(21) - expect(versions.first).to eq( - nuspec_url: "https://api.nuget.org/v3-flatcontainer/" \ - "microsoft.extensions.dependencymodel/1.0.0-rc2-002702/" \ - "microsoft.extensions.dependencymodel.nuspec", - repo_url: "https://api.nuget.org/v3/index.json", - source_url: nil, - version: Dependabot::Nuget::Version.new("1.0.0-rc2-002702") - ) - end - end -end diff --git a/nuget/spec/dependabot/nuget/update_checker_spec.rb b/nuget/spec/dependabot/nuget/update_checker_spec.rb index 8b99530899e..e7301b1ed08 100644 --- a/nuget/spec/dependabot/nuget/update_checker_spec.rb +++ b/nuget/spec/dependabot/nuget/update_checker_spec.rb @@ -4,9 +4,14 @@ require "spec_helper" require "dependabot/dependency" require "dependabot/dependency_file" +require "dependabot/nuget/analysis/analysis_json_reader" +require "dependabot/nuget/native_discovery/native_discovery_json_reader" +require "dependabot/nuget/file_parser" require "dependabot/nuget/update_checker" +require "dependabot/nuget/requirement" require "dependabot/nuget/version" require_common_spec "update_checkers/shared_examples_for_update_checkers" + RSpec.describe Dependabot::Nuget::UpdateChecker do let(:version_class) { Dependabot::Nuget::Version } let(:security_advisories) { [] } @@ -37,119 +42,409 @@ package_manager: "nuget" ) end - let(:checker) do - described_class.new( + + let(:stub_native_tools) { true } # set to `false` to allow invoking the native tools during tests + let(:report_stub_debug_information) { false } # set to `true` to write native tool stubbing information to the screen + + let(:repo_contents_path) { write_tmp_repo(dependency_files) } + let(:source) do + Dependabot::Source.new( + provider: "github", + repo: "gocardless/bump", + directory: "/" + ) + end + + before do + Dependabot::Experiments.register(:nuget_native_analysis, true) + end + + it_behaves_like "an update checker" + + def run_analyze_test(&_block) + # caching is explicitly required for these tests + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "false" + + # don't allow a previous test to pollute the file parser cache + Dependabot::Nuget::FileParser.file_dependency_cache.clear + + # calling `#parse` is necessary to force `discover` which is stubbed below + Dependabot::Nuget::FileParser.new(dependency_files: dependency_files, + source: source, + repo_contents_path: repo_contents_path).parse + + # create the checker... + checker = described_class.new( dependency: dependency, dependency_files: dependency_files, credentials: credentials, + repo_contents_path: repo_contents_path, ignored_versions: ignored_versions, security_advisories: security_advisories ) - end - - it_behaves_like "an update checker" - def nuspec_url(name, version) - "https://api.nuget.org/v3-flatcontainer/#{name.downcase}/#{version}/#{name.downcase}.nuspec" + # ...and invoke the actual test + yield checker + ensure + Dependabot::Nuget::NativeDiscoveryJsonReader.clear_discovery_file_path_from_cache(dependency_files) + ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" end def registration_index_url(name) "https://api.nuget.org/v3/registration5-gz-semver2/#{name.downcase}/index.json" end - describe "up_to_date?" do - subject(:up_to_date?) { checker.up_to_date? } - - context "with a property dependency" do - context "when a dependency's property couldn't be found" do - let(:dependency_name) { "Nuke.Common" } - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] + def intercept_native_tools(discovery_content_hash:, dependency_name:, analysis_content_hash:) + return unless stub_native_tools + + # don't allow `FileParser#parse` to call into the native tool; just fake it + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_discover_tool) + .and_wrap_original do |_original_method, *args, &_block| + discovery_json_path = args[0][:output_path] + FileUtils.mkdir_p(File.dirname(discovery_json_path)) + if report_stub_debug_information + puts "stubbing call to `run_nuget_discover_tool` with args #{args}; writing prefabricated discovery " \ + "response to discovery.json to #{discovery_json_path}" end - let(:dependency_version) { "$(NukeVersion)" } - - it { is_expected.to be(true) } + discovery_json_content = discovery_content_hash.to_json + File.write(discovery_json_path, discovery_json_content) end - end - - context "with a transient dependency" do - context "with no vulnerability" do - let(:dependency_name) { "Nuke.Common" } - let(:dependency_requirements) { [] } - let(:dependency_version) { "2.0.0" } - it { is_expected.to be(true) } + # prevent calling the analysis tool + allow(Dependabot::Nuget::NativeHelpers) + .to receive(:run_nuget_analyze_tool) + .and_wrap_original do |_original_method, *args, &_block| + # write prefabricated analysis json file + analysis_json_path = Dependabot::Nuget::AnalysisJsonReader.analysis_file_path(dependency_name: dependency_name) + if report_stub_debug_information + puts "stubbing call to `run_nuget_analyze_tool` with args #{args}; writing prefabricated analysis response " \ + "to #{analysis_json_path}" + end + analysis_json_content = analysis_content_hash.to_json + FileUtils.mkdir_p(File.dirname(analysis_json_path)) + File.write(analysis_json_path, analysis_json_content) end - end end - describe "#latest_version" do - subject { checker.latest_version } + describe "up_to_date?" do + context "with a property dependency whose property couldn't be found" do + let(:dependency_name) { "Nuke.Common" } + let(:dependency_requirements) do + [{ + requirement: "$(NukeVersion)", + file: "my.csproj", + groups: ["dependencies"], + source: nil, + metadata: { property_name: "NukeVersion" } + }] + end + let(:dependency_version) { "$(NukeVersion)" } - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:latest_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [], # dependency not found + IsSuccess: true, + Properties: [ + { + Name: "TargetFrameworks", + Value: "netstandard1.6;net462", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net462", "netstandard1.6"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "$(NukeVersion)", + CanUpdate: false, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + } + ) + end - expect(checker.latest_version).to eq("1.2.3") + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.up_to_date?).to be(true) + end + end end - context "when the package could not be found on any source" do + context "with a dependency that is not reported" do + let(:dependency_name) { "Nuke.Common" } + let(:dependency_requirements) { [] } + let(:dependency_version) { "2.0.0" } + before do - stub_request(:get, registration_index_url("microsoft.extensions.dependencymodel")) - .to_return(status: 404) + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [], # dependency not found + IsSuccess: true, + Properties: [ + { + Name: "TargetFrameworks", + Value: "netstandard1.6;net462", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net462", "netstandard1.6"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "2.0.0", + CanUpdate: false, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [] + } + ) end - it "reports the current version" do - expect(checker.latest_version).to eq("1.1.1") + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.up_to_date?).to be(true) + end end end - end - describe "#lowest_security_fix_version" do - subject { checker.lowest_security_fix_version } - - it "delegates to the VersionFinder class" do - version_finder_class = described_class::VersionFinder - dummy_version_finder = instance_double(version_finder_class) - allow(version_finder_class) - .to receive(:new) - .and_return(dummy_version_finder) - allow(dummy_version_finder) - .to receive(:lowest_security_fix_version_details) - .and_return(version: Dependabot::Nuget::Version.new("1.2.3")) - - expect(checker.lowest_security_fix_version).to eq("1.2.3") + context "with a dependency that can be updated" do + let(:dependency_name) { "Nuke.Common" } + let(:dependency_requirements) { [] } + let(:dependency_version) { "2.0.0" } + + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Nuke.Common", + Version: "2.0.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net462", "netstandard1.6"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFrameworks", + Value: "netstandard1.6;net462", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net462", "netstandard1.6"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "2.0.1", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Nuke.Common", + Version: "2.0.1", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net462", "netstandard1.6"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end + + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.up_to_date?).to be(false) + end + end end end describe "#latest_resolvable_version" do - subject(:latest_resolvable_version) { checker.latest_resolvable_version } + context "when a partial unlock cannot be performed" do + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "1.1.2", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.2", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end - it { is_expected.to be_nil } + it "reports `nil`" do + run_analyze_test do |checker| + expect(checker.latest_resolvable_version).to be_nil + end + end + end end describe "#latest_resolvable_version_with_no_unlock" do - subject { checker.latest_resolvable_version_with_no_unlock } + context "when a full unlock cannot be performed" do + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "1.1.2", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.2", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end - it { is_expected.to be_nil } + it "returns `nil`" do + run_analyze_test do |checker| + expect(checker.latest_resolvable_version_with_no_unlock).to be_nil + end + end + end end - describe "#can_update?(requirements_to_unlock: :all)" do - subject(:can_update) { checker.can_update?(requirements_to_unlock: :all) } - - context "with a property dependency" do + describe "#requirements_unlocked_or_can_be?" do + context "when there is a newer package available" do let(:dependency_requirements) do [{ requirement: "0.1.434", @@ -162,101 +457,75 @@ def registration_index_url(name) let(:dependency_name) { "Nuke.Common" } let(:dependency_version) { "0.1.434" } - context "when a property is used for multiple dependencies" do - let(:csproj_body) do - fixture("csproj", "property_version.csproj") - end - - context "when all dependencies can update to the latest version" do - before do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive_messages(all_property_based_dependencies: [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ], latest_version: "0.9.0", property_updater: dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - end - - it { is_expected.to be(true) } - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Nuke.Common", + Version: "0.1.434", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "6.3.0", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Nuke.Common", + Version: "6.3.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] + } + ) + end - context "when all dependencies cannot update to the latest version" do - before do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive_messages(all_property_based_dependencies: [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ], latest_version: "0.9.0", property_updater: dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(false) - end - - it { is_expected.to be(false) } + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.requirements_unlocked_or_can_be?).to be(true) end end end end - describe "#updated_requirements" do - subject(:updated_requirements) { checker.updated_requirements } - - let(:target_version) { "2.1.0" } - - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:latest_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] - ) - end - - context "with a security vulnerability" do + describe "#lowest_security_fix_version" do + context "when an appropriate version is returned" do let(:target_version) { "2.0.0" } let(:vulnerable_versions) { ["< 2.0.0"] } let(:security_advisories) do @@ -269,120 +538,142 @@ def registration_index_url(name) ] end - it "delegates to the RequirementsUpdater" do - allow(checker).to receive(:lowest_security_fix_version_details).and_return( - { - version: target_version, - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" - } - ) - - expect(described_class::RequirementsUpdater).to receive(:new).with( - requirements: dependency_requirements, - latest_version: target_version, - source_details: { - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version), - repo_url: "https://api.nuget.org/v3/index.json" + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "2.0.0", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "2.0.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }] } - ).and_call_original - expect(updated_requirements).to eq( - [{ - file: "my.csproj", - requirement: target_version, - groups: ["dependencies"], - source: { - type: "nuget_repo", - url: "https://api.nuget.org/v3/index.json", - source_url: nil, - nuspec_url: nuspec_url(dependency_name, target_version) - } - }] ) end - context "when the security vulnerability excludes all compatible packages" do - subject(:updated_requirement_version) { updated_requirements[0].fetch(:requirement) } - - let(:target_version) { "1.1.1" } - let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad - - before do - # only vulnerable versions are returned - stub_request(:get, registration_index_url(dependency_name)) - .to_return( - status: 200, - body: { - items: [ - items: [ - { - catalogEntry: { - version: "1.1.1" # the currently installed version, but it's vulnerable - } - }, - { - catalogEntry: { - version: "3.0.0" # newer version, but it's still vulnerable - } - } - ] - ] - }.to_json - ) - end - - it "reports the currently installed version" do - expect(updated_requirement_version).to eq(target_version) + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.lowest_security_fix_version).to eq(target_version) end end end - end - describe "#requirements_unlocked_or_can_be?" do - subject(:requirements_unlocked_or_can_be) do - checker.requirements_unlocked_or_can_be? - end + context "when the security vulnerability excludes all compatible packages" do + let(:target_version) { "1.1.1" } + let(:vulnerable_versions) { ["< 999.999.999"] } # it's all bad + let(:security_advisories) do + [ + Dependabot::SecurityAdvisory.new( + dependency_name: dependency_name, + package_manager: "nuget", + vulnerable_versions: vulnerable_versions + ) + ] + end - context "with a property dependency" do - let(:dependency_requirements) do - [{ - requirement: "0.1.434", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Microsoft.Extensions.DependencyModel", + Version: "1.1.1", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Microsoft.Extensions.DependencyModel", + analysis_content_hash: { + UpdatedVersion: "1.1.1", + CanUpdate: false, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [] + } + ) end - let(:dependency_name) { "Nuke.Common" } - let(:dependency_version) { "0.1.434" } - it { is_expected.to be(true) } - - context "when a dependency's property couldn't be found" do - let(:dependency_requirements) do - [{ - requirement: "$(NukeVersion)", - file: "my.csproj", - groups: ["dependencies"], - source: nil, - metadata: { property_name: "NukeVersion" } - }] + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.lowest_security_fix_version).to eq(target_version) end - let(:dependency_version) { "$(NukeVersion)" } - - it { is_expected.to be(false) } end end end describe "#updated_dependencies(requirements_to_unlock: :all)" do - subject(:updated_dependencies) do - checker.updated_dependencies(requirements_to_unlock: :all) - end - - context "with a property dependency" do + context "when all dependencies can update to the latest version" do let(:dependency_requirements) do [{ requirement: "0.1.434", @@ -395,38 +686,135 @@ def registration_index_url(name) let(:dependency_name) { "Nuke.Common" } let(:dependency_version) { "0.1.434" } - context "when a property is used for multiple dependencies" do - let(:csproj_body) do - fixture("csproj", "property_version.csproj") - end + before do + intercept_native_tools( + discovery_content_hash: { + Path: "", + IsSuccess: true, + Projects: [ + { + FilePath: "my.csproj", + Dependencies: [{ + Name: "Nuke.CodeGeneration", + Version: "0.1.434", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }, { + Name: "Nuke.Common", + Version: "0.1.434", + Type: "PackageReference", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: nil + }], + IsSuccess: true, + Properties: [ + { + Name: "TargetFramework", + Value: "net8.0", + SourceFilePath: "my.csproj" + } + ], + TargetFrameworks: ["net8.0"], + ReferencedProjectPaths: [] + } + ], + DirectoryPackagesProps: nil, + GlobalJson: nil, + DotNetToolsJson: nil + }, + dependency_name: "Nuke.Common", + analysis_content_hash: { + UpdatedVersion: "6.3.0", + CanUpdate: true, + VersionComesFromMultiDependencyProperty: false, + UpdatedDependencies: [{ + Name: "Nuke.CodeGeneration", + Version: "6.3.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: "https://nuget.example.com/nuke.codegeneration" + }, { + Name: "Nuke.Common", + Version: "6.3.0", + Type: "Unknown", + EvaluationResult: nil, + TargetFrameworks: ["net8.0"], + IsDevDependency: false, + IsDirect: true, + IsTransitive: false, + IsOverride: false, + IsUpdate: false, + InfoUrl: "https://nuget.example.com/nuke.common" + }] + } + ) + end - context "when all dependencies can update to the latest version" do - before do - allow(checker).to receive_messages(latest_version: "0.9.0", all_property_based_dependencies: [ - Dependabot::Dependency.new( - name: "Nuke.Common", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ), - Dependabot::Dependency.new( - name: "Nuke.CodeGeneration", - version: "0.1.434", - requirements: dependency_requirements, - package_manager: "nuget" - ) - ]) - end - - it "delegates to PropertyUpdater" do - property_updater_class = described_class::PropertyUpdater - dummy_property_updater = instance_double(property_updater_class) - allow(checker).to receive(:property_updater).and_return(dummy_property_updater) - allow(dummy_property_updater).to receive(:update_possible?).and_return(true) - expect(dummy_property_updater).to receive(:updated_dependencies).and_return([dependency]) - - updated_dependencies - end + it "reports the expected result" do + run_analyze_test do |checker| + expect(checker.updated_dependencies(requirements_to_unlock: :all)).to eq([ + Dependabot::Dependency.new( + name: "Nuke.CodeGeneration", + version: "6.3.0", + previous_version: "0.1.434", + requirements: [{ + requirement: "6.3.0", + file: "/my.csproj", + groups: ["dependencies"], + source: { + type: "nuget_repo", + source_url: "https://nuget.example.com/nuke.codegeneration" + } + }], + previous_requirements: [{ + requirement: "0.1.434", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }], + package_manager: "nuget" + ), + Dependabot::Dependency.new( + name: "Nuke.Common", + version: "6.3.0", + previous_version: "0.1.434", + requirements: [{ + requirement: "6.3.0", + file: "/my.csproj", + groups: ["dependencies"], + source: { + type: "nuget_repo", + source_url: "https://nuget.example.com/nuke.common" + } + }], + previous_requirements: [{ + requirement: "0.1.434", + file: "/my.csproj", + groups: ["dependencies"], + source: nil + }], + package_manager: "nuget" + ) + ]) end end end diff --git a/nuget/spec/fixtures/csproj/property_version.csproj b/nuget/spec/fixtures/csproj/property_version.csproj index 62557f88433..4d8198bc238 100644 --- a/nuget/spec/fixtures/csproj/property_version.csproj +++ b/nuget/spec/fixtures/csproj/property_version.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + net5.0 false False diff --git a/nuget/spec/spec_helper.rb b/nuget/spec/spec_helper.rb index a23c3fa17f4..a06f2f997b8 100644 --- a/nuget/spec/spec_helper.rb +++ b/nuget/spec/spec_helper.rb @@ -1,6 +1,9 @@ # typed: true # frozen_string_literal: true +# require "dependabot/experiments" +# Dependabot::Experiments.register(:nuget_native_analysis, true) + ENV["DEPENDABOT_NUGET_TEST_RUN"] = "true" ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] = "true" diff --git a/pub/.rubocop.yml b/pub/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/pub/.rubocop.yml +++ b/pub/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/pub/lib/dependabot/pub/file_fetcher.rb b/pub/lib/dependabot/pub/file_fetcher.rb index 6345ca002b7..62e81d7ed2d 100644 --- a/pub/lib/dependabot/pub/file_fetcher.rb +++ b/pub/lib/dependabot/pub/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strong # frozen_string_literal: true require "sorbet-runtime" @@ -13,10 +13,12 @@ class FileFetcher < Dependabot::FileFetchers::Base extend T::Sig extend T::Helpers + sig { override.params(filenames: T::Array[String]).returns(T::Boolean) } def self.required_files_in?(filenames) filenames.include?("pubspec.yaml") end + sig { override.returns(String) } def self.required_files_message "Repo must contain a pubspec.yaml." end @@ -38,14 +40,16 @@ def fetch_files private + sig { returns(DependencyFile) } def pubspec_yaml - @pubspec_yaml ||= fetch_file_from_host("pubspec.yaml") + @pubspec_yaml ||= T.let(fetch_file_from_host("pubspec.yaml"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(DependencyFile)) } def pubspec_lock return @pubspec_lock if defined?(@pubspec_lock) - @pubspec_lock = fetch_file_if_present("pubspec.lock") + @pubspec_lock = T.let(fetch_file_if_present("pubspec.lock"), T.nilable(Dependabot::DependencyFile)) end end end diff --git a/pub/lib/dependabot/pub/requirement.rb b/pub/lib/dependabot/pub/requirement.rb index ac0a89669d2..be6ca5485b3 100644 --- a/pub/lib/dependabot/pub/requirement.rb +++ b/pub/lib/dependabot/pub/requirement.rb @@ -1,4 +1,4 @@ -# typed: true +# typed: strict # frozen_string_literal: true # For details on pub version constraints see: @@ -20,11 +20,16 @@ class Requirement < Dependabot::Requirement quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|") version_pattern = Pub::Version::VERSION_PATTERN - PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze + PATTERN_RAW = T.let("\\s*(#{quoted})?\\s*(#{version_pattern})\\s*".freeze, String) PATTERN = /\A#{PATTERN_RAW}\z/ # Use Pub::Version rather than Gem::Version to ensure that # pre-release versions aren't transformed. + sig do + params( + obj: T.any(String, Gem::Version, Pub::Version) + ).returns(T::Array[T.any(String, Pub::Version)]) + end def self.parse(obj) return ["=", Pub::Version.new(obj.to_s)] if obj.is_a?(Gem::Version) @@ -43,9 +48,10 @@ def self.parse(obj) # contains a single element. sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) } def self.requirements_array(requirement_string) - [new(requirement_string)] + [new(T.must(requirement_string))] end + sig { params(requirements: T.any(String, T::Array[String]), raw_constraint: T.nilable(String)).void } def initialize(*requirements, raw_constraint: nil) requirements = requirements.flatten.flat_map do |req_string| req_string.split(",").map(&:strip).map do |r| @@ -57,6 +63,7 @@ def initialize(*requirements, raw_constraint: nil) @raw_constraint = raw_constraint end + sig { returns(String) } def to_s if @raw_constraint.nil? as_list.join " " @@ -67,6 +74,7 @@ def to_s private + sig { params(req_string: String).returns(T.any(String, T::Array[T.nilable(String)])) } def convert_dart_constraint_to_ruby_constraint(req_string) if req_string.empty? || req_string == "any" then ">= 0" elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string) @@ -77,18 +85,21 @@ def convert_dart_constraint_to_ruby_constraint(req_string) end end + sig { params(req_string: String).returns(String) } def convert_tilde_req(req_string) version = req_string.gsub(/^~/, "") parts = version.split(".") "~> #{parts.join('.')}" end + sig { params(req_string: String).returns(T::Array[T.nilable(String)]) } def convert_range_req(req_string) req_string.scan( /((?:>|<|=|<=|>=)\s*#{Pub::Version::VERSION_PATTERN})\s*/o - ).map { |x| x[0].strip } + ).map { |x| x[0]&.strip } end + sig { params(req_string: String).returns(String) } def ruby_range(req_string) parts = req_string.split(".") @@ -103,6 +114,7 @@ def ruby_range(req_string) "~> #{parts.join('.')}" end + sig { params(req_string: String).returns(T::Array[String]) } def convert_caret_req(req_string) # Copied from Cargo::Requirement which allows less than 3 components # so we could be more strict in the parsing here. @@ -112,7 +124,7 @@ def convert_caret_req(req_string) first_non_zero_index = first_non_zero ? parts.index(first_non_zero) : parts.count - 1 upper_bound = parts.map.with_index do |part, i| - if i < first_non_zero_index then part + if i < T.must(first_non_zero_index) then part elsif i == first_non_zero_index then (part.to_i + 1).to_s else 0 diff --git a/pub/spec/dependabot/pub/file_updater_spec.rb b/pub/spec/dependabot/pub/file_updater_spec.rb index c5adf480fcf..eac37910f23 100644 --- a/pub/spec/dependabot/pub/file_updater_spec.rb +++ b/pub/spec/dependabot/pub/file_updater_spec.rb @@ -11,11 +11,13 @@ RSpec.describe Dependabot::Pub::FileUpdater do let(:project) { "can_update" } + let(:dev_null) { WEBrick::Log.new("/dev/null", 7) } + let(:server) { WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) } let(:dependency_files) do files = project_dependency_files(project) files.each do |file| # Simulate that the lockfile was from localhost: - file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{@server[:Port]}") + file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{server[:Port]}") end files end @@ -31,7 +33,7 @@ "password" => "token" }], options: { - pub_hosted_url: "http://localhost:#{@server[:Port]}" + pub_hosted_url: "http://localhost:#{server[:Port]}" } ) end @@ -41,22 +43,20 @@ after do sample_files.each do |f| package = File.basename(f, ".json") - @server.unmount "/api/packages/#{package}" + server.unmount "/api/packages/#{package}" end - @server.shutdown + server.shutdown end before do # Because we do the networking in dependency_services we have to run an # actual web server. - dev_null = WEBrick::Log.new("/dev/null", 7) - @server = WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) Thread.new do - @server.start + server.start end sample_files.each do |f| package = File.basename(f, ".json") - @server.mount_proc "/api/packages/#{package}" do |_req, res| + server.mount_proc "/api/packages/#{package}" do |_req, res| res.body = File.read(File.join("..", "..", "..", f)) end end diff --git a/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb b/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb index dbfadca0612..1e7b7fedfdf 100644 --- a/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb +++ b/pub/spec/dependabot/pub/infer_sdk_versions_spec.rb @@ -8,26 +8,26 @@ require "webrick" RSpec.describe "Helpers" do + let(:dev_null) { WEBrick::Log.new("/dev/null", 7) } + let(:inferred_result) do + Dependabot::Pub::Helpers.run_infer_sdk_versions \ + File.join("spec", "fixtures", "projects", project), url: "http://localhost:#{server[:Port]}/flutter_releases.json" + end + let(:server) { WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) } + before do # Because we do the networking in infer_sdk_versions we have to run an # actual web server. - dev_null = WEBrick::Log.new("/dev/null", 7) - @server = WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) Thread.new do - @server.start + server.start end - @server.mount_proc "/flutter_releases.json" do |_req, res| + server.mount_proc "/flutter_releases.json" do |_req, res| res.body = File.read(File.join(__dir__, "..", "..", "fixtures", "flutter_releases.json")) end end after do - @server.shutdown - end - - let(:inferred_result) do - Dependabot::Pub::Helpers.run_infer_sdk_versions \ - File.join("spec", "fixtures", "projects", project), url: "http://localhost:#{@server[:Port]}/flutter_releases.json" + server.shutdown end describe "Will resolve to latest beta if needed" do diff --git a/pub/spec/dependabot/pub/update_checker_spec.rb b/pub/spec/dependabot/pub/update_checker_spec.rb index 105bf1e7c07..6bede9e6c7f 100644 --- a/pub/spec/dependabot/pub/update_checker_spec.rb +++ b/pub/spec/dependabot/pub/update_checker_spec.rb @@ -18,11 +18,13 @@ let(:can_update) { checker.can_update?(requirements_to_unlock: requirements_to_unlock) } let(:directory) { nil } let(:project) { "can_update" } + let(:dev_null) { WEBrick::Log.new("/dev/null", 7) } + let(:server) { WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) } let(:dependency_files) do files = project_dependency_files(project) files.each do |file| # Simulate that the lockfile was from localhost: - file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{@server[:Port]}") + file.content.gsub!("https://pub.dartlang.org", "http://localhost:#{server[:Port]}") if defined?(git_dir) file.content.gsub!("$GIT_DIR", git_dir) file.content.gsub!("$REF", dependency_version) @@ -58,8 +60,8 @@ }], ignored_versions: ignored_versions, options: { - pub_hosted_url: "http://localhost:#{@server[:Port]}", - flutter_releases_url: "http://localhost:#{@server[:Port]}/flutter_releases.json" + pub_hosted_url: "http://localhost:#{server[:Port]}", + flutter_releases_url: "http://localhost:#{server[:Port]}/flutter_releases.json" }, raise_on_ignored: raise_on_ignored, security_advisories: security_advisories, @@ -72,26 +74,24 @@ after do sample_files.each do |f| package = File.basename(f, ".json") - @server.unmount "/api/packages/#{package}" + server.unmount "/api/packages/#{package}" end - @server.shutdown + server.shutdown end before do # Because we do the networking in dependency_services we have to run an # actual web server. - dev_null = WEBrick::Log.new("/dev/null", 7) - @server = WEBrick::HTTPServer.new({ Port: 0, AccessLog: [], Logger: dev_null }) Thread.new do - @server.start + server.start end sample_files.each do |f| package = File.basename(f, ".json") - @server.mount_proc "/api/packages/#{package}" do |_req, res| + server.mount_proc "/api/packages/#{package}" do |_req, res| res.body = File.read(File.join("..", "..", "..", f)) end end - @server.mount_proc "/flutter_releases.json" do |_req, res| + server.mount_proc "/flutter_releases.json" do |_req, res| res.body = File.read(File.join(__dir__, "..", "..", "fixtures", "flutter_releases.json")) end end @@ -521,7 +521,7 @@ WebMock.allow_net_connect! # To find the vulnerable versions we do a package listing before invoking the helper. # Stub this out here: - stub_request(:get, "http://localhost:#{@server[:Port]}/api/packages/#{dependency.name}").to_return( + stub_request(:get, "http://localhost:#{server[:Port]}/api/packages/#{dependency.name}").to_return( status: 200, body: fixture("pub_dev_responses/simple/#{dependency.name}.json"), headers: {} @@ -595,7 +595,7 @@ WebMock.allow_net_connect! # To find the vulnerable versions we do a package listing before invoking the helper. # Stub this out here: - stub_request(:get, "http://localhost:#{@server[:Port]}/api/packages/#{dependency.name}").to_return( + stub_request(:get, "http://localhost:#{server[:Port]}/api/packages/#{dependency.name}").to_return( status: 200, body: fixture("pub_dev_responses/simple/#{dependency.name}.json"), headers: {} diff --git a/python/helpers/requirements.txt b/python/helpers/requirements.txt index ef35b13a34a..4aad2ff2eb0 100644 --- a/python/helpers/requirements.txt +++ b/python/helpers/requirements.txt @@ -4,7 +4,7 @@ flake8==7.1.0 hashin==1.0.1 pipenv==2023.12.1 plette==2.1.0 -poetry==1.8.2 +poetry==1.8.3 # TODO: Replace 3p package `toml` with 3.11's new stdlib `tomllib` once we drop support for Python 3.10. toml==0.10.2 diff --git a/python/lib/dependabot/python/authed_url_builder.rb b/python/lib/dependabot/python/authed_url_builder.rb index 37c10eb560d..e2eb95ed7f1 100644 --- a/python/lib/dependabot/python/authed_url_builder.rb +++ b/python/lib/dependabot/python/authed_url_builder.rb @@ -7,6 +7,7 @@ class AuthedUrlBuilder def self.authed_url(credential:) token = credential.fetch("token", nil) url = credential.fetch("index-url", nil) + return "" unless url return url unless token basic_auth_details = diff --git a/python/lib/dependabot/python/file_fetcher.rb b/python/lib/dependabot/python/file_fetcher.rb index 910c95f43cb..e878452fc1a 100644 --- a/python/lib/dependabot/python/file_fetcher.rb +++ b/python/lib/dependabot/python/file_fetcher.rb @@ -1,4 +1,4 @@ -# typed: false +# typed: true # frozen_string_literal: true require "toml-rb" @@ -139,7 +139,7 @@ def python_version_file # Check the top-level for a .python-version file, too reverse_path = Pathname.new(directory[0]).relative_path_from(directory) - @python_version_file ||= + @python_version_file = fetch_support_file(File.join(reverse_path, ".python-version")) &.tap { |f| f.name = ".python-version" } end diff --git a/python/lib/dependabot/python/file_updater.rb b/python/lib/dependabot/python/file_updater.rb index c7d4a7c88bc..cc02c3160b4 100644 --- a/python/lib/dependabot/python/file_updater.rb +++ b/python/lib/dependabot/python/file_updater.rb @@ -1,19 +1,23 @@ -# typed: true +# typed: strict # frozen_string_literal: true require "toml-rb" require "dependabot/file_updaters" require "dependabot/file_updaters/base" require "dependabot/shared_helpers" +require "sorbet-runtime" module Dependabot module Python class FileUpdater < Dependabot::FileUpdaters::Base + extend T::Sig + require_relative "file_updater/pipfile_file_updater" require_relative "file_updater/pip_compile_file_updater" require_relative "file_updater/poetry_file_updater" require_relative "file_updater/requirement_file_updater" + sig { override.returns(T::Array[Regexp]) } def self.updated_files_regex [ /^Pipfile$/, @@ -27,6 +31,7 @@ def self.updated_files_regex ] end + sig { override.returns(T::Array[DependencyFile]) } def updated_dependency_files updated_files = case resolver_type @@ -48,6 +53,8 @@ def updated_dependency_files private # rubocop:disable Metrics/PerceivedComplexity + + sig { returns(Symbol) } def resolver_type reqs = dependencies.flat_map(&:requirements) changed_reqs = reqs.zip(dependencies.flat_map(&:previous_requirements)) @@ -76,6 +83,7 @@ def resolver_type end # rubocop:enable Metrics/PerceivedComplexity + sig { returns(Symbol) } def subdependency_resolver return :pipfile if pipfile_lock return :poetry if poetry_lock @@ -84,6 +92,7 @@ def subdependency_resolver raise "Claimed to be a sub-dependency, but no lockfile exists!" end + sig { returns(T::Array[DependencyFile]) } def updated_pipfile_based_files PipfileFileUpdater.new( dependencies: dependencies, @@ -93,6 +102,7 @@ def updated_pipfile_based_files ).updated_dependency_files end + sig { returns(T::Array[DependencyFile]) } def updated_poetry_based_files PoetryFileUpdater.new( dependencies: dependencies, @@ -101,6 +111,7 @@ def updated_poetry_based_files ).updated_dependency_files end + sig { returns(T::Array[DependencyFile]) } def updated_pip_compile_based_files PipCompileFileUpdater.new( dependencies: dependencies, @@ -110,6 +121,7 @@ def updated_pip_compile_based_files ).updated_dependency_files end + sig { returns(T::Array[DependencyFile]) } def updated_requirement_based_files RequirementFileUpdater.new( dependencies: dependencies, @@ -119,6 +131,7 @@ def updated_requirement_based_files ).updated_dependency_files end + sig { returns(T::Array[String]) } def pip_compile_index_urls if credentials.any?(&:replaces_base?) credentials.select(&:replaces_base?).map { |cred| AuthedUrlBuilder.authed_url(credential: cred) } @@ -130,6 +143,7 @@ def pip_compile_index_urls end end + sig { override.void } def check_required_files filenames = dependency_files.map(&:name) return if filenames.any? { |name| name.end_with?(".txt", ".in") } @@ -141,31 +155,39 @@ def check_required_files raise "Missing required files!" end + sig { returns(T::Boolean) } def poetry_based? return false unless pyproject - !TomlRB.parse(pyproject.content).dig("tool", "poetry").nil? + !TomlRB.parse(pyproject&.content).dig("tool", "poetry").nil? end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pipfile - @pipfile ||= get_original_file("Pipfile") + @pipfile ||= T.let(get_original_file("Pipfile"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pipfile_lock - @pipfile_lock ||= get_original_file("Pipfile.lock") + @pipfile_lock ||= T.let(get_original_file("Pipfile.lock"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def pyproject - @pyproject ||= get_original_file("pyproject.toml") + @pyproject ||= T.let(get_original_file("pyproject.toml"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T.nilable(Dependabot::DependencyFile)) } def poetry_lock - @poetry_lock ||= get_original_file("poetry.lock") + @poetry_lock ||= T.let(get_original_file("poetry.lock"), T.nilable(Dependabot::DependencyFile)) end + sig { returns(T::Array[DependencyFile]) } def pip_compile_files - @pip_compile_files ||= - dependency_files.select { |f| f.name.end_with?(".in") } + @pip_compile_files ||= T.let( + dependency_files.select { |f| f.name.end_with?(".in") }, + T.nilable(T::Array[DependencyFile]) + ) end end end diff --git a/python/lib/dependabot/python/pip_compile_file_matcher.rb b/python/lib/dependabot/python/pip_compile_file_matcher.rb index 11e14beb837..7ac2bbb65dc 100644 --- a/python/lib/dependabot/python/pip_compile_file_matcher.rb +++ b/python/lib/dependabot/python/pip_compile_file_matcher.rb @@ -1,29 +1,35 @@ -# typed: true +# typed: strict # frozen_string_literal: true module Dependabot module Python class PipCompileFileMatcher + extend T::Sig + + sig { params(requirements_in_files: T::Array[Dependabot::Python::Requirement]).void } def initialize(requirements_in_files) @requirements_in_files = requirements_in_files end + sig { params(file: Dependabot::DependencyFile).returns(T::Boolean) } def lockfile_for_pip_compile_file?(file) return false unless requirements_in_files.any? name = file.name return false unless name.end_with?(".txt") - return true if file.content.match?(output_file_regex(name)) + return true if file.content&.match?(output_file_regex(name)) basename = name.gsub(/\.txt$/, "") - requirements_in_files.any? { |f| f.name == basename + ".in" } + requirements_in_files.any? { |f| f.instance_variable_get(:@name) == basename + ".in" } end private + sig { returns(T::Array[Dependabot::Python::Requirement]) } attr_reader :requirements_in_files + sig { params(filename: T.any(String, Symbol)).returns(String) } def output_file_regex(filename) "--output-file[=\s]+#{Regexp.escape(filename)}(?:\s|$)" end diff --git a/python/lib/dependabot/python/version.rb b/python/lib/dependabot/python/version.rb index d3ff1648e83..419502301fd 100644 --- a/python/lib/dependabot/python/version.rb +++ b/python/lib/dependabot/python/version.rb @@ -58,7 +58,7 @@ def <=>(other) epoch_comparison = epoch_comparison(other) return epoch_comparison unless epoch_comparison.zero? - version_comparison = super(other) + version_comparison = super return version_comparison unless version_comparison&.zero? post_version_comparison = post_version_comparison(other) diff --git a/python/spec/dependabot/python/authed_url_builder_spec.rb b/python/spec/dependabot/python/authed_url_builder_spec.rb index a3ee9d7633d..a84526da7c1 100644 --- a/python/spec/dependabot/python/authed_url_builder_spec.rb +++ b/python/spec/dependabot/python/authed_url_builder_spec.rb @@ -9,6 +9,20 @@ describe ".authed_url" do subject(:authed_url) { described_class.authed_url(credential: credential) } + context "without index-url" do + let(:credential) do + Dependabot::Credential.new({ + "type" => "python_index", + "replaces-base" => true + }) + end + + it "returns empty string" do + expect(authed_url) + .to eq("") + end + end + context "without a token" do let(:credential) do Dependabot::Credential.new({ diff --git a/silent/.rubocop.yml b/silent/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/silent/.rubocop.yml +++ b/silent/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/sorbet/rbi/gems/regexp_parser@2.9.0.rbi b/sorbet/rbi/gems/regexp_parser@2.9.2.rbi similarity index 99% rename from sorbet/rbi/gems/regexp_parser@2.9.0.rbi rename to sorbet/rbi/gems/regexp_parser@2.9.2.rbi index a53e6e1ee1a..5253fea7d13 100644 --- a/sorbet/rbi/gems/regexp_parser@2.9.0.rbi +++ b/sorbet/rbi/gems/regexp_parser@2.9.2.rbi @@ -1132,7 +1132,7 @@ end # source://regexp_parser//lib/regexp_parser/expression/classes/keep.rb#2 module Regexp::Expression::Keep; end -# TOOD: in regexp_parser v3.0.0 this should possibly be a Subexpression +# TODO: in regexp_parser v3.0.0 this should possibly be a Subexpression # that contains all expressions to its left. # # source://regexp_parser//lib/regexp_parser/expression/classes/keep.rb#5 @@ -2368,7 +2368,7 @@ class Regexp::Parser # source://regexp_parser//lib/regexp_parser/parser.rb#262 def assign_effective_number(exp); end - # Assigns referenced expressions to refering expressions, e.g. if there is + # Assigns referenced expressions to referring expressions, e.g. if there is # an instance of Backreference::Number, its #referenced_expression is set to # the instance of Group::Capture that it refers to via its number. # @@ -2888,7 +2888,7 @@ end # Base for all scanner validation errors # -# source://regexp_parser//lib/regexp_parser/scanner/errors/validation_error.rb#4 +# source://regexp_parser//lib/regexp_parser/scanner/errors/validation_error.rb#3 class Regexp::Scanner::ValidationError < ::Regexp::Scanner::ScannerError class << self # Centralizes and unifies the handling of validation related errors. @@ -3069,7 +3069,7 @@ class Regexp::Syntax::Base end # source://regexp_parser//lib/regexp_parser/syntax/versions.rb#8 -Regexp::Syntax::CURRENT = Regexp::Syntax::V3_1_0 +Regexp::Syntax::CURRENT = Regexp::Syntax::V3_2_0 # source://regexp_parser//lib/regexp_parser/syntax/version_lookup.rb#6 class Regexp::Syntax::InvalidVersionNameError < ::Regexp::Syntax::SyntaxError diff --git a/sorbet/rbi/gems/rubocop@1.63.2.rbi b/sorbet/rbi/gems/rubocop@1.65.0.rbi similarity index 97% rename from sorbet/rbi/gems/rubocop@1.63.2.rbi rename to sorbet/rbi/gems/rubocop@1.65.0.rbi index 2578a2da18e..043692fda01 100644 --- a/sorbet/rbi/gems/rubocop@1.63.2.rbi +++ b/sorbet/rbi/gems/rubocop@1.65.0.rbi @@ -97,44 +97,44 @@ class RuboCop::CLI private - # source://rubocop//lib/rubocop/cli.rb#152 + # source://rubocop//lib/rubocop/cli.rb#156 def act_on_options; end - # source://rubocop//lib/rubocop/cli.rb#194 + # source://rubocop//lib/rubocop/cli.rb#198 def apply_default_formatter; end - # source://rubocop//lib/rubocop/cli.rb#121 + # source://rubocop//lib/rubocop/cli.rb#125 def execute_runners; end - # source://rubocop//lib/rubocop/cli.rb#178 + # source://rubocop//lib/rubocop/cli.rb#182 def handle_editor_mode; end # @raise [Finished] # - # source://rubocop//lib/rubocop/cli.rb#183 + # source://rubocop//lib/rubocop/cli.rb#187 def handle_exiting_options; end - # source://rubocop//lib/rubocop/cli.rb#140 + # source://rubocop//lib/rubocop/cli.rb#144 def parallel_by_default!; end - # source://rubocop//lib/rubocop/cli.rb#76 + # source://rubocop//lib/rubocop/cli.rb#80 def profile_if_needed; end - # source://rubocop//lib/rubocop/cli.rb#109 + # source://rubocop//lib/rubocop/cli.rb#113 def require_gem(name); end - # source://rubocop//lib/rubocop/cli.rb#117 + # source://rubocop//lib/rubocop/cli.rb#121 def run_command(name); end - # source://rubocop//lib/rubocop/cli.rb#170 + # source://rubocop//lib/rubocop/cli.rb#174 def set_options_to_config_loader; end - # source://rubocop//lib/rubocop/cli.rb#129 + # source://rubocop//lib/rubocop/cli.rb#133 def suggest_extensions; end # @raise [OptionArgumentError] # - # source://rubocop//lib/rubocop/cli.rb#133 + # source://rubocop//lib/rubocop/cli.rb#137 def validate_options_vs_config; end end @@ -541,7 +541,7 @@ end # # @api private # -# source://rubocop//lib/rubocop/cli/command/suggest_extensions.rb#12 +# source://rubocop//lib/rubocop/cli/command/suggest_extensions.rb#11 class RuboCop::CLI::Command::SuggestExtensions < ::RuboCop::CLI::Command::Base # @api private # @@ -744,6 +744,11 @@ class RuboCop::CachedData # source://rubocop//lib/rubocop/cached_data.rb#47 def deserialize_offenses(offenses); end + # @api private + # + # source://rubocop//lib/rubocop/cached_data.rb#56 + def location_from_source_buffer(offense, source_buffer); end + # @api private # # source://rubocop//lib/rubocop/cached_data.rb#40 @@ -771,7 +776,7 @@ class RuboCop::CommentConfig # source://rubocop//lib/rubocop/comment_config.rb#63 def comment_only_line?(line_number); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def config(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/comment_config.rb#51 @@ -795,7 +800,7 @@ class RuboCop::CommentConfig # source://rubocop//lib/rubocop/comment_config.rb#30 def processed_source; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def registry(*args, **_arg1, &block); end private @@ -971,10 +976,10 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#30 def initialize(hash = T.unsafe(nil), loaded_path = T.unsafe(nil)); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def [](*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def []=(*args, **_arg1, &block); end # @return [Boolean] @@ -1013,13 +1018,13 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#142 def clusivity_config_for_badge?(badge); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def delete(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config.rb#108 def deprecation_check; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def dig(*args, **_arg1, &block); end # @return [Boolean] @@ -1027,10 +1032,10 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#162 def disabled_new_cops?; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def each(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def each_key(*args, **_arg1, &block); end # @return [Boolean] @@ -1038,7 +1043,7 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#166 def enabled_new_cops?; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def fetch(*args, **_arg1, &block); end # @return [Boolean] @@ -1092,10 +1097,10 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#76 def internal?; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def key?(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def keys(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config.rb#47 @@ -1109,10 +1114,10 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#81 def make_excludes_absolute; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def map(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def merge(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config.rb#247 @@ -1138,7 +1143,7 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#207 def possibly_include_hidden?; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def replace(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config.rb#71 @@ -1150,22 +1155,22 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#251 def target_rails_version; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def target_ruby_version(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def to_h(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def to_hash(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config.rb#67 def to_s; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def transform_values(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def validate(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config.rb#58 @@ -1173,12 +1178,12 @@ class RuboCop::Config private - # source://rubocop//lib/rubocop/config.rb#347 + # source://rubocop//lib/rubocop/config.rb#346 def department_of(qualified_cop_name); end # @return [Boolean] # - # source://rubocop//lib/rubocop/config.rb#335 + # source://rubocop//lib/rubocop/config.rb#334 def enable_cop?(qualified_cop_name, cop_options); end # @param gem_version [Gem::Version] an object like `Gem::Version.new("7.1.2.3")` @@ -1187,7 +1192,7 @@ class RuboCop::Config # source://rubocop//lib/rubocop/config.rb#321 def gem_version_to_major_minor_float(gem_version); end - # source://rubocop//lib/rubocop/config.rb#328 + # source://rubocop//lib/rubocop/config.rb#327 def read_gem_versions_from_target_lockfile; end # @return [Float, nil] The Rails version as a `major.minor` Float. @@ -2258,10 +2263,10 @@ class RuboCop::ConfigValidator # source://rubocop//lib/rubocop/config_validator.rb#26 def initialize(config); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def for_all_cops(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def smart_loaded_path(*args, **_arg1, &block); end # source://rubocop//lib/rubocop/config_validator.rb#62 @@ -2406,7 +2411,7 @@ module RuboCop::Cop::Alignment # source://rubocop//lib/rubocop/cop/mixin/alignment.rb#45 def each_bad_alignment(items, base_column); end - # @deprecated Use processed_source.comment_at_line(line) + # @deprecated Use processed_source.line_with_comment?(line) # # source://rubocop//lib/rubocop/cop/mixin/alignment.rb#69 def end_of_line_comment(line); end @@ -2419,7 +2424,7 @@ module RuboCop::Cop::Alignment # @api private # - # source://rubocop//lib/rubocop/cop/mixin/alignment.rb#74 + # source://rubocop//lib/rubocop/cop/mixin/alignment.rb#78 def register_offense(offense_node, message_node); end # @api public @@ -2532,21 +2537,20 @@ module RuboCop::Cop::AllowedMethods # @api public # - # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#21 + # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#27 def allowed_methods; end - # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#29 + # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#35 def cop_config_allowed_methods; end - # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#33 + # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#39 def cop_config_deprecated_values; end - # @api public # @deprecated Use allowed_method? instead # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#13 - def ignored_method?(name); end + # source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#18 + def ignored_method?; end end # This module encapsulates the ability to ignore certain lines when @@ -2561,31 +2565,31 @@ module RuboCop::Cop::AllowedPattern # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#10 def allowed_line?(line); end - # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#30 + # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#42 def allowed_patterns; end - # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#46 + # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#58 def cop_config_deprecated_methods_values; end - # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#40 + # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#52 def cop_config_patterns_values; end # @deprecated Use allowed_line? instead # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#10 - def ignored_line?(line); end + # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#21 + def ignored_line?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#23 + # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#29 def matches_allowed_pattern?(line); end - # @deprecated Use matches_allowed_pattern?? instead + # @deprecated Use matches_allowed_pattern? instead # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#23 - def matches_ignored_pattern?(line); end + # source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#34 + def matches_ignored_pattern?; end end # This module encapsulates the ability to allow certain receivers in a cop. @@ -2933,18 +2937,18 @@ class RuboCop::Cop::Base # @return [Base] a new instance of Base # - # source://rubocop//lib/rubocop/cop/base.rb#153 + # source://rubocop//lib/rubocop/cop/base.rb#156 def initialize(config = T.unsafe(nil), options = T.unsafe(nil)); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#269 + # source://rubocop//lib/rubocop/cop/base.rb#272 def active_support_extensions_enabled?; end # Adds an offense that has no particular location. # No correction can be applied to global offenses # - # source://rubocop//lib/rubocop/cop/base.rb#186 + # source://rubocop//lib/rubocop/cop/base.rb#189 def add_global_offense(message = T.unsafe(nil), severity: T.unsafe(nil)); end # Adds an offense on the specified range (or node with an expression) @@ -2952,25 +2956,25 @@ class RuboCop::Cop::Base # to provide the cop the opportunity to autocorrect the offense. # If message is not specified, the method `message` will be called. # - # source://rubocop//lib/rubocop/cop/base.rb#198 + # source://rubocop//lib/rubocop/cop/base.rb#201 def add_offense(node_or_range, message: T.unsafe(nil), severity: T.unsafe(nil), &block); end # @api private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#340 + # source://rubocop//lib/rubocop/cop/base.rb#343 def always_autocorrect?; end # Called before any investigation # # @api private # - # source://rubocop//lib/rubocop/cop/base.rb#326 + # source://rubocop//lib/rubocop/cop/base.rb#329 def begin_investigation(processed_source, offset: T.unsafe(nil), original: T.unsafe(nil)); end # @api private # - # source://rubocop//lib/rubocop/cop/base.rb#311 + # source://rubocop//lib/rubocop/cop/base.rb#314 def callbacks_needed; end # Returns the value of attribute config. @@ -2978,29 +2982,29 @@ class RuboCop::Cop::Base # source://rubocop//lib/rubocop/cop/base.rb#43 def config; end - # source://rubocop//lib/rubocop/cop/base.rb#249 + # source://rubocop//lib/rubocop/cop/base.rb#252 def config_to_allow_offenses; end - # source://rubocop//lib/rubocop/cop/base.rb#253 + # source://rubocop//lib/rubocop/cop/base.rb#256 def config_to_allow_offenses=(hash); end # @api private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#346 + # source://rubocop//lib/rubocop/cop/base.rb#349 def contextual_autocorrect?; end # Configuration Helpers # - # source://rubocop//lib/rubocop/cop/base.rb#243 + # source://rubocop//lib/rubocop/cop/base.rb#246 def cop_config; end - # source://rubocop//lib/rubocop/cop/base.rb#235 + # source://rubocop//lib/rubocop/cop/base.rb#238 def cop_name; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#282 + # source://rubocop//lib/rubocop/cop/base.rb#285 def excluded_file?(file); end # This method should be overridden when a cop's behavior depends @@ -3019,51 +3023,51 @@ class RuboCop::Cop::Base # ResultCache system when those external dependencies change, # ie when the ResultCache should be invalidated. # - # source://rubocop//lib/rubocop/cop/base.rb#231 + # source://rubocop//lib/rubocop/cop/base.rb#234 def external_dependency_checksum; end - # source://rubocop//lib/rubocop/cop/base.rb#350 + # source://rubocop//lib/rubocop/cop/base.rb#353 def inspect; end # Gets called if no message is specified when calling `add_offense` or # `add_global_offense` # Cops are discouraged to override this; instead pass your message directly # - # source://rubocop//lib/rubocop/cop/base.rb#180 + # source://rubocop//lib/rubocop/cop/base.rb#183 def message(_range = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/base.rb#235 + # source://rubocop//lib/rubocop/cop/base.rb#238 def name; end # @deprecated Make potential errors with previous API more obvious # - # source://rubocop//lib/rubocop/cop/base.rb#302 + # source://rubocop//lib/rubocop/cop/base.rb#305 def offenses; end # Called after all on_... have been called # When refining this method, always call `super` # - # source://rubocop//lib/rubocop/cop/base.rb#167 + # source://rubocop//lib/rubocop/cop/base.rb#170 def on_investigation_end; end # Called before all on_... have been called # When refining this method, always call `super` # - # source://rubocop//lib/rubocop/cop/base.rb#161 + # source://rubocop//lib/rubocop/cop/base.rb#164 def on_new_investigation; end # Called instead of all on_... callbacks for unrecognized files / syntax errors # When refining this method, always call `super` # - # source://rubocop//lib/rubocop/cop/base.rb#173 + # source://rubocop//lib/rubocop/cop/base.rb#176 def on_other_file; end # There should be very limited reasons for a Cop to do it's own parsing # - # source://rubocop//lib/rubocop/cop/base.rb#287 + # source://rubocop//lib/rubocop/cop/base.rb#290 def parse(source, path = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/base.rb#261 + # source://rubocop//lib/rubocop/cop/base.rb#264 def parser_engine; end # Returns the value of attribute processed_source. @@ -3075,104 +3079,106 @@ class RuboCop::Cop::Base # # @api private # - # source://rubocop//lib/rubocop/cop/base.rb#293 + # source://rubocop//lib/rubocop/cop/base.rb#296 def ready; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#273 + # source://rubocop//lib/rubocop/cop/base.rb#276 def relevant_file?(file); end - # source://rubocop//lib/rubocop/cop/base.rb#265 + # source://rubocop//lib/rubocop/cop/base.rb#268 def target_rails_version; end - # source://rubocop//lib/rubocop/cop/base.rb#257 + # source://rubocop//lib/rubocop/cop/base.rb#260 def target_ruby_version; end private - # source://rubocop//lib/rubocop/cop/base.rb#478 + # source://rubocop//lib/rubocop/cop/base.rb#471 def annotate(message); end - # source://rubocop//lib/rubocop/cop/base.rb#362 + # source://rubocop//lib/rubocop/cop/base.rb#365 def apply_correction(corrector); end # @return [Symbol] offense status # - # source://rubocop//lib/rubocop/cop/base.rb#442 + # source://rubocop//lib/rubocop/cop/base.rb#435 def attempt_correction(range, corrector); end # Reserved for Cop::Cop # - # source://rubocop//lib/rubocop/cop/base.rb#358 + # source://rubocop//lib/rubocop/cop/base.rb#361 def callback_argument(range); end # Called to complete an investigation # - # source://rubocop//lib/rubocop/cop/base.rb#391 + # source://rubocop//lib/rubocop/cop/base.rb#394 def complete_investigation; end # @return [Symbol, Corrector] offense status # - # source://rubocop//lib/rubocop/cop/base.rb#416 + # source://rubocop//lib/rubocop/cop/base.rb#409 def correct(range); end - # source://rubocop//lib/rubocop/cop/base.rb#376 + # source://rubocop//lib/rubocop/cop/base.rb#379 def current_corrector; end # Reserved for Commissioner: # - # source://rubocop//lib/rubocop/cop/base.rb#368 + # source://rubocop//lib/rubocop/cop/base.rb#371 def current_offense_locations; end - # source://rubocop//lib/rubocop/cop/base.rb#380 + # source://rubocop//lib/rubocop/cop/base.rb#383 def current_offenses; end - # source://rubocop//lib/rubocop/cop/base.rb#372 + # source://rubocop//lib/rubocop/cop/base.rb#375 def currently_disabled_lines; end - # source://rubocop//lib/rubocop/cop/base.rb#506 + # source://rubocop//lib/rubocop/cop/base.rb#499 def custom_severity; end - # source://rubocop//lib/rubocop/cop/base.rb#502 + # source://rubocop//lib/rubocop/cop/base.rb#495 def default_severity; end - # source://rubocop//lib/rubocop/cop/base.rb#456 + # source://rubocop//lib/rubocop/cop/base.rb#449 def disable_uncorrectable(range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#492 + # source://rubocop//lib/rubocop/cop/base.rb#485 def enabled_line?(line_number); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#484 + # source://rubocop//lib/rubocop/cop/base.rb#477 def file_name_matches_any?(file, parameter, default_result); end - # source://rubocop//lib/rubocop/cop/base.rb#474 + # source://rubocop//lib/rubocop/cop/base.rb#467 def find_message(range, message); end - # source://rubocop//lib/rubocop/cop/base.rb#498 + # source://rubocop//lib/rubocop/cop/base.rb#491 def find_severity(_range, severity); end - # source://rubocop//lib/rubocop/cop/base.rb#519 + # source://rubocop//lib/rubocop/cop/base.rb#512 def range_for_original(range); end - # source://rubocop//lib/rubocop/cop/base.rb#463 + # source://rubocop//lib/rubocop/cop/base.rb#456 def range_from_node_or_range(node_or_range); end - # source://rubocop//lib/rubocop/cop/base.rb#411 + # Actually private methods + # + # source://rubocop//lib/rubocop/cop/base.rb#404 def reset_investigation; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#527 + # source://rubocop//lib/rubocop/cop/base.rb#520 def target_satisfies_all_gem_version_requirements?; end # @return [Symbol] offense status # - # source://rubocop//lib/rubocop/cop/base.rb#431 + # source://rubocop//lib/rubocop/cop/base.rb#424 def use_corrector(range, corrector); end class << self @@ -3187,51 +3193,54 @@ class RuboCop::Cop::Base # Naming # - # source://rubocop//lib/rubocop/cop/base.rb#90 + # source://rubocop//lib/rubocop/cop/base.rb#93 def badge; end # @api private # - # source://rubocop//lib/rubocop/cop/base.rb#316 + # source://rubocop//lib/rubocop/cop/base.rb#319 def callbacks_needed; end - # source://rubocop//lib/rubocop/cop/base.rb#94 + # source://rubocop//lib/rubocop/cop/base.rb#97 def cop_name; end - # source://rubocop//lib/rubocop/cop/base.rb#98 + # source://rubocop//lib/rubocop/cop/base.rb#101 def department; end - # Cops (other than builtin) are encouraged to implement this + # Returns an url to view this cops documentation online. + # Requires 'DocumentationBaseURL' to be set for your department. + # Will follow the convention of RuboCops own documentation structure, + # overwrite this method to accommodate your custom layout. # # @api public # @return [String, nil] # - # source://rubocop//lib/rubocop/cop/base.rb#67 - def documentation_url; end + # source://rubocop//lib/rubocop/cop/base.rb#70 + def documentation_url(config = T.unsafe(nil)); end # Call for abstract Cop classes # - # source://rubocop//lib/rubocop/cop/base.rb#78 + # source://rubocop//lib/rubocop/cop/base.rb#81 def exclude_from_registry; end # Returns the value of attribute gem_requirements. # - # source://rubocop//lib/rubocop/cop/base.rb#135 + # source://rubocop//lib/rubocop/cop/base.rb#138 def gem_requirements; end # @private # - # source://rubocop//lib/rubocop/cop/base.rb#71 + # source://rubocop//lib/rubocop/cop/base.rb#74 def inherited(subclass); end # Override and return the Force class(es) you need to join # - # source://rubocop//lib/rubocop/cop/base.rb#115 + # source://rubocop//lib/rubocop/cop/base.rb#118 def joining_forces; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#102 + # source://rubocop//lib/rubocop/cop/base.rb#105 def lint?; end # Returns true if the cop name or the cop namespace matches any of the @@ -3239,7 +3248,7 @@ class RuboCop::Cop::Base # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#108 + # source://rubocop//lib/rubocop/cop/base.rb#111 def match?(given_names); end # Register a version requirement for the given gem name. @@ -3254,7 +3263,7 @@ class RuboCop::Cop::Base # # https://guides.rubygems.org/patterns/#declaring-dependencies # - # source://rubocop//lib/rubocop/cop/base.rb#148 + # source://rubocop//lib/rubocop/cop/base.rb#151 def requires_gem(gem_name, *version_requirements); end # Returns if class supports autocorrect. @@ -3262,7 +3271,7 @@ class RuboCop::Cop::Base # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#84 + # source://rubocop//lib/rubocop/cop/base.rb#87 def support_autocorrect?; end # Override if your cop should be called repeatedly for multiple investigations @@ -3275,22 +3284,17 @@ class RuboCop::Cop::Base # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/base.rb#126 + # source://rubocop//lib/rubocop/cop/base.rb#129 def support_multiple_source?; end private - # @return [Boolean] - # - # source://rubocop//lib/rubocop/cop/base.rb#402 - def builtin?; end - - # source://rubocop//lib/rubocop/cop/base.rb#384 + # source://rubocop//lib/rubocop/cop/base.rb#387 def restrict_on_send; end end end -# source://rubocop//lib/rubocop/cop/base.rb#388 +# source://rubocop//lib/rubocop/cop/base.rb#391 RuboCop::Cop::Base::EMPTY_OFFENSES = T.let(T.unsafe(nil), Array) # Reports of an investigation. @@ -3829,35 +3833,35 @@ class RuboCop::Cop::Bundler::GemVersion < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#113 + # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#111 def forbidden_offense?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#119 + # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#117 def forbidden_style?; end # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#93 - def message(range); end + def message(_range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#103 + # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#101 def offense?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#107 + # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#105 def required_offense?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#123 + # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#121 def required_style?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#127 + # source://rubocop//lib/rubocop/cop/bundler/gem_version.rb#125 def version_specification?(expression); end end @@ -4854,7 +4858,7 @@ end # Handles `Max` configuration parameters, especially setting them to an # appropriate value with --auto-gen-config. # -# @deprecated Use `exclude_limit ParameterName` instead. +# @deprecated Use `exclude_limit ` instead. # # source://rubocop//lib/rubocop/cop/mixin/configurable_max.rb#8 module RuboCop::Cop::ConfigurableMax @@ -4863,7 +4867,7 @@ module RuboCop::Cop::ConfigurableMax # source://rubocop//lib/rubocop/cop/mixin/configurable_max.rb#11 def max=(value); end - # source://rubocop//lib/rubocop/cop/mixin/configurable_max.rb#19 + # source://rubocop//lib/rubocop/cop/mixin/configurable_max.rb#23 def max_parameter_name; end end @@ -4896,22 +4900,22 @@ RuboCop::Cop::ConfigurableNumbering::FORMATS = T.let(T.unsafe(nil), Hash) # # source://rubocop//lib/rubocop/cop/cop.rb#11 class RuboCop::Cop::Cop < ::RuboCop::Cop::Base - # source://rubocop//lib/rubocop/cop/cop.rb#53 + # source://rubocop//lib/rubocop/cop/cop.rb#65 def add_offense(node_or_range, location: T.unsafe(nil), message: T.unsafe(nil), severity: T.unsafe(nil), &block); end # Called before any investigation # # @api private # - # source://rubocop//lib/rubocop/cop/cop.rb#103 + # source://rubocop//lib/rubocop/cop/cop.rb#121 def begin_investigation(processed_source, offset: T.unsafe(nil), original: T.unsafe(nil)); end # @deprecated # - # source://rubocop//lib/rubocop/cop/cop.rb#82 + # source://rubocop//lib/rubocop/cop/cop.rb#97 def corrections; end - # source://rubocop//lib/rubocop/cop/cop.rb#70 + # source://rubocop//lib/rubocop/cop/cop.rb#82 def find_location(node, loc); end # Returns the value of attribute offenses. @@ -4921,53 +4925,53 @@ class RuboCop::Cop::Cop < ::RuboCop::Cop::Base # Called after all on_... have been called # - # source://rubocop//lib/rubocop/cop/cop.rb#96 + # source://rubocop//lib/rubocop/cop/cop.rb#114 def on_investigation_end; end # Called before all on_... have been called # - # source://rubocop//lib/rubocop/cop/cop.rb#90 + # source://rubocop//lib/rubocop/cop/cop.rb#108 def on_new_investigation; end # @deprecated Use class method # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/cop.rb#76 + # source://rubocop//lib/rubocop/cop/cop.rb#88 def support_autocorrect?; end private - # source://rubocop//lib/rubocop/cop/cop.rb#121 + # source://rubocop//lib/rubocop/cop/cop.rb#139 def apply_correction(corrector); end # Override Base # - # source://rubocop//lib/rubocop/cop/cop.rb#117 + # source://rubocop//lib/rubocop/cop/cop.rb#135 def callback_argument(_range); end - # source://rubocop//lib/rubocop/cop/cop.rb#138 + # source://rubocop//lib/rubocop/cop/cop.rb#156 def correction_lambda; end - # source://rubocop//lib/rubocop/cop/cop.rb#144 + # source://rubocop//lib/rubocop/cop/cop.rb#162 def dedupe_on_node(node); end # Just for legacy # # @yield [corrector] # - # source://rubocop//lib/rubocop/cop/cop.rb#126 + # source://rubocop//lib/rubocop/cop/cop.rb#144 def emulate_v0_callsequence(corrector); end - # source://rubocop//lib/rubocop/cop/cop.rb#157 + # source://rubocop//lib/rubocop/cop/cop.rb#175 def range_for_original(range); end - # source://rubocop//lib/rubocop/cop/cop.rb#151 + # source://rubocop//lib/rubocop/cop/cop.rb#169 def suppress_clobbering; end class << self # @deprecated Use Registry.all # - # source://rubocop//lib/rubocop/cop/cop.rb#44 + # source://rubocop//lib/rubocop/cop/cop.rb#48 def all; end # source://rubocop//lib/rubocop/cop/cop.rb#29 @@ -4975,7 +4979,7 @@ class RuboCop::Cop::Cop < ::RuboCop::Cop::Base # @deprecated Use Registry.qualified_cop_name # - # source://rubocop//lib/rubocop/cop/cop.rb#49 + # source://rubocop//lib/rubocop/cop/cop.rb#57 def qualified_cop_name(name, origin); end # @deprecated Use Registry.global @@ -5087,7 +5091,7 @@ class RuboCop::Cop::Corrector < ::Parser::Source::TreeRewriter # Legacy # - # source://parser/3.3.0.5/lib/parser/source/tree_rewriter.rb#252 + # source://parser/3.3.1.0/lib/parser/source/tree_rewriter.rb#252 def rewrite; end # Swaps sources at the given ranges. @@ -5158,7 +5162,12 @@ module RuboCop::Cop::Documentation # @api private # - # source://rubocop//lib/rubocop/cop/documentation.rb#34 + # source://rubocop//lib/rubocop/cop/documentation.rb#40 + def builtin?(cop_class); end + + # @api private + # + # source://rubocop//lib/rubocop/cop/documentation.rb#35 def default_base_url; end # @api private @@ -5177,9 +5186,15 @@ module RuboCop::Cop::Documentation # source://rubocop//lib/rubocop/cop/documentation.rb#24 def base_url_for(cop_class, config); end + # @api private + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/documentation.rb#40 + def builtin?(cop_class); end + # @api private # - # source://rubocop//lib/rubocop/cop/documentation.rb#34 + # source://rubocop//lib/rubocop/cop/documentation.rb#35 def default_base_url; end # @api private @@ -5519,37 +5534,53 @@ RuboCop::Cop::ForToEachCorrector::CORRECTION = T.let(T.unsafe(nil), String) class RuboCop::Cop::Force # @return [Force] a new instance of Force # - # source://rubocop//lib/rubocop/cop/force.rb#22 + # source://rubocop//lib/rubocop/cop/force.rb#32 def initialize(cops); end # Returns the value of attribute cops. # - # source://rubocop//lib/rubocop/cop/force.rb#7 + # source://rubocop//lib/rubocop/cop/force.rb#17 def cops; end - # source://rubocop//lib/rubocop/cop/force.rb#38 + # source://rubocop//lib/rubocop/cop/force.rb#50 def investigate(_processed_source); end - # source://rubocop//lib/rubocop/cop/force.rb#26 + # source://rubocop//lib/rubocop/cop/force.rb#36 def name; end - # source://rubocop//lib/rubocop/cop/force.rb#30 + # source://rubocop//lib/rubocop/cop/force.rb#40 def run_hook(method_name, *args); end class << self - # source://rubocop//lib/rubocop/cop/force.rb#9 + # source://rubocop//lib/rubocop/cop/force.rb#19 def all; end - # source://rubocop//lib/rubocop/cop/force.rb#18 + # source://rubocop//lib/rubocop/cop/force.rb#28 def force_name; end # @private # - # source://rubocop//lib/rubocop/cop/force.rb#13 + # source://rubocop//lib/rubocop/cop/force.rb#23 def inherited(subclass); end end end +# @api private +# +# source://rubocop//lib/rubocop/cop/force.rb#8 +class RuboCop::Cop::Force::HookError < ::StandardError + # @api private + # @return [HookError] a new instance of HookError + # + # source://rubocop//lib/rubocop/cop/force.rb#11 + def initialize(joining_cop); end + + # @api private + # + # source://rubocop//lib/rubocop/cop/force.rb#9 + def joining_cop; end +end + # Common functionality for dealing with frozen string literals. # # source://rubocop//lib/rubocop/cop/mixin/frozen_string_literal.rb#6 @@ -5610,9 +5641,38 @@ module RuboCop::Cop::GemDeclaration def gem_declaration?(param0 = T.unsafe(nil)); end end -# source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#5 +# source://rubocop//lib/rubocop/cop/gemspec/add_runtime_dependency.rb#5 module RuboCop::Cop::Gemspec; end +# Prefer `add_dependency` over `add_runtime_dependency` as the latter is +# considered soft-deprecated. +# +# @example +# +# # bad +# Gem::Specification.new do |spec| +# spec.add_runtime_dependency('rubocop') +# end +# +# # good +# Gem::Specification.new do |spec| +# spec.add_dependency('rubocop') +# end +# +# source://rubocop//lib/rubocop/cop/gemspec/add_runtime_dependency.rb#21 +class RuboCop::Cop::Gemspec::AddRuntimeDependency < ::RuboCop::Cop::Base + extend ::RuboCop::Cop::AutoCorrector + + # source://rubocop//lib/rubocop/cop/gemspec/add_runtime_dependency.rb#28 + def on_send(node); end +end + +# source://rubocop//lib/rubocop/cop/gemspec/add_runtime_dependency.rb#24 +RuboCop::Cop::Gemspec::AddRuntimeDependency::MSG = T.let(T.unsafe(nil), String) + +# source://rubocop//lib/rubocop/cop/gemspec/add_runtime_dependency.rb#26 +RuboCop::Cop::Gemspec::AddRuntimeDependency::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array) + # Enforce that gem dependency version specifications or a commit reference (branch, # ref, or tag) are either required or forbidden. # @@ -5680,7 +5740,7 @@ class RuboCop::Cop::Gemspec::DependencyVersion < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#120 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#118 def add_dependency_method?(method_name); end # @return [Boolean] @@ -5693,40 +5753,40 @@ class RuboCop::Cop::Gemspec::DependencyVersion < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#134 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#132 def forbidden_offense?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#140 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#138 def forbidden_style?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#114 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#112 def match_block_variable_name?(receiver_name); end # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#104 - def message(range); end + def message(_range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#124 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#122 def offense?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#128 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#126 def required_offense?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#144 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#142 def required_style?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#148 + # source://rubocop//lib/rubocop/cop/gemspec/dependency_version.rb#146 def version_specification?(expression); end end @@ -5915,8 +5975,8 @@ RuboCop::Cop::Gemspec::DevelopmentDependencies::RESTRICT_ON_SEND = T.let(T.unsaf # # # good # Gem::Specification.new do |spec| -# spec.add_runtime_dependency('parallel', '~> 1.10') -# spec.add_runtime_dependency('parser', '>= 2.3.3.1', '< 3.0') +# spec.add_dependency('parallel', '~> 1.10') +# spec.add_dependency('parser', '>= 2.3.3.1', '< 3.0') # end # # source://rubocop//lib/rubocop/cop/gemspec/duplicated_assignment.rb#37 @@ -6219,15 +6279,15 @@ RuboCop::Cop::Gemspec::RequiredRubyVersion::RESTRICT_ON_SEND = T.let(T.unsafe(ni # # bad # Gem::Specification.new do |spec| # if RUBY_VERSION >= '3.0' -# spec.add_runtime_dependency 'gem_a' +# spec.add_dependency 'gem_a' # else -# spec.add_runtime_dependency 'gem_b' +# spec.add_dependency 'gem_b' # end # end # # # good # Gem::Specification.new do |spec| -# spec.add_runtime_dependency 'gem_a' +# spec.add_dependency 'gem_a' # end # # source://rubocop//lib/rubocop/cop/gemspec/ruby_version_globals_usage.rb#28 @@ -6612,40 +6672,45 @@ module RuboCop::Cop::HashShorthandSyntax # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#125 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#126 def brackets?(method_dispatch_node); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#155 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#156 def breakdown_value_types_of_hash(hash_node); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#102 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#103 def def_node_that_require_parentheses(node); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#179 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#185 def each_omittable_value_pair(hash_value_type_breakdown, &block); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#175 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#181 def each_omitted_value_pair(hash_value_type_breakdown, &block); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#80 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#81 def enforced_shorthand_syntax; end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#117 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#118 def find_ancestor_method_dispatch_node(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#167 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#168 def hash_with_mixed_shorthand_syntax?(hash_value_type_breakdown); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#171 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#172 def hash_with_values_that_cant_be_omitted?(hash_value_type_breakdown); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#74 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#176 + def ignore_explicit_omissible_hash_shorthand_syntax?(hash_value_type_breakdown); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#75 def ignore_hash_shorthand_syntax?(pair_node); end # @return [Boolean] @@ -6655,18 +6720,18 @@ module RuboCop::Cop::HashShorthandSyntax # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#140 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#141 def last_expression?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#148 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#149 def method_dispatch_as_argument?(method_dispatch_node); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#183 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#189 def mixed_shorthand_syntax_check(hash_value_type_breakdown); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#199 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#205 def no_mixed_shorthand_syntax_check(hash_value_type_breakdown); end # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#49 @@ -6674,22 +6739,22 @@ module RuboCop::Cop::HashShorthandSyntax # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#84 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#85 def require_hash_value?(hash_key_source, node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#93 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#94 def require_hash_value_for_around_hash_literal?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#129 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#130 def use_element_of_hash_literal_as_receiver?(ancestor, parent); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#134 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#135 def use_modifier_form_without_parenthesized_method_call?(ancestor); end end @@ -6702,12 +6767,12 @@ RuboCop::Cop::HashShorthandSyntax::DO_NOT_MIX_MSG_PREFIX = T.let(T.unsafe(nil), # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#11 RuboCop::Cop::HashShorthandSyntax::DO_NOT_MIX_OMIT_VALUE_MSG = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#209 +# source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#216 class RuboCop::Cop::HashShorthandSyntax::DefNode < ::Struct - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#218 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#225 def first_argument; end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#222 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#229 def last_argument; end # Returns the value of attribute node @@ -6721,7 +6786,7 @@ class RuboCop::Cop::HashShorthandSyntax::DefNode < ::Struct # @return [Object] the newly set value def node=(_); end - # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#210 + # source://rubocop//lib/rubocop/cop/mixin/hash_shorthand_syntax.rb#217 def selector; end class << self @@ -7026,7 +7091,7 @@ RuboCop::Cop::IfThenCorrector::DEFAULT_INDENTATION_WIDTH = T.let(T.unsafe(nil), # @deprecated IgnoredMethods class has been replaced with AllowedMethods. # -# source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#40 +# source://rubocop//lib/rubocop/cop/mixin/allowed_methods.rb#46 RuboCop::Cop::IgnoredMethods = RuboCop::Cop::AllowedMethods # Handles adding and checking ignored nodes. @@ -7054,7 +7119,7 @@ end # @deprecated IgnoredPattern class has been replaced with AllowedPattern. # -# source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#54 +# source://rubocop//lib/rubocop/cop/mixin/allowed_pattern.rb#66 RuboCop::Cop::IgnoredPattern = RuboCop::Cop::AllowedPattern # Common functionality for checking integer nodes. @@ -8766,35 +8831,35 @@ class RuboCop::Cop::Layout::EmptyComment < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#131 + # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#133 def allow_border_comment?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#135 + # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#137 def allow_margin_comment?; end # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#97 def autocorrect(corrector, node); end - # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#127 + # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#129 def comment_text(comment); end # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#108 def concat_consecutive_comments(comments); end - # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#139 + # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#141 def current_token(comment); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#117 + # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#119 def empty_comment_only?(comment_text); end # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#85 def investigate(comments); end - # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#143 + # source://rubocop//lib/rubocop/cop/layout/empty_comment.rb#145 def previous_token(node); end end @@ -10508,8 +10573,8 @@ RuboCop::Cop::Layout::ExtraSpacing::MSG_UNNECESSARY = T.let(T.unsafe(nil), Strin # second_params # @example EnforcedStyle: special_for_inner_method_call # # The first argument should normally be indented one step more than -# # the preceding line, but if it's a argument for a method call that -# # is itself a argument in a method call, then the inner argument +# # the preceding line, but if it's an argument for a method call that +# # is itself an argument in a method call, then the inner argument # # should be indented relative to the inner method. # # # good @@ -10706,43 +10771,43 @@ class RuboCop::Cop::Layout::FirstArrayElementIndentation < ::RuboCop::Cop::Base # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#94 def on_array(node); end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#98 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#100 def on_csend(node); end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#98 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#100 def on_send(node); end private - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#189 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#191 def array_alignment_config; end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#109 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#111 def autocorrect(corrector, node); end # Returns the description of what the correct indentation is based on. # - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#147 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#149 def base_description(indent_base_type); end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#113 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#115 def brace_alignment_style; end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#117 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#119 def check(array_node, left_parenthesis); end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#131 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#133 def check_right_bracket(right_bracket, first_elem, left_bracket, left_parenthesis); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#183 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#185 def enforce_first_argument_with_fixed_indentation?; end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#160 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#162 def message(base_description); end - # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#168 + # source://rubocop//lib/rubocop/cop/layout/first_array_element_indentation.rb#170 def message_for_right_bracket(indent_base_type); end end @@ -11745,7 +11810,7 @@ RuboCop::Cop::Layout::HeredocArgumentClosingParenthesis::MSG = T.let(T.unsafe(ni # Checks the indentation of the here document bodies. The bodies # are indented one step. # -# Note: When ``Layout/LineLength``'s `AllowHeredoc` is false (not default), +# NOTE: When ``Layout/LineLength``'s `AllowHeredoc` is false (not default), # this cop does not add any offenses for long here documents to # avoid ``Layout/LineLength``'s offenses. # @@ -12735,29 +12800,29 @@ RuboCop::Cop::Layout::LineEndStringConcatenationIndentation::PARENT_TYPES_FOR_IN # split across lines. These include arrays, hashes, and # method calls with argument lists. # -# If autocorrection is enabled, the following Layout cops +# If autocorrection is enabled, the following cops # are recommended to further format the broken lines. # (Many of these are enabled by default.) # -# * ArgumentAlignment -# * ArrayAlignment -# * BlockAlignment -# * BlockDelimiters -# * BlockEndNewline -# * ClosingParenthesisIndentation -# * FirstArgumentIndentation -# * FirstArrayElementIndentation -# * FirstHashElementIndentation -# * FirstParameterIndentation -# * HashAlignment -# * IndentationWidth -# * MultilineArrayLineBreaks -# * MultilineBlockLayout -# * MultilineHashBraceLayout -# * MultilineHashKeyLineBreaks -# * MultilineMethodArgumentLineBreaks -# * MultilineMethodParameterLineBreaks -# * ParameterAlignment +# * `Layout/ArgumentAlignment` +# * `Layout/ArrayAlignment` +# * `Layout/BlockAlignment` +# * `Layout/BlockEndNewline` +# * `LayoutClosingParenthesisIndentation` +# * `LayoutFirstArgumentIndentation` +# * `LayoutFirstArrayElementIndentation` +# * `LayoutFirstHashElementIndentation` +# * `LayoutFirstParameterIndentation` +# * `LayoutHashAlignment` +# * `LayoutIndentationWidth` +# * `LayoutMultilineArrayLineBreaks` +# * `LayoutMultilineBlockLayout` +# * `LayoutMultilineHashBraceLayout` +# * `LayoutMultilineHashKeyLineBreaks` +# * `LayoutMultilineMethodArgumentLineBreaks` +# * `LayoutMultilineMethodParameterLineBreaks` +# * `Layout/ParameterAlignment` +# * `Style/BlockDelimiters` # # Together, these cops will pretty print hashes, arrays, # method calls, etc. For example, let's say the max columns @@ -15095,44 +15160,44 @@ class RuboCop::Cop::Layout::SpaceAroundOperators < ::RuboCop::Cop::Base private - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#256 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#259 def align_hash_cop_config; end - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#195 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#198 def autocorrect(corrector, range, right_operand); end # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#179 def check_operator(type, operator, right_operand); end - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#209 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#212 def enclose_operator_with_space(corrector, range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#236 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#239 def excess_leading_space?(type, operator, with_space); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#251 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#254 def excess_trailing_space?(right_operand, with_space); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#274 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#277 def force_equal_sign_alignment?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#260 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#263 def hash_table_style?; end # @yield [msg] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#190 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#193 def offense(type, operator, with_space, right_operand); end - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#222 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#225 def offense_message(type, operator, with_space, right_operand); end # @return [Boolean] @@ -15147,17 +15212,17 @@ class RuboCop::Cop::Layout::SpaceAroundOperators < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#278 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#281 def should_not_have_surrounding_space?(operator, right_operand); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#264 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#267 def space_around_exponent_operator?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#268 + # source://rubocop//lib/rubocop/cop/layout/space_around_operators.rb#271 def space_around_slash_operator?(right_operand); end class << self @@ -16245,23 +16310,20 @@ class RuboCop::Cop::Layout::SpaceInsideStringInterpolation < ::RuboCop::Cop::Bas include ::RuboCop::Cop::ConfigurableEnforcedStyle extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#31 + # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#30 def on_interpolation(begin_node); end private - # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#47 + # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#46 def autocorrect(corrector, begin_node); end - # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#57 + # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#56 def delimiters(begin_node); end end # source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#28 -RuboCop::Cop::Layout::SpaceInsideStringInterpolation::NO_SPACE_MSG = T.let(T.unsafe(nil), String) - -# source://rubocop//lib/rubocop/cop/layout/space_inside_string_interpolation.rb#29 -RuboCop::Cop::Layout::SpaceInsideStringInterpolation::SPACE_MSG = T.let(T.unsafe(nil), String) +RuboCop::Cop::Layout::SpaceInsideStringInterpolation::MSG = T.let(T.unsafe(nil), String) # Looks for trailing blank lines and a final newline in the # source code. @@ -19134,36 +19196,45 @@ class RuboCop::Cop::Lint::ErbNewArguments < ::RuboCop::Cop::Base extend ::RuboCop::Cop::AutoCorrector extend ::RuboCop::Cop::TargetRubyVersion - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#83 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#81 def erb_new_with_non_keyword_arguments(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#88 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#86 def on_send(node); end private - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#153 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#160 def arguments_range(node); end - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#108 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#115 def autocorrect(corrector, node); end - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#123 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#130 def build_kwargs(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#119 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#126 def correct_arguments?(arguments); end - # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#140 + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#104 + def message(positional_argument_index, arg_value); end + + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#147 def override_by_legacy_args(kwargs, node); end end +# source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#74 +RuboCop::Cop::Lint::ErbNewArguments::MESSAGE_EOUTVAR = T.let(T.unsafe(nil), String) + # source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#68 -RuboCop::Cop::Lint::ErbNewArguments::MESSAGES = T.let(T.unsafe(nil), Array) +RuboCop::Cop::Lint::ErbNewArguments::MESSAGE_SAFE_LEVEL = T.let(T.unsafe(nil), String) + +# source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#71 +RuboCop::Cop::Lint::ErbNewArguments::MESSAGE_TRIM_MODE = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#80 +# source://rubocop//lib/rubocop/cop/lint/erb_new_arguments.rb#78 RuboCop::Cop::Lint::ErbNewArguments::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array) # Looks for uses of flip-flop operator @@ -19319,7 +19390,7 @@ RuboCop::Cop::Lint::FloatOutOfRange::MSG = T.let(T.unsafe(nil), String) # # format('Numbered format: %1$s and numbered %2$s', a_value, another) # -# source://rubocop//lib/rubocop/cop/lint/format_parameter_mismatch.rb#38 +# source://rubocop//lib/rubocop/cop/lint/format_parameter_mismatch.rb#37 class RuboCop::Cop::Lint::FormatParameterMismatch < ::RuboCop::Cop::Base # source://rubocop//lib/rubocop/cop/lint/format_parameter_mismatch.rb#100 def called_on_string?(param0 = T.unsafe(nil)); end @@ -19627,41 +19698,43 @@ RuboCop::Cop::Lint::IdentityComparison::RESTRICT_ON_SEND = T.let(T.unsafe(nil), # # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#25 class RuboCop::Cop::Lint::ImplicitStringConcatenation < ::RuboCop::Cop::Base - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#33 + extend ::RuboCop::Cop::AutoCorrector + + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#36 def on_dstr(node); end private - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#83 + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#90 def display_str(node); end - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#50 + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#57 def each_bad_cons(node); end - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#65 + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#72 def ending_delimiter(str); end - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#91 + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#98 def str_content(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#75 + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#82 def string_literal?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#79 + # source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#86 def string_literals?(node1, node2); end end -# source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#28 +# source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#30 RuboCop::Cop::Lint::ImplicitStringConcatenation::FOR_ARRAY = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#30 +# source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#32 RuboCop::Cop::Lint::ImplicitStringConcatenation::FOR_METHOD = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#26 +# source://rubocop//lib/rubocop/cop/lint/implicit_string_concatenation.rb#28 RuboCop::Cop::Lint::ImplicitStringConcatenation::MSG = T.let(T.unsafe(nil), String) # Checks for `IO.select` that is incompatible with Fiber Scheduler since Ruby 3.0. @@ -23900,12 +23973,12 @@ class RuboCop::Cop::Lint::ToEnumArguments < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/to_enum_arguments.rb#83 + # source://rubocop//lib/rubocop/cop/lint/to_enum_arguments.rb#76 def argument_match?(send_arg, def_arg); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/to_enum_arguments.rb#68 + # source://rubocop//lib/rubocop/cop/lint/to_enum_arguments.rb#61 def arguments_match?(arguments, def_node); end end @@ -24329,31 +24402,31 @@ class RuboCop::Cop::Lint::UnmodifiedReduceAccumulator < ::RuboCop::Cop::Base # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#190 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#191 def acceptable_return?(return_val, element_name); end # Exclude `begin` nodes inside a `dstr` from being collected by `return_values` # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#198 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#199 def allowed_type?(parent_node); end - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#158 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#159 def block_arg_name(node, index); end - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#141 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#142 def check_return_values(block_node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#175 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#176 def potential_offense?(return_values, block_body, element_name, accumulator_name); end # Return values in a block are either the value given to next, # the last line of a multiline block, or the only line of the block # - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#127 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#128 def return_values(block_body_node); end # Look for an index of the accumulator being returned, except where the index @@ -24361,7 +24434,7 @@ class RuboCop::Cop::Lint::UnmodifiedReduceAccumulator < ::RuboCop::Cop::Base # This is always an offense, in order to try to catch potential exceptions # due to type mismatches # - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#166 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#167 def returned_accumulator_index(return_values, accumulator_name, element_name); end # If the accumulator is used in any return value, the node is acceptable since @@ -24369,7 +24442,7 @@ class RuboCop::Cop::Lint::UnmodifiedReduceAccumulator < ::RuboCop::Cop::Base # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#182 + # source://rubocop//lib/rubocop/cop/lint/unmodified_reduce_accumulator.rb#183 def returns_accumulator_anywhere?(return_values, accumulator_name); end end @@ -25670,13 +25743,13 @@ class RuboCop::Cop::Lint::Void < ::RuboCop::Cop::Base private - # source://rubocop//lib/rubocop/cop/lint/void.rb#217 + # source://rubocop//lib/rubocop/cop/lint/void.rb#222 def autocorrect_nonmutating_send(corrector, node, suggestion); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#213 + # source://rubocop//lib/rubocop/cop/lint/void.rb#216 def autocorrect_void_expression(corrector, node); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#201 + # source://rubocop//lib/rubocop/cop/lint/void.rb#204 def autocorrect_void_op(corrector, node); end # source://rubocop//lib/rubocop/cop/lint/void.rb#99 @@ -25685,32 +25758,32 @@ class RuboCop::Cop::Lint::Void < ::RuboCop::Cop::Base # source://rubocop//lib/rubocop/cop/lint/void.rb#113 def check_expression(expr); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#151 + # source://rubocop//lib/rubocop/cop/lint/void.rb#154 def check_literal(node); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#175 + # source://rubocop//lib/rubocop/cop/lint/void.rb#178 def check_nonmutating(node); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#159 + # source://rubocop//lib/rubocop/cop/lint/void.rb#162 def check_self(node); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#133 + # source://rubocop//lib/rubocop/cop/lint/void.rb#136 def check_var(node); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#167 + # source://rubocop//lib/rubocop/cop/lint/void.rb#170 def check_void_expression(node); end - # source://rubocop//lib/rubocop/cop/lint/void.rb#123 + # source://rubocop//lib/rubocop/cop/lint/void.rb#125 def check_void_op(node, &block); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/void.rb#226 + # source://rubocop//lib/rubocop/cop/lint/void.rb#231 def entirely_literal?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/lint/void.rb#193 + # source://rubocop//lib/rubocop/cop/lint/void.rb#196 def in_void_context?(node); end end @@ -26076,12 +26149,12 @@ end # source://rubocop//lib/rubocop/cop/metrics/block_length.rb#49 RuboCop::Cop::Metrics::BlockLength::LABEL = T.let(T.unsafe(nil), String) -# Checks for excessive nesting of conditional and looping -# constructs. +# Checks for excessive nesting of conditional and looping constructs. # -# You can configure if blocks are considered using the `CountBlocks` -# option. When set to `false` (the default) blocks are not counted -# towards the nesting level. Set to `true` to count blocks as well. +# You can configure if blocks are considered using the `CountBlocks` and `CountModifierForms` +# options. When both are set to `false` (the default) blocks and modifier forms are not +# counted towards the nesting level. Set them to `true` to include these in the nesting level +# calculation as well. # # The maximum level of nesting allowed is configurable. # @@ -26100,15 +26173,25 @@ class RuboCop::Cop::Metrics::BlockNesting < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#44 + # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#52 def consider_node?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#54 + # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#62 def count_blocks?; end - # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#50 + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#44 + def count_if_block?(node); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#66 + def count_modifier_forms?; end + + # source://rubocop//lib/rubocop/cop/metrics/block_nesting.rb#58 def message(max); end end @@ -30606,6 +30689,17 @@ module RuboCop::Cop::Style; end # EnforcedStyle config covers only method definitions. # Applications of visibility methods to symbols can be controlled # using AllowModifiersOnSymbols config. +# Also, the visibility of `attr*` methods can be controlled using +# AllowModifiersOnAttrs config. +# +# In Ruby 3.0, `attr*` methods now return an array of defined method names +# as symbols. So we can write the modifier and `attr*` in inline style. +# AllowModifiersOnAttrs config allows `attr*` methods to be written in +# inline style without modifying applications that have been maintained +# for a long time in group style. Furthermore, developers who are not very +# familiar with Ruby may know that the modifier applies to `def`, but they +# may not know that it also applies to `attr*` methods. It would be easier +# to understand if we could write `attr*` methods in inline style. # # @example EnforcedStyle: group (default) # # bad @@ -30657,94 +30751,128 @@ module RuboCop::Cop::Style; end # private :bar, :baz # # end +# @example AllowModifiersOnAttrs: true (default) +# # good +# class Foo +# +# public attr_reader :bar +# protected attr_writer :baz +# private attr_accessor :qux +# private attr :quux +# +# def public_method; end +# +# private +# +# def private_method; end +# +# end +# @example AllowModifiersOnAttrs: false +# # bad +# class Foo +# +# public attr_reader :bar +# protected attr_writer :baz +# private attr_accessor :qux +# private attr :quux +# +# end # -# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#70 +# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#109 class RuboCop::Cop::Style::AccessModifierDeclarations < ::RuboCop::Cop::Base include ::RuboCop::Cop::ConfigurableEnforcedStyle include ::RuboCop::Cop::RangeHelp extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#91 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#135 + def access_modifier_with_attr?(param0 = T.unsafe(nil)); end + + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#130 def access_modifier_with_symbol?(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#95 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#140 def on_send(node); end private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#145 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#195 def access_modifier_is_inlined?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#149 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#199 def access_modifier_is_not_inlined?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#127 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#177 + def allow_modifiers_on_attrs?(node); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#173 def allow_modifiers_on_symbols?(node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#112 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#158 def autocorrect(corrector, node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#219 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#269 def def_source(node, def_node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#180 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#230 def find_argument_less_modifier_node(node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#169 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#219 def find_corresponding_def_node(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#137 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#187 def group_style?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#141 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#191 def inline_style?; end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#211 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#261 def insert_inline_modifier(corrector, node, modifier_name); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#159 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#209 def message(range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#131 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#181 def offense?(node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#215 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#265 def remove_node(corrector, node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#194 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#244 def replace_def(corrector, node, def_node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#153 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#203 def right_siblings_same_inline_method?(node); end - # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#188 + # source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#238 def select_grouped_def_nodes(node); end end -# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#88 +# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#127 RuboCop::Cop::Style::AccessModifierDeclarations::ALLOWED_NODE_TYPES = T.let(T.unsafe(nil), Array) -# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#76 +# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#115 RuboCop::Cop::Style::AccessModifierDeclarations::GROUP_STYLE_MESSAGE = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#81 +# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#120 RuboCop::Cop::Style::AccessModifierDeclarations::INLINE_STYLE_MESSAGE = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#86 +# source://rubocop//lib/rubocop/cop/style/access_modifier_declarations.rb#125 RuboCop::Cop::Style::AccessModifierDeclarations::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array) # Checks for grouping of accessors in `class` and `module` bodies. @@ -31081,6 +31209,8 @@ RuboCop::Cop::Style::AndOr::MSG = T.let(T.unsafe(nil), String) # # Names not on this list are likely to be meaningful and are allowed by default. # +# This cop handles not only method forwarding but also forwarding to `super`. +# # @example RedundantBlockArgumentNames: ['blk', 'block', 'proc'] (default) # # bad - But it is good with `EnforcedStyle: explicit` set for `Naming/BlockForwarding`. # def foo(&block) @@ -31168,222 +31298,222 @@ RuboCop::Cop::Style::AndOr::MSG = T.let(T.unsafe(nil), String) # bar(...) # end # -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#125 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#127 class RuboCop::Cop::Style::ArgumentsForwarding < ::RuboCop::Cop::Base include ::RuboCop::Cop::RangeHelp extend ::RuboCop::Cop::AutoCorrector extend ::RuboCop::Cop::TargetRubyVersion - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#144 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#146 def on_def(node); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#144 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#146 def on_defs(node); end private - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#185 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#187 def add_forward_all_offenses(node, send_classifications, forwardable_args); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#351 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#353 def add_parens_if_missing(node, corrector); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#212 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#214 def add_post_ruby_32_offenses(def_node, send_classifications, forwardable_args); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#343 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#345 def allow_only_rest_arguments?; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#335 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#337 def arguments_range(node, first_node); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#263 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#265 def classification_and_forwards(def_node, send_node, referenced_lvars, forwardable_args); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#248 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#250 def classify_send_nodes(def_node, send_nodes, referenced_lvars, forwardable_args); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#495 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#497 def explicit_block_name?; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#168 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#170 def extract_forwardable_args(args); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#238 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#240 def non_splat_or_block_pass_lvar_references(body); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#180 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#182 def only_forwards_all?(send_classifications); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#292 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#294 def outside_block?(node); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#172 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#174 def redundant_forwardable_named_args(restarg, kwrestarg, blockarg); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#282 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#284 def redundant_named_arg(arg, config_name, keyword); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#325 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#327 def register_forward_all_offense(def_or_send, send_or_arguments, rest_or_splat); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#298 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#300 def register_forward_args_offense(def_arguments_or_send, rest_arg_or_splat); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#314 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#316 def register_forward_block_arg_offense(add_parens, def_arguments_or_send, block_arg); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#306 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#308 def register_forward_kwargs_offense(add_parens, def_arguments_or_send, kwrest_arg_or_splat); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#347 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#349 def use_anonymous_forwarding?; end class << self - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#140 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#142 def autocorrect_incompatible_with; end end end -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#133 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#135 RuboCop::Cop::Style::ArgumentsForwarding::ADDITIONAL_ARG_TYPES = T.let(T.unsafe(nil), Array) -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#136 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#138 RuboCop::Cop::Style::ArgumentsForwarding::ARGS_MSG = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#138 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#140 RuboCop::Cop::Style::ArgumentsForwarding::BLOCK_MSG = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#132 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#134 RuboCop::Cop::Style::ArgumentsForwarding::FORWARDING_LVAR_TYPES = T.let(T.unsafe(nil), Array) -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#135 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#137 RuboCop::Cop::Style::ArgumentsForwarding::FORWARDING_MSG = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#137 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#139 RuboCop::Cop::Style::ArgumentsForwarding::KWARGS_MSG = T.let(T.unsafe(nil), String) # Classifies send nodes for possible rest/kwrest/all (including block) forwarding. # -# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#358 +# source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#360 class RuboCop::Cop::Style::ArgumentsForwarding::SendNodeClassifier extend ::RuboCop::AST::NodePattern::Macros # @return [SendNodeClassifier] a new instance of SendNodeClassifier # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#370 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#372 def initialize(def_node, send_node, referenced_lvars, forwardable_args, **config); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#398 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#400 def classification; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#365 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#367 def extract_forwarded_kwrest_arg(param0 = T.unsafe(nil), param1); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#392 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#394 def forwarded_block_arg; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#368 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#370 def forwarded_block_arg?(param0 = T.unsafe(nil), param1); end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#386 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#388 def forwarded_kwrest_arg; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#380 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#382 def forwarded_rest_arg; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#362 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#364 def forwarded_rest_arg?(param0 = T.unsafe(nil), param1); end private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#466 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#468 def additional_kwargs?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#462 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#464 def additional_kwargs_or_forwarded_kwargs?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#476 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#478 def allow_offense_for_no_block?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#447 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#449 def any_arg_referenced?; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#431 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#433 def arguments; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#410 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#412 def can_forward_all?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#470 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#472 def forward_additional_kwargs?; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#427 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#429 def forwarded_rest_and_kwrest_args; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#489 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#491 def missing_rest_arg_or_kwrest_arg?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#480 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#482 def no_additional_args?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#455 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#457 def no_post_splat_args?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#423 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#425 def offensive_block_forwarding?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#443 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#445 def referenced_block_arg?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#439 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#441 def referenced_kwrest_arg?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#435 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#437 def referenced_rest_arg?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#419 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#421 def ruby_32_missing_rest_or_kwest?; end - # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#451 + # source://rubocop//lib/rubocop/cop/style/arguments_forwarding.rb#453 def target_ruby_version; end end @@ -34159,10 +34289,10 @@ class RuboCop::Cop::Style::Copyright < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/copyright.rb#83 + # source://rubocop//lib/rubocop/cop/style/copyright.rb#86 def encoding_token?(processed_source, token_index); end - # source://rubocop//lib/rubocop/cop/style/copyright.rb#69 + # source://rubocop//lib/rubocop/cop/style/copyright.rb#72 def insert_notice_before(processed_source); end # source://rubocop//lib/rubocop/cop/style/copyright.rb#52 @@ -34170,12 +34300,12 @@ class RuboCop::Cop::Style::Copyright < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/copyright.rb#90 + # source://rubocop//lib/rubocop/cop/style/copyright.rb#93 def notice_found?(processed_source); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/copyright.rb#76 + # source://rubocop//lib/rubocop/cop/style/copyright.rb#79 def shebang_token?(processed_source, token_index); end # @raise [Warning] @@ -34843,34 +34973,52 @@ RuboCop::Cop::Style::Documentation::MSG = T.let(T.unsafe(nil), String) # def do_something # end # end +# @example AllowedMethods: ['method_missing', 'respond_to_missing?'] +# +# # good +# class Foo +# def method_missing(name, *args) +# end +# +# def respond_to_missing?(symbol, include_private) +# end +# end # -# source://rubocop//lib/rubocop/cop/style/documentation_method.rb#98 +# source://rubocop//lib/rubocop/cop/style/documentation_method.rb#109 class RuboCop::Cop::Style::DocumentationMethod < ::RuboCop::Cop::Base include ::RuboCop::Cop::DocumentationComment include ::RuboCop::Cop::VisibilityHelp include ::RuboCop::Cop::DefNode - # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#105 + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#116 def modifier_node?(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#109 + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#120 def on_def(node); end - # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#109 + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#120 def on_defs(node); end private - # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#119 + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#146 + def allowed_methods; end + + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#130 def check(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#126 + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#142 + def method_allowed?(node); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/documentation_method.rb#138 def require_for_non_public_methods?; end end -# source://rubocop//lib/rubocop/cop/style/documentation_method.rb#102 +# source://rubocop//lib/rubocop/cop/style/documentation_method.rb#113 RuboCop::Cop::Style::DocumentationMethod::MSG = T.let(T.unsafe(nil), String) # Detects double disable comments on one line. This is mostly to catch @@ -37743,9 +37891,9 @@ RuboCop::Cop::Style::HashEachMethods::UNUSED_BLOCK_ARG_MSG = T.let(T.unsafe(nil) # {foo: 1, bar: 2, baz: 3}.reject {|k, v| k == :bar } # {foo: 1, bar: 2, baz: 3}.select {|k, v| k != :bar } # {foo: 1, bar: 2, baz: 3}.filter {|k, v| k != :bar } -# {foo: 1, bar: 2, baz: 3}.reject {|k, v| %i[foo bar].include?(k) } -# {foo: 1, bar: 2, baz: 3}.select {|k, v| !%i[foo bar].include?(k) } -# {foo: 1, bar: 2, baz: 3}.filter {|k, v| !%i[foo bar].include?(k) } +# {foo: 1, bar: 2, baz: 3}.reject {|k, v| %i[bar].include?(k) } +# {foo: 1, bar: 2, baz: 3}.select {|k, v| !%i[bar].include?(k) } +# {foo: 1, bar: 2, baz: 3}.filter {|k, v| !%i[bar].include?(k) } # # # good # {foo: 1, bar: 2, baz: 3}.except(:bar) @@ -37772,42 +37920,42 @@ class RuboCop::Cop::Style::HashExcept < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#94 - def bad_method?(block); end + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#95 + def bad_method?(method_name, block); end - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#166 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#169 def decorate_source(value); end - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#174 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#177 def except_key(node); end - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#153 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#156 def except_key_source(key); end - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#147 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#150 def extract_body_if_negated(body); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#128 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#131 def included?(negated, body); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#132 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#135 def not_included?(negated, body); end - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#183 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#186 def offense_range(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#136 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#139 def safe_to_register_offense?(block, except_key); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_except.rb#112 + # source://rubocop//lib/rubocop/cop/style/hash_except.rb#115 def semantically_except_method?(send, block); end end @@ -37896,10 +38044,12 @@ RuboCop::Cop::Style::HashLikeCase::MSG = T.let(T.unsafe(nil), String) # * never - forces use of explicit hash literal value # * either - accepts both shorthand and explicit use of hash literal value # * consistent - forces use of the 3.1 syntax only if all values can be omitted in the hash +# * either_consistent - accepts both shorthand and explicit use of hash literal value, +# but they must be consistent # -# @example EnforcedShorthandSyntax: consistent +# @example EnforcedShorthandSyntax: either_consistent # -# # bad - `foo` and `bar` values can be omitted +# # good - `foo` and `bar` values can be omitted, but they are consistent, so it's accepted # {foo: foo, bar: bar} # # # bad - `bar` value can be omitted @@ -37960,6 +38110,22 @@ RuboCop::Cop::Style::HashLikeCase::MSG = T.let(T.unsafe(nil), String) # # # good # {foo:, bar:} +# @example EnforcedShorthandSyntax: consistent +# +# # bad - `foo` and `bar` values can be omitted +# {foo: foo, bar: bar} +# +# # bad - `bar` value can be omitted +# {foo:, bar: bar} +# +# # bad - mixed syntaxes +# {foo:, bar: baz} +# +# # good +# {foo:, bar:} +# +# # good - can't omit `baz` +# {foo: foo, bar: baz} # @example EnforcedStyle: ruby19 (default) # # bad # {:a => 2} @@ -37970,84 +38136,84 @@ RuboCop::Cop::Style::HashLikeCase::MSG = T.let(T.unsafe(nil), String) # {:c => 2, 'd' => 2} # acceptable since 'd' isn't a symbol # {d: 1, 'e' => 2} # technically not forbidden # -# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#113 +# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#131 class RuboCop::Cop::Style::HashSyntax < ::RuboCop::Cop::Base include ::RuboCop::Cop::ConfigurableEnforcedStyle include ::RuboCop::Cop::HashShorthandSyntax include ::RuboCop::Cop::RangeHelp extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#167 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#185 def alternative_style; end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#145 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#163 def hash_rockets_check(pairs); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#159 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#177 def no_mixed_keys_check(pairs); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#123 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#141 def on_hash(node); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#141 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#159 def ruby19_check(pairs); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#149 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#167 def ruby19_no_mixed_keys_check(pairs); end private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#199 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#217 def acceptable_19_syntax_symbol?(sym_name); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#256 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#274 def argument_without_space?(node); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#178 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#196 def autocorrect(corrector, node); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#260 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#278 def autocorrect_hash_rockets(corrector, pair_node); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#269 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#287 def autocorrect_no_mixed_keys(corrector, pair_node); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#235 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#253 def autocorrect_ruby19(corrector, pair_node); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#220 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#238 def check(pairs, delim, msg); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#277 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#295 def force_hash_rockets?(pairs); end - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#248 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#266 def range_for_autocorrect_ruby19(pair_node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#188 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#206 def sym_indices?(pairs); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#192 + # source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#210 def word_symbol_pair?(pair); end end -# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#119 +# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#137 RuboCop::Cop::Style::HashSyntax::MSG_19 = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#121 +# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#139 RuboCop::Cop::Style::HashSyntax::MSG_HASH_ROCKETS = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#120 +# source://rubocop//lib/rubocop/cop/style/hash_syntax.rb#138 RuboCop::Cop::Style::HashSyntax::MSG_NO_MIXED_KEYS = T.let(T.unsafe(nil), String) # Looks for uses of `\_.each_with_object({}) {...}`, @@ -38692,7 +38858,7 @@ class RuboCop::Cop::Style::IfWithBooleanLiteralBranches < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#134 + # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#136 def assume_boolean_value?(condition); end # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#114 @@ -38708,20 +38874,20 @@ class RuboCop::Cop::Style::IfWithBooleanLiteralBranches < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#151 + # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#153 def opposite_condition?(node); end - # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#141 + # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#143 def replacement_condition(node, condition); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#156 + # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#158 def require_parentheses?(condition); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#120 + # source://rubocop//lib/rubocop/cop/style/if_with_boolean_literal_branches.rb#122 def return_boolean_value?(condition); end end @@ -39669,10 +39835,10 @@ class RuboCop::Cop::Style::MagicCommentFormat::CommentRange # source://rubocop//lib/rubocop/cop/style/magic_comment_format.rb#125 def directives; end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def loc(*args, **_arg1, &block); end - # source://forwardable/1.3.2/forwardable.rb#229 + # source://forwardable/1.3.3/forwardable.rb#231 def text(*args, **_arg1, &block); end # A magic comment can contain one value (normal style) or @@ -39701,6 +39867,7 @@ RuboCop::Cop::Style::MagicCommentFormat::MSG_VALUE = T.let(T.unsafe(nil), String RuboCop::Cop::Style::MagicCommentFormat::SNAKE_SEPARATOR = T.let(T.unsafe(nil), String) # Prefer `select` or `reject` over `map { ... }.compact`. +# This cop also handles `filter_map { ... }`, similar to `map { ... }.compact`. # # @example # @@ -39708,6 +39875,9 @@ RuboCop::Cop::Style::MagicCommentFormat::SNAKE_SEPARATOR = T.let(T.unsafe(nil), # array.map { |e| some_condition? ? e : next }.compact # # # bad +# array.filter_map { |e| some_condition? ? e : next } +# +# # bad # array.map do |e| # if some_condition? # e @@ -39734,48 +39904,60 @@ RuboCop::Cop::Style::MagicCommentFormat::SNAKE_SEPARATOR = T.let(T.unsafe(nil), # # good # array.reject { |e| some_condition? } # -# source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#40 +# source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#44 class RuboCop::Cop::Style::MapCompactWithConditionalBlock < ::RuboCop::Cop::Base extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#46 - def map_and_compact?(param0 = T.unsafe(nil)); end + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#51 + def conditional_block(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#72 + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#76 def on_csend(node); end - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#72 + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#76 def on_send(node); end private - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#126 - def range(node); end + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#148 + def current(node); end + + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#162 + def filter_map_range(node); end + + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#96 + def inspect(node, block_argument_node, condition_node, return_value_node, range); end + + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#158 + def map_with_compact_range(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#92 + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#114 def returns_block_argument?(block_argument_node, return_value_node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#96 + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#118 def truthy_branch?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#116 + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#138 def truthy_branch_for_guard?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#106 + # source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#128 def truthy_branch_for_if?(node); end end -# source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#43 +# source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#47 RuboCop::Cop::Style::MapCompactWithConditionalBlock::MSG = T.let(T.unsafe(nil), String) +# source://rubocop//lib/rubocop/cop/style/map_compact_with_conditional_block.rb#48 +RuboCop::Cop::Style::MapCompactWithConditionalBlock::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array) + # Checks for usages of `each` with `<<`, `push`, or `append` which # can be replaced by `map`. # @@ -40201,110 +40383,110 @@ module RuboCop::Cop::Style::MethodCallWithArgsParentheses::OmitParentheses # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#70 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#75 def allowed_camel_case_method_call?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#174 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#179 def allowed_chained_call_with_parentheses?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#170 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#175 def allowed_multiline_call_with_parentheses?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#75 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#80 def allowed_string_interpolation_method_call?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#183 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#188 def ambiguous_literal?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#212 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#217 def assigned_before?(node, target); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#220 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#225 def assignment_in_condition?(node); end - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#31 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#32 def autocorrect(corrector, node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#151 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#156 def call_as_argument_or_chain?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#144 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#149 def call_in_argument_with_block?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#100 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#105 def call_in_literals?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#111 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#116 def call_in_logical_operators?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#157 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#162 def call_in_match_pattern?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#120 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#125 def call_in_optional_arguments?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#124 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#129 def call_in_single_line_inheritance?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#128 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#133 def call_with_ambiguous_arguments?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#140 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#145 def call_with_braced_block?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#230 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#235 def forwards_anonymous_rest_arguments?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#199 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#204 def hash_literal?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#163 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#168 def hash_literal_in_arguments?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#44 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#45 def inside_endless_method_def?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#216 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#221 def inside_string_interpolation?(node); end # Require hash value omission be enclosed in parentheses to prevent the following issue: @@ -40312,20 +40494,25 @@ module RuboCop::Cop::Style::MethodCallWithArgsParentheses::OmitParentheses # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#58 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#59 def last_expression?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#87 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#92 def legitimate_call_with_parentheses?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#195 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#200 def logical_operator?(node); end - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#40 + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#67 + def method_call_before_constant_resolution?(node); end + + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#41 def offense_range(node); end # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#16 @@ -40333,42 +40520,42 @@ module RuboCop::Cop::Style::MethodCallWithArgsParentheses::OmitParentheses # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#80 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#85 def parentheses_at_the_end_of_multiline_call?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#203 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#208 def regexp_slash_literal?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#49 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#50 def require_parentheses_for_hash_value_omission?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#187 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#192 def splat?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#66 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#71 def super_call_without_arguments?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#62 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#63 def syntax_like_method_call?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#191 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#196 def ternary_if?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#207 + # source://rubocop//lib/rubocop/cop/style/method_call_with_args_parentheses/omit_parentheses.rb#212 def unary_literal?(node); end end @@ -41802,14 +41989,14 @@ RuboCop::Cop::Style::MultipleComparison::MSG = T.let(T.unsafe(nil), String) # # shareable_constant_value: literal # CONST = [1, 2, 3] # -# source://rubocop//lib/rubocop/cop/style/mutable_constant.rb#87 +# source://rubocop//lib/rubocop/cop/style/mutable_constant.rb#83 class RuboCop::Cop::Style::MutableConstant < ::RuboCop::Cop::Base include ::RuboCop::Cop::Style::MutableConstant::ShareableConstantValue include ::RuboCop::Cop::FrozenStringLiteral include ::RuboCop::Cop::ConfigurableEnforcedStyle extend ::RuboCop::Cop::AutoCorrector - # source://rubocop-sorbet/0.8.1/lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb#18 + # source://rubocop-sorbet/0.8.3/lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb#18 def on_assignment(value); end # source://rubocop//lib/rubocop/cop/style/mutable_constant.rb#127 @@ -41827,7 +42014,7 @@ class RuboCop::Cop::Style::MutableConstant < ::RuboCop::Cop::Base # source://rubocop//lib/rubocop/cop/style/mutable_constant.rb#217 def splat_value(param0 = T.unsafe(nil)); end - # source://rubocop-sorbet/0.8.1/lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb#12 + # source://rubocop-sorbet/0.8.3/lib/rubocop/cop/sorbet/mutable_constant_sorbet_aware_behaviour.rb#12 def t_let(param0 = T.unsafe(nil)); end private @@ -43135,16 +43322,16 @@ class RuboCop::Cop::Style::NumericPredicate < ::RuboCop::Cop::Base include ::RuboCop::Cop::AllowedPattern extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#166 + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#174 def comparison(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#171 + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#179 def inverted_comparison(param0 = T.unsafe(nil)); end # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#90 def on_send(node); end - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#161 + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#169 def predicate(param0 = T.unsafe(nil)); end private @@ -43157,23 +43344,28 @@ class RuboCop::Cop::Style::NumericPredicate < ::RuboCop::Cop::Base # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#111 def check(node); end - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#152 + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#154 def invert; end - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#132 + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#162 + def negated?(node); end + + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#134 def parenthesized_source(node); end # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#124 - def replacement(numeric, operation); end + def replacement(node, numeric, operation); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#144 + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#146 def replacement_supported?(operator); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#140 + # source://rubocop//lib/rubocop/cop/style/numeric_predicate.rb#142 def require_parentheses?(node); end end @@ -44205,7 +44397,7 @@ RuboCop::Cop::Style::Proc::MSG = T.let(T.unsafe(nil), String) # # String interpolation is always kept in double quotes. # -# Note: `Lint/SymbolConversion` can be used in parallel to ensure that symbols +# NOTE: `Lint/SymbolConversion` can be used in parallel to ensure that symbols # are not quoted that don't need to be. This cop is for configuring the quoting # style to use for symbols that require quotes. # @@ -45405,7 +45597,7 @@ RuboCop::Cop::Style::RedundantFetchBlock::MSG = T.let(T.unsafe(nil), String) # Checks for the presence of superfluous `.rb` extension in # the filename provided to `require` and `require_relative`. # -# Note: If the extension is omitted, Ruby tries adding '.rb', '.so', +# NOTE: If the extension is omitted, Ruby tries adding '.rb', '.so', # and so on to the name until found. If the file named cannot be found, # a `LoadError` will be raised. # There is an edge case where `foo.so` file is loaded instead of a `LoadError` @@ -45882,47 +46074,47 @@ class RuboCop::Cop::Style::RedundantLineContinuation < ::RuboCop::Cop::Base include ::RuboCop::Cop::MatchRange extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#78 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#79 def on_new_investigation; end private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#180 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#183 def argument_is_method?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#144 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#147 def argument_newline?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#101 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#102 def ends_with_backslash_without_comment?(source_line); end - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#160 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#163 def find_node_for_line(line); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#131 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#132 def inside_string_literal?(range, token); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#109 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#110 def inside_string_literal_or_method_with_argument?(range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#117 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#118 def leading_dot_method_chain_with_blank_line?(range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#187 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#190 def method_call_with_arguments?(node); end # A method call without parentheses such as the following cannot remove `\`: @@ -45932,38 +46124,41 @@ class RuboCop::Cop::Style::RedundantLineContinuation < ::RuboCop::Cop::Base # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#139 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#140 def method_with_argument?(current_token, next_token); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#123 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#124 def redundant_line_continuation?(range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#93 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#94 def require_line_continuation?(range); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#166 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#169 def same_line?(node, line); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#191 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#194 def start_with_arithmetic_operator?(source_line); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#105 + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#106 def string_concatenation?(source_line); end end # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#72 RuboCop::Cop::Style::RedundantLineContinuation::ALLOWED_STRING_TOKENS = T.let(T.unsafe(nil), Array) +# source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#77 +RuboCop::Cop::Style::RedundantLineContinuation::ARGUMENT_TAKING_FLOW_TOKEN_TYPES = T.let(T.unsafe(nil), Array) + # source://rubocop//lib/rubocop/cop/style/redundant_line_continuation.rb#73 RuboCop::Cop::Style::RedundantLineContinuation::ARGUMENT_TYPES = T.let(T.unsafe(nil), Array) @@ -48283,12 +48478,12 @@ RuboCop::Cop::Style::Semicolon::MSG = T.let(T.unsafe(nil), String) # # @example # # bad -# Foo.send(:bar) -# quuz.send(:fred) +# Foo.send(bar) +# quuz.send(fred) # # # good -# Foo.__send__(:bar) -# quuz.public_send(:fred) +# Foo.__send__(bar) +# quuz.public_send(fred) # # source://rubocop//lib/rubocop/cop/style/send.rb#16 class RuboCop::Cop::Style::Send < ::RuboCop::Cop::Base @@ -48305,6 +48500,83 @@ RuboCop::Cop::Style::Send::MSG = T.let(T.unsafe(nil), String) # source://rubocop//lib/rubocop/cop/style/send.rb#18 RuboCop::Cop::Style::Send::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array) +# Detects the use of the `public_send` method with a literal method name argument. +# Since the `send` method can be used to call private methods, by default, +# only the `public_send` method is detected. +# +# NOTE: Writer methods with names ending in `=` are always permitted because their +# behavior differs as follows: +# +# [source,ruby] +# ---- +# def foo=(foo) +# @foo = foo +# 42 +# end +# +# self.foo = 1 # => 1 +# send(:foo=, 1) # => 42 +# ---- +# +# @example +# # bad +# obj.public_send(:method_name) +# obj.public_send('method_name') +# +# # good +# obj.method_name +# @example AllowSend: true (default) +# # good +# obj.send(:method_name) +# obj.send('method_name') +# obj.__send__(:method_name) +# obj.__send__('method_name') +# @example AllowSend: false +# # bad +# obj.send(:method_name) +# obj.send('method_name') +# obj.__send__(:method_name) +# obj.__send__('method_name') +# +# # good +# obj.method_name +# +# source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#54 +class RuboCop::Cop::Style::SendWithLiteralMethodName < ::RuboCop::Cop::Base + extend ::RuboCop::Cop::AutoCorrector + + # source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#68 + def on_send(node); end + + private + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#90 + def allow_send?; end + + # source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#94 + def offense_range(node); end + + # source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#98 + def removal_argument_range(first_argument, second_argument); end +end + +# source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#60 +RuboCop::Cop::Style::SendWithLiteralMethodName::METHOD_NAME_PATTERN = T.let(T.unsafe(nil), Regexp) + +# source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#57 +RuboCop::Cop::Style::SendWithLiteralMethodName::MSG = T.let(T.unsafe(nil), String) + +# source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#61 +RuboCop::Cop::Style::SendWithLiteralMethodName::RESERVED_WORDS = T.let(T.unsafe(nil), Array) + +# source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#58 +RuboCop::Cop::Style::SendWithLiteralMethodName::RESTRICT_ON_SEND = T.let(T.unsafe(nil), Array) + +# source://rubocop//lib/rubocop/cop/style/send_with_literal_method_name.rb#59 +RuboCop::Cop::Style::SendWithLiteralMethodName::STATIC_METHOD_NAME_NODE_TYPES = T.let(T.unsafe(nil), Array) + # Checks for uses of `fail` and `raise`. # # @example EnforcedStyle: only_raise (default) @@ -48914,13 +49186,30 @@ RuboCop::Cop::Style::SoleNestedConditional::MSG = T.let(T.unsafe(nil), String) # will add a require statement to the top of the file if # enabled by RequireEnglish config. # -# Like `use_perl_names` but allows builtin global vars. -# +# @example EnforcedStyle: use_english_names (default) # # good +# require 'English' # or this could be in another file. +# # puts $LOAD_PATH # puts $LOADED_FEATURES # puts $PROGRAM_NAME -# puts ARGV +# puts $ERROR_INFO +# puts $ERROR_POSITION +# puts $FIELD_SEPARATOR # or $FS +# puts $OUTPUT_FIELD_SEPARATOR # or $OFS +# puts $INPUT_RECORD_SEPARATOR # or $RS +# puts $OUTPUT_RECORD_SEPARATOR # or $ORS +# puts $INPUT_LINE_NUMBER # or $NR +# puts $LAST_READ_LINE +# puts $DEFAULT_OUTPUT +# puts $DEFAULT_INPUT +# puts $PROCESS_ID # or $PID +# puts $CHILD_STATUS +# puts $LAST_MATCH_INFO +# puts $IGNORECASE +# puts $ARGV # or ARGV +# @example EnforcedStyle: use_perl_names +# # good # puts $: # puts $" # puts $0 @@ -48939,31 +49228,14 @@ RuboCop::Cop::Style::SoleNestedConditional::MSG = T.let(T.unsafe(nil), String) # puts $~ # puts $= # puts $* +# @example EnforcedStyle: use_builtin_english_names # -# @example EnforcedStyle: use_english_names (default) # # good -# require 'English' # or this could be in another file. -# +# # Like `use_perl_names` but allows builtin global vars. # puts $LOAD_PATH # puts $LOADED_FEATURES # puts $PROGRAM_NAME -# puts $ERROR_INFO -# puts $ERROR_POSITION -# puts $FIELD_SEPARATOR # or $FS -# puts $OUTPUT_FIELD_SEPARATOR # or $OFS -# puts $INPUT_RECORD_SEPARATOR # or $RS -# puts $OUTPUT_RECORD_SEPARATOR # or $ORS -# puts $INPUT_LINE_NUMBER # or $NR -# puts $LAST_READ_LINE -# puts $DEFAULT_OUTPUT -# puts $DEFAULT_INPUT -# puts $PROCESS_ID # or $PID -# puts $CHILD_STATUS -# puts $LAST_MATCH_INFO -# puts $IGNORECASE -# puts $ARGV # or ARGV -# @example EnforcedStyle: use_perl_names -# # good +# puts ARGV # puts $: # puts $" # puts $0 @@ -48982,90 +49254,89 @@ RuboCop::Cop::Style::SoleNestedConditional::MSG = T.let(T.unsafe(nil), String) # puts $~ # puts $= # puts $* -# @example EnforcedStyle: use_builtin_english_names # -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#87 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#86 class RuboCop::Cop::Style::SpecialGlobalVars < ::RuboCop::Cop::Base include ::RuboCop::Cop::ConfigurableEnforcedStyle include ::RuboCop::Cop::RangeHelp include ::RuboCop::Cop::RequireLibrary extend ::RuboCop::Cop::AutoCorrector - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#176 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#175 def autocorrect(corrector, node, global_var); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#168 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#167 def message(global_var); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#152 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#151 def on_gvar(node); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#147 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#146 def on_new_investigation; end private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#247 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#246 def add_require_english?; end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#241 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#240 def english_name_replacement(preferred_name, node); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#190 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#189 def format_english_message(global_var); end # For now, we assume that lists are 2 items or less. Easy grammar! # - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#212 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#211 def format_list(items); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#198 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#197 def format_message(english, regular, global); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#235 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#234 def matching_styles(global); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#227 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#226 def preferred_names(global); end - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#216 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#215 def replacement(node, global_var); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#251 + # source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#250 def should_require_english?(global_var); end end -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#128 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#127 RuboCop::Cop::Style::SpecialGlobalVars::BUILTIN_VARS = T.let(T.unsafe(nil), Hash) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#100 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#99 RuboCop::Cop::Style::SpecialGlobalVars::ENGLISH_VARS = T.let(T.unsafe(nil), Hash) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#145 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#144 RuboCop::Cop::Style::SpecialGlobalVars::LIBRARY_NAME = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#93 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#92 RuboCop::Cop::Style::SpecialGlobalVars::MSG_BOTH = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#96 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#95 RuboCop::Cop::Style::SpecialGlobalVars::MSG_ENGLISH = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#98 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#97 RuboCop::Cop::Style::SpecialGlobalVars::MSG_REGULAR = T.let(T.unsafe(nil), String) # Anything *not* in this set is provided by the English library. # -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#122 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#121 RuboCop::Cop::Style::SpecialGlobalVars::NON_ENGLISH_VARS = T.let(T.unsafe(nil), Set) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#124 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#123 RuboCop::Cop::Style::SpecialGlobalVars::PERL_VARS = T.let(T.unsafe(nil), Hash) -# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#139 +# source://rubocop//lib/rubocop/cop/style/special_global_vars.rb#138 RuboCop::Cop::Style::SpecialGlobalVars::STYLE_VARS_MAP = T.let(T.unsafe(nil), Hash) # Check for parentheses around stabby lambda arguments. @@ -49667,6 +49938,118 @@ end # source://rubocop//lib/rubocop/cop/style/struct_inheritance.rb#30 RuboCop::Cop::Style::StructInheritance::MSG = T.let(T.unsafe(nil), String) +# Checks for redundant argument forwarding when calling super with arguments identical to +# the method definition. +# +# Using zero arity `super` within a `define_method` block results in `RuntimeError`: +# +# [source,ruby] +# ---- +# def m +# define_method(:foo) { super() } # => OK +# end +# +# def m +# define_method(:foo) { super } # => RuntimeError +# end +# ---- +# +# Furthermore, any arguments accompanied by a block may potentially be delegating to +# `define_method`, therefore, `super` used within these blocks will be allowed. +# This approach might result in false negatives, yet ensuring safe detection takes precedence. +# +# @example +# # bad +# def method(*args, **kwargs) +# super(*args, **kwargs) +# end +# +# # good - implicitly passing all arguments +# def method(*args, **kwargs) +# super +# end +# +# # good - forwarding a subset of the arguments +# def method(*args, **kwargs) +# super(*args) +# end +# +# # good - forwarding no arguments +# def method(*args, **kwargs) +# super() +# end +# +# # good - assigning to the block variable before calling super +# def method(&block) +# # Assigning to the block variable would pass the old value to super, +# # under this circumstance the block must be referenced explicitly. +# block ||= proc { 'fallback behavior' } +# super(&block) +# end +# +# source://rubocop//lib/rubocop/cop/style/super_arguments.rb#54 +class RuboCop::Cop::Style::SuperArguments < ::RuboCop::Cop::Base + extend ::RuboCop::Cop::AutoCorrector + + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#62 + def on_super(super_node); end + + private + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#81 + def arguments_identical?(def_node, def_args, super_args); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#136 + def block_arg_same?(def_node, def_arg, super_arg); end + + # Reassigning the block argument will still pass along the original block to super + # https://bugs.ruby-lang.org/issues/20505 + # + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#147 + def block_reassigned?(def_node, block_arg_name); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#158 + def forward_arg_same?(def_arg, super_arg); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#116 + def keyword_arg_same?(def_arg, super_arg); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#126 + def keyword_rest_arg_same?(def_arg, super_arg); end + + # @return [Boolean] + # + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#99 + def positional_arg_same?(def_arg, super_arg); end + + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#106 + def positional_rest_arg_same(def_arg, super_arg); end + + # source://rubocop//lib/rubocop/cop/style/super_arguments.rb#162 + def preprocess_super_args(super_args); end +end + +# source://rubocop//lib/rubocop/cop/style/super_arguments.rb#58 +RuboCop::Cop::Style::SuperArguments::ASSIGN_TYPES = T.let(T.unsafe(nil), Array) + +# source://rubocop//lib/rubocop/cop/style/super_arguments.rb#57 +RuboCop::Cop::Style::SuperArguments::DEF_TYPES = T.let(T.unsafe(nil), Array) + +# source://rubocop//lib/rubocop/cop/style/super_arguments.rb#60 +RuboCop::Cop::Style::SuperArguments::MSG = T.let(T.unsafe(nil), String) + # Enforces the presence of parentheses in `super` containing arguments. # # `super` is a keyword and is provided as a distinct cop from those designed for method call. @@ -49890,9 +50273,11 @@ RuboCop::Cop::Style::SymbolLiteral::MSG = T.let(T.unsafe(nil), String) # `define_method?` methods are allowed by default. # These are customizable with `AllowedMethods` option. # -# @example AllowedPatterns: ['map'] (default) +# @example AllCops:ActiveSupportExtensionsEnabled: true # # good -# something.map { |s| s.upcase } +# ->(x) { x.foo } +# proc { |x| x.foo } +# Proc.new { |x| x.foo } # @example AllowMethodsWithArguments: false (default) # # bad # something.do_something(foo) { |o| o.bar } @@ -49922,6 +50307,19 @@ RuboCop::Cop::Style::SymbolLiteral::MSG = T.let(T.unsafe(nil), String) # @example AllowedPatterns: [] (default) # # bad # something.map { |s| s.upcase } +# @example AllowedPatterns: ['map'] (default) +# # good +# something.map { |s| s.upcase } +# @example AllCops:ActiveSupportExtensionsEnabled: false (default) +# # bad +# ->(x) { x.foo } +# proc { |x| x.foo } +# Proc.new { |x| x.foo } +# +# # good +# lambda(&:foo) +# proc(&:foo) +# Proc.new(&:foo) # @example # # bad # something.map { |s| s.upcase } @@ -49930,7 +50328,7 @@ RuboCop::Cop::Style::SymbolLiteral::MSG = T.let(T.unsafe(nil), String) # # good # something.map(&:upcase) # -# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#123 +# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#140 class RuboCop::Cop::Style::SymbolProc < ::RuboCop::Cop::Base include ::RuboCop::Cop::CommentsHelp include ::RuboCop::Cop::RangeHelp @@ -49940,81 +50338,87 @@ class RuboCop::Cop::Style::SymbolProc < ::RuboCop::Cop::Base # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#172 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#189 def destructuring_block_argument?(argument_node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#152 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#170 def on_block(node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#152 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#170 def on_numblock(node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#134 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#152 def proc_node?(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#140 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#158 def symbol_proc?(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#137 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#155 def symbol_proc_receiver?(param0 = T.unsafe(nil)); end private # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#240 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#274 def allow_comments?; end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#236 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#270 def allow_if_method_has_argument?(send_node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#187 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#204 def allowed_method_name?(name); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#200 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#217 def autocorrect(corrector, node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#212 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#247 + def autocorrect_lambda_block(corrector, node); end + + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#238 def autocorrect_with_args(corrector, node, args, method_name); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#208 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#225 def autocorrect_without_args(corrector, node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#226 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#260 def begin_pos_for_replacement(node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#221 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#255 def block_range_with_space(node); end - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#191 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#208 def register_offense(node, method_name, block_method_name); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#183 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#200 def unsafe_array_usage?(node); end # See: https://github.com/rubocop/rubocop/issues/10864 # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#179 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#196 def unsafe_hash_usage?(node); end class << self - # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#147 + # source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#165 def autocorrect_incompatible_with; end end end -# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#130 +# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#149 +RuboCop::Cop::Style::SymbolProc::LAMBDA_OR_PROC = T.let(T.unsafe(nil), Array) + +# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#147 RuboCop::Cop::Style::SymbolProc::MSG = T.let(T.unsafe(nil), String) -# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#131 +# source://rubocop//lib/rubocop/cop/style/symbol_proc.rb#148 RuboCop::Cop::Style::SymbolProc::SUPER_TYPES = T.let(T.unsafe(nil), Array) # Corrector to correct conditional assignment in ternary conditions. @@ -51782,39 +52186,42 @@ class RuboCop::Cop::Style::ZeroLengthPredicate < ::RuboCop::Cop::Base # implement `#size`, but not `#empty`. We ignore those to # reduce false positives. # - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#139 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#143 def non_polymorphic_collection?(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#109 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#110 def nonzero_length_comparison(param0 = T.unsafe(nil)); end + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#45 + def on_csend(node); end + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#45 def on_send(node); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#130 - def other_receiver(param0 = T.unsafe(nil)); end + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#134 + def other_length_node(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#101 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#102 def zero_length_comparison(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#96 - def zero_length_predicate(param0 = T.unsafe(nil)); end + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#126 + def zero_length_node(param0 = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#122 - def zero_length_receiver(param0 = T.unsafe(nil)); end + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#97 + def zero_length_predicate?(param0 = T.unsafe(nil)); end private - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#80 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#81 def check_nonzero_length_comparison(node); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#65 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#66 def check_zero_length_comparison(node); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#53 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#54 def check_zero_length_predicate(node); end - # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#114 + # source://rubocop//lib/rubocop/cop/style/zero_length_predicate.rb#115 def replacement(node); end end @@ -51961,12 +52368,12 @@ class RuboCop::Cop::Team # source://rubocop//lib/rubocop/cop/team.rb#51 def errors; end - # source://rubocop//lib/rubocop/cop/team.rb#114 + # source://rubocop//lib/rubocop/cop/team.rb#122 def external_dependency_checksum; end # @deprecated # - # source://rubocop//lib/rubocop/cop/team.rb#110 + # source://rubocop//lib/rubocop/cop/team.rb#114 def forces; end # source://rubocop//lib/rubocop/cop/team.rb#76 @@ -51974,7 +52381,7 @@ class RuboCop::Cop::Team # @return [Commissioner::InvestigationReport] # - # source://rubocop//lib/rubocop/cop/team.rb#81 + # source://rubocop//lib/rubocop/cop/team.rb#85 def investigate(processed_source, offset: T.unsafe(nil), original: T.unsafe(nil)); end # Returns the value of attribute updated_source_file. @@ -51994,57 +52401,57 @@ class RuboCop::Cop::Team private - # source://rubocop//lib/rubocop/cop/team.rb#121 + # source://rubocop//lib/rubocop/cop/team.rb#129 def autocorrect(processed_source, report, original:, offset:); end - # source://rubocop//lib/rubocop/cop/team.rb#185 + # source://rubocop//lib/rubocop/cop/team.rb#193 def autocorrect_report(report, offset:, original:); end - # source://rubocop//lib/rubocop/cop/team.rb#140 + # source://rubocop//lib/rubocop/cop/team.rb#148 def be_ready; end - # source://rubocop//lib/rubocop/cop/team.rb#191 + # source://rubocop//lib/rubocop/cop/team.rb#199 def collate_corrections(report, offset:, original:); end - # source://rubocop//lib/rubocop/cop/team.rb#207 + # source://rubocop//lib/rubocop/cop/team.rb#215 def each_corrector(report); end - # source://rubocop//lib/rubocop/cop/team.rb#257 + # source://rubocop//lib/rubocop/cop/team.rb#267 def handle_error(error, location, cop); end - # source://rubocop//lib/rubocop/cop/team.rb#249 + # source://rubocop//lib/rubocop/cop/team.rb#259 def handle_warning(error, location); end # @return [Commissioner::InvestigationReport] # - # source://rubocop//lib/rubocop/cop/team.rb#154 + # source://rubocop//lib/rubocop/cop/team.rb#162 def investigate_partial(cops, processed_source, offset:, original:); end - # source://rubocop//lib/rubocop/cop/team.rb#234 + # source://rubocop//lib/rubocop/cop/team.rb#242 def process_errors(file, errors); end - # source://rubocop//lib/rubocop/cop/team.rb#148 + # source://rubocop//lib/rubocop/cop/team.rb#156 def reset; end # @return [Array] # - # source://rubocop//lib/rubocop/cop/team.rb#160 + # source://rubocop//lib/rubocop/cop/team.rb#168 def roundup_relevant_cops(processed_source); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/team.rb#176 + # source://rubocop//lib/rubocop/cop/team.rb#184 def support_target_rails_version?(cop); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/team.rb#170 + # source://rubocop//lib/rubocop/cop/team.rb#178 def support_target_ruby_version?(cop); end - # source://rubocop//lib/rubocop/cop/team.rb#222 + # source://rubocop//lib/rubocop/cop/team.rb#230 def suppress_clobbering; end - # source://rubocop//lib/rubocop/cop/team.rb#228 + # source://rubocop//lib/rubocop/cop/team.rb#236 def validate_config; end class << self @@ -52295,19 +52702,19 @@ module RuboCop::Cop::Util private - # source://rubocop//lib/rubocop/cop/util.rb#35 + # source://rubocop//lib/rubocop/cop/util.rb#39 def add_parentheses(node, corrector); end - # source://rubocop//lib/rubocop/cop/util.rb#56 + # source://rubocop//lib/rubocop/cop/util.rb#60 def any_descendant?(node, *types); end - # source://rubocop//lib/rubocop/cop/util.rb#71 + # source://rubocop//lib/rubocop/cop/util.rb#75 def args_begin(node); end - # source://rubocop//lib/rubocop/cop/util.rb#83 + # source://rubocop//lib/rubocop/cop/util.rb#87 def args_end(node); end - # source://rubocop//lib/rubocop/cop/util.rb#104 + # source://rubocop//lib/rubocop/cop/util.rb#108 def begins_its_line?(range); end # This is a bad API @@ -52320,78 +52727,78 @@ module RuboCop::Cop::Util # source://rubocop//lib/rubocop/cop/util.rb#22 def comment_lines?(node); end - # source://rubocop//lib/rubocop/cop/util.rb#192 + # source://rubocop//lib/rubocop/cop/util.rb#198 def compatible_external_encoding_for?(src); end # If converting a string to Ruby string literal source code, must # double quotes be used? # - # source://rubocop//lib/rubocop/cop/util.rb#130 + # source://rubocop//lib/rubocop/cop/util.rb#134 def double_quotes_required?(string); end - # source://rubocop//lib/rubocop/cop/util.rb#144 + # source://rubocop//lib/rubocop/cop/util.rb#148 def escape_string(string); end # Returns, for example, a bare `if` node if the given node is an `if` # with calls chained to the end of it. # - # source://rubocop//lib/rubocop/cop/util.rb#114 + # source://rubocop//lib/rubocop/cop/util.rb#118 def first_part_of_call_chain(node); end - # source://rubocop//lib/rubocop/cop/util.rb#197 + # source://rubocop//lib/rubocop/cop/util.rb#203 def include_or_equal?(source, target); end - # source://rubocop//lib/rubocop/cop/util.rb#179 + # source://rubocop//lib/rubocop/cop/util.rb#185 def indent(node, offset: T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/util.rb#161 + # source://rubocop//lib/rubocop/cop/util.rb#165 def interpret_string_escapes(string); end - # source://rubocop//lib/rubocop/cop/util.rb#165 + # source://rubocop//lib/rubocop/cop/util.rb#169 def line(node_or_range); end - # source://rubocop//lib/rubocop/cop/util.rb#26 + # source://rubocop//lib/rubocop/cop/util.rb#30 def line_range(node); end - # source://rubocop//lib/rubocop/cop/util.rb#140 + # source://rubocop//lib/rubocop/cop/util.rb#144 def needs_escaping?(string); end - # source://rubocop//lib/rubocop/cop/util.rb#87 + # source://rubocop//lib/rubocop/cop/util.rb#91 def on_node(syms, sexp, excludes = T.unsafe(nil), &block); end - # source://rubocop//lib/rubocop/cop/util.rb#30 + # source://rubocop//lib/rubocop/cop/util.rb#34 def parentheses?(node); end - # source://rubocop//lib/rubocop/cop/util.rb#173 + # source://rubocop//lib/rubocop/cop/util.rb#177 def same_line?(node1, node2); end - # source://rubocop//lib/rubocop/cop/util.rb#148 + # source://rubocop//lib/rubocop/cop/util.rb#152 def to_string_literal(string); end - # source://rubocop//lib/rubocop/cop/util.rb#185 + # source://rubocop//lib/rubocop/cop/util.rb#191 def to_supported_styles(enforced_style); end - # source://rubocop//lib/rubocop/cop/util.rb#157 + # source://rubocop//lib/rubocop/cop/util.rb#161 def trim_string_interpolation_escape_character(str); end class << self - # source://rubocop//lib/rubocop/cop/util.rb#35 + # source://rubocop//lib/rubocop/cop/util.rb#39 def add_parentheses(node, corrector); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/util.rb#56 + # source://rubocop//lib/rubocop/cop/util.rb#60 def any_descendant?(node, *types); end - # source://rubocop//lib/rubocop/cop/util.rb#71 + # source://rubocop//lib/rubocop/cop/util.rb#75 def args_begin(node); end - # source://rubocop//lib/rubocop/cop/util.rb#83 + # source://rubocop//lib/rubocop/cop/util.rb#87 def args_end(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/util.rb#104 + # source://rubocop//lib/rubocop/cop/util.rb#108 def begins_its_line?(range); end # This is a bad API @@ -52412,62 +52819,62 @@ module RuboCop::Cop::Util # # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/util.rb#130 + # source://rubocop//lib/rubocop/cop/util.rb#134 def double_quotes_required?(string); end - # source://rubocop//lib/rubocop/cop/util.rb#144 + # source://rubocop//lib/rubocop/cop/util.rb#148 def escape_string(string); end # Returns, for example, a bare `if` node if the given node is an `if` # with calls chained to the end of it. # - # source://rubocop//lib/rubocop/cop/util.rb#114 + # source://rubocop//lib/rubocop/cop/util.rb#118 def first_part_of_call_chain(node); end - # source://rubocop//lib/rubocop/cop/util.rb#179 + # source://rubocop//lib/rubocop/cop/util.rb#185 def indent(node, offset: T.unsafe(nil)); end - # source://rubocop//lib/rubocop/cop/util.rb#161 + # source://rubocop//lib/rubocop/cop/util.rb#165 def interpret_string_escapes(string); end - # source://rubocop//lib/rubocop/cop/util.rb#165 + # source://rubocop//lib/rubocop/cop/util.rb#169 def line(node_or_range); end - # source://rubocop//lib/rubocop/cop/util.rb#26 + # source://rubocop//lib/rubocop/cop/util.rb#30 def line_range(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/util.rb#140 + # source://rubocop//lib/rubocop/cop/util.rb#144 def needs_escaping?(string); end # @yield [sexp] # - # source://rubocop//lib/rubocop/cop/util.rb#87 + # source://rubocop//lib/rubocop/cop/util.rb#91 def on_node(syms, sexp, excludes = T.unsafe(nil), &block); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/util.rb#30 + # source://rubocop//lib/rubocop/cop/util.rb#34 def parentheses?(node); end # @return [Boolean] # - # source://rubocop//lib/rubocop/cop/util.rb#173 + # source://rubocop//lib/rubocop/cop/util.rb#177 def same_line?(node1, node2); end - # source://rubocop//lib/rubocop/cop/util.rb#148 + # source://rubocop//lib/rubocop/cop/util.rb#152 def to_string_literal(string); end - # source://rubocop//lib/rubocop/cop/util.rb#185 + # source://rubocop//lib/rubocop/cop/util.rb#191 def to_supported_styles(enforced_style); end - # source://rubocop//lib/rubocop/cop/util.rb#157 + # source://rubocop//lib/rubocop/cop/util.rb#161 def trim_string_interpolation_escape_character(str); end end end -# source://rubocop//lib/rubocop/cop/util.rb#99 +# source://rubocop//lib/rubocop/cop/util.rb#103 RuboCop::Cop::Util::LINE_BEGINS_REGEX_CACHE = T.let(T.unsafe(nil), Hash) # Match literal regex characters, not including anchors, character @@ -52479,7 +52886,7 @@ RuboCop::Cop::Util::LITERAL_REGEX = T.let(T.unsafe(nil), Regexp) # Arbitrarily chosen value, should be enough to cover # the most nested source code in real world projects. # -# source://rubocop//lib/rubocop/cop/util.rb#98 +# source://rubocop//lib/rubocop/cop/util.rb#102 RuboCop::Cop::Util::MAX_LINE_BEGINS_REGEX_INDEX = T.let(T.unsafe(nil), Integer) # source://rubocop//lib/rubocop/cop/utils/format_string.rb#5 @@ -54215,7 +54622,7 @@ module RuboCop::Ext::RegexpParser::Expression; end module RuboCop::Ext::RegexpParser::Expression::Base # Shortcut to `loc.expression` # - # source://rubocop//lib/rubocop/ext/regexp_parser.rb#27 + # source://rubocop//lib/rubocop/ext/regexp_parser.rb#26 def expression; end # E.g. @@ -54228,7 +54635,7 @@ module RuboCop::Ext::RegexpParser::Expression::Base # # Please open issue if you need other locations # - # source://rubocop//lib/rubocop/ext/regexp_parser.rb#61 + # source://rubocop//lib/rubocop/ext/regexp_parser.rb#44 def loc; end # Returns the value of attribute origin. @@ -54245,15 +54652,15 @@ module RuboCop::Ext::RegexpParser::Expression::Base private - # source://rubocop//lib/rubocop/ext/regexp_parser.rb#67 + # source://rubocop//lib/rubocop/ext/regexp_parser.rb#50 def build_location; end end # Provide `CharacterSet` with `begin` and `end` locations. # -# source://rubocop//lib/rubocop/ext/regexp_parser.rb#79 +# source://rubocop//lib/rubocop/ext/regexp_parser.rb#62 module RuboCop::Ext::RegexpParser::Expression::CharacterSet - # source://rubocop//lib/rubocop/ext/regexp_parser.rb#80 + # source://rubocop//lib/rubocop/ext/regexp_parser.rb#63 def build_location; end end @@ -54652,16 +55059,16 @@ class RuboCop::Formatter::DisabledConfigFormatter < ::RuboCop::Formatter::BaseFo # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#73 def command; end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#161 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#165 def cop_config_params(default_cfg, cfg); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#181 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#185 def default_config(cop_name); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#225 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#229 def excludes(offending_files, cop_name, parent); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#196 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#200 def filtered_config(cfg); end # Returns true if the given arr include the given elm or if any of the @@ -54669,38 +55076,38 @@ class RuboCop::Formatter::DisabledConfigFormatter < ::RuboCop::Formatter::BaseFo # # @return [Boolean] # - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#273 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#277 def include_or_match?(arr, elm); end # @return [Boolean] # - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#246 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#250 def merge_mode_for_exclude?(cfg); end # @return [Boolean] # - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#267 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#271 def no_exclude_limit?; end # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#102 def output_cop(cop_name, offense_count); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#133 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#137 def output_cop_comments(output_buffer, cfg, cop_name, offense_count); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#185 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#189 def output_cop_config(output_buffer, cfg, cop_name); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#168 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#172 def output_cop_param_comments(output_buffer, params, default_cfg); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#215 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#219 def output_exclude_list(output_buffer, offending_files, cop_name); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#250 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#254 def output_exclude_path(output_buffer, exclude_path, parent); end - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#204 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#208 def output_offending_files(output_buffer, cfg, cop_name); end # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#96 @@ -54708,12 +55115,17 @@ class RuboCop::Formatter::DisabledConfigFormatter < ::RuboCop::Formatter::BaseFo # @return [Boolean] # - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#263 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#267 def safe_autocorrect?(config); end # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#116 def set_max(cfg, cop_name); end + # @return [Boolean] + # + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#125 + def should_set_max?(cop_name); end + # @return [Boolean] # # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#65 @@ -54726,12 +55138,12 @@ class RuboCop::Formatter::DisabledConfigFormatter < ::RuboCop::Formatter::BaseFo # @return [Boolean] # - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#153 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#157 def supports_safe_autocorrect?(cop_class, default_cfg); end # @return [Boolean] # - # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#157 + # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#161 def supports_unsafe_autocorrect?(cop_class, default_cfg); end # source://rubocop//lib/rubocop/formatter/disabled_config_formatter.rb#92 @@ -54804,43 +55216,46 @@ end class RuboCop::Formatter::FormatterSet < ::Array # @return [FormatterSet] a new instance of FormatterSet # - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#39 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#40 def initialize(options = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#55 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#56 def add_formatter(formatter_type, output_path = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#67 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#68 def close_output_files; end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#50 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#51 def file_finished(file, offenses); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#44 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#45 def file_started(file, options); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#34 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#35 def finished(*args); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#34 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#35 def started(*args); end private - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#86 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#87 def builtin_formatter_class(specified_key); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#99 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#105 def custom_formatter_class(specified_class_name); end - # source://rubocop//lib/rubocop/formatter/formatter_set.rb#75 + # source://rubocop//lib/rubocop/formatter/formatter_set.rb#76 def formatter_class(formatter_type); end end # source://rubocop//lib/rubocop/formatter/formatter_set.rb#11 RuboCop::Formatter::FormatterSet::BUILTIN_FORMATTERS_FOR_KEYS = T.let(T.unsafe(nil), Hash) -# source://rubocop//lib/rubocop/formatter/formatter_set.rb#31 +# source://rubocop//lib/rubocop/formatter/formatter_set.rb#30 +RuboCop::Formatter::FormatterSet::BUILTIN_FORMATTER_NAMES = T.let(T.unsafe(nil), Array) + +# source://rubocop//lib/rubocop/formatter/formatter_set.rb#32 RuboCop::Formatter::FormatterSet::FORMATTER_APIS = T.let(T.unsafe(nil), Array) # This formatter displays a progress bar and shows details of offenses as @@ -54940,15 +55355,15 @@ end # This class provides helper methods used in the ERB CSS template. # -# source://rubocop//lib/rubocop/formatter/html_formatter.rb#135 +# source://rubocop//lib/rubocop/formatter/html_formatter.rb#137 class RuboCop::Formatter::HTMLFormatter::CSSContext # Make Kernel#binding public. # - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#146 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#148 def binding; end end -# source://rubocop//lib/rubocop/formatter/html_formatter.rb#136 +# source://rubocop//lib/rubocop/formatter/html_formatter.rb#138 RuboCop::Formatter::HTMLFormatter::CSSContext::SEVERITY_COLORS = T.let(T.unsafe(nil), Hash) # source://rubocop//lib/rubocop/formatter/html_formatter.rb#12 @@ -55020,60 +55435,60 @@ RuboCop::Formatter::HTMLFormatter::ELLIPSES = T.let(T.unsafe(nil), String) # This class provides helper methods used in the ERB template. # -# source://rubocop//lib/rubocop/formatter/html_formatter.rb#61 +# source://rubocop//lib/rubocop/formatter/html_formatter.rb#63 class RuboCop::Formatter::HTMLFormatter::ERBContext include ::RuboCop::PathUtil include ::RuboCop::Formatter::TextUtil # @return [ERBContext] a new instance of ERBContext # - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#69 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#71 def initialize(files, summary); end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#116 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#118 def base64_encoded_logo_image; end # Make Kernel#binding public. # - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#76 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#78 def binding; end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#81 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#83 def decorated_message(offense); end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#112 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#114 def escape(string); end # Returns the value of attribute files. # - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#67 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#69 def files; end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#92 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#94 def highlight_source_tag(offense); end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#85 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#87 def highlighted_source_line(offense); end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#108 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#110 def possible_ellipses(location); end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#124 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#126 def render_css; end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#103 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#105 def source_after_highlight(offense); end - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#98 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#100 def source_before_highlight(offense); end # Returns the value of attribute summary. # - # source://rubocop//lib/rubocop/formatter/html_formatter.rb#67 + # source://rubocop//lib/rubocop/formatter/html_formatter.rb#69 def summary; end end -# source://rubocop//lib/rubocop/formatter/html_formatter.rb#65 +# source://rubocop//lib/rubocop/formatter/html_formatter.rb#67 RuboCop::Formatter::HTMLFormatter::ERBContext::LOGO_IMAGE_PATH = T.let(T.unsafe(nil), String) # source://rubocop//lib/rubocop/formatter/html_formatter.rb#25 @@ -55601,7 +56016,7 @@ module RuboCop::LSP # @return [void] # # source://rubocop//lib/rubocop/lsp.rb#25 - def disable; end + def disable(&block); end # Enable LSP. # @@ -55623,7 +56038,7 @@ module RuboCop::LSP # @return [void] # # source://rubocop//lib/rubocop/lsp.rb#25 - def disable; end + def disable(&block); end # Enable LSP. # @@ -55675,52 +56090,52 @@ class RuboCop::LSP::Routes # source://rubocop//lib/rubocop/lsp/routes.rb#38 def handle_initialize(request); end - # source://rubocop//lib/rubocop/lsp/routes.rb#61 + # source://rubocop//lib/rubocop/lsp/routes.rb#57 def handle_initialized(_request); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#170 + # source://rubocop//lib/rubocop/lsp/routes.rb#167 def handle_method_missing(request); end - # source://rubocop//lib/rubocop/lsp/routes.rb#67 + # source://rubocop//lib/rubocop/lsp/routes.rb#64 def handle_shutdown(request); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#159 + # source://rubocop//lib/rubocop/lsp/routes.rb#156 def handle_unsupported_method(request, method = T.unsafe(nil)); end private # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#208 + # source://rubocop//lib/rubocop/lsp/routes.rb#205 def diagnostic(file_uri, text); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#178 + # source://rubocop//lib/rubocop/lsp/routes.rb#175 def extract_initialization_options_from(request); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#188 + # source://rubocop//lib/rubocop/lsp/routes.rb#185 def format_file(file_uri, command: T.unsafe(nil)); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#222 + # source://rubocop//lib/rubocop/lsp/routes.rb#219 def remove_file_protocol_from(uri); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#226 + # source://rubocop//lib/rubocop/lsp/routes.rb#223 def to_diagnostic(offense); end # @api private # - # source://rubocop//lib/rubocop/lsp/routes.rb#238 + # source://rubocop//lib/rubocop/lsp/routes.rb#235 def to_range(location); end class << self @@ -55814,32 +56229,32 @@ class RuboCop::LSP::Server # @api private # - # source://rubocop//lib/rubocop/lsp/server.rb#59 + # source://rubocop//lib/rubocop/lsp/server.rb#61 def configure(options); end # @api private # - # source://rubocop//lib/rubocop/lsp/server.rb#51 + # source://rubocop//lib/rubocop/lsp/server.rb#53 def format(path, text, command:); end # @api private # - # source://rubocop//lib/rubocop/lsp/server.rb#55 + # source://rubocop//lib/rubocop/lsp/server.rb#57 def offenses(path, text); end # @api private # - # source://rubocop//lib/rubocop/lsp/server.rb#32 + # source://rubocop//lib/rubocop/lsp/server.rb#34 def start; end # @api private # - # source://rubocop//lib/rubocop/lsp/server.rb#65 + # source://rubocop//lib/rubocop/lsp/server.rb#67 def stop(&block); end # @api private # - # source://rubocop//lib/rubocop/lsp/server.rb#47 + # source://rubocop//lib/rubocop/lsp/server.rb#49 def write(response); end end @@ -55867,13 +56282,13 @@ RuboCop::LSP::Severity::SEVERITIES = T.let(T.unsafe(nil), Hash) # # @api private # -# source://rubocop//lib/rubocop/lockfile.rb#7 +# source://rubocop//lib/rubocop/lockfile.rb#15 class RuboCop::Lockfile # @api private # @param lockfile_path [String, Pathname, nil] # @return [Lockfile] a new instance of Lockfile # - # source://rubocop//lib/rubocop/lockfile.rb#9 + # source://rubocop//lib/rubocop/lockfile.rb#17 def initialize(lockfile_path = T.unsafe(nil)); end # Gems that the bundle directly depends on. @@ -55881,7 +56296,7 @@ class RuboCop::Lockfile # @api private # @return [Array, nil] # - # source://rubocop//lib/rubocop/lockfile.rb#17 + # source://rubocop//lib/rubocop/lockfile.rb#29 def dependencies; end # Returns the locked versions of gems from this lockfile. @@ -55890,7 +56305,7 @@ class RuboCop::Lockfile # @param include_transitive_dependencies: [Boolean] When false, only direct dependencies # are returned, i.e. those listed explicitly in the `Gemfile`. # - # source://rubocop//lib/rubocop/lockfile.rb#37 + # source://rubocop//lib/rubocop/lockfile.rb#49 def gem_versions(include_transitive_dependencies: T.unsafe(nil)); end # All activated gems, including transitive dependencies. @@ -55898,7 +56313,7 @@ class RuboCop::Lockfile # @api private # @return [Array, nil] # - # source://rubocop//lib/rubocop/lockfile.rb#25 + # source://rubocop//lib/rubocop/lockfile.rb#37 def gems; end # Whether this lockfile includes the named gem, directly or indirectly. @@ -55907,7 +56322,7 @@ class RuboCop::Lockfile # @param name [String] # @return [Boolean] # - # source://rubocop//lib/rubocop/lockfile.rb#53 + # source://rubocop//lib/rubocop/lockfile.rb#65 def includes_gem?(name); end private @@ -55915,13 +56330,13 @@ class RuboCop::Lockfile # @api private # @return [Boolean] # - # source://rubocop//lib/rubocop/lockfile.rb#73 + # source://rubocop//lib/rubocop/lockfile.rb#85 def bundler_lock_parser_defined?; end # @api private # @return [Bundler::LockfileParser, nil] # - # source://rubocop//lib/rubocop/lockfile.rb#60 + # source://rubocop//lib/rubocop/lockfile.rb#72 def parser; end end @@ -56945,145 +57360,145 @@ RuboCop::ResultCache::NON_CHANGING = T.let(T.unsafe(nil), Array) class RuboCop::Runner # @return [Runner] a new instance of Runner # - # source://rubocop//lib/rubocop/runner.rb#62 + # source://rubocop//lib/rubocop/runner.rb#63 def initialize(options, config_store); end # Sets the attribute aborting # # @param value the value to set the attribute aborting to. # - # source://rubocop//lib/rubocop/runner.rb#60 + # source://rubocop//lib/rubocop/runner.rb#61 def aborting=(_arg0); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#86 + # source://rubocop//lib/rubocop/runner.rb#87 def aborting?; end # Returns the value of attribute errors. # - # source://rubocop//lib/rubocop/runner.rb#59 + # source://rubocop//lib/rubocop/runner.rb#60 def errors; end - # source://rubocop//lib/rubocop/runner.rb#70 + # source://rubocop//lib/rubocop/runner.rb#71 def run(paths); end # Returns the value of attribute warnings. # - # source://rubocop//lib/rubocop/runner.rb#59 + # source://rubocop//lib/rubocop/runner.rb#60 def warnings; end private - # source://rubocop//lib/rubocop/runner.rb#199 + # source://rubocop//lib/rubocop/runner.rb#200 def add_redundant_disables(file, offenses, source); end - # source://rubocop//lib/rubocop/runner.rb#173 + # source://rubocop//lib/rubocop/runner.rb#174 def cached_result(file, team); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#253 + # source://rubocop//lib/rubocop/runner.rb#254 def cached_run?; end # Check whether a run created source identical to a previous run, which # means that we definitely have an infinite loop. # - # source://rubocop//lib/rubocop/runner.rb#331 + # source://rubocop//lib/rubocop/runner.rb#332 def check_for_infinite_loop(processed_source, offenses_by_iteration); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#225 + # source://rubocop//lib/rubocop/runner.rb#226 def check_for_redundant_disables?(source); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#426 + # source://rubocop//lib/rubocop/runner.rb#427 def considered_failure?(offense); end - # source://rubocop//lib/rubocop/runner.rb#459 + # source://rubocop//lib/rubocop/runner.rb#460 def default_config(cop_name); end - # source://rubocop//lib/rubocop/runner.rb#275 + # source://rubocop//lib/rubocop/runner.rb#276 def do_inspection_loop(file); end - # source://rubocop//lib/rubocop/runner.rb#136 + # source://rubocop//lib/rubocop/runner.rb#137 def each_inspected_file(files); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#239 + # source://rubocop//lib/rubocop/runner.rb#240 def except_redundant_cop_disable_directive?; end - # source://rubocop//lib/rubocop/runner.rb#360 + # source://rubocop//lib/rubocop/runner.rb#361 def extract_ruby_sources(processed_source); end - # source://rubocop//lib/rubocop/runner.rb#248 + # source://rubocop//lib/rubocop/runner.rb#249 def file_finished(file, offenses); end - # source://rubocop//lib/rubocop/runner.rb#177 + # source://rubocop//lib/rubocop/runner.rb#178 def file_offense_cache(file); end - # source://rubocop//lib/rubocop/runner.rb#165 + # source://rubocop//lib/rubocop/runner.rb#166 def file_offenses(file); end - # source://rubocop//lib/rubocop/runner.rb#243 + # source://rubocop//lib/rubocop/runner.rb#244 def file_started(file); end - # source://rubocop//lib/rubocop/runner.rb#406 + # source://rubocop//lib/rubocop/runner.rb#407 def filter_cop_classes(cop_classes, config); end - # source://rubocop//lib/rubocop/runner.rb#107 + # source://rubocop//lib/rubocop/runner.rb#108 def find_target_files(paths); end - # source://rubocop//lib/rubocop/runner.rb#417 + # source://rubocop//lib/rubocop/runner.rb#418 def formatter_set; end - # source://rubocop//lib/rubocop/runner.rb#474 + # source://rubocop//lib/rubocop/runner.rb#475 def get_processed_source(file); end - # source://rubocop//lib/rubocop/runner.rb#345 + # source://rubocop//lib/rubocop/runner.rb#346 def inspect_file(processed_source, team = T.unsafe(nil)); end - # source://rubocop//lib/rubocop/runner.rb#118 + # source://rubocop//lib/rubocop/runner.rb#119 def inspect_files(files); end - # source://rubocop//lib/rubocop/runner.rb#306 + # source://rubocop//lib/rubocop/runner.rb#307 def iterate_until_no_changes(source, offenses_by_iteration); end - # source://rubocop//lib/rubocop/runner.rb#151 + # source://rubocop//lib/rubocop/runner.rb#152 def list_files(paths); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#455 + # source://rubocop//lib/rubocop/runner.rb#456 def mark_as_safe_by_config?(config); end - # source://rubocop//lib/rubocop/runner.rb#463 + # source://rubocop//lib/rubocop/runner.rb#464 def minimum_severity_to_fail; end - # source://rubocop//lib/rubocop/runner.rb#367 + # source://rubocop//lib/rubocop/runner.rb#368 def mobilize_team(processed_source); end - # source://rubocop//lib/rubocop/runner.rb#372 + # source://rubocop//lib/rubocop/runner.rb#373 def mobilized_cop_classes(config); end - # source://rubocop//lib/rubocop/runner.rb#435 + # source://rubocop//lib/rubocop/runner.rb#436 def offenses_to_report(offenses); end - # source://rubocop//lib/rubocop/runner.rb#155 + # source://rubocop//lib/rubocop/runner.rb#156 def process_file(file); end - # source://rubocop//lib/rubocop/runner.rb#396 + # source://rubocop//lib/rubocop/runner.rb#397 def qualify_option_cop_names; end # @yield [cop] # - # source://rubocop//lib/rubocop/runner.rb#231 + # source://rubocop//lib/rubocop/runner.rb#232 def redundant_cop_disable_directive(file); end - # source://rubocop//lib/rubocop/runner.rb#265 + # source://rubocop//lib/rubocop/runner.rb#266 def save_in_cache(cache, offenses); end # A Cop::Team instance is stateful and may change when inspecting. @@ -57091,41 +57506,41 @@ class RuboCop::Runner # otherwise dormant team that can be used for config- and option- # level caching in ResultCache. # - # source://rubocop//lib/rubocop/runner.rb#502 + # source://rubocop//lib/rubocop/runner.rb#503 def standby_team(config); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#413 + # source://rubocop//lib/rubocop/runner.rb#414 def style_guide_cops_only?(config); end # @return [Boolean] # - # source://rubocop//lib/rubocop/runner.rb#447 + # source://rubocop//lib/rubocop/runner.rb#448 def supports_safe_autocorrect?(offense); end # @yield [team] # - # source://rubocop//lib/rubocop/runner.rb#214 + # source://rubocop//lib/rubocop/runner.rb#215 def team_for_redundant_disables(file, offenses, source); end # Warms up the RuboCop cache by forking a suitable number of RuboCop # instances that each inspects its allotted group of files. # - # source://rubocop//lib/rubocop/runner.rb#94 + # source://rubocop//lib/rubocop/runner.rb#95 def warm_cache(target_files); end class << self # @return [Array<#call>] # - # source://rubocop//lib/rubocop/runner.rb#32 + # source://rubocop//lib/rubocop/runner.rb#33 def ruby_extractors; end private # @return [#call] # - # source://rubocop//lib/rubocop/runner.rb#39 + # source://rubocop//lib/rubocop/runner.rb#40 def default_ruby_extractor; end end end @@ -57148,12 +57563,12 @@ end # @api private # -# source://rubocop//lib/rubocop/runner.rb#52 +# source://rubocop//lib/rubocop/runner.rb#53 RuboCop::Runner::MAX_ITERATIONS = T.let(T.unsafe(nil), Integer) # @api private # -# source://rubocop//lib/rubocop/runner.rb#55 +# source://rubocop//lib/rubocop/runner.rb#56 RuboCop::Runner::REDUNDANT_COP_DISABLE_DIRECTIVE_RULES = T.let(T.unsafe(nil), Array) # Take a string with embedded escapes, and convert the escapes as the Ruby diff --git a/swift/.rubocop.yml b/swift/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/swift/.rubocop.yml +++ b/swift/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/terraform/.rubocop.yml b/terraform/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/terraform/.rubocop.yml +++ b/terraform/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/terraform/Dockerfile b/terraform/Dockerfile index 15623217884..474e0291842 100644 --- a/terraform/Dockerfile +++ b/terraform/Dockerfile @@ -2,13 +2,13 @@ FROM ghcr.io/dependabot/dependabot-updater-core ARG TARGETARCH # See https://github.com/hashicorp/terraform/releases or https://releases.hashicorp.com/terraform/ -ARG TERRAFORM_VERSION=1.8.0 +ARG TERRAFORM_VERSION=1.9.1 # curl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS" | grep "terraform_${TERRAFORM_VERSION}_linux_amd64.zip" -ARG TERRAFORM_AMD64_CHECKSUM=dcc4670379a22213e72faa6cb709b3391e7e54967e40288ecf591e2b83cfd39e +ARG TERRAFORM_AMD64_CHECKSUM=c3e1dade1c81fdc5e293529e480709f047c0113ea9feb8d9f35002df09ec6a34 # curl "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_SHA256SUMS" | grep "terraform_${TERRAFORM_VERSION}_linux_arm64.zip" -ARG TERRAFORM_ARM64_CHECKSUM=47cbde7184ce260160ff0355065d454ffa5628a2259ba325736dbcf740351193 +ARG TERRAFORM_ARM64_CHECKSUM=f1426fccbf2500202b37993ef6b92e1fc60d114dd32c79bfadbc843929b2c7e2 RUN cd /tmp \ && curl -o terraform-${TARGETARCH}.tar.gz https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_${TARGETARCH}.zip \ diff --git a/updater/.rubocop.yml b/updater/.rubocop.yml index b8168698e85..fc2019d46a3 100644 --- a/updater/.rubocop.yml +++ b/updater/.rubocop.yml @@ -1,4 +1 @@ inherit_from: ../.rubocop.yml - -Sorbet/TrueSigil: - Enabled: true diff --git a/updater/Gemfile.lock b/updater/Gemfile.lock index 377e075bc1b..d71f85c2920 100644 --- a/updater/Gemfile.lock +++ b/updater/Gemfile.lock @@ -1,20 +1,20 @@ PATH remote: ../bundler specs: - dependabot-bundler (0.262.0) - dependabot-common (= 0.262.0) + dependabot-bundler (0.265.0) + dependabot-common (= 0.265.0) parallel (~> 1.24) PATH remote: ../cargo specs: - dependabot-cargo (0.262.0) - dependabot-common (= 0.262.0) + dependabot-cargo (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../common specs: - dependabot-common (0.262.0) + dependabot-common (0.265.0) aws-sdk-codecommit (~> 1.28) aws-sdk-ecr (~> 1.5) bundler (>= 1.16, < 3.0.0) @@ -37,113 +37,113 @@ PATH PATH remote: ../composer specs: - dependabot-composer (0.262.0) - dependabot-common (= 0.262.0) + dependabot-composer (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../devcontainers specs: - dependabot-devcontainers (0.262.0) - dependabot-common (= 0.262.0) + dependabot-devcontainers (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../docker specs: - dependabot-docker (0.262.0) - dependabot-common (= 0.262.0) + dependabot-docker (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../elm specs: - dependabot-elm (0.262.0) - dependabot-common (= 0.262.0) + dependabot-elm (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../git_submodules specs: - dependabot-git_submodules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-git_submodules (0.265.0) + dependabot-common (= 0.265.0) parseconfig (~> 1.0, < 1.1.0) PATH remote: ../github_actions specs: - dependabot-github_actions (0.262.0) - dependabot-common (= 0.262.0) + dependabot-github_actions (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../go_modules specs: - dependabot-go_modules (0.262.0) - dependabot-common (= 0.262.0) + dependabot-go_modules (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../gradle specs: - dependabot-gradle (0.262.0) - dependabot-common (= 0.262.0) - dependabot-maven (= 0.262.0) + dependabot-gradle (0.265.0) + dependabot-common (= 0.265.0) + dependabot-maven (= 0.265.0) PATH remote: ../hex specs: - dependabot-hex (0.262.0) - dependabot-common (= 0.262.0) + dependabot-hex (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../maven specs: - dependabot-maven (0.262.0) - dependabot-common (= 0.262.0) + dependabot-maven (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../npm_and_yarn specs: - dependabot-npm_and_yarn (0.262.0) - dependabot-common (= 0.262.0) + dependabot-npm_and_yarn (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../nuget specs: - dependabot-nuget (0.262.0) - dependabot-common (= 0.262.0) + dependabot-nuget (0.265.0) + dependabot-common (= 0.265.0) rubyzip (>= 2.3.2, < 3.0) PATH remote: ../pub specs: - dependabot-pub (0.262.0) - dependabot-common (= 0.262.0) + dependabot-pub (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../python specs: - dependabot-python (0.262.0) - dependabot-common (= 0.262.0) + dependabot-python (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../silent specs: - dependabot-silent (0.262.0) - dependabot-common (= 0.262.0) + dependabot-silent (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../swift specs: - dependabot-swift (0.262.0) - dependabot-common (= 0.262.0) + dependabot-swift (0.265.0) + dependabot-common (= 0.265.0) PATH remote: ../terraform specs: - dependabot-terraform (0.262.0) - dependabot-common (= 0.262.0) + dependabot-terraform (0.265.0) + dependabot-common (= 0.265.0) GEM remote: https://rubygems.org/ specs: - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) ast (2.4.2) aws-eventstream (1.3.0) aws-partitions (1.881.0) @@ -161,7 +161,7 @@ GEM aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) base64 (0.1.1) - bigdecimal (3.1.7) + bigdecimal (3.1.8) citrus (3.0.2) commonmarker (0.23.10) concurrent-ruby (1.2.3) @@ -274,22 +274,22 @@ GEM opentelemetry-semantic_conventions opentelemetry-semantic_conventions (1.10.0) opentelemetry-api (~> 1.0) - parallel (1.24.0) - parallel_tests (4.6.1) + parallel (1.25.1) + parallel_tests (4.7.1) parallel parseconfig (1.0.8) - parser (3.3.1.0) + parser (3.3.4.0) ast (~> 2.4.1) racc psych (5.1.2) stringio - public_suffix (5.0.5) - racc (1.7.3) + public_suffix (6.0.0) + racc (1.8.0) rainbow (3.1.1) rake (13.2.1) rdoc (6.6.3.1) psych (>= 4.0.0) - regexp_parser (2.9.0) + regexp_parser (2.9.2) reline (0.5.2) io-console (~> 0.5) rest-client (2.1.0) @@ -297,32 +297,33 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.2.6) + rexml (3.3.1) + strscan rspec (3.13.0) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-expectations (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-its (1.3.0) rspec-core (>= 3.0.0) rspec-expectations (>= 3.0.0) - rspec-mocks (3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-sorbet (1.9.2) sorbet-runtime rspec-support (3.13.1) - rubocop (1.63.2) + rubocop (1.65.0) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) + regexp_parser (>= 2.4, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) @@ -333,7 +334,7 @@ GEM rubocop (~> 1.41) rubocop-factory_bot (2.25.1) rubocop (~> 1.41) - rubocop-performance (1.21.0) + rubocop-performance (1.21.1) rubocop (>= 1.48.1, < 2.0) rubocop-ast (>= 1.31.1, < 2.0) rubocop-rspec (2.29.1) @@ -365,11 +366,12 @@ GEM sorbet-runtime (0.5.11444) stackprof (0.2.25) stringio (3.1.0) + strscan (3.1.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) toml-rb (2.2.0) citrus (~> 3.0, > 3.0) - turbo_tests (2.2.3) + turbo_tests (2.2.4) parallel_tests (>= 3.3.0, < 5) rspec (>= 3.10) unf (0.1.4) @@ -377,7 +379,7 @@ GEM unf_ext (0.0.8.2) unicode-display_width (2.5.0) vcr (6.2.0) - webmock (3.23.0) + webmock (3.23.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -422,7 +424,7 @@ DEPENDENCIES rspec (~> 3.12) rspec-its (~> 1.3) rspec-sorbet (~> 1.9.2) - rubocop (~> 1.63.2) + rubocop (~> 1.65.0) rubocop-performance (~> 1.21.0) rubocop-rspec (~> 2.29.1) rubocop-sorbet (~> 0.8.1) diff --git a/updater/lib/dependabot/environment.rb b/updater/lib/dependabot/environment.rb index 7069d877547..230423dd223 100644 --- a/updater/lib/dependabot/environment.rb +++ b/updater/lib/dependabot/environment.rb @@ -1,64 +1,97 @@ -# typed: true +# typed: strict # frozen_string_literal: true +require "sorbet-runtime" + module Dependabot module Environment + extend T::Sig + extend T::Generic + + sig { returns(String) } def self.job_id - @job_id ||= environment_variable("DEPENDABOT_JOB_ID") + @job_id ||= T.let(environment_variable("DEPENDABOT_JOB_ID"), T.nilable(String)) end + sig { returns(String) } def self.job_token - @job_token ||= environment_variable("DEPENDABOT_JOB_TOKEN") + @job_token ||= T.let(environment_variable("DEPENDABOT_JOB_TOKEN"), T.nilable(String)) end + sig { returns(T::Boolean) } def self.debug_enabled? - @debug_enabled ||= job_debug_enabled? || environment_debug_enabled? + @debug_enabled ||= T.let(job_debug_enabled? || environment_debug_enabled?, T.nilable(T::Boolean)) end + sig { returns(Symbol) } def self.log_level debug_enabled? ? :debug : :info end + sig { returns(String) } def self.api_url - @api_url ||= environment_variable("DEPENDABOT_API_URL", "http://localhost:3001") + @api_url ||= T.let(environment_variable("DEPENDABOT_API_URL", "http://localhost:3001"), T.nilable(String)) end + sig { returns(String) } def self.job_path - @job_path ||= environment_variable("DEPENDABOT_JOB_PATH") + @job_path ||= T.let(environment_variable("DEPENDABOT_JOB_PATH"), T.nilable(String)) end + sig { returns(String) } def self.output_path - @output_path ||= environment_variable("DEPENDABOT_OUTPUT_PATH") + @output_path ||= T.let(environment_variable("DEPENDABOT_OUTPUT_PATH"), T.nilable(String)) end + sig { returns(T.nilable(String)) } def self.repo_contents_path - @repo_contents_path ||= environment_variable("DEPENDABOT_REPO_CONTENTS_PATH", nil) + @repo_contents_path ||= T.let(environment_variable("DEPENDABOT_REPO_CONTENTS_PATH", nil), T.nilable(String)) end + sig { returns(T::Boolean) } def self.github_actions? - @github_actions ||= environment_variable("GITHUB_ACTIONS", false) + b = T.cast(environment_variable("GITHUB_ACTIONS", false), T::Boolean) + @github_actions ||= T.let(b, T.nilable(T::Boolean)) end + sig { returns(T::Boolean) } def self.deterministic_updates? - @deterministic_updates ||= environment_variable("UPDATER_DETERMINISTIC", false) + b = T.cast(environment_variable("UPDATER_DETERMINISTIC", false), T::Boolean) + @deterministic_updates ||= T.let(b, T.nilable(T::Boolean)) end + sig { returns(T::Hash[String, T.untyped]) } def self.job_definition - @job_definition ||= JSON.parse(File.read(job_path)) + @job_definition ||= T.let(JSON.parse(File.read(job_path)), T.nilable(T::Hash[String, T.untyped])) end + sig do + type_parameters(:T) + .params(variable_name: String, default: T.any(Symbol, T.type_parameter(:T))) + .returns(T.any(String, T.type_parameter(:T))) + end private_class_method def self.environment_variable(variable_name, default = :_undefined) - return ENV.fetch(variable_name, default) unless default == :_undefined - - ENV.fetch(variable_name) do - raise ArgumentError, "Missing environment variable #{variable_name}" + case default + when :_undefined + ENV.fetch(variable_name) do + raise ArgumentError, "Missing environment variable #{variable_name}" + end + else + val = ENV.fetch(variable_name, T.cast(default, T.type_parameter(:T))) + case val + when String + val = T.must(val.casecmp("true")).zero? if [true, false].include? default + end + T.cast(val, T.type_parameter(:T)) end end + sig { returns(T::Boolean) } private_class_method def self.job_debug_enabled? !!job_definition.dig("job", "debug") end + sig { returns(T::Boolean) } private_class_method def self.environment_debug_enabled? !!environment_variable("DEPENDABOT_DEBUG", false) end diff --git a/updater/lib/dependabot/service.rb b/updater/lib/dependabot/service.rb index 1237cb01084..5e69e3d27a8 100644 --- a/updater/lib/dependabot/service.rb +++ b/updater/lib/dependabot/service.rb @@ -128,7 +128,8 @@ def capture_exception(error:, job: nil, dependency: nil, dependency_group: nil, ErrorAttributes::PACKAGE_MANAGER => job&.package_manager, ErrorAttributes::JOB_ID => job&.id, ErrorAttributes::DEPENDENCIES => dependency&.name || job&.dependencies, - ErrorAttributes::DEPENDENCY_GROUPS => dependency_group&.name || job&.dependency_groups + ErrorAttributes::DEPENDENCY_GROUPS => dependency_group&.name || job&.dependency_groups, + ErrorAttributes::SECURITY_UPDATE => job&.security_updates_only? }.compact record_update_job_unknown_error(error_type: "unknown_error", error_details: error_details) end diff --git a/updater/spec/dependabot/file_fetcher_command_spec.rb b/updater/spec/dependabot/file_fetcher_command_spec.rb index 2d855cef072..e97647bfde0 100644 --- a/updater/spec/dependabot/file_fetcher_command_spec.rb +++ b/updater/spec/dependabot/file_fetcher_command_spec.rb @@ -163,7 +163,8 @@ Dependabot::ErrorAttributes::CLASS => "StandardError", Dependabot::ErrorAttributes::PACKAGE_MANAGER => "bundler", Dependabot::ErrorAttributes::JOB_ID => "123123", - Dependabot::ErrorAttributes::DEPENDENCY_GROUPS => [] + Dependabot::ErrorAttributes::DEPENDENCY_GROUPS => [], + Dependabot::ErrorAttributes::SECURITY_UPDATE => false } ) expect(api_client).to receive(:mark_job_as_processed) diff --git a/updater/spec/dependabot/service_spec.rb b/updater/spec/dependabot/service_spec.rb index 2352dd82123..a0f9572d71b 100644 --- a/updater/spec/dependabot/service_spec.rb +++ b/updater/spec/dependabot/service_spec.rb @@ -371,6 +371,25 @@ ) end + it "extracts information from a security job if provided" do + job = OpenStruct.new(id: 1234, package_manager: "npm_and_yarn", repo_private?: false, repo_owner: "foo", + security_updates_only?: true) + service.capture_exception(error: error, job: job) + + expect(mock_client) + .to have_received(:record_update_job_unknown_error) + .with( + error_type: "unknown_error", + error_details: hash_including( + Dependabot::ErrorAttributes::CLASS => "Dependabot::DependabotError", + Dependabot::ErrorAttributes::MESSAGE => "Something went wrong", + Dependabot::ErrorAttributes::JOB_ID => job.id, + Dependabot::ErrorAttributes::PACKAGE_MANAGER => job.package_manager, + Dependabot::ErrorAttributes::SECURITY_UPDATE => true + ) + ) + end + it "extracts information from a dependency_group if provided" do dependency_group = OpenStruct.new(name: "all-the-things") allow(dependency_group).to receive(:is_a?).with(Dependabot::DependencyGroup).and_return(true)