Skip to content

Commit

Permalink
Strong type Dependabot::FileFetchers::Base::DependencySet (dependab…
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee authored Jan 22, 2024
1 parent 793fab0 commit 9130f77
Show file tree
Hide file tree
Showing 6 changed files with 78 additions and 38 deletions.
10 changes: 5 additions & 5 deletions common/lib/dependabot/dependency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,15 +88,15 @@ def self.register_name_normaliser(package_manager, name_builder)
sig do
params(
name: String,
requirements: T::Array[T::Hash[String, String]],
requirements: T::Array[T::Hash[T.any(Symbol, String), T.untyped]],
package_manager: String,
# TODO: Make version a Dependabot::Version everywhere
version: T.nilable(T.any(String, Dependabot::Version)),
previous_version: T.nilable(String),
previous_requirements: T.nilable(T::Array[T::Hash[String, String]]),
subdependency_metadata: T.nilable(T::Array[T::Hash[String, String]]),
subdependency_metadata: T.nilable(T::Array[T::Hash[T.any(Symbol, String), String]]),
removed: T::Boolean,
metadata: T.nilable(T::Hash[String, String])
metadata: T.nilable(T::Hash[T.any(Symbol, String), String])
).void
end
def initialize(name:, requirements:, package_manager:, version: nil,
Expand All @@ -110,7 +110,7 @@ def initialize(name:, requirements:, package_manager:, version: nil,
end,
T.nilable(String)
)
@requirements = T.let(requirements.map { |req| symbolize_keys(req) }, T::Array[T::Hash[Symbol, String]])
@requirements = T.let(requirements.map { |req| symbolize_keys(req) }, T::Array[T::Hash[Symbol, T.untyped]])
@previous_version = previous_version
@previous_requirements = T.let(
previous_requirements&.map { |req| symbolize_keys(req) },
Expand Down Expand Up @@ -391,7 +391,7 @@ def check_subdependency_metadata
end
end

sig { params(hash: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
sig { params(hash: T::Hash[T.any(Symbol, String), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def symbolize_keys(hash)
hash.keys.to_h { |k| [k.to_sym, hash[k]] }
end
Expand Down
78 changes: 59 additions & 19 deletions common/lib/dependabot/file_parsers/base/dependency_set.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# typed: true
# typed: strong
# frozen_string_literal: true

require "sorbet-runtime"
require "dependabot/dependency"
require "dependabot/file_parsers/base"
require "dependabot/utils"
Expand All @@ -9,28 +10,36 @@ module Dependabot
module FileParsers
class Base
class DependencySet
extend T::Sig

sig do
params(
dependencies: T::Array[Dependency],
case_sensitive: T::Boolean
)
.void
end
def initialize(dependencies = [], case_sensitive: false)
unless dependencies.is_a?(Array) &&
dependencies.all?(Dependency)
raise ArgumentError, "must be an array of Dependency objects"
end

@case_sensitive = case_sensitive
@dependencies = Hash.new { |hsh, key| hsh[key] = DependencySlot.new }
@dependencies = T.let(
Hash.new { |hsh, key| hsh[key] = DependencySlot.new },
T::Hash[String, DependencySlot]
)
dependencies.each { |dep| self << dep }
end

sig { returns(T::Array[Dependency]) }
def dependencies
@dependencies.values.filter_map(&:combined)
end

sig { params(dep: Dependabot::Dependency).returns(T.untyped) }
def <<(dep)
raise ArgumentError, "must be a Dependency object" unless dep.is_a?(Dependency)

@dependencies[key_for_dependency(dep)] << dep
T.must(@dependencies[key_for_dependency(dep)]) << dep
self
end

sig { params(other: Object).returns(T.self_type) }
def +(other)
raise ArgumentError, "must be a DependencySet" unless other.is_a?(DependencySet)

Expand All @@ -43,26 +52,31 @@ def +(other)
self
end

sig { params(name: String).returns(T::Array[Dependabot::Dependency]) }
def all_versions_for_name(name)
key = key_for_name(name)
@dependencies.key?(key) ? @dependencies[key].all_versions : []
@dependencies.key?(key) ? T.must(@dependencies[key]).all_versions : []
end

sig { params(name: String).returns(T.nilable(Dependabot::Dependency)) }
def dependency_for_name(name)
key = key_for_name(name)
@dependencies.key?(key) ? @dependencies[key].combined : nil
@dependencies.key?(key) ? T.must(@dependencies[key]).combined : nil
end

private

sig { returns(T::Boolean) }
def case_sensitive?
@case_sensitive
end

sig { params(name: String).returns(String) }
def key_for_name(name)
case_sensitive? ? name : name.downcase
end

sig { params(dep: Dependabot::Dependency).returns(String) }
def key_for_dependency(dep)
key_for_name(dep.name)
end
Expand All @@ -79,13 +93,21 @@ def key_for_dependency(dep)
# `DependencySet#dependency_for_name`. The list of individual versions of the
# dependency is accessible via `DependencySet#all_versions_for_name`.
class DependencySlot
attr_reader :all_versions, :combined
extend T::Sig

sig { returns(T::Array[Dependabot::Dependency]) }
attr_reader :all_versions

sig { returns(T.nilable(Dependabot::Dependency)) }
attr_reader :combined

sig { void }
def initialize
@all_versions = []
@combined = nil
@all_versions = T.let([], T::Array[Dependabot::Dependency])
@combined = T.let(nil, T.nilable(Dependabot::Dependency))
end

sig { params(dep: Dependabot::Dependency).returns(T.self_type) }
def <<(dep)
return self if @all_versions.include?(dep)

Expand All @@ -102,7 +124,7 @@ def <<(dep)
@all_versions << dep
else
same_version = @all_versions[index_of_same_version]
@all_versions[index_of_same_version] = combined_dependency(same_version, dep)
@all_versions[index_of_same_version] = combined_dependency(T.must(same_version), dep)
end

self
Expand All @@ -114,6 +136,13 @@ def <<(dep)
# `new_dep`. Requirements and subdependency metadata will be combined and deduped.
# The version of the combined dependency is determined by the
# `#combined_version` method below.
sig do
params(
old_dep: Dependabot::Dependency,
new_dep: Dependabot::Dependency
)
.returns(Dependabot::Dependency)
end
def combined_dependency(old_dep, new_dep)
version = combined_version(old_dep, new_dep)
requirements = (old_dep.requirements + new_dep.requirements).uniq
Expand All @@ -132,11 +161,18 @@ def combined_dependency(old_dep, new_dep)
)
end

sig do
params(
old_dep: Dependabot::Dependency,
new_dep: Dependabot::Dependency
)
.returns(T.nilable(String))
end
def combined_version(old_dep, new_dep)
if old_dep.version.nil? ^ new_dep.version.nil?
[old_dep, new_dep].find(&:version).version
T.must([old_dep, new_dep].find(&:version)).version
elsif old_dep.top_level? ^ new_dep.top_level? # Prefer a direct dependency over a transitive one
[old_dep, new_dep].find(&:top_level?).version
T.must([old_dep, new_dep].find(&:top_level?)).version
elsif !version_class.correct?(new_dep.version)
old_dep.version
elsif !version_class.correct?(old_dep.version)
Expand All @@ -148,8 +184,12 @@ def combined_version(old_dep, new_dep)
end
end

sig { returns(T.class_of(Gem::Version)) }
def version_class
@version_class ||= @combined.version_class
@version_class ||= T.let(
T.must(@combined).version_class,
T.nilable(T.class_of(Gem::Version))
)
end
end
private_constant :DependencySlot
Expand Down
12 changes: 6 additions & 6 deletions common/spec/dependabot/file_parsers/base/dependency_set_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@

it "raises a helpful error" do
expect { described_class.new(:a) }
.to raise_error(ArgumentError) do |error|
expect(error.message).to include("array of Dependency objects")
.to raise_error(TypeError) do |error|
expect(error.message).to include("Expected type T::Array[Dependabot::Dependency]")
end
end
end
Expand All @@ -46,8 +46,8 @@

it "raises a helpful error" do
expect { described_class.new(:a) }
.to raise_error(ArgumentError) do |error|
expect(error.message).to eq "must be an array of Dependency objects"
.to raise_error(TypeError) do |error|
expect(error.message).to include("Expected type T::Array[Dependabot::Dependency]")
end
end
end
Expand Down Expand Up @@ -208,8 +208,8 @@

it "raises a helpful error" do
expect { dependency_set << dependency }
.to raise_error(ArgumentError) do |error|
expect(error.message).to eq("must be a Dependency object")
.to raise_error(TypeError) do |error|
expect(error.message).to include("Expected type Dependabot::Dependency")
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions github_actions/lib/dependabot/github_actions/file_parser.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "yaml"
Expand Down Expand Up @@ -82,8 +82,8 @@ def workfile_file_dependencies(file)
end

def build_github_dependency(file, string)
unless source.hostname == "github.com"
dep = github_dependency(file, string, source.hostname)
unless source&.hostname == "github.com"
dep = github_dependency(file, string, T.must(source).hostname)
git_checker = Dependabot::GitCommitChecker.new(dependency: dep, credentials: credentials)
return dep if git_checker.git_repo_reachable?
end
Expand Down
8 changes: 4 additions & 4 deletions nuget/lib/dependabot/nuget/file_parser.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "nokogiri"
Expand Down Expand Up @@ -85,7 +85,7 @@ def project_files

def packages_config_files
dependency_files.select do |f|
f.name.split("/").last.casecmp("packages.config").zero?
f.name.split("/").last&.casecmp("packages.config")&.zero?
end
end

Expand All @@ -103,11 +103,11 @@ def nuget_configs
end

def global_json
dependency_files.find { |f| f.name.casecmp("global.json").zero? }
dependency_files.find { |f| f.name.casecmp("global.json")&.zero? }
end

def dotnet_tools_json
dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json").zero? }
dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json")&.zero? }
end

def check_required_files
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
# frozen_string_literal: true

require "toml-rb"
Expand Down

0 comments on commit 9130f77

Please sign in to comment.