dependabot-nuget 0.246.0 → 0.247.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +40 -6
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +27 -0
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +18 -0
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +110 -0
  6. data/lib/dependabot/nuget/cache_manager.rb +9 -3
  7. data/lib/dependabot/nuget/file_fetcher/import_paths_finder.rb +15 -12
  8. data/lib/dependabot/nuget/file_fetcher/sln_project_paths_finder.rb +13 -3
  9. data/lib/dependabot/nuget/file_fetcher.rb +79 -31
  10. data/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +10 -2
  11. data/lib/dependabot/nuget/file_parser/global_json_parser.rb +10 -2
  12. data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +11 -2
  13. data/lib/dependabot/nuget/file_parser/project_file_parser.rb +140 -41
  14. data/lib/dependabot/nuget/file_parser/property_value_finder.rb +57 -5
  15. data/lib/dependabot/nuget/file_parser.rb +3 -3
  16. data/lib/dependabot/nuget/file_updater/property_value_updater.rb +25 -8
  17. data/lib/dependabot/nuget/file_updater.rb +74 -38
  18. data/lib/dependabot/nuget/http_response_helpers.rb +6 -1
  19. data/lib/dependabot/nuget/metadata_finder.rb +27 -3
  20. data/lib/dependabot/nuget/nuget_client.rb +23 -0
  21. data/lib/dependabot/nuget/requirement.rb +4 -1
  22. data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +26 -15
  23. data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +11 -13
  24. data/lib/dependabot/nuget/update_checker/repository_finder.rb +25 -3
  25. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +2 -2
  26. data/lib/dependabot/nuget/update_checker/version_finder.rb +15 -6
  27. data/lib/dependabot/nuget/update_checker.rb +4 -4
  28. data/lib/dependabot/nuget/version.rb +7 -2
  29. metadata +19 -5
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "nokogiri"
@@ -11,25 +11,38 @@ module Dependabot
11
11
  module Nuget
12
12
  class FileUpdater
13
13
  class PropertyValueUpdater
14
+ extend T::Sig
15
+
16
+ sig { params(dependency_files: T::Array[Dependabot::DependencyFile]).void }
14
17
  def initialize(dependency_files:)
15
18
  @dependency_files = dependency_files
16
19
  end
17
20
 
21
+ sig do
22
+ params(property_name: String,
23
+ updated_value: String,
24
+ callsite_file: Dependabot::DependencyFile)
25
+ .returns(T::Array[Dependabot::DependencyFile])
26
+ end
18
27
  def update_files_for_property_change(property_name:, updated_value:,
19
28
  callsite_file:)
20
29
  declaration_details =
