diff --git a/maven/lib/dependabot/maven/shared/base_version_finder.rb b/maven/lib/dependabot/maven/shared/base_version_finder.rb
new file mode 100644
index 0000000000..dcba0d5fa3
--- /dev/null
+++ b/maven/lib/dependabot/maven/shared/base_version_finder.rb
@@ -0,0 +1,105 @@
+# typed: strong
+# frozen_string_literal: true
+
+require "sorbet-runtime"
+require "dependabot/maven/shared/shared_version_finder"
+
+module Dependabot
+ module Maven
+ module Shared
+ # Intermediate class for ecosystems (Maven, SBT) that use a package_details-based
+ # release pipeline with HEAD-check verification. Gradle uses its own filter chain
+ # and inherits directly from SharedVersionFinder.
+ class BaseVersionFinder < SharedVersionFinder
+ extend T::Sig
+ extend T::Helpers
+
+ abstract!
+
+ sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
+ def releases
+ (package_details&.releases || []).reverse
+ end
+
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
+ def latest_version_details
+ release = fetch_latest_release
+ release&.version ? { version: release.version, source_url: release.url } : nil
+ end
+
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
+ def lowest_security_fix_version_details
+ release = fetch_lowest_security_fix_release
+ release&.version ? { version: release.version, source_url: release.url } : nil
+ end
+
+ protected
+
+ sig do
+ params(language_version: T.nilable(T.any(String, Dependabot::Version)))
+ .returns(T.nilable(Dependabot::Version))
+ end
+ def fetch_latest_version(language_version: nil)
+ fetch_latest_release(language_version: language_version)&.version
+ end
+
+ sig do
+ params(language_version: T.nilable(T.any(String, Dependabot::Version)))
+ .returns(T.nilable(Dependabot::Version))
+ end
+ def fetch_latest_version_with_no_unlock(language_version:)
+ fetch_latest_release(language_version: language_version)&.version
+ end
+
+ sig do
+ params(language_version: T.nilable(T.any(String, Dependabot::Version)))
+ .returns(T.nilable(Dependabot::Version))
+ end
+ def fetch_lowest_security_fix_version(language_version: nil)
+ fetch_lowest_security_fix_release(language_version: language_version)&.version
+ end
+
+ sig do
+ params(language_version: T.nilable(T.any(String, Dependabot::Version)))
+ .returns(T.nilable(Dependabot::Package::PackageRelease))
+ end
+ def fetch_latest_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument
+ possible_releases = filter_prerelease_versions(releases)
+ possible_releases = filter_date_based_versions(possible_releases)
+ possible_releases = filter_version_types(possible_releases)
+ possible_releases = filter_ignored_versions(possible_releases)
+ possible_releases = filter_by_cooldown(possible_releases)
+ possible_releases_reverse = possible_releases.reverse
+
+ possible_releases_reverse.find do |r|
+ released?(r.version)
+ end
+ end
+
+ sig do
+ params(language_version: T.nilable(T.any(String, Dependabot::Version)))
+ .returns(T.nilable(Dependabot::Package::PackageRelease))
+ end
+ def fetch_lowest_security_fix_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument
+ possible_releases = filter_prerelease_versions(releases)
+ possible_releases = filter_date_based_versions(possible_releases)
+ possible_releases = filter_version_types(possible_releases)
+ possible_releases = Dependabot::UpdateCheckers::VersionFilters
+ .filter_vulnerable_versions(
+ possible_releases,
+ security_advisories
+ )
+ possible_releases = filter_ignored_versions(possible_releases)
+ possible_releases = filter_lower_versions(possible_releases)
+
+ possible_releases.find { |r| released?(r.version) }
+ end
+
+ private
+
+ sig { abstract.params(version: Dependabot::Version).returns(T::Boolean) }
+ def released?(version); end
+ end
+ end
+ end
+end
diff --git a/maven/lib/dependabot/maven/shared/shared_version_finder.rb b/maven/lib/dependabot/maven/shared/shared_version_finder.rb
index d2062d27f4..7f1630b0dd 100644
--- a/maven/lib/dependabot/maven/shared/shared_version_finder.rb
+++ b/maven/lib/dependabot/maven/shared/shared_version_finder.rb
@@ -11,6 +11,9 @@ module Maven
module Shared
class SharedVersionFinder < Dependabot::Package::PackageLatestVersionFinder
extend T::Sig
+ extend T::Helpers
+
+ abstract!
# Regex to match common Maven release qualifiers that indicate stable releases.
# See https://github.com/apache/maven/blob/848fbb4bf2d427b72bdb2471c22fced7ebd9a7a1/maven-artifact/src/main/java/org/apache/maven/artifact/versioning/ComparableVersion.java#L315-L320
@@ -123,6 +126,11 @@ def version_class
dependency.version_class
end
+ sig { returns(T::Boolean) }
+ def cooldown_enabled?
+ true
+ end
+
private
# Determines whether two versions have compatible suffixes.
@@ -405,11 +413,6 @@ def extract_suffix_from_part(part)
suffix.empty? ? nil : suffix
end
-
- sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) }
- def package_details
- raise NotImplementedError, "Subclasses must implement `package_details`"
- end
end
end
end
diff --git a/maven/lib/dependabot/maven/update_checker/version_finder.rb b/maven/lib/dependabot/maven/update_checker/version_finder.rb
index c3d291aa87..be44857a49 100644
--- a/maven/lib/dependabot/maven/update_checker/version_finder.rb
+++ b/maven/lib/dependabot/maven/update_checker/version_finder.rb
@@ -6,13 +6,13 @@
require "dependabot/update_checkers/version_filters"
require "dependabot/maven/package/package_details_fetcher"
require "dependabot/maven/update_checker"
-require "dependabot/maven/shared/shared_version_finder"
+require "dependabot/maven/shared/base_version_finder"
require "sorbet-runtime"
module Dependabot
module Maven
class UpdateChecker
- class VersionFinder < Dependabot::Maven::Shared::SharedVersionFinder
+ class VersionFinder < Dependabot::Maven::Shared::BaseVersionFinder
extend T::Sig
sig do
@@ -52,92 +52,13 @@ def package_details
@package_details ||= package_details_fetcher.fetch
end
- sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
- def releases
- (package_details&.releases || []).reverse
- end
-
- sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
- def latest_version_details
- release = fetch_latest_release
- release&.version ? { version: release.version, source_url: release.url } : nil
- end
-
- sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
- def lowest_security_fix_version_details
- release = fetch_lowest_security_fix_release
- release&.version ? { version: release.version, source_url: release.url } : nil
- end
-
- protected
-
- sig { returns(T::Boolean) }
- def cooldown_enabled?
- true
- end
-
- sig do
- params(language_version: T.nilable(T.any(String, Dependabot::Version)))
- .returns(T.nilable(Dependabot::Version))
- end
- def fetch_latest_version(language_version: nil)
- fetch_latest_release(language_version: language_version)&.version
- end
-
- sig do
- params(language_version: T.nilable(T.any(String, Dependabot::Version)))
- .returns(T.nilable(Dependabot::Version))
- end
- def fetch_latest_version_with_no_unlock(language_version:)
- fetch_latest_release(language_version: language_version)&.version
- end
-
- sig do
- params(language_version: T.nilable(T.any(String, Dependabot::Version)))
- .returns(T.nilable(Dependabot::Version))
- end
- def fetch_lowest_security_fix_version(language_version: nil)
- fetch_lowest_security_fix_release(language_version: language_version)&.version
- end
-
- sig do
- params(language_version: T.nilable(T.any(String, Dependabot::Version)))
- .returns(T.nilable(Dependabot::Package::PackageRelease))
- end
- def fetch_latest_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument
- possible_releases = filter_prerelease_versions(releases)
- possible_releases = filter_date_based_versions(possible_releases)
- possible_releases = filter_version_types(possible_releases)
- possible_releases = filter_ignored_versions(possible_releases)
- possible_releases = filter_by_cooldown(possible_releases)
- possible_releases_reverse = possible_releases.reverse
-
- possible_releases_reverse.find do |r|
- package_details_fetcher.released?(r.version)
- end
- end
-
- sig do
- params(language_version: T.nilable(T.any(String, Dependabot::Version)))
- .returns(T.nilable(Dependabot::Package::PackageRelease))
- end
- def fetch_lowest_security_fix_release(language_version: nil) # rubocop:disable Lint/UnusedMethodArgument
- possible_releases = filter_prerelease_versions(releases)
- possible_releases = filter_date_based_versions(possible_releases)
- possible_releases = filter_version_types(possible_releases)
- possible_releases = Dependabot::UpdateCheckers::VersionFilters
- .filter_vulnerable_versions(
- possible_releases,
- security_advisories
- )
- possible_releases = filter_ignored_versions(possible_releases)
- possible_releases = filter_lower_versions(possible_releases)
+ private
- possible_releases.find { |r| package_details_fetcher.released?(r.version) }
+ sig { override.params(version: Dependabot::Version).returns(T::Boolean) }
+ def released?(version)
+ package_details_fetcher.released?(version)
end
- private
-
sig { returns(Package::PackageDetailsFetcher) }
def package_details_fetcher
@package_details_fetcher ||= Package::PackageDetailsFetcher.new(
diff --git a/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb b/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb
index ca4c1d61c5..1b40ba8de0 100644
--- a/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb
+++ b/maven/spec/dependabot/maven/shared/shared_version_finder_spec.rb
@@ -10,8 +10,17 @@
require "dependabot/package/package_release"
RSpec.describe Dependabot::Maven::Shared::SharedVersionFinder do
+ # SharedVersionFinder is abstract, so use a concrete subclass for testing
+ let(:concrete_class) do
+ Class.new(described_class) do
+ def package_details
+ nil
+ end
+ end
+ end
+
let(:finder) do
- described_class.new(
+ concrete_class.new(
dependency: dependency,
dependency_files: dependency_files,
credentials: credentials,
diff --git a/sbt/lib/dependabot/sbt/package/package_details_fetcher.rb b/sbt/lib/dependabot/sbt/package/package_details_fetcher.rb
new file mode 100644
index 0000000000..6136665f6d
--- /dev/null
+++ b/sbt/lib/dependabot/sbt/package/package_details_fetcher.rb
@@ -0,0 +1,175 @@
+# typed: strict
+# frozen_string_literal: true
+
+require "nokogiri"
+require "sorbet-runtime"
+require "dependabot/registry_client"
+require "dependabot/package/package_release"
+require "dependabot/package/package_details"
+require "dependabot/sbt/file_parser/repositories_finder"
+require "dependabot/sbt/version"
+require "dependabot/sbt/requirement"
+require "dependabot/maven/shared/shared_package_details_fetcher"
+require "dependabot/maven/utils/auth_headers_finder"
+
+module Dependabot
+ module Sbt
+ module Package
+ class PackageDetailsFetcher < Dependabot::Maven::Shared::SharedPackageDetailsFetcher
+ extend T::Sig
+
+ sig do
+ params(
+ dependency: Dependabot::Dependency,
+ dependency_files: T::Array[Dependabot::DependencyFile],
+ credentials: T::Array[Dependabot::Credential]
+ ).void
+ end
+ def initialize(dependency:, dependency_files:, credentials:)
+ @dependency = T.let(dependency, Dependabot::Dependency)
+ @dependency_files = T.let(dependency_files, T::Array[Dependabot::DependencyFile])
+ @credentials = T.let(credentials, T::Array[Dependabot::Credential])
+
+ @repositories_cache = T.let(nil, T.nilable(T::Array[T::Hash[String, T.untyped]]))
+ @repository_finder = T.let(nil, T.nilable(Sbt::FileParser::RepositoriesFinder))
+ @package_details = T.let(nil, T.nilable(Dependabot::Package::PackageDetails))
+ end
+
+ sig { override.returns(Dependabot::Dependency) }
+ attr_reader :dependency
+
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
+ attr_reader :dependency_files
+
+ sig { override.returns(T::Array[Dependabot::Credential]) }
+ attr_reader :credentials
+
+ sig { returns(Dependabot::Package::PackageDetails) }
+ def fetch
+ return @package_details if @package_details
+
+ releases = versions.map do |version_details|
+ Dependabot::Package::PackageRelease.new(
+ version: version_details.fetch(:version),
+ released_at: version_details.fetch(:release_date, nil),
+ url: version_details.fetch(:source_url)
+ )
+ end
+
+ @package_details = Dependabot::Package::PackageDetails.new(
+ dependency: dependency,
+ releases: releases
+ )
+
+ @package_details
+ end
+
+ sig { returns(T::Array[Dependabot::Package::PackageRelease]) }
+ def releases
+ fetch.releases
+ end
+
+ # Assembles the list of Maven repositories to search: credential repos + SBT resolver repos.
+ sig { override.returns(T::Array[T::Hash[String, T.untyped]]) }
+ def repositories
+ return @repositories_cache if @repositories_cache
+
+ @repositories_cache = credentials_repository_details
+
+ sbt_repository_details.each do |repo|
+ @repositories_cache << repo unless @repositories_cache.any? do |r|
+ r[URL_KEY] == repo[URL_KEY]
+ end
+ end
+
+ @repositories_cache
+ end
+
+ sig { override.returns(String) }
+ def central_repo_url
+ Sbt::FileParser::RepositoriesFinder::CENTRAL_REPO_URL
+ end
+
+ # Override to always use "jar" for the HEAD check. The SBT parser sets
+ # packaging_type to "cross-versioned" as metadata for %% dependencies, but the
+ # actual Maven artifact is always a .jar file.
+ sig { override.params(repository_url: String, version: Dependabot::Version).returns(String) }
+ def dependency_files_url(repository_url, version)
+ _, artifact_id = dependency_parts
+ base_url = dependency_base_url(repository_url)
+
+ "#{base_url}/#{version}/#{artifact_id}-#{version}.jar"
+ end
+
+ # Override to handle SBT plugin cross-versioning.
+ # SBT plugins are published with a double-suffix: artifact_scalaVersion_sbtVersion
+ # e.g., sbt-jmh_2.12_1.0 for SBT 1.x plugins.
+ sig { override.returns([String, String]) }
+ def dependency_parts
+ @dependency_parts = T.let(@dependency_parts, T.nilable([String, String]))
+ return @dependency_parts if @dependency_parts
+
+ group_id, artifact_id = dependency.name.split(":")
+ group_path = T.must(group_id).tr(".", "/")
+
+ artifact_id = "#{artifact_id}_#{plugin_scala_version}_#{sbt_binary_version}" if sbt_plugin?
+
+ @dependency_parts = [group_path, T.must(artifact_id)]
+ end
+
+ private
+
+ sig { returns(Sbt::FileParser::RepositoriesFinder) }
+ def repository_finder
+ @repository_finder ||= Sbt::FileParser::RepositoriesFinder.new(
+ dependency_files: dependency_files,
+ credentials: credentials
+ )
+ end
+
+ # Returns the repository details discovered from SBT build files.
+ sig { returns(T::Array[T::Hash[String, T.untyped]]) }
+ def sbt_repository_details
+ repository_finder
+ .repository_urls
+ .map do |url|
+ { URL_KEY => url, AUTH_HEADERS_KEY => auth_headers(url) }
+ end
+ end
+
+ # SBT plugins are identified by having "plugins" in their groups.
+ sig { returns(T::Boolean) }
+ def sbt_plugin?
+ dependency.requirements.any? { |req| req.fetch(:groups, []).include?("plugins") }
+ end
+
+ # SBT 1.x plugins use Scala 2.12; SBT 2.x plugins use Scala 3.
+ sig { returns(String) }
+ def plugin_scala_version
+ sbt_major_version >= 2 ? "3" : "2.12"
+ end
+
+ # SBT binary version for plugin cross-versioning: "1.0" for SBT 1.x, "2.0" for SBT 2.x.
+ sig { returns(String) }
+ def sbt_binary_version
+ "#{sbt_major_version}.0"
+ end
+
+ sig { returns(Integer) }
+ def sbt_major_version
+ build_properties = dependency_files.find { |f| f.name.end_with?("build.properties") }
+ return 1 unless build_properties&.content
+
+ T.must(build_properties.content).each_line do |line|
+ match = line.strip.match(Sbt::FileParser::SBT_VERSION_REGEX)
+ next unless match
+
+ return T.must(match[:version]).strip.split(".").first.to_i
+ end
+
+ 1
+ end
+ end
+ end
+ end
+end
diff --git a/sbt/lib/dependabot/sbt/update_checker.rb b/sbt/lib/dependabot/sbt/update_checker.rb
index 014773a409..4fa92f748f 100644
--- a/sbt/lib/dependabot/sbt/update_checker.rb
+++ b/sbt/lib/dependabot/sbt/update_checker.rb
@@ -1,54 +1,175 @@
-# typed: strong
+# typed: strict
# frozen_string_literal: true
+require "sorbet-runtime"
require "dependabot/update_checkers"
require "dependabot/update_checkers/base"
+require "dependabot/sbt/file_parser"
+require "dependabot/sbt/file_parser/property_value_finder"
module Dependabot
module Sbt
class UpdateChecker < Dependabot::UpdateCheckers::Base
extend T::Sig
- sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
+ require_relative "update_checker/requirements_updater"
+ require_relative "update_checker/version_finder"
+
+ sig { override.returns(T.nilable(Dependabot::Version)) }
def latest_version
- # TODO: Implement logic to find the latest version
- # This should check the package registry/repository for updates
- nil
+ latest_version_details&.fetch(:version)
end
- sig { override.returns(T.nilable(T.any(String, Gem::Version))) }
+ sig { override.returns(T.nilable(Dependabot::Version)) }
def latest_resolvable_version
- # TODO: Implement logic to find the latest resolvable version
- # This might be the same as latest_version for simple ecosystems
+ # SBT has no transitive dependency resolution constraints in manifest files.
+ # Return nil if version comes from a multi-dependency property (needs full unlock).
+ return nil if version_comes_from_multi_dependency_property?
+
latest_version
end
- sig { override.returns(T.nilable(String)) }
+ sig { override.returns(T.nilable(Dependabot::Version)) }
+ def lowest_security_fix_version
+ lowest_security_fix_version_details&.fetch(:version)
+ end
+
+ sig { override.returns(T.nilable(Dependabot::Version)) }
+ def lowest_resolvable_security_fix_version
+ return nil if version_comes_from_multi_dependency_property?
+
+ lowest_security_fix_version
+ end
+
+ sig { override.returns(T.nilable(Dependabot::Version)) }
def latest_resolvable_version_with_no_unlock
- # TODO: Implement logic for version resolution without unlocking
- dependency.version
+ # SBT uses exact versions in build files, so no constraint resolution needed.
+ nil
end
sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
def updated_requirements
- # TODO: Implement logic to update requirements
- # Return updated requirement hashes
- dependency.requirements
+ property_names =
+ declarations_using_a_property
+ .filter_map { |req| req.dig(:metadata, :property_name) }
+
+ RequirementsUpdater.new(
+ requirements: dependency.requirements,
+ latest_version: preferred_resolvable_version&.to_s,
+ source_url: preferred_version_details&.fetch(:source_url),
+ properties_to_update: property_names
+ ).updated_requirements
+ end
+
+ sig { override.returns(T::Boolean) }
+ def requirements_unlocked_or_can_be?
+ # If any requirement uses a val we couldn't resolve, we can't update
+ !dependency.version&.include?("${")
end
private
sig { override.returns(T::Boolean) }
def latest_version_resolvable_with_full_unlock?
- # TODO: Implement resolvability check
+ return false unless version_comes_from_multi_dependency_property?
+
+ # Full unlock via property updates can be added later
false
end
sig { override.returns(T::Array[Dependabot::Dependency]) }
def updated_dependencies_after_full_unlock
- # TODO: Return updated dependencies if full unlock is needed
[]
end
+
+ sig { override.returns(T::Boolean) }
+ def numeric_version_up_to_date?
+ return false unless version_class.correct?(dependency.version)
+
+ super
+ end
+
+ sig { override.params(requirements_to_unlock: T.nilable(Symbol)).returns(T::Boolean) }
+ def numeric_version_can_update?(requirements_to_unlock:)
+ return false unless version_class.correct?(dependency.version)
+
+ super
+ end
+
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
+ def preferred_version_details
+ return lowest_security_fix_version_details if vulnerable?
+
+ latest_version_details
+ end
+
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
+ def latest_version_details
+ version_finder.latest_version_details
+ end
+
+ sig { returns(T.nilable(T::Hash[T.untyped, T.untyped])) }
+ def lowest_security_fix_version_details
+ version_finder.lowest_security_fix_version_details
+ end
+
+ sig { returns(VersionFinder) }
+ def version_finder
+ @version_finder ||= T.let(
+ VersionFinder.new(
+ dependency: dependency,
+ dependency_files: dependency_files,
+ credentials: credentials,
+ ignored_versions: ignored_versions,
+ cooldown_options: update_cooldown,
+ raise_on_ignored: raise_on_ignored,
+ security_advisories: security_advisories
+ ),
+ T.nilable(VersionFinder)
+ )
+ end
+
+ sig { returns(T::Boolean) }
+ def version_comes_from_multi_dependency_property?
+ declarations_using_a_property.any? do |requirement|
+ property_name = requirement.dig(:metadata, :property_name)
+ property_source = requirement.dig(:metadata, :property_source)
+
+ next false unless property_name
+
+ all_property_based_dependencies.any? do |dep|
+ next false if dep.name == dependency.name
+
+ dep.requirements.any? do |req|
+ next unless req.dig(:metadata, :property_name) == property_name
+
+ req.dig(:metadata, :property_source) == property_source
+ end
+ end
+ end
+ end
+
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
+ def declarations_using_a_property
+ @declarations_using_a_property ||= T.let(
+ dependency.requirements
+ .select { |req| req.dig(:metadata, :property_name) },
+ T.nilable(T::Array[T::Hash[Symbol, T.untyped]])
+ )
+ end
+
+ sig { returns(T::Array[Dependabot::Dependency]) }
+ def all_property_based_dependencies
+ @all_property_based_dependencies ||= T.let(
+ Sbt::FileParser.new(
+ dependency_files: dependency_files,
+ source: nil
+ ).parse.select do |dep|
+ dep.requirements.any? { |req| req.dig(:metadata, :property_name) }
+ end,
+ T.nilable(T::Array[Dependabot::Dependency])
+ )
+ end
end
end
end
diff --git a/sbt/lib/dependabot/sbt/update_checker/requirements_updater.rb b/sbt/lib/dependabot/sbt/update_checker/requirements_updater.rb
new file mode 100644
index 0000000000..e0d831fc2e
--- /dev/null
+++ b/sbt/lib/dependabot/sbt/update_checker/requirements_updater.rb
@@ -0,0 +1,98 @@
+# typed: strict
+# frozen_string_literal: true
+
+require "sorbet-runtime"
+require "dependabot/requirements_updater/base"
+require "dependabot/sbt/update_checker"
+require "dependabot/sbt/version"
+require "dependabot/sbt/requirement"
+
+module Dependabot
+ module Sbt
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
+ class RequirementsUpdater
+ extend T::Sig
+ extend T::Generic
+
+ Version = type_member { { fixed: Dependabot::Sbt::Version } }
+ Requirement = type_member { { fixed: Dependabot::Sbt::Requirement } }
+
+ include Dependabot::RequirementsUpdater::Base
+
+ sig do
+ params(
+ requirements: T::Array[T::Hash[Symbol, T.untyped]],
+ latest_version: T.nilable(T.any(Version, String)),
+ source_url: T.nilable(String),
+ properties_to_update: T::Array[String]
+ ).void
+ end
+ def initialize(
+ requirements:,
+ latest_version:,
+ source_url:,
+ properties_to_update:
+ )
+ @requirements = requirements
+ @source_url = source_url
+ @properties_to_update = properties_to_update
+ return unless latest_version
+
+ @latest_version = T.let(version_class.new(latest_version), Version)
+ end
+
+ sig { override.returns(T::Array[T::Hash[Symbol, T.untyped]]) }
+ def updated_requirements
+ return requirements unless latest_version
+
+ requirements.map do |req|
+ next req if req.fetch(:requirement).nil?
+ next req if req.fetch(:requirement).include?(",")
+
+ property_name = req.dig(:metadata, :property_name)
+ next req if property_name && !properties_to_update.include?(property_name)
+
+ new_req = update_requirement(req[:requirement])
+ req.merge(requirement: new_req, source: updated_source)
+ end
+ end
+
+ private
+
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
+ attr_reader :requirements
+
+ sig { returns(T.nilable(Version)) }
+ attr_reader :latest_version
+
+ sig { returns(T.nilable(String)) }
+ attr_reader :source_url
+
+ sig { returns(T::Array[String]) }
+ attr_reader :properties_to_update
+
+ sig { params(req_string: String).returns(String) }
+ def update_requirement(req_string)
+ old_version = requirement_class.new(req_string)
+ .requirements.first.last
+ req_string.gsub(old_version.to_s, T.must(latest_version).to_s)
+ end
+
+ sig { override.returns(T::Class[Version]) }
+ def version_class
+ Sbt::Version
+ end
+
+ sig { override.returns(T::Class[Requirement]) }
+ def requirement_class
+ Sbt::Requirement
+ end
+
+ sig { returns(T::Hash[Symbol, T.untyped]) }
+ def updated_source
+ { type: "maven_repo", url: source_url }
+ end
+ end
+ end
+ end
+end
diff --git a/sbt/lib/dependabot/sbt/update_checker/version_finder.rb b/sbt/lib/dependabot/sbt/update_checker/version_finder.rb
new file mode 100644
index 0000000000..161c5ce184
--- /dev/null
+++ b/sbt/lib/dependabot/sbt/update_checker/version_finder.rb
@@ -0,0 +1,76 @@
+# typed: strong
+# frozen_string_literal: true
+
+require "sorbet-runtime"
+require "dependabot/package/package_latest_version_finder"
+require "dependabot/package/release_cooldown_options"
+require "dependabot/update_checkers/version_filters"
+require "dependabot/maven/shared/base_version_finder"
+require "dependabot/sbt/update_checker"
+require "dependabot/sbt/package/package_details_fetcher"
+
+module Dependabot
+ module Sbt
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
+ class VersionFinder < Dependabot::Maven::Shared::BaseVersionFinder
+ extend T::Sig
+
+ 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],
+ cooldown_options: T.nilable(Dependabot::Package::ReleaseCooldownOptions),
+ raise_on_ignored: T::Boolean
+ ).void
+ end
+ def initialize(
+ dependency:,
+ dependency_files:,
+ credentials:,
+ ignored_versions:,
+ security_advisories:,
+ cooldown_options: nil,
+ raise_on_ignored: false
+ )
+ @package_details_fetcher = T.let(nil, T.nilable(Package::PackageDetailsFetcher))
+ @package_details = T.let(nil, T.nilable(Dependabot::Package::PackageDetails))
+
+ super(
+ dependency: dependency,
+ dependency_files: dependency_files,
+ credentials: credentials,
+ ignored_versions: ignored_versions,
+ security_advisories: security_advisories,
+ cooldown_options: cooldown_options,
+ raise_on_ignored: raise_on_ignored,
+ options: {}
+ )
+ end
+
+ sig { override.returns(T.nilable(Dependabot::Package::PackageDetails)) }
+ def package_details
+ @package_details ||= package_details_fetcher.fetch
+ end
+
+ private
+
+ sig { override.params(version: Dependabot::Version).returns(T::Boolean) }
+ def released?(version)
+ package_details_fetcher.released?(version)
+ end
+
+ sig { returns(Package::PackageDetailsFetcher) }
+ def package_details_fetcher
+ @package_details_fetcher ||= Package::PackageDetailsFetcher.new(
+ dependency: dependency,
+ dependency_files: dependency_files,
+ credentials: credentials
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/sbt/spec/dependabot/sbt/package/package_details_fetcher_spec.rb b/sbt/spec/dependabot/sbt/package/package_details_fetcher_spec.rb
new file mode 100644
index 0000000000..7c53adaf5a
--- /dev/null
+++ b/sbt/spec/dependabot/sbt/package/package_details_fetcher_spec.rb
@@ -0,0 +1,203 @@
+# typed: false
+# frozen_string_literal: true
+
+require "spec_helper"
+require "dependabot/credential"
+require "dependabot/dependency"
+require "dependabot/dependency_file"
+require "dependabot/sbt/package/package_details_fetcher"
+
+RSpec.describe Dependabot::Sbt::Package::PackageDetailsFetcher do
+ let(:fetcher) do
+ described_class.new(
+ dependency: dependency,
+ dependency_files: dependency_files,
+ credentials: credentials
+ )
+ end
+
+ let(:credentials) { [] }
+ let(:dependency_files) { [build_sbt] }
+ let(:build_sbt) do
+ Dependabot::DependencyFile.new(
+ name: "build.sbt",
+ content: fixture("buildfiles", "basic_build.sbt")
+ )
+ end
+
+ let(:dependency) do
+ Dependabot::Dependency.new(
+ name: dependency_name,
+ version: dependency_version,
+ requirements: dependency_requirements,
+ package_manager: "sbt"
+ )
+ end
+ let(:dependency_name) { "com.google.guava:guava" }
+ let(:dependency_version) { "33.0.0-jre" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "33.0.0-jre",
+ groups: [],
+ source: nil,
+ metadata: nil
+ }]
+ end
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/maven-metadata.xml"
+ end
+ let(:maven_central_base_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "guava.xml")
+ end
+
+ before do
+ stub_request(:get, maven_central_metadata_url)
+ .to_return(status: 200, body: maven_central_releases)
+ stub_request(:get, maven_central_base_url)
+ .to_return(status: 404)
+ end
+
+ describe "#fetch" do
+ subject(:package_details) { fetcher.fetch }
+
+ it "returns a PackageDetails object" do
+ expect(package_details).to be_a(Dependabot::Package::PackageDetails)
+ end
+
+ it "includes the correct releases" do
+ versions = package_details.releases.map { |r| r.version.to_s }
+ expect(versions).to include("33.0.0-jre", "33.4.0-jre")
+ end
+
+ it "includes all versions from metadata including android variants" do
+ versions = package_details.releases.map { |r| r.version.to_s }
+ expect(versions).to include("33.0.0-android")
+ end
+
+ it "returns releases sorted by version in descending order" do
+ versions = package_details.releases.map(&:version)
+ expect(versions).to eq(versions.sort.reverse)
+ end
+ end
+
+ describe "#releases" do
+ subject(:releases) { fetcher.releases }
+
+ it "returns PackageRelease objects" do
+ expect(releases.first).to be_a(Dependabot::Package::PackageRelease)
+ end
+
+ it "includes source_url in releases" do
+ expect(releases.first.url).to eq("https://repo.maven.apache.org/maven2")
+ end
+ end
+
+ describe "#repositories" do
+ subject(:repositories) { fetcher.repositories }
+
+ it "includes Maven Central by default" do
+ urls = repositories.map { |r| r["url"] }
+ expect(urls).to include("https://repo.maven.apache.org/maven2")
+ end
+
+ context "with custom resolvers in build file" do
+ let(:build_sbt) do
+ Dependabot::DependencyFile.new(
+ name: "build.sbt",
+ content: fixture("buildfiles", "custom_repos_build.sbt")
+ )
+ end
+
+ it "includes custom resolver URLs" do
+ urls = repositories.map { |r| r["url"] }
+ expect(urls).to include("https://oss.sonatype.org/content/repositories/releases")
+ expect(urls).to include("https://repo.artima.com/releases")
+ end
+ end
+
+ context "with credentials" do
+ let(:credentials) do
+ [
+ Dependabot::Credential.new(
+ {
+ "type" => "maven_repository",
+ "url" => "https://private.repo.example.com/maven2"
+ }
+ )
+ ]
+ end
+
+ it "includes credential-based repositories" do
+ urls = repositories.map { |r| r["url"] }
+ expect(urls).to include("https://private.repo.example.com/maven2")
+ end
+ end
+ end
+
+ describe "#released?" do
+ subject { fetcher.released?(version) }
+
+ let(:version) { Dependabot::Sbt::Version.new("33.4.0-jre") }
+
+ before do
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar"
+ ).to_return(status: 200)
+ end
+
+ it { is_expected.to be true }
+
+ context "when the artifact is not found" do
+ before do
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar"
+ ).to_return(status: 404)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe "cross-versioned artifact resolution" do
+ let(:dependency_name) { "org.typelevel:cats-core_2.13" }
+ let(:dependency_version) { "2.10.0" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "2.10.0",
+ groups: [],
+ source: nil,
+ metadata: { packaging_type: "cross-versioned" }
+ }]
+ end
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13/maven-metadata.xml"
+ end
+ let(:maven_central_base_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "cats_core_2.13.xml")
+ end
+
+ it "correctly resolves the artifact path with Scala version suffix" do
+ details = fetcher.fetch
+ versions = details.releases.map { |r| r.version.to_s }
+ expect(versions).to include("2.10.0", "2.11.0", "2.12.0")
+ end
+ end
+end
diff --git a/sbt/spec/dependabot/sbt/update_checker/requirements_updater_spec.rb b/sbt/spec/dependabot/sbt/update_checker/requirements_updater_spec.rb
new file mode 100644
index 0000000000..8f333ef656
--- /dev/null
+++ b/sbt/spec/dependabot/sbt/update_checker/requirements_updater_spec.rb
@@ -0,0 +1,136 @@
+# typed: false
+# frozen_string_literal: true
+
+require "spec_helper"
+require "dependabot/sbt/update_checker/requirements_updater"
+
+RSpec.describe Dependabot::Sbt::UpdateChecker::RequirementsUpdater do
+ let(:updater) do
+ described_class.new(
+ requirements: requirements,
+ latest_version: latest_version,
+ source_url: "https://repo.maven.apache.org/maven2",
+ properties_to_update: properties_to_update
+ )
+ end
+
+ let(:version_class) { Dependabot::Sbt::Version }
+ let(:requirements) { [sbt_req] }
+ let(:properties_to_update) { [] }
+
+ let(:sbt_req) do
+ {
+ file: "build.sbt",
+ requirement: sbt_req_string,
+ groups: [],
+ source: nil,
+ metadata: nil
+ }
+ end
+ let(:sbt_req_string) { "2.10.0" }
+ let(:latest_version) { version_class.new("2.12.0") }
+
+ describe "#updated_requirements" do
+ subject(:updated_requirements) { updater.updated_requirements }
+
+ specify { expect(updated_requirements.count).to eq(1) }
+
+ context "when there is no latest version" do
+ let(:latest_version) { nil }
+
+ it "returns the existing requirements unchanged" do
+ expect(updated_requirements.first).to eq(sbt_req)
+ end
+ end
+
+ context "when there is a latest version" do
+ let(:latest_version) { version_class.new("2.12.0") }
+
+ context "with a simple exact version" do
+ let(:sbt_req_string) { "2.10.0" }
+
+ its(:first) do
+ is_expected.to eq(
+ file: "build.sbt",
+ requirement: "2.12.0",
+ groups: [],
+ source: { type: "maven_repo", url: "https://repo.maven.apache.org/maven2" },
+ metadata: nil
+ )
+ end
+ end
+
+ context "with a nil requirement" do
+ let(:sbt_req_string) { nil }
+
+ it "returns the requirement unchanged" do
+ expect(updated_requirements.first).to eq(sbt_req)
+ end
+ end
+
+ context "with a jre-suffixed version" do
+ let(:sbt_req_string) { "33.0.0-jre" }
+ let(:latest_version) { version_class.new("33.4.0-jre") }
+
+ its(:first) do
+ is_expected.to include(requirement: "33.4.0-jre")
+ end
+ end
+
+ context "with a range requirement (comma-separated)" do
+ let(:sbt_req_string) { "[2.10.0,2.11.0]" }
+
+ it "does not update range requirements" do
+ expect(updated_requirements.first).to eq(sbt_req)
+ end
+ end
+ end
+
+ context "with property-based requirements" do
+ let(:sbt_req) do
+ {
+ file: "build.sbt",
+ requirement: "2.10.0",
+ groups: [],
+ source: nil,
+ metadata: { property_name: "catsVersion", property_source: "build.sbt" }
+ }
+ end
+
+ context "when the property is in properties_to_update" do
+ let(:properties_to_update) { ["catsVersion"] }
+
+ its(:first) do
+ is_expected.to include(requirement: "2.12.0")
+ end
+ end
+
+ context "when the property is NOT in properties_to_update" do
+ let(:properties_to_update) { [] }
+
+ it "does not update the requirement" do
+ expect(updated_requirements.first[:requirement]).to eq("2.10.0")
+ end
+ end
+ end
+
+ context "with multiple requirements" do
+ let(:requirements) { [sbt_req, other_req] }
+ let(:other_req) do
+ {
+ file: "project/plugins.sbt",
+ requirement: "2.10.0",
+ groups: ["plugins"],
+ source: nil,
+ metadata: nil
+ }
+ end
+
+ it "updates both requirements" do
+ expect(updated_requirements.count).to eq(2)
+ expect(updated_requirements[0][:requirement]).to eq("2.12.0")
+ expect(updated_requirements[1][:requirement]).to eq("2.12.0")
+ end
+ end
+ end
+end
diff --git a/sbt/spec/dependabot/sbt/update_checker/version_finder_spec.rb b/sbt/spec/dependabot/sbt/update_checker/version_finder_spec.rb
new file mode 100644
index 0000000000..37dc9082dc
--- /dev/null
+++ b/sbt/spec/dependabot/sbt/update_checker/version_finder_spec.rb
@@ -0,0 +1,240 @@
+# typed: false
+# frozen_string_literal: true
+
+require "spec_helper"
+require "dependabot/credential"
+require "dependabot/dependency"
+require "dependabot/dependency_file"
+require "dependabot/sbt/update_checker/version_finder"
+
+RSpec.describe Dependabot::Sbt::UpdateChecker::VersionFinder do
+ let(:finder) do
+ 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,
+ cooldown_options: cooldown_options
+ )
+ end
+ let(:version_class) { Dependabot::Sbt::Version }
+ let(:credentials) { [] }
+ let(:ignored_versions) { [] }
+ let(:raise_on_ignored) { false }
+ let(:security_advisories) { [] }
+ let(:cooldown_options) { nil }
+
+ let(:dependency) do
+ Dependabot::Dependency.new(
+ name: dependency_name,
+ version: dependency_version,
+ requirements: dependency_requirements,
+ package_manager: "sbt"
+ )
+ end
+ let(:dependency_files) { [build_sbt] }
+ let(:build_sbt) do
+ Dependabot::DependencyFile.new(
+ name: "build.sbt",
+ content: fixture("buildfiles", "basic_build.sbt")
+ )
+ end
+
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: dependency_version,
+ groups: [],
+ source: nil,
+ metadata: nil
+ }]
+ end
+ let(:dependency_name) { "com.google.guava:guava" }
+ let(:dependency_version) { "33.0.0-jre" }
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/maven-metadata.xml"
+ end
+ let(:maven_central_base_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "guava.xml")
+ end
+ let(:maven_central_version_files_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar"
+ end
+
+ before do
+ stub_request(:get, maven_central_metadata_url)
+ .to_return(status: 200, body: maven_central_releases)
+ stub_request(:get, maven_central_base_url)
+ .to_return(status: 404)
+ stub_request(:head, maven_central_version_files_url)
+ .to_return(status: 200)
+ end
+
+ describe "class hierarchy" do
+ it "inherits from SharedVersionFinder" do
+ expect(described_class < Dependabot::Maven::Shared::SharedVersionFinder).to be true
+ end
+ end
+
+ describe "#latest_version_details" do
+ subject(:latest_version_details) { finder.latest_version_details }
+
+ its([:version]) { is_expected.to eq(version_class.new("33.4.0-jre")) }
+
+ its([:source_url]) do
+ is_expected.to eq("https://repo.maven.apache.org/maven2")
+ end
+
+ context "when the latest version hasn't actually been released" do
+ before do
+ stub_request(:head, maven_central_version_files_url)
+ .to_return(status: 404)
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.3.0-jre/guava-33.3.0-jre.jar"
+ ).to_return(status: 200)
+ end
+
+ its([:version]) { is_expected.to eq(version_class.new("33.3.0-jre")) }
+ end
+
+ context "with a cross-versioned Scala dependency" do
+ let(:dependency_name) { "org.typelevel:cats-core_2.13" }
+ let(:dependency_version) { "2.10.0" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "2.10.0",
+ groups: [],
+ source: nil,
+ metadata: { packaging_type: "cross-versioned" }
+ }]
+ end
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13/maven-metadata.xml"
+ end
+ let(:maven_central_base_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "cats_core_2.13.xml")
+ end
+ let(:maven_central_version_files_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13/2.12.0/cats-core_2.13-2.12.0.jar"
+ end
+
+ its([:version]) { is_expected.to eq(version_class.new("2.12.0")) }
+
+ it "excludes pre-release versions" do
+ # 2.13.0-RC1 should not be returned
+ expect(latest_version_details[:version]).not_to eq(version_class.new("2.13.0-RC1"))
+ end
+ end
+
+ context "when the user wants a pre-release" do
+ let(:dependency_name) { "com.typesafe.akka:akka-actor_2.13" }
+ let(:dependency_version) { "2.9.0-M1" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "2.9.0-M1",
+ groups: [],
+ source: nil,
+ metadata: { packaging_type: "cross-versioned" }
+ }]
+ end
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/typesafe/akka/akka-actor_2.13/maven-metadata.xml"
+ end
+ let(:maven_central_base_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/typesafe/akka/akka-actor_2.13"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "akka_actor_2.13.xml")
+ end
+ let(:maven_central_version_files_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/typesafe/akka/akka-actor_2.13/2.9.3/akka-actor_2.13-2.9.3.jar"
+ end
+
+ its([:version]) { is_expected.to eq(version_class.new("2.9.3")) }
+ end
+
+ context "with ignored versions" do
+ let(:ignored_versions) { [">= 33.3.0"] }
+
+ before do
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.2.0-jre/guava-33.2.0-jre.jar"
+ ).to_return(status: 200)
+ end
+
+ its([:version]) { is_expected.to eq(version_class.new("33.2.0-jre")) }
+ end
+
+ context "with custom repositories" do
+ let(:build_sbt) do
+ Dependabot::DependencyFile.new(
+ name: "build.sbt",
+ content: fixture("buildfiles", "custom_repos_build.sbt")
+ )
+ end
+
+ before do
+ stub_request(:get, "https://oss.sonatype.org/content/repositories/releases/com/google/guava/guava/maven-metadata.xml")
+ .to_return(status: 404)
+ stub_request(:get, "https://repo.artima.com/releases/com/google/guava/guava/maven-metadata.xml")
+ .to_return(status: 404)
+ end
+
+ its([:version]) { is_expected.to eq(version_class.new("33.4.0-jre")) }
+ end
+ end
+
+ describe "#lowest_security_fix_version_details" do
+ subject { finder.lowest_security_fix_version_details }
+
+ let(:security_advisories) do
+ [
+ Dependabot::SecurityAdvisory.new(
+ dependency_name: dependency_name,
+ package_manager: "sbt",
+ vulnerable_versions: ["< 33.2.0-jre"]
+ )
+ ]
+ end
+
+ before do
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.2.0-jre/guava-33.2.0-jre.jar"
+ ).to_return(status: 200)
+ end
+
+ its([:version]) { is_expected.to eq(version_class.new("33.2.0-jre")) }
+
+ its([:source_url]) do
+ is_expected.to eq("https://repo.maven.apache.org/maven2")
+ end
+ end
+end
diff --git a/sbt/spec/dependabot/sbt/update_checker_spec.rb b/sbt/spec/dependabot/sbt/update_checker_spec.rb
index 555f6024ca..d90825a53c 100644
--- a/sbt/spec/dependabot/sbt/update_checker_spec.rb
+++ b/sbt/spec/dependabot/sbt/update_checker_spec.rb
@@ -2,9 +2,223 @@
# frozen_string_literal: true
require "spec_helper"
+require "dependabot/dependency"
+require "dependabot/dependency_file"
require "dependabot/sbt/update_checker"
+require "dependabot/sbt/version"
require_common_spec "update_checkers/shared_examples_for_update_checkers"
RSpec.describe Dependabot::Sbt::UpdateChecker do
+ let(:version_class) { Dependabot::Sbt::Version }
+ let(:credentials) do
+ [{
+ "type" => "git_source",
+ "host" => "github.com",
+ "username" => "x-access-token",
+ "password" => "token"
+ }]
+ end
+ let(:security_advisories) { [] }
+ let(:ignored_versions) { [] }
+ let(:cooldown_options) { nil }
+ let(:dependency_files) { [build_sbt] }
+ let(:build_sbt) do
+ Dependabot::DependencyFile.new(
+ name: "build.sbt",
+ content: fixture("buildfiles", "basic_build.sbt")
+ )
+ end
+
+ let(:dependency_name) { "com.google.guava:guava" }
+ let(:dependency_version) { "33.0.0-jre" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "33.0.0-jre",
+ groups: [],
+ source: nil,
+ metadata: nil
+ }]
+ end
+ let(:dependency) do
+ Dependabot::Dependency.new(
+ name: dependency_name,
+ version: dependency_version,
+ requirements: dependency_requirements,
+ package_manager: "sbt"
+ )
+ end
+
+ let(:checker) do
+ described_class.new(
+ dependency: dependency,
+ dependency_files: dependency_files,
+ credentials: credentials,
+ ignored_versions: ignored_versions,
+ security_advisories: security_advisories,
+ update_cooldown: cooldown_options
+ )
+ end
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/maven-metadata.xml"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "guava.xml")
+ end
+ let(:maven_central_version_files_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.4.0-jre/guava-33.4.0-jre.jar"
+ end
+
+ before do
+ stub_request(:get, maven_central_metadata_url)
+ .to_return(status: 200, body: maven_central_releases)
+ stub_request(:get, "https://repo.maven.apache.org/maven2/com/google/guava/guava")
+ .to_return(status: 404)
+ stub_request(:head, maven_central_version_files_url)
+ .to_return(status: 200)
+ end
+
it_behaves_like "an update checker"
+
+ describe "#latest_version" do
+ subject { checker.latest_version }
+
+ it { is_expected.to eq(version_class.new("33.4.0-jre")) }
+
+ context "when the latest version hasn't been released" do
+ before do
+ stub_request(:head, maven_central_version_files_url)
+ .to_return(status: 404)
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.3.0-jre/guava-33.3.0-jre.jar"
+ ).to_return(status: 200)
+ end
+
+ it { is_expected.to eq(version_class.new("33.3.0-jre")) }
+ end
+
+ context "with a cross-versioned dependency" do
+ let(:dependency_name) { "org.typelevel:cats-core_2.13" }
+ let(:dependency_version) { "2.10.0" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "2.10.0",
+ groups: [],
+ source: nil,
+ metadata: { packaging_type: "cross-versioned" }
+ }]
+ end
+
+ let(:maven_central_metadata_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13/maven-metadata.xml"
+ end
+ let(:maven_central_releases) do
+ fixture("maven_metadata", "cats_core_2.13.xml")
+ end
+ let(:maven_central_version_files_url) do
+ "https://repo.maven.apache.org/maven2/" \
+ "org/typelevel/cats-core_2.13/2.12.0/cats-core_2.13-2.12.0.jar"
+ end
+
+ it { is_expected.to eq(version_class.new("2.12.0")) }
+ end
+ end
+
+ describe "#latest_resolvable_version" do
+ subject { checker.latest_resolvable_version }
+
+ it { is_expected.to eq(version_class.new("33.4.0-jre")) }
+
+ context "when the version comes from a multi-dependency property" do
+ let(:build_sbt) do
+ Dependabot::DependencyFile.new(
+ name: "build.sbt",
+ content: fixture("buildfiles", "val_based_build.sbt")
+ )
+ end
+ let(:dependency_name) { "org.typelevel:cats-core_2.13" }
+ let(:dependency_version) { "2.10.0" }
+ let(:dependency_requirements) do
+ [{
+ file: "build.sbt",
+ requirement: "2.10.0",
+ groups: [],
+ source: nil,
+ metadata: { property_name: "catsVersion", property_source: "build.sbt" }
+ }]
+ end
+
+ before do
+ stub_request(:get, "https://repo.maven.apache.org/maven2/org/typelevel/cats-core_2.13/maven-metadata.xml")
+ .to_return(status: 200, body: fixture("maven_metadata", "cats_core_2.13.xml"))
+ stub_request(:get, "https://repo.maven.apache.org/maven2/org/typelevel/cats-core_2.13")
+ .to_return(status: 404)
+ stub_request(:head, "https://repo.maven.apache.org/maven2/org/typelevel/cats-core_2.13/2.12.0/cats-core_2.13-2.12.0.jar")
+ .to_return(status: 200)
+ end
+
+ # catsVersion is only used by cats-core in val_based_build.sbt, so it's NOT multi-dep
+ it { is_expected.to eq(version_class.new("2.12.0")) }
+ end
+ end
+
+ describe "#lowest_security_fix_version" do
+ subject { checker.lowest_security_fix_version }
+
+ let(:dependency_version) { "33.0.0-jre" }
+ let(:security_advisories) do
+ [
+ Dependabot::SecurityAdvisory.new(
+ dependency_name: dependency_name,
+ package_manager: "sbt",
+ vulnerable_versions: ["< 33.2.0-jre"]
+ )
+ ]
+ end
+
+ before do
+ stub_request(
+ :head,
+ "https://repo.maven.apache.org/maven2/" \
+ "com/google/guava/guava/33.2.0-jre/guava-33.2.0-jre.jar"
+ ).to_return(status: 200)
+ end
+
+ it { is_expected.to eq(version_class.new("33.2.0-jre")) }
+ end
+
+ describe "#latest_resolvable_version_with_no_unlock" do
+ subject { checker.latest_resolvable_version_with_no_unlock }
+
+ it { is_expected.to be_nil }
+ end
+
+ describe "#updated_requirements" do
+ subject(:updated_requirements) { checker.updated_requirements }
+
+ it "updates the requirement version" do
+ expect(updated_requirements).to eq(
+ [{
+ file: "build.sbt",
+ requirement: "33.4.0-jre",
+ groups: [],
+ source: { type: "maven_repo", url: "https://repo.maven.apache.org/maven2" },
+ metadata: nil
+ }]
+ )
+ end
+ end
+
+ describe "#requirements_unlocked_or_can_be?" do
+ subject { checker.requirements_unlocked_or_can_be? }
+
+ it { is_expected.to be(true) }
+ end
end
diff --git a/sbt/spec/fixtures/maven_metadata/akka_actor_2.13.xml b/sbt/spec/fixtures/maven_metadata/akka_actor_2.13.xml
new file mode 100644
index 0000000000..c424ab205e
--- /dev/null
+++ b/sbt/spec/fixtures/maven_metadata/akka_actor_2.13.xml
@@ -0,0 +1,23 @@
+
+ com.typesafe.akka
+ akka-actor_2.13
+
+ 2.9.3
+ 2.9.3
+
+ 2.6.0
+ 2.6.1
+ 2.6.19
+ 2.6.20
+ 2.7.0
+ 2.8.0
+ 2.8.5
+ 2.9.0-M1
+ 2.9.0
+ 2.9.1
+ 2.9.2
+ 2.9.3
+
+ 20240501000000
+
+
diff --git a/sbt/spec/fixtures/maven_metadata/cats_core_2.13.xml b/sbt/spec/fixtures/maven_metadata/cats_core_2.13.xml
new file mode 100644
index 0000000000..a2b4ac6cdb
--- /dev/null
+++ b/sbt/spec/fixtures/maven_metadata/cats_core_2.13.xml
@@ -0,0 +1,26 @@
+
+ org.typelevel
+ cats-core_2.13
+
+ 2.12.0
+ 2.12.0
+
+ 2.0.0
+ 2.1.0
+ 2.2.0
+ 2.3.0
+ 2.4.0
+ 2.5.0
+ 2.6.0
+ 2.6.1
+ 2.7.0
+ 2.8.0
+ 2.9.0
+ 2.10.0
+ 2.11.0
+ 2.12.0
+ 2.13.0-RC1
+
+ 20240101000000
+
+
diff --git a/sbt/spec/fixtures/maven_metadata/guava.xml b/sbt/spec/fixtures/maven_metadata/guava.xml
new file mode 100644
index 0000000000..c3b584d739
--- /dev/null
+++ b/sbt/spec/fixtures/maven_metadata/guava.xml
@@ -0,0 +1,29 @@
+
+ com.google.guava
+ guava
+
+ 33.4.0-jre
+ 33.4.0-jre
+
+ 31.0-jre
+ 31.0-android
+ 31.1-jre
+ 31.1-android
+ 32.0.0-jre
+ 32.0.0-android
+ 32.1.0-jre
+ 32.1.0-android
+ 33.0.0-jre
+ 33.0.0-android
+ 33.1.0-jre
+ 33.1.0-android
+ 33.2.0-jre
+ 33.2.0-android
+ 33.3.0-jre
+ 33.3.0-android
+ 33.4.0-jre
+ 33.4.0-android
+
+ 20240601000000
+
+
diff --git a/sbt/spec/fixtures/maven_metadata/scalatest_2.13.xml b/sbt/spec/fixtures/maven_metadata/scalatest_2.13.xml
new file mode 100644
index 0000000000..4ed208a88a
--- /dev/null
+++ b/sbt/spec/fixtures/maven_metadata/scalatest_2.13.xml
@@ -0,0 +1,19 @@
+
+ org.scalatest
+ scalatest_2.13
+
+ 3.2.19
+ 3.2.19
+
+ 3.0.0
+ 3.1.0
+ 3.2.0
+ 3.2.10
+ 3.2.15
+ 3.2.17
+ 3.2.18
+ 3.2.19
+
+ 20240601000000
+
+