dependabot-common 0.237.0 → 0.239.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,224 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "sorbet-runtime"
5
5
  require "dependabot/utils"
6
6
 
7
7
  module Dependabot
8
+ extend T::Sig
9
+
10
+ # rubocop:disable Metrics/MethodLength
11
+ sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
12
+ def self.fetcher_error_details(error)
13
+ case error
14
+ when Dependabot::ToolVersionNotSupported
15
+ {
16
+ "error-type": "tool_version_not_supported",
17
+ "error-detail": {
18
+ "tool-name": error.tool_name,
19
+ "detected-version": error.detected_version,
20
+ "supported-versions": error.supported_versions
21
+ }
22
+ }
23
+ when Dependabot::BranchNotFound
24
+ {
25
+ "error-type": "branch_not_found",
26
+ "error-detail": { "branch-name": error.branch_name }
27
+ }
28
+ when Dependabot::DirectoryNotFound
29
+ {
30
+ "error-type": "directory_not_found",
31
+ "error-detail": { "directory-name": error.directory_name }
32
+ }
33
+ when Dependabot::RepoNotFound
34
+ # This happens if the repo gets removed after a job gets kicked off.
35
+ # This also happens when a configured personal access token is not authz'd to fetch files from the job repo.
36
+ {
37
+ "error-type": "job_repo_not_found",
38
+ "error-detail": { message: error.message }
39
+ }
40
+ when Dependabot::DependencyFileNotParseable
41
+ {
42
+ "error-type": "dependency_file_not_parseable",
43
+ "error-detail": {
44
+ message: error.message,
45
+ "file-path": error.file_path
46
+ }
47
+ }
48
+ when Dependabot::DependencyFileNotFound
49
+ {
50
+ "error-type": "dependency_file_not_found",
51
+ "error-detail": { "file-path": error.file_path }
52
+ }
53
+ when Dependabot::OutOfDisk
54
+ {
55
+ "error-type": "out_of_disk",
56
+ "error-detail": {}
57
+ }
58
+ when Dependabot::PathDependenciesNotReachable
59
+ {
60
+ "error-type": "path_dependencies_not_reachable",
61
+ "error-detail": { dependencies: error.dependencies }
62
+ }
63
+ when Octokit::Unauthorized
64
+ { "error-type": "octokit_unauthorized" }
65
+ when Octokit::ServerError
66
+ # If we get a 500 from GitHub there's very little we can do about it,
67
+ # and responsibility for fixing it is on them, not us. As a result we
68
+ # quietly log these as errors
69
+ { "error-type": "server_error" }
70
+ when *Octokit::RATE_LIMITED_ERRORS
71
+ # If we get a rate-limited error we let dependabot-api handle the
72
+ # retry by re-enqueing the update job after the reset
73
+ {
74
+ "error-type": "octokit_rate_limited",
75
+ "error-detail": {
76
+ "rate-limit-reset": T.cast(error, Octokit::Error).response_headers["X-RateLimit-Reset"]
77
+ }
78
+ }
79
+ end
80
+ end
81
+
82
+ sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
83
+ def self.parser_error_details(error)
84
+ case error
85
+ when Dependabot::DependencyFileNotEvaluatable
86
+ {
87
+ "error-type": "dependency_file_not_evaluatable",
88
+ "error-detail": { message: error.message }
89
+ }
90
+ when Dependabot::DependencyFileNotResolvable
91
+ {
92
+ "error-type": "dependency_file_not_resolvable",
93
+ "error-detail": { message: error.message }
94
+ }
95
+ when Dependabot::BranchNotFound
96
+ {
97
+ "error-type": "branch_not_found",
98
+ "error-detail": { "branch-name": error.branch_name }
99
+ }
100
+ when Dependabot::DependencyFileNotParseable
101
+ {
102
+ "error-type": "dependency_file_not_parseable",
103
+ "error-detail": {
104
+ message: error.message,
105
+ "file-path": error.file_path
106
+ }
107
+ }
108
+ when Dependabot::DependencyFileNotFound
109
+ {
110
+ "error-type": "dependency_file_not_found",
111
+ "error-detail": { "file-path": error.file_path }
112
+ }
113
+ when Dependabot::PathDependenciesNotReachable
114
+ {
115
+ "error-type": "path_dependencies_not_reachable",
116
+ "error-detail": { dependencies: error.dependencies }
117
+ }
118
+ when Dependabot::PrivateSourceAuthenticationFailure
119
+ {
120
+ "error-type": "private_source_authentication_failure",
121
+ "error-detail": { source: error.source }
122
+ }
123
+ when Dependabot::GitDependenciesNotReachable
124
+ {
125
+ "error-type": "git_dependencies_not_reachable",
126
+ "error-detail": { "dependency-urls": error.dependency_urls }
127
+ }
128
+ when Dependabot::NotImplemented
129
+ {
130
+ "error-type": "not_implemented",
131
+ "error-detail": {
132
+ message: error.message
133
+ }
134
+ }
135
+ when Octokit::ServerError
136
+ # If we get a 500 from GitHub there's very little we can do about it,
137
+ # and responsibility for fixing it is on them, not us. As a result we
138
+ # quietly log these as errors
139
+ { "error-type": "server_error" }
140
+ end
141
+ end
142
+
143
+ sig { params(error: StandardError).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
144
+ def self.updater_error_details(error)
145
+ case error
146
+ when Dependabot::DependencyFileNotResolvable
147
+ {
148
+ "error-type": "dependency_file_not_resolvable",
149
+ "error-detail": { message: error.message }
150
+ }
151
+ when Dependabot::DependencyFileNotEvaluatable
152
+ {
153
+ "error-type": "dependency_file_not_evaluatable",
154
+ "error-detail": { message: error.message }
155
+ }
156
+ when Dependabot::GitDependenciesNotReachable
157
+ {
158
+ "error-type": "git_dependencies_not_reachable",
159
+ "error-detail": { "dependency-urls": error.dependency_urls }
160
+ }
161
+ when Dependabot::MisconfiguredTooling
162
+ {
163
+ "error-type": "misconfigured_tooling",
164
+ "error-detail": { "tool-name": error.tool_name, message: error.tool_message }
165
+ }
166
+ when Dependabot::GitDependencyReferenceNotFound
167
+ {
168
+ "error-type": "git_dependency_reference_not_found",
169
+ "error-detail": { dependency: error.dependency }
170
+ }
171
+ when Dependabot::PrivateSourceAuthenticationFailure
172
+ {
173
+ "error-type": "private_source_authentication_failure",
174
+ "error-detail": { source: error.source }
175
+ }
176
+ when Dependabot::PrivateSourceTimedOut
177
+ {
178
+ "error-type": "private_source_timed_out",
179
+ "error-detail": { source: error.source }
180
+ }
181
+ when Dependabot::PrivateSourceCertificateFailure
182
+ {
183
+ "error-type": "private_source_certificate_failure",
184
+ "error-detail": { source: error.source }
185
+ }
186
+ when Dependabot::MissingEnvironmentVariable
187
+ {
188
+ "error-type": "missing_environment_variable",
189
+ "error-detail": {
190
+ "environment-variable": error.environment_variable
191
+ }
192
+ }
193
+ when Dependabot::GoModulePathMismatch
194
+ {
195
+ "error-type": "go_module_path_mismatch",
196
+ "error-detail": {
197
+ "declared-path": error.declared_path,
198
+ "discovered-path": error.discovered_path,
199
+ "go-mod": error.go_mod
200
+ }
201
+ }
202
+ when Dependabot::NotImplemented
203
+ {
204
+ "error-type": "not_implemented",
205
+ "error-detail": {
206
+ message: error.message
207
+ }
208
+ }
209
+ when *Octokit::RATE_LIMITED_ERRORS
210
+ # If we get a rate-limited error we let dependabot-api handle the
211
+ # retry by re-enqueing the update job after the reset
212
+ {
213
+ "error-type": "octokit_rate_limited",
214
+ "error-detail": {
215
+ "rate-limit-reset": T.cast(error, Octokit::Error).response_headers["X-RateLimit-Reset"]
216
+ }
217
+ }
218
+ end
219
+ end
220
+ # rubocop:enable Metrics/MethodLength
221
+
8
222
  class DependabotError < StandardError
9
223
  extend T::Sig
10
224
 
@@ -109,6 +323,31 @@ module Dependabot
109
323
  # File level errors #
110
324
  #####################
111
325
 
326
+ class MisconfiguredTooling < DependabotError
327
+ extend T::Sig
328
+
329
+ sig { returns(String) }
330
+ attr_reader :tool_name
331
+
332
+ sig { returns(String) }
333
+ attr_reader :tool_message
334
+
335
+ sig do
336
+ params(
337
+ tool_name: String,
338
+ tool_message: String
339
+ ).void
340
+ end
341
+ def initialize(tool_name, tool_message)
342
+ @tool_name = tool_name
343
+ @tool_message = tool_message
344
+
345
+ msg = "Dependabot detected that #{tool_name} is misconfigured in this repository. " \
346
+ "Running `#{tool_name.downcase}` results in the following error: #{tool_message}"
347
+ super(msg)
348
+ end
349
+ end
350
+
112
351
  class ToolVersionNotSupported < DependabotError
113
352
  extend T::Sig
114
353
 
@@ -142,23 +381,28 @@ module Dependabot
142
381
  class DependencyFileNotFound < DependabotError
143
382
  extend T::Sig
144
383
 
145
- sig { returns(String) }
384
+ sig { returns(T.nilable(String)) }
146
385
  attr_reader :file_path
147
386
 
387
+ sig { params(file_path: T.nilable(String), msg: T.nilable(String)).void }
148
388
  def initialize(file_path, msg = nil)
149
389
  @file_path = file_path
150
390
  super(msg || "#{file_path} not found")
151
391
  end
152
392
 
153
- sig { returns(String) }
393
+ sig { returns(T.nilable(String)) }
154
394
  def file_name
155
- T.must(file_path.split("/").last)
395
+ return unless file_path
396
+
397
+ T.must(file_path).split("/").last
156
398
  end
157
399
 
158
- sig { returns(String) }
400
+ sig { returns(T.nilable(String)) }
159
401
  def directory
160
402
  # Directory should always start with a `/`
161
- T.must(file_path.split("/")[0..-2]).join("/").sub(%r{^/*}, "/")
403
+ return unless file_path
404
+
405
+ T.must(T.must(file_path).split("/")[0..-2]).join("/").sub(%r{^/*}, "/")
162
406
  end
163
407
  end
164
408
 
@@ -200,8 +444,9 @@ module Dependabot
200
444
  sig { returns(String) }
201
445
  attr_reader :source
202
446
 
447
+ sig { params(source: T.nilable(String)).void }
203
448
  def initialize(source)
204
- @source = T.let(sanitize_source(source), String)
449
+ @source = T.let(sanitize_source(T.must(source)), String)
205
450
  msg = "The following source could not be reached as it requires " \
206
451
  "authentication (and any provided details were invalid or lacked " \
207
452
  "the required permissions): #{@source}"
@@ -1,18 +1,25 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  module Dependabot
5
7
  module Experiments
6
- @experiments = {}
8
+ extend T::Sig
9
+
10
+ @experiments = T.let({}, T::Hash[T.any(String, Symbol), T.untyped])
7
11
 
12
+ sig { returns(T::Hash[T.any(String, Symbol), T.untyped]) }
8
13
  def self.reset!
9
14
  @experiments = {}
10
15
  end
11
16
 
17
+ sig { params(name: T.any(String, Symbol), value: T.untyped).void }
12
18
  def self.register(name, value)
13
19
  @experiments[name.to_sym] = value
14
20
  end
15
21
 
22
+ sig { params(name: T.any(String, Symbol)).returns(T::Boolean) }
16
23
  def self.enabled?(name)
17
24
  !!@experiments[name.to_sym]
18
25
  end
@@ -73,7 +73,7 @@ module Dependabot
73
73
 
74
74
  sig { params(file: Dependabot::DependencyFile, dependency: Dependabot::Dependency).returns(T::Boolean) }
75
75
  def requirement_changed?(file, dependency)
76
- changed_requirements = dependency.requirements - dependency.previous_requirements
76
+ changed_requirements = dependency.requirements - T.must(dependency.previous_requirements)
77
77
 
78
78
  changed_requirements.any? { |f| f[:file] == file.name }
79
79
  end
@@ -153,6 +153,12 @@ module Dependabot
153
153
  @local_tag_for_pinned_sha = most_specific_version_tag_for_sha(ref) if pinned_ref_looks_like_commit_sha?
154
154
  end
155
155
 
156
+ def version_for_pinned_sha
157
+ return unless local_tag_for_pinned_sha && version_class.correct?(local_tag_for_pinned_sha)
158
+
159
+ version_class.new(local_tag_for_pinned_sha)
160
+ end
161
+
156
162
  def git_repo_reachable?
157
163
  local_upload_pack
158
164
  true
@@ -1,49 +1,73 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "excon"
5
5
  require "open3"
6
+ require "sorbet-runtime"
7
+
6
8
  require "dependabot/errors"
9
+ require "dependabot/git_ref"
7
10
 
8
11
  module Dependabot
9
12
  class GitMetadataFetcher
13
+ extend T::Sig
14
+
10
15
  KNOWN_HOSTS = /github\.com|bitbucket\.org|gitlab.com/i
11
16
 
17
+ sig do
18
+ params(
19
+ url: String,
20
+ credentials: T::Array[T::Hash[String, String]]
21
+ )
22
+ .void
23
+ end
12
24
  def initialize(url:, credentials:)
13
25
  @url = url
14
26
  @credentials = credentials
15
27
  end
16
28
 
29
+ sig { returns(T.nilable(String)) }
17
30
  def upload_pack
18
- @upload_pack ||= fetch_upload_pack_for(url)
31
+ @upload_pack ||= T.let(fetch_upload_pack_for(url), T.nilable(String))
19
32
  rescue Octokit::ClientError
20
33
  raise Dependabot::GitDependenciesNotReachable, [url]
21
34
  end
22
35
 
36
+ sig { returns(T::Array[GitRef]) }
23
37
  def tags
24
38
  return [] unless upload_pack
25
39
 
26
- @tags ||= tags_for_upload_pack.map do |ref|
27
- OpenStruct.new(
28
- name: ref.name,
29
- tag_sha: ref.ref_sha,
30
- commit_sha: ref.commit_sha
31
- )
32
- end
40
+ @tags ||= T.let(
41
+ tags_for_upload_pack.map do |ref|
42
+ GitRef.new(
43
+ name: ref.name,
44
+ tag_sha: ref.ref_sha,
45
+ commit_sha: ref.commit_sha
46
+ )
47
+ end,
48
+ T.nilable(T::Array[GitRef])
49
+ )
33
50
  end
34
51
 
52
+ sig { returns(T::Array[GitRef]) }
35
53
  def tags_for_upload_pack
36
- @tags_for_upload_pack ||= refs_for_upload_pack.select { |ref| ref.ref_type == :tag }
54
+ @tags_for_upload_pack ||= T.let(
55
+ refs_for_upload_pack.select { |ref| ref.ref_type == RefType::Tag },
56
+ T.nilable(T::Array[GitRef])
57
+ )
37
58
  end
38
59
 
60
+ sig { returns(T::Array[GitRef]) }
39
61
  def refs_for_upload_pack
40
- @refs_for_upload_pack ||= parse_refs_for_upload_pack
62
+ @refs_for_upload_pack ||= T.let(parse_refs_for_upload_pack, T.nilable(T::Array[GitRef]))
41
63
  end
42
64
 
65
+ sig { returns(T::Array[String]) }
43
66
  def ref_names
44
67
  refs_for_upload_pack.map(&:name)
45
68
  end
46
69
 
70
+ sig { params(ref: String).returns(T.nilable(String)) }
47
71
  def head_commit_for_ref(ref)
48
72
  if ref == "HEAD"
49
73
  # Remove the opening clause of the upload pack as this isn't always
@@ -51,8 +75,8 @@ module Dependabot
51
75
  # causes problems for our `sha_for_update_pack_line` logic. The format
52
76
  # of this opening clause is documented at
53
77
  # https://git-scm.com/docs/http-protocol#_smart_server_response
54
- line = upload_pack.gsub(/^[0-9a-f]{4}# service=git-upload-pack/, "")
55
- .lines.find { |l| l.include?(" HEAD") }
78
+ line = T.must(upload_pack).gsub(/^[0-9a-f]{4}# service=git-upload-pack/, "")
79
+ .lines.find { |l| l.include?(" HEAD") }
56
80
  return sha_for_update_pack_line(line) if line
57
81
  end
58
82
 
@@ -61,6 +85,7 @@ module Dependabot
61
85
  &.commit_sha
62
86
  end
63
87
 
88
+ sig { params(ref: String).returns(T.nilable(String)) }
64
89
  def head_commit_for_ref_sha(ref)
65
90
  refs_for_upload_pack
66
91
  .find { |r| r.ref_sha == ref }
@@ -69,8 +94,13 @@ module Dependabot
69
94
 
70
95
  private
71
96
 
72
- attr_reader :url, :credentials
97
+ sig { returns(String) }
98
+ attr_reader :url
99
+
100
+ sig { returns(T::Array[T::Hash[String, String]]) }
101
+ attr_reader :credentials
73
102
 
103
+ sig { params(uri: String).returns(String) }
74
104
  def fetch_upload_pack_for(uri)
75
105
  response = fetch_raw_upload_pack_for(uri)
76
106
  return response.body if response.status == 200
@@ -97,6 +127,7 @@ module Dependabot
97
127
  raise Dependabot::GitDependenciesNotReachable, [uri]
98
128
  end
99
129
 
130
+ sig { params(uri: String).returns(Excon::Response) }
100
131
  def fetch_raw_upload_pack_for(uri)
101
132
  url = service_pack_uri(uri)
102
133
  url = url.rpartition("@").tap { |a| a.first.gsub!("@", "%40") }.join
@@ -107,6 +138,7 @@ module Dependabot
107
138
  )
108
139
  end
109
140
 
141
+ sig { params(uri: String).returns(T.untyped) }
110
142
  def fetch_raw_upload_pack_with_git_for(uri)
111
143
  service_pack_uri = uri
112
144
  service_pack_uri += ".git" unless service_pack_uri.end_with?(".git") || skip_git_suffix(uri)
@@ -129,11 +161,12 @@ module Dependabot
129
161
  end
130
162
  end
131
163
 
164
+ sig { returns(T::Array[GitRef]) }
132
165
  def parse_refs_for_upload_pack
133
166
  peeled_lines = []
134
167
 
135
- result = upload_pack.lines.each_with_object({}) do |line, res|
136
- full_ref_name = line.split.last
168
+ result = T.must(upload_pack).lines.each_with_object({}) do |line, res|
169
+ full_ref_name = T.must(line.split.last)
137
170
  next unless full_ref_name.start_with?("refs/tags", "refs/heads")
138
171
 
139
172
  (peeled_lines << line) && next if line.strip.end_with?("^{}")
@@ -141,10 +174,10 @@ module Dependabot
141
174
  ref_name = full_ref_name.sub(%r{^refs/(tags|heads)/}, "").strip
142
175
  sha = sha_for_update_pack_line(line)
143
176
 
144
- res[ref_name] = OpenStruct.new(
177
+ res[ref_name] = GitRef.new(
145
178
  name: ref_name,
146
179
  ref_sha: sha,
147
- ref_type: full_ref_name.start_with?("refs/tags") ? :tag : :head,
180
+ ref_type: full_ref_name.start_with?("refs/tags") ? RefType::Tag : RefType::Head,
148
181
  commit_sha: sha
149
182
  )
150
183
  end
@@ -162,6 +195,7 @@ module Dependabot
162
195
  result.values
163
196
  end
164
197
 
198
+ sig { params(uri: String).returns(String) }
165
199
  def service_pack_uri(uri)
166
200
  service_pack_uri = uri_with_auth(uri)
167
201
  service_pack_uri = service_pack_uri.gsub(%r{/$}, "")
@@ -169,6 +203,7 @@ module Dependabot
169
203
  service_pack_uri + "/info/refs?service=git-upload-pack"
170
204
  end
171
205
 
206
+ sig { params(uri: String).returns(T::Boolean) }
172
207
  def skip_git_suffix(uri)
173
208
  # TODO: Unlike the other providers (GitHub, GitLab, BitBucket), as of 2023-01-18 Azure DevOps does not support the
174
209
  # ".git" suffix. It will return a 404.
@@ -188,6 +223,7 @@ module Dependabot
188
223
 
189
224
  # Add in username and password if present in credentials.
190
225
  # Credentials are never present for production Dependabot.
226
+ sig { params(uri: String).returns(String) }
191
227
  def uri_with_auth(uri)
192
228
  uri = SharedHelpers.scp_to_standard(uri)
193
229
  uri = URI(uri)
@@ -196,7 +232,7 @@ module Dependabot
196
232
 
197
233
  uri.scheme = "https" if uri.scheme != "http"
198
234
 
199
- if !uri.password && cred&.fetch("username", nil) && cred&.fetch("password", nil)
235
+ if !uri.password && cred && cred.fetch("username", nil) && cred.fetch("password", nil)
200
236
  # URI doesn't have authentication details, but we have credentials
201
237
  uri.user = URI.encode_www_form_component(cred["username"])
202
238
  uri.password = URI.encode_www_form_component(cred["password"])
@@ -205,10 +241,12 @@ module Dependabot
205
241
  uri.to_s
206
242
  end
207
243
 
244
+ sig { params(line: String).returns(String) }
208
245
  def sha_for_update_pack_line(line)
209
- line.split.first.chars.last(40).join
246
+ T.must(line.split.first).chars.last(40).join
210
247
  end
211
248
 
249
+ sig { returns(T::Hash[Symbol, T.untyped]) }
212
250
  def excon_defaults
213
251
  # Some git hosts are slow when returning a large number of tags
214
252
  SharedHelpers.excon_defaults(read_timeout: 20)
@@ -0,0 +1,71 @@
1
+ # typed: strong
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ class GitRef
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_accessor :name
12
+
13
+ sig { returns(String) }
14
+ attr_accessor :commit_sha
15
+
16
+ sig { returns(T.nilable(String)) }
17
+ attr_reader :tag_sha
18
+
19
+ sig { returns(T.nilable(String)) }
20
+ attr_reader :ref_sha
21
+
22
+ sig { returns(T.nilable(RefType)) }
23
+ attr_reader :ref_type
24
+
25
+ sig do
26
+ params(
27
+ name: String,
28
+ commit_sha: String,
29
+ tag_sha: T.nilable(String),
30
+ ref_sha: T.nilable(String),
31
+ ref_type: T.nilable(RefType)
32
+ )
33
+ .void
34
+ end
35
+ def initialize(name:, commit_sha:, tag_sha: nil, ref_sha: nil, ref_type: nil)
36
+ @name = name
37
+ @commit_sha = commit_sha
38
+ @ref_sha = ref_sha
39
+ @tag_sha = tag_sha
40
+ @ref_type = ref_type
41
+ end
42
+
43
+ sig { params(other: BasicObject).returns(T::Boolean) }
44
+ def ==(other)
45
+ case other
46
+ when GitRef
47
+ to_h == other.to_h
48
+ else
49
+ false
50
+ end
51
+ end
52
+
53
+ sig { returns(T::Hash[Symbol, T.nilable(String)]) }
54
+ def to_h
55
+ {
56
+ name: name,
57
+ commit_sha: commit_sha,
58
+ tag_sha: tag_sha,
59
+ ref_sha: ref_sha,
60
+ ref_type: ref_type
61
+ }.compact
62
+ end
63
+ end
64
+
65
+ class RefType < T::Enum
66
+ enums do
67
+ Tag = new
68
+ Head = new
69
+ end
70
+ end
71
+ end
@@ -1,10 +1,16 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+ require "dependabot/metadata_finders/base"
6
+
4
7
  module Dependabot
5
8
  module MetadataFinders
6
- @metadata_finders = {}
9
+ extend T::Sig
10
+
11
+ @metadata_finders = T.let({}, T::Hash[String, T.class_of(Dependabot::MetadataFinders::Base)])
7
12
 
13
+ sig { params(package_manager: String).returns(T.class_of(Dependabot::MetadataFinders::Base)) }
8
14
  def self.for_package_manager(package_manager)
9
15
  metadata_finder = @metadata_finders[package_manager]
10
16
  return metadata_finder if metadata_finder
@@ -12,6 +18,7 @@ module Dependabot
12
18
  raise "Unsupported package_manager #{package_manager}"
13
19
  end
14
20
 
21
+ sig { params(package_manager: String, metadata_finder: T.class_of(Dependabot::MetadataFinders::Base)).void }
15
22
  def self.register(package_manager, metadata_finder)
16
23
  @metadata_finders[package_manager] = metadata_finder
17
24
  end