21
- property_value_finder
22
- .property_details(
30
+ property_value_finder.property_details(
23
31
  property_name: property_name,
24
32
  callsite_file: callsite_file
25
33
  )
34
+ throw "Unable to locate property details" unless declaration_details
26
35
 
36
+ declaration_filename = declaration_details.fetch(:file)
27
37
  declaration_file = dependency_files.find do |f|
28
- declaration_details.fetch(:file) == f.name
38
+ declaration_filename == f.name
29
39
  end
40
+ throw "Unable to locate declaration file" unless declaration_file
41
+
42
+ content = T.must(declaration_file.content)
30
43
  node = declaration_details.fetch(:node)
31
44
 
32
- updated_content = declaration_file.content.sub(
45
+ updated_content = content.sub(
33
46
  %r{(<#{Regexp.quote(node.name)}(?:\s[^>]*)?>)
34
47
  \s*#{Regexp.quote(node.content)}\s*
35
48
  </#{Regexp.quote(node.name)}>}xm,
@@ -37,21 +50,25 @@ module Dependabot
37
50
  )
38
51
 
39
52
  files = dependency_files.dup
40
- files[files.index(declaration_file)] =
53
+ file_index = T.must(files.index(declaration_file))
54
+ files[file_index] =
41
55
  update_file(file: declaration_file, content: updated_content)
42
56
  files
43
57
  end
44
58
 
45
59
  private
46
60
 
61
+ sig { returns(T::Array[DependencyFile]) }
47
62
  attr_reader :dependency_files
48
63
 
64
+ sig { returns(FileParser::PropertyValueFinder) }
49
65
  def property_value_finder
50
66
  @property_value_finder ||=
51
- Nuget::FileParser::PropertyValueFinder
52
- .new(dependency_files: dependency_files)
67
+ T.let(FileParser::PropertyValueFinder
68
+ .new(dependency_files: dependency_files), T.nilable(FileParser::PropertyValueFinder))
53
69
  end
54
70
 
71
+ sig { params(file: DependencyFile, content: String).returns(DependencyFile) }
55
72
  def update_file(file:, content:)
56
73
  updated_file = file.dup
57
74
  updated_file.content = content
@@ -1,10 +1,11 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/dependency_file"
5
5
  require "dependabot/file_updaters"
6
6
  require "dependabot/file_updaters/base"
7
7
  require "dependabot/nuget/native_helpers"
8
+ require "dependabot/shared_helpers"
8
9
  require "sorbet-runtime"
9
10
 
10
11
  module Dependabot
@@ -17,6 +18,7 @@ module Dependabot
17
18
  require_relative "file_parser/dotnet_tools_json_parser"
18
19
  require_relative "file_parser/packages_config_parser"
19
20
 
21
+ sig { override.returns(T::Array[Regexp]) }
20
22
  def self.updated_files_regex
21
23
  [
22
24
  %r{^[^/]*\.([a-z]{2})?proj$},
@@ -29,32 +31,33 @@ module Dependabot
29
31
  ]
30
32
  end
31
33
 
34
+ sig { override.returns(T::Array[Dependabot::DependencyFile]) }
32
35
  def updated_dependency_files
33
- dependencies.each do |dependency|
34
- try_update_projects(dependency) || try_update_json(dependency)
35
- end
36
+ base_dir = T.must(dependency_files.first).directory
37
+ SharedHelpers.in_a_temporary_repo_directory(base_dir, repo_contents_path) do
38
+ dependencies.each do |dependency|
39
+ try_update_projects(dependency) || try_update_json(dependency)
40
+ end
41
+ updated_files = dependency_files.filter_map do |f|
42
+ updated_content = File.read(dependency_file_path(f))
43
+ next if updated_content == f.content
36
44
 
37
- # update all with content from disk
38
- updated_files = dependency_files.filter_map do |f|
39
- updated_content = File.read(dependency_file_path(f))
40
- next if updated_content == f.content
45
+ normalized_content = normalize_content(f, updated_content)
46
+ next if normalized_content == f.content
41
47
 
42
- normalized_content = normalize_content(f, updated_content)
43
- next if normalized_content == f.content
48
+ next if only_deleted_lines?(f.content, normalized_content)
44
49
 
45
- puts "The contents of file [#{f.name}] were updated."
50
+ puts "The contents of file [#{f.name}] were updated."
46
51
 
47
- updated_file(file: f, content: normalized_content)
52
+ updated_file(file: f, content: normalized_content)
53
+ end
54
+ updated_files
48
55
  end
49
-
50
- # reset repo files
51
- SharedHelpers.reset_git_repo(T.cast(repo_contents_path, String)) if repo_contents_path
52
-
53
- updated_files
54
56
  end
55
57
 
56
58
  private
57
59
 
60
+ sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
58
61
  def try_update_projects(dependency)
59
62
  update_ran = T.let(false, T::Boolean)
60
63
  checked_files = Set.new
@@ -64,7 +67,7 @@ module Dependabot
64
67
  project_dependencies = project_dependencies(project_file)
65
68
  proj_path = dependency_file_path(project_file)
66
69
 
67
- next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
70
+ next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name)&.zero? }
68
71
 
69
72
  next unless repo_contents_path
70
73
 
@@ -79,16 +82,16 @@ module Dependabot
79
82
  end
80
83
  update_ran = true
81
84
  end
82
-
83
85
  update_ran
84
86
  end
85
87
 
88
+ sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) }
86
89
  def try_update_json(dependency)
