Skip to content

Commit

Permalink
Add types to requirement classes
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee committed Sep 14, 2023
1 parent 6e1996c commit cb5ad53
Show file tree
Hide file tree
Showing 20 changed files with 254 additions and 78 deletions.
6 changes: 5 additions & 1 deletion bundler/lib/dependabot/bundler/requirement.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
# typed: true
# typed: strong
# frozen_string_literal: true

require "dependabot/utils"

module Dependabot
module Bundler
class Requirement < Gem::Requirement
extend T::Sig

# For consistency with other languages, we define a requirements array.
# Ruby doesn't have an `OR` separator for requirements, so it always
# contains a single element.
sig { params(requirement_string: String).returns(T::Array[Dependabot::Bundler::Requirement]) }
def self.requirements_array(requirement_string)
[new(requirement_string)]
end

# Patches Gem::Requirement to make it accept requirement strings like
# "~> 4.2.5, >= 4.2.5.1" without first needing to split them.
sig { params(requirements: String).void }
def initialize(*requirements)
requirements = requirements.flatten.flat_map do |req_string|
req_string.split(",").map(&:strip)
Expand Down
17 changes: 14 additions & 3 deletions cargo/lib/dependabot/cargo/requirement.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

################################################################################
Expand All @@ -7,20 +7,25 @@
# - https://steveklabnik.github.io/semver/semver/index.html #
################################################################################

require "sorbet-runtime"

require "dependabot/utils"
require "dependabot/cargo/version"

module Dependabot
module Cargo
class Requirement < Gem::Requirement
extend T::Sig

quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
version_pattern = Cargo::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 Cargo::Version rather than Gem::Version to ensure that
# pre-release versions aren't transformed.
sig { override.params(obj: Object).returns([String, Dependabot::Cargo::Version]) }
def self.parse(obj)
return ["=", Cargo::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)

Expand All @@ -37,10 +42,12 @@ def self.parse(obj)
# For consistency with other languages, we define a requirements array.
# Rust doesn't have an `OR` separator for requirements, so it always
# contains a single element.
sig { params(requirement_string: String).returns(T::Array[Dependabot::Cargo::Requirement]) }
def self.requirements_array(requirement_string)
[new(requirement_string)]
end

sig { override.params(requirements: String).void }
def initialize(*requirements)
requirements = requirements.flatten.flat_map do |req_string|
req_string.split(",").map(&:strip).map do |r|
Expand All @@ -53,6 +60,7 @@ def initialize(*requirements)

private

sig { params(req_string: String).returns(T.any(T::Array[String], String)) }
def convert_rust_constraint_to_ruby_constraint(req_string)
if req_string.include?("*")
ruby_range(req_string.gsub(/(?:\.|^)[*]/, "").gsub(/^[^\d]/, ""))
Expand All @@ -64,13 +72,15 @@ def convert_rust_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 << "0" if parts.count < 3
"~> #{parts.join('.')}"
end

sig { params(req_string: String).returns(String) }
def ruby_range(req_string)
parts = req_string.split(".")

Expand All @@ -85,14 +95,15 @@ def ruby_range(req_string)
"~> #{parts.join('.')}"
end

sig { params(req_string: String).returns(T::Array[String]) }
def convert_caret_req(req_string)
version = req_string.gsub(/^\^/, "")
parts = version.split(".")
first_non_zero = parts.find { |d| d != "0" }
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.unsafe(first_non_zero_index) then part
elsif i == first_non_zero_index then (part.to_i + 1).to_s
else
0
Expand Down
21 changes: 16 additions & 5 deletions common/lib/dependabot/utils.rb
Original file line number Diff line number Diff line change
@@ -1,48 +1,59 @@
# typed: true
# typed: strong
# frozen_string_literal: true

require "tmpdir"
require "set"
require "sorbet-runtime"

require "dependabot/version"

# TODO: in due course, these "registries" should live in a wrapper gem, not
# dependabot-core.
module Dependabot
module Utils
extend T::Sig

BUMP_TMP_FILE_PREFIX = "dependabot_"
BUMP_TMP_DIR_PATH = File.expand_path(Dir::Tmpname.create("", "tmp") { nil })
BUMP_TMP_DIR_PATH = T.let(File.expand_path(Dir::Tmpname.create("", "tmp") { nil }), String)

@version_classes = {}
@version_classes = T.let({}, T::Hash[String, T.class_of(Dependabot::Version)])