87
- if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? } ||
88
- global_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
90
+ if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name)&.zero? } ||
91
+ global_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name)&.zero? }
89
92
 
90
93
  # We just need to feed the updater a project file, grab the first
91
- project_file = project_files.first
94
+ project_file = T.must(project_files.first)
92
95
  proj_path = dependency_file_path(project_file)
93
96
 
94
97
  return false unless repo_contents_path
@@ -109,13 +112,14 @@ module Dependabot
109
112
  # Tests need to track how many times we call the tooling updater to ensure we don't recurse needlessly
110
113
  # Ideally we should find a way to not run this code in prod
111
114
  # (or a better way to track calls made to NativeHelpers)
112
- @update_tooling_calls ||= {}
115
+ @update_tooling_calls ||= T.let({}, T.nilable(T::Hash[String, Integer]))
113
116
  key = proj_path + dependency.name
114
- if @update_tooling_calls[key]
115
- @update_tooling_calls[key] += 1
116
- else
117
- @update_tooling_calls[key] = 1
118
- end
117
+ @update_tooling_calls[key] =
118
+ if @update_tooling_calls[key]
119
+ T.must(@update_tooling_calls[key]) + 1
120
+ else
121
+ 1
122
+ end
119
123
  end
120
124
 
121
125
  # Don't call this from outside tests, we're only checking that we aren't recursing needlessly
@@ -124,6 +128,7 @@ module Dependabot
124
128
  @update_tooling_calls
125
129
  end
126
130
 
131
+ sig { params(project_file: Dependabot::DependencyFile).returns(T::Array[Dependabot::Dependency]) }
127
132
  def project_dependencies(project_file)
128
133
  # Collect all dependencies from the project file and associated packages.config
129
134
  dependencies = project_file_parser.dependency_set(project_file: project_file).dependencies
@@ -134,38 +139,54 @@ module Dependabot
134
139
  .dependency_set.dependencies
135
140
  end
136
141
 
142
+ sig { params(project_file: Dependabot::DependencyFile).returns(T.nilable(Dependabot::DependencyFile)) }
137
143
  def find_packages_config(project_file)
138
144
  project_file_name = File.basename(project_file.name)
139
145
  packages_config_path = project_file.name.gsub(project_file_name, "packages.config")
140
146
  packages_config_files.find { |f| f.name == packages_config_path }
141
147
  end
142
148
 
149
+ sig { returns(Dependabot::Nuget::FileParser::ProjectFileParser) }
143
150
  def project_file_parser
144
151
  @project_file_parser ||=
145
- FileParser::ProjectFileParser.new(
146
- dependency_files: dependency_files,
147
- credentials: credentials,
148
- repo_contents_path: repo_contents_path
152
+ T.let(
153
+ FileParser::ProjectFileParser.new(
154
+ dependency_files: dependency_files,
155
+ credentials: credentials,
156
+ repo_contents_path: repo_contents_path
157
+ ),
158
+ T.nilable(Dependabot::Nuget::FileParser::ProjectFileParser)
149
159
  )
150
160
  end
151
161
 
162
+ sig { returns(T::Array[Dependabot::Dependency]) }
152
163
  def global_json_dependencies
153
164
  return [] unless global_json
154
165
 
155
166
  @global_json_dependencies ||=
156
- FileParser::GlobalJsonParser.new(global_json: global_json).dependency_set.dependencies
167
+ T.let(
168
+ FileParser::GlobalJsonParser.new(global_json: T.must(global_json)).dependency_set.dependencies,
169
+ T.nilable(T::Array[Dependabot::Dependency])
170
+ )
157
171
  end
158
172
 
173
+ sig { returns(T::Array[Dependabot::Dependency]) }
159
174
  def dotnet_tools_json_dependencies
160
175
  return [] unless dotnet_tools_json
161
176
 
162
177
  @dotnet_tools_json_dependencies ||=
163
- FileParser::DotNetToolsJsonParser.new(dotnet_tools_json: dotnet_tools_json).dependency_set.dependencies
178
+ T.let(
179
+ FileParser::DotNetToolsJsonParser.new(dotnet_tools_json: T.must(dotnet_tools_json))
180
+ .dependency_set.dependencies,
181
+ T.nilable(T::Array[Dependabot::Dependency])
182
+ )
164
183
  end
165
184
 
185
+ # rubocop:disable Metrics/PerceivedComplexity
186
+ sig { params(dependency_file: Dependabot::DependencyFile, updated_content: String).returns(String) }
166
187
  def normalize_content(dependency_file, updated_content)
167
188
  # Fix up line endings
168
- if dependency_file.content.include?("\r\n") && updated_content.match?(/(?<!\r)\n/)
189
+ if dependency_file.content&.include?("\r\n") && updated_content.match?(/(?<!\r)\n/)
169
190
  # The original content contain windows style newlines.
170
191
  # Ensure the updated content also uses windows style newlines.
171
192
  updated_content = updated_content.gsub(/(?<!\r)\n/, "\r\n")
@@ -178,19 +199,21 @@ module Dependabot
178
199
  end
179
200
 
180
201
  # Fix up BOM
181
- if !dependency_file.content.start_with?("\uFEFF") && updated_content.start_with?("\uFEFF")
202
+ if !dependency_file.content&.start_with?("\uFEFF") && updated_content.start_with?("\uFEFF")
182
203
  updated_content = updated_content.delete_prefix("\uFEFF")
183
204
  puts "Removing BOM from [#{dependency_file.name}]."
184
- elsif dependency_file.content.start_with?("\uFEFF") && !updated_content.start_with?("\uFEFF")
205
+ elsif dependency_file.content&.start_with?("\uFEFF") && !updated_content.start_with?("\uFEFF")
185
206
  updated_content = "\uFEFF" + updated_content
186
207
  puts "Adding BOM to [#{dependency_file.name}]."
187
208
  end
188
209
 
189
210
  updated_content
190
211
  end
212
+ # rubocop:enable Metrics/PerceivedComplexity
191
213
 
214
+ sig { params(dependency_file: Dependabot::DependencyFile).returns(String) }
192
215
  def dependency_file_path(dependency_file)
193
- if dependency_file.directory.start_with?(repo_contents_path)
216
+ if dependency_file.directory.start_with?(T.must(repo_contents_path))
194
217
  File.join(dependency_file.directory, dependency_file.name)
195
218
  else
196
219
  file_directory = dependency_file.directory
@@ -199,29 +222,42 @@ module Dependabot
199
222
  end
200
223
  end
201
224
 
225
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
202
226
  def project_files
203
227
  dependency_files.select { |df| df.name.match?(/\.([a-z]{2})?proj$/) }
204
228
  end
205
229
 
230
+ sig { returns(T::Array[Dependabot::DependencyFile]) }
206
231
  def packages_config_files
207
232
  dependency_files.select do |f|
208
233
  T.must(T.must(f.name.split("/").last).casecmp("packages.config")).zero?
209
234
  end
210
235
  end
211
236
 
237
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
212
238
  def global_json
213
239
  dependency_files.find { |f| T.must(f.name.casecmp("global.json")).zero? }
214
240
  end
215
241
 
242
+ sig { returns(T.nilable(Dependabot::DependencyFile)) }
216
243
  def dotnet_tools_json
217
244
  dependency_files.find { |f| T.must(f.name.casecmp(".config/dotnet-tools.json")).zero? }
218
245
  end
219
246
 
247
+ sig { override.void }
220
248
  def check_required_files
221
249
  return if project_files.any? || packages_config_files.any?
222
250
 
223
251
  raise "No project file or packages.config!"
224
252
  end
253
+
254
+ sig { params(original_content: T.nilable(String), updated_content: String).returns(T::Boolean) }
255
+ def only_deleted_lines?(original_content, updated_content)
256
+ original_lines = original_content&.lines || []
257
+ updated_lines = updated_content.lines
258
+
259
+ original_lines.count > updated_lines.count
260
+ end
225
261
  end
226
262
  end
227
263
  end
@@ -1,9 +1,14 @@
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 Nuget
6
8
  module HttpResponseHelpers
9
+ extend T::Sig
10
+
11
+ sig { params(string: String).returns(String) }
7
12
  def self.remove_wrapping_zero_width_chars(string)
8
13
  string.force_encoding("UTF-8").encode
9
14
  .gsub(/\A[\u200B-\u200D\uFEFF]/, "")
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "nokogiri"
@@ -12,13 +12,28 @@ module Dependabot
12
12
  class MetadataFinder < Dependabot::MetadataFinders::Base
13
13
  extend T::Sig
14
14
 
15
+ sig do
16
+ override
17
+ .params(
18
+ dependency: Dependabot::Dependency,
19
+ credentials: T::Array[Dependabot::Credential]
20
+ )
21
+ .void
22
+ end
23
+ def initialize(dependency:, credentials:)
24
+ @dependency_nuspec_file = T.let(nil, T.nilable(Nokogiri::XML::Document))
25
+
26
+ super
27
+ end
28
+
15
29
  private
16
30
 
31
+ sig { override.returns(T.nilable(Dependabot::Source)) }
17
32
  def look_up_source
18
33
  return Source.from_url(dependency_source_url) if dependency_source_url
19
34
 
20
35
  if dependency_nuspec_file
21
- src_repo = look_up_source_in_nuspec(dependency_nuspec_file)
36
+ src_repo = look_up_source_in_nuspec(T.must(dependency_nuspec_file))
22
37
  return src_repo if src_repo
23
38
  end
24
39
 
@@ -33,6 +48,7 @@ module Dependabot
33
48
  nil
34
49
  end
35
50
 
51
+ sig { returns(T.nilable(Dependabot::Source)) }
36
52
  def src_repo_from_project
37
53
  source = dependency.requirements.find { |r| r.fetch(:source) }&.fetch(:source)
38
54
  return unless source
@@ -60,6 +76,7 @@ module Dependabot
60
76
  # Ignored, this is expected for some registries that don't handle these request.
61
77
  end
62
78
 
79
+ sig { params(body: String).returns(T.nilable(String)) }
63
80
  def extract_search_url(body)
64
81
  JSON.parse(body)
65
82
  .fetch("resources", [])
@@ -67,6 +84,7 @@ module Dependabot
67
84
  &.fetch("@id")
68
85
  end
69
86
 
87
+ sig { params(body: String).returns(T.nilable(Dependabot::Source)) }
70
88
  def extract_source_repo(body)
71
89
  JSON.parse(body).fetch("data", []).each do |search_result|
72
90
  next unless search_result["id"].casecmp(dependency.name).zero?
@@ -84,6 +102,7 @@ module Dependabot
84
102
  nil
85
103
  end
86
104
 
105
+ sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(Dependabot::Source)) }
87
106
  def look_up_source_in_nuspec(nuspec)