sig { params(package_manager: String).returns(T.class_of(Dependabot::Version)) }
def self.version_class_for_package_manager(package_manager)
version_class = @version_classes[package_manager]
return version_class if version_class

raise "Unsupported package_manager #{package_manager}"
end

sig { params(package_manager: String, version_class: T.class_of(Dependabot::Version)).void }
def self.register_version_class(package_manager, version_class)
@version_classes[package_manager] = version_class
end

@requirement_classes = {}
@requirement_classes = T.let({}, T::Hash[String, T.class_of(Gem::Requirement)])

sig { params(package_manager: String).returns(T.class_of(Gem::Requirement)) }
def self.requirement_class_for_package_manager(package_manager)
requirement_class = @requirement_classes[package_manager]
return requirement_class if requirement_class

raise "Unsupported package_manager #{package_manager}"
end

sig { params(package_manager: String, requirement_class: T.class_of(Gem::Requirement)).void }
def self.register_requirement_class(package_manager, requirement_class)
@requirement_classes[package_manager] = requirement_class
end

@cloning_package_managers = Set[]
@cloning_package_managers = T.let(Set[], T::Set[String])

sig { params(package_manager: String).returns(T::Boolean) }
def self.always_clone_for_package_manager?(package_manager)
@cloning_package_managers.include?(package_manager)
end

sig { params(package_manager: String).void }
def self.register_always_clone(package_manager)
@cloning_package_managers << package_manager
end
Expand Down
7 changes: 6 additions & 1 deletion common/lib/dependabot/version.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
# typed: true
# typed: strong
# frozen_string_literal: true

module Dependabot
class Version < Gem::Version
extend T::Sig

sig { override.params(version: String).void }
def initialize(version)
@original_version = version

super
end

# Opt-in to Rubygems 4 behavior
sig { override.params(version: Object).returns(T::Boolean) }
def self.correct?(version)
return false if version.nil?

version.to_s.match?(ANCHORED_VERSION_PATTERN)
end

sig { returns(String) }
def to_semver
@original_version
end
Expand Down
20 changes: 16 additions & 4 deletions composer/lib/dependabot/composer/requirement.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"

require "dependabot/utils"

module Dependabot
module Composer
class Requirement < Gem::Requirement
extend T::Sig

AND_SEPARATOR = /(?<=[a-zA-Z0-9*])(?<!\sas)[\s,]+(?![\s,]*[|-]|as)/
OR_SEPARATOR = /(?<=[a-zA-Z0-9*])[\s,]*\|\|?\s*/

sig { override.params(obj: String).returns(Dependabot::Composer::Requirement) }
def self.parse(obj)
new_obj = obj.gsub(/@\w+/, "").gsub(/[a-z0-9\-_\.]*\sas\s+/i, "")
return DefaultRequirement if new_obj == ""
Expand All @@ -18,12 +23,14 @@ def self.parse(obj)

# Returns an array of requirements. At least one requirement from the
# returned array must be satisfied for a version to be valid.
sig { params(requirement_string: String).returns(T::Array[Dependabot::Composer::Requirement]) }
def self.requirements_array(requirement_string)
requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
new(req_string)
end
end

sig { override.params(requirements: String).void }
def initialize(*requirements)
requirements =
requirements.flatten
Expand All @@ -35,6 +42,7 @@ def initialize(*requirements)

private

sig { params(req_string: String).returns(T.any(T::Array[String], String)) }
def convert_php_constraint_to_ruby_constraint(req_string)
req_string = req_string.strip.gsub(/v(?=\d)/, "").gsub(/\.$/, "")

Expand All @@ -53,6 +61,7 @@ def convert_php_constraint_to_ruby_constraint(req_string)
end
end

sig { params(req_string: String).returns(String) }
def convert_wildcard_req(req_string)
if req_string.start_with?(">", "<")
msg = "Illformed requirement [#{req_string.inspect}]"
Expand All @@ -63,19 +72,21 @@ def convert_wildcard_req(req_string)
"~> #{version}.0"
end

sig { params(req_string: String).returns(String) }
def convert_tilde_req(req_string)
version = req_string.gsub(/^~/, "")
"~> #{version}"
end

sig { params(req_string: String).returns(T::Array[String]) }
def convert_caret_req(req_string)
version = req_string.gsub(/^\^/, "").gsub("x-dev", "0")
parts = version.split(".")
first_non_zero = parts.find { |d| d != "0" }
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.unsafe(first_non_zero_index) then part
elsif i == first_non_zero_index then (part.to_i + 1).to_s
else
0
Expand All @@ -85,10 +96,11 @@ def convert_caret_req(req_string)
[">= #{version}", "< #{upper_bound}"]
end