88
107
  potential_source_urls = [
89
108
  nuspec.at_css("package > metadata > repository")
@@ -99,6 +118,7 @@ module Dependabot
99
118
  Source.from_url(source_url)
100
119
  end
101
120
 
121
+ sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(String)) }
102
122
  def source_from_anywhere_in_nuspec(nuspec)
103
123
  github_urls = []
104
124
  nuspec.to_s.force_encoding(Encoding::UTF_8)
@@ -112,19 +132,21 @@ module Dependabot
112
132
  end
113
133
  end
114
134
 
135
+ sig { returns(T.nilable(Nokogiri::XML::Document)) }
115
136
  def dependency_nuspec_file
116
137
  return @dependency_nuspec_file unless @dependency_nuspec_file.nil?
117
138
 
118
139
  return if dependency_nuspec_url.nil?
119
140
 
120
141
  response = Dependabot::RegistryClient.get(
121
- url: dependency_nuspec_url,
142
+ url: T.must(dependency_nuspec_url),
122
143
  headers: auth_header
123
144
  )
124
145
 
125
146
  @dependency_nuspec_file = Nokogiri::XML(response.body)
126
147
  end
127
148
 
149
+ sig { returns(T.nilable(String)) }
128
150
  def dependency_nuspec_url
129
151
  source = dependency.requirements
130
152
  .find { |r| r.fetch(:source) }&.fetch(:source)
@@ -132,6 +154,7 @@ module Dependabot
132
154
  source.fetch(:nuspec_url) if source&.key?(:nuspec_url)
133
155
  end
134
156
 
157
+ sig { returns(T.nilable(String)) }
135
158
  def dependency_source_url
136
159
  source = dependency.requirements
137
160
  .find { |r| r.fetch(:source) }&.fetch(:source)
@@ -143,6 +166,7 @@ module Dependabot
143
166
  end
144
167
 
145
168
  # rubocop:disable Metrics/PerceivedComplexity
169
+ sig { returns(T::Hash[String, String]) }
146
170
  def auth_header
147
171
  source = dependency.requirements
148
172
  .find { |r| r.fetch(:source) }&.fetch(:source)
@@ -21,11 +21,33 @@ module Dependabot
21
21
  get_package_versions_v3(dependency_name, repository_details)
22
22
  elsif repository_type == "v2"
23
23
  get_package_versions_v2(dependency_name, repository_details)
24
+ elsif repository_type == "local"
25
+ get_package_versions_local(dependency_name, repository_details)
24
26
  else
25
27
  raise "Unknown repository type: #{repository_type}"
26
28
  end
27
29
  end
28
30
 
31
+ sig do
32
+ params(dependency_name: String, repository_details: T::Hash[Symbol, String])
33
+ .returns(T.nilable(T::Set[String]))
34
+ end
35
+ private_class_method def self.get_package_versions_local(dependency_name, repository_details)
36
+ url = repository_details.fetch(:base_url)
37
+ raise "Local repo #{url} doesn't exist or isn't a directory" unless File.exist?(url) && File.directory?(url)
38
+
39
+ package_dir = File.join(url, dependency_name)
40
+
41
+ versions = Set.new
42
+ return versions unless File.exist?(package_dir) && File.directory?(package_dir)
43
+
44
+ Dir.each_child(package_dir) do |child|
45
+ versions.add(child) if File.directory?(File.join(package_dir, child))
46
+ end
47
+
48
+ versions
49
+ end
50
+
29
51
  sig do
30
52
  params(dependency_name: String, repository_details: T::Hash[Symbol, String])
31
53
  .returns(T.nilable(T::Set[String]))
@@ -133,6 +155,7 @@ module Dependabot
133
155
  &.find { |d| d.fetch("id").casecmp(dependency_name.downcase).zero? }
134
156
  &.fetch("versions")
135
157
  &.map { |d| d.fetch("version") }
158
+ &.to_set
136
159
  end
137
160
 
138
161
  sig do
@@ -60,6 +60,7 @@ module Dependabot
60
60
  convert_wildcard_req(req_string)
61
61
  end
62
62
 
63
+ # rubocop:disable Metrics/PerceivedComplexity
63
64
  def convert_dotnet_range_to_ruby_range(req_string)
64
65
  lower_b, upper_b = req_string.split(",").map(&:strip).map do |bound|
65
66
  next convert_range_wildcard_req(bound) if bound.include?("*")
@@ -75,7 +76,8 @@ module Dependabot
75
76
  end
76
77
 
77
78
  upper_b =
78
- if [")", "]"].include?(upper_b) then nil
79
+ if !upper_b then nil
80
+ elsif [")", "]"].include?(upper_b) then nil
79
81
  elsif upper_b.end_with?(")") then "< #{upper_b.sub(/\s*\)/, '')}"
80
82
  else