sig { params(req_string: String).returns(T::Array[String]) }
def convert_hyphen_req(req_string)
lower_bound, upper_bound = req_string.split(/\s+-\s+/)
if upper_bound.split(".").count < 3
upper_bound_parts = upper_bound.split(".")
if T.unsafe(upper_bound).split(".").count < 3
upper_bound_parts = T.unsafe(upper_bound).split(".")
upper_bound_parts[-1] = (upper_bound_parts[-1].to_i + 1).to_s
upper_bound = upper_bound_parts.join(".")

Expand Down
6 changes: 5 additions & 1 deletion docker/lib/dependabot/docker/requirement.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: true
# typed: strict
# frozen_string_literal: true

require "dependabot/utils"
Expand All @@ -7,19 +7,23 @@ module Dependabot
module Docker
# Lifted from the bundler package manager
class Requirement < Gem::Requirement
extend T::Sig
# For consistency with other languages, we define a requirements array.
# Ruby doesn't have an `OR` separator for requirements, so it always
# contains a single element.
sig { params(requirement_string: String).returns([Dependabot::Docker::Requirement]) }
def self.requirements_array(requirement_string)
[new(requirement_string)]
end

sig { override.params(version: Dependabot::Docker::Version).returns(T::Boolean) }
def satisfied_by?(version)
super(version.release_part)
end

# Patches Gem::Requirement to make it accept requirement strings like
# "~> 4.2.5, >= 4.2.5.1" without first needing to split them.
sig { override.params(requirements: String).void }
def initialize(*requirements)
requirements = requirements.flatten.flat_map do |req_string|
req_string.split(",").map(&:strip)
Expand Down
20 changes: 15 additions & 5 deletions elm/lib/dependabot/elm/requirement.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
# typed: false
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"

require "dependabot/utils"
require "dependabot/elm/version"

module Dependabot
module Elm
class Requirement < Gem::Requirement
extend T::Sig

ELM_PATTERN_RAW =
"(#{Elm::Version::VERSION_PATTERN}) (<=?) v (<=?) " \
"(#{Elm::Version::VERSION_PATTERN})".freeze
T.let(
"(#{Elm::Version::VERSION_PATTERN}) (<=?) v (<=?) (#{Elm::Version::VERSION_PATTERN})".freeze,
String
)
ELM_PATTERN = /\A#{ELM_PATTERN_RAW}\z/
ELM_EXACT_PATTERN = /\A#{Elm::Version::VERSION_PATTERN}\z/

# Returns an array of requirements. At least one requirement from the
# returned array must be satisfied for a version to be valid.
sig { params(requirement_string: String).returns(T::Array[Dependabot::Elm::Requirement]) }
def self.requirements_array(requirement_string)
[new(requirement_string)]
end

sig { override.params(requirements: T.nilable(String)).void }
def initialize(*requirements)
requirements = requirements.flatten.flat_map do |req_string|
raise BadRequirementError, "Nil requirement not supported in Elm" if req_string.nil?
Expand All @@ -31,6 +39,7 @@ def initialize(*requirements)
super(requirements)
end

sig { override.params(version: T.any(String, Dependabot::Elm::Version)).returns(T::Boolean) }
def satisfied_by?(version)
version = Elm::Version.new(version.to_s)
super
Expand All @@ -40,6 +49,7 @@ def satisfied_by?(version)

# Override the parser to create Elm::Versions and return an
# array of parsed requirements
sig { params(obj: String).returns(T.any(String, T::Array[String])) }
def convert_elm_constraint_to_ruby_constraint(obj)
# If a version is given this is an equals requirement
return obj if ELM_EXACT_PATTERN.match?(obj.to_s)
Expand All @@ -48,10 +58,10 @@ def convert_elm_constraint_to_ruby_constraint(obj)

# If the two versions specified are identical this is an equals
# requirement
return matches[4] if matches[1] == matches[4] && matches[3] == "<="
return T.unsafe(matches[4]) if matches[1] == matches[4] && matches[3] == "<="

[
[matches[2].tr("<", ">"), matches[1]].join(" "),
[T.unsafe(matches[2]).tr("<", ">"), matches[1]].join(" "),
[matches[3], matches[4]].join(" ")
]
end
Expand Down
Loading

0 comments on commit cb5ad53

Please sign in to comment.