81
83
  "<= #{upper_b.sub(/\s*\]/, '').strip}"
@@ -83,6 +85,7 @@ module Dependabot
83
85
 
84
86
  [lower_b, upper_b].compact
85
87
  end
88
+ # rubocop:enable Metrics/PerceivedComplexity
86
89
 
87
90
  def convert_range_wildcard_req(req_string)
88
91
  range_end = req_string[-1]
@@ -66,23 +66,34 @@ module Dependabot
66
66
  end
67
67
 
68
68
  def fetch_package_tfms(dependency_version)
69
- nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
70
- return [] unless nupkg_buffer
71
-
72
- # Parse tfms from the folders beneath the lib folder
73
- folder_name = "lib/"
74
- tfms = Set.new
75
- Zip::File.open_buffer(nupkg_buffer) do |zip|
76
- lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
77
- # If there is no lib folder in this package, assume it is a development dependency
78
- return nil if lib_file_entries.empty?
79
-
80
- lib_file_entries.each do |entry|
81
- _, tfm = entry.name.split("/").first(2)
82
- tfms << tfm
69
+ cache = CacheManager.cache("compatibility_checker_tfms_cache")
70
+ key = "#{dependency.name}::#{dependency_version}"
71
+
72
+ cache[key] ||= begin
73
+ nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
74
+ return [] unless nupkg_buffer
75
+
76
+ # Parse tfms from the folders beneath the lib folder
77
+ folder_name = "lib/"
78
+ tfms = Set.new
79
+ Zip::File.open_buffer(nupkg_buffer) do |zip|
80
+ lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
81
+ # If there is no lib folder in this package, assume it is a development dependency
82
+ return nil if lib_file_entries.empty?
83
+
84
+ lib_file_entries.each do |entry|
85
+ _, tfm = entry.name.split("/").first(2)
86
+
87
+ # some zip compressors create empty directory entries (in this case `lib/`) which can cause the string
88
+ # split to return `nil`, so we have to explicitly guard against that
89
+ tfms << tfm if tfm
90
+ end
83
91
  end
92
+
93
+ tfms.to_a
84
94
  end
85
- tfms.to_a
95
+
96
+ cache[key]
86
97
  end
87
98
  end
88
99
  end
@@ -111,25 +111,19 @@ module Dependabot
111
111
  current_redirects = 0
112
112
 
113
113
  loop do
114
- connection = Excon.new(current_url, persistent: true)
115
-
116
- package_data = StringIO.new
117
- response_block = lambda do |chunk, _remaining_bytes, _total_bytes|
118
- package_data.write(chunk)
119
- end
120
-
121
- response = connection.request(
122
- method: :get,
114
+ # Directly download the stream without any additional settings _except_ for `omit_default_port: true` which
115
+ # is necessary to not break the URL signing that some NuGet feeds use.
116
+ response = Excon.get(
117
+ current_url,
123
118
  headers: auth_header,
124
- response_block: response_block
119
+ omit_default_port: true
125
120
  )
126
121
 
127
122
  # redirect the HTTP response as appropriate based on documentation here:
128
123
  # https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
129
124
  case response.status
130
125
  when 200
131
- package_data.rewind
132
- return package_data
126
+ return response.body
133
127
  when 301, 302, 303, 307, 308
134
128
  current_redirects += 1
135
129
  return nil if current_redirects > max_redirects
@@ -142,10 +136,14 @@ module Dependabot
142
136
  end
143
137
 
144
138
  def self.fetch_url(url, repository_details)
139
+ fetch_url_with_auth(url, repository_details.fetch(:auth_header))
140
+ end
141
+
142
+ def self.fetch_url_with_auth(url, auth_header)
145
143
  cache = CacheManager.cache("nupkg_fetcher_cache")
146
144
  cache[url] ||= Dependabot::RegistryClient.get(
147
145
  url: url,
148
- headers: repository_details.fetch(:auth_header)
146
+ headers: auth_header
149
147
  )
150
148
 
151
149
  cache[url]