dependabot-nuget 0.245.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/PackagesConfigUpdater.cs +42 -7
  3. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/SdkPackageUpdater.cs +164 -90
  4. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Updater/UpdaterWorker.cs +38 -2
  5. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/MSBuildHelper.cs +92 -18
  6. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/NuGetHelper.cs +1 -1
  7. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core/Utilities/PathHelper.cs +27 -0
  8. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTestBase.cs +115 -14
  9. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/{UpdateWorker.DirsProj.cs → UpdateWorkerTests.DirsProj.cs} +22 -24
  10. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.PackagesConfig.cs +66 -0
  11. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Update/UpdateWorkerTests.Sdk.cs +373 -83
  12. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/MSBuildHelperTests.cs +117 -4
  13. data/lib/dependabot/nuget/cache_manager.rb +9 -3
  14. data/lib/dependabot/nuget/file_fetcher/import_paths_finder.rb +15 -12
  15. data/lib/dependabot/nuget/file_fetcher/sln_project_paths_finder.rb +13 -3
  16. data/lib/dependabot/nuget/file_fetcher.rb +79 -31
  17. data/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +10 -2
  18. data/lib/dependabot/nuget/file_parser/global_json_parser.rb +10 -2
  19. data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +11 -2
  20. data/lib/dependabot/nuget/file_parser/project_file_parser.rb +140 -45
  21. data/lib/dependabot/nuget/file_parser/property_value_finder.rb +57 -5
  22. data/lib/dependabot/nuget/file_parser.rb +18 -4
  23. data/lib/dependabot/nuget/file_updater/property_value_updater.rb +25 -8
  24. data/lib/dependabot/nuget/file_updater.rb +74 -38
  25. data/lib/dependabot/nuget/http_response_helpers.rb +19 -0
  26. data/lib/dependabot/nuget/metadata_finder.rb +32 -4
  27. data/lib/dependabot/nuget/nuget_client.rb +31 -13
  28. data/lib/dependabot/nuget/requirement.rb +4 -1
  29. data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +26 -15
  30. data/lib/dependabot/nuget/update_checker/dependency_finder.rb +23 -13
  31. data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +83 -21
  32. data/lib/dependabot/nuget/update_checker/repository_finder.rb +29 -13
  33. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +2 -2
  34. data/lib/dependabot/nuget/update_checker/version_finder.rb +15 -6
  35. data/lib/dependabot/nuget/update_checker.rb +6 -7
  36. data/lib/dependabot/nuget/version.rb +7 -2
  37. metadata +21 -7
  38. data/helpers/lib/NuGetUpdater/NuGetUpdater.Core.Test/Utilities/SdkPackageUpdaterTests.cs +0 -317
@@ -1,4 +1,4 @@
1
- # typed: strict
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "nokogiri"
@@ -30,7 +30,21 @@ module Dependabot
30
30
  dependency_set += packages_config_dependencies
31
31
  dependency_set += global_json_dependencies if global_json
32
32
  dependency_set += dotnet_tools_json_dependencies if dotnet_tools_json
33
- dependency_set.dependencies
33
+
34
+ (dependencies, deps_with_unresolved_versions) = dependency_set.dependencies.partition do |d|
35
+ # try to parse the version; don't care about result, just that it succeeded
36
+ _ = Version.new(d.version)
37
+ true
38
+ rescue ArgumentError
39
+ # version could not be parsed
40
+ false
41
+ end
42
+
43
+ deps_with_unresolved_versions.each do |d|
44
+ Dependabot.logger.warn "Dependency '#{d.name}' excluded due to unparsable version: #{d.version}"
45
+ end
46
+
47
+ dependencies
34
48
  end
35
49
 
36
50
  private
@@ -63,14 +77,14 @@ module Dependabot
63
77
  def global_json_dependencies
64
78
  return DependencySet.new unless global_json
65
79
 
66
- GlobalJsonParser.new(global_json: global_json).dependency_set
80
+ GlobalJsonParser.new(global_json: T.must(global_json)).dependency_set
67
81
  end
68
82
 
69
83
  sig { returns(Dependabot::FileParsers::Base::DependencySet) }
70
84
  def dotnet_tools_json_dependencies
71
85
  return DependencySet.new unless dotnet_tools_json
72
86
 
73
- DotNetToolsJsonParser.new(dotnet_tools_json: dotnet_tools_json).dependency_set
87
+ DotNetToolsJsonParser.new(dotnet_tools_json: T.must(dotnet_tools_json)).dependency_set
74
88
  end
75
89
 
76
90
  sig { returns(Dependabot::Nuget::FileParser::ProjectFileParser) }
@@ -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
@@ -0,0 +1,19 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ module HttpResponseHelpers
9
+ extend T::Sig
10
+
11
+ sig { params(string: String).returns(String) }
12
+ def self.remove_wrapping_zero_width_chars(string)
13
+ string.force_encoding("UTF-8").encode
14
+ .gsub(/\A[\u200B-\u200D\uFEFF]/, "")
15
+ .gsub(/[\u200B-\u200D\uFEFF]\Z/, "")
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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,30 @@ 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
- src_repo = look_up_source_in_nuspec(dependency_nuspec_file)
21
- return src_repo if src_repo
35
+ if dependency_nuspec_file
36
+ src_repo = look_up_source_in_nuspec(T.must(dependency_nuspec_file))
37
+ return src_repo if src_repo
38
+ end
22
39
 
23
40
  # Fallback to getting source from the search result's projectUrl or licenseUrl.
24
41
  # GitHub Packages doesn't support getting the `.nuspec`, switch to getting
@@ -31,6 +48,7 @@ module Dependabot
31
48
  nil
32
49
  end
33
50
 
51
+ sig { returns(T.nilable(Dependabot::Source)) }
34
52
  def src_repo_from_project
35
53
  source = dependency.requirements.find { |r| r.fetch(:source) }&.fetch(:source)
36
54
  return unless source
@@ -58,6 +76,7 @@ module Dependabot
58
76
  # Ignored, this is expected for some registries that don't handle these request.
59
77
  end
60
78
 
79
+ sig { params(body: String).returns(T.nilable(String)) }
61
80
  def extract_search_url(body)
62
81
  JSON.parse(body)
63
82
  .fetch("resources", [])
@@ -65,6 +84,7 @@ module Dependabot
65
84
  &.fetch("@id")
66
85
  end
67
86
 
87
+ sig { params(body: String).returns(T.nilable(Dependabot::Source)) }
68
88
  def extract_source_repo(body)
69
89
  JSON.parse(body).fetch("data", []).each do |search_result|
70
90
  next unless search_result["id"].casecmp(dependency.name).zero?
@@ -82,6 +102,7 @@ module Dependabot
82
102
  nil
83
103
  end
84
104
 
105
+ sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(Dependabot::Source)) }
85
106
  def look_up_source_in_nuspec(nuspec)
86
107
  potential_source_urls = [
87
108
  nuspec.at_css("package > metadata > repository")
@@ -97,6 +118,7 @@ module Dependabot
97
118
  Source.from_url(source_url)
98
119
  end
99
120
 
121
+ sig { params(nuspec: Nokogiri::XML::Document).returns(T.nilable(String)) }
100
122
  def source_from_anywhere_in_nuspec(nuspec)
101
123
  github_urls = []
102
124
  nuspec.to_s.force_encoding(Encoding::UTF_8)
@@ -110,17 +132,21 @@ module Dependabot
110
132
  end
111
133
  end
112
134
 
135
+ sig { returns(T.nilable(Nokogiri::XML::Document)) }
113
136
  def dependency_nuspec_file
114
137
  return @dependency_nuspec_file unless @dependency_nuspec_file.nil?
115
138
 
139
+ return if dependency_nuspec_url.nil?
140
+
116
141
  response = Dependabot::RegistryClient.get(
117
- url: dependency_nuspec_url,
142
+ url: T.must(dependency_nuspec_url),
118
143
  headers: auth_header
119
144
  )
120
145
 
121
146
  @dependency_nuspec_file = Nokogiri::XML(response.body)
122
147
  end
123
148
 
149
+ sig { returns(T.nilable(String)) }
124
150
  def dependency_nuspec_url
125
151
  source = dependency.requirements
126
152
  .find { |r| r.fetch(:source) }&.fetch(:source)
@@ -128,6 +154,7 @@ module Dependabot
128
154
  source.fetch(:nuspec_url) if source&.key?(:nuspec_url)
129
155
  end
130
156
 
157
+ sig { returns(T.nilable(String)) }
131
158
  def dependency_source_url
132
159
  source = dependency.requirements
133
160
  .find { |r| r.fetch(:source) }&.fetch(:source)
@@ -139,6 +166,7 @@ module Dependabot
139
166
  end
140
167
 
141
168
  # rubocop:disable Metrics/PerceivedComplexity
169
+ sig { returns(T::Hash[String, String]) }
142
170
  def auth_header
143
171
  source = dependency.requirements
144
172
  .find { |r| r.fetch(:source) }&.fetch(:source)
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "dependabot/nuget/cache_manager"
5
+ require "dependabot/nuget/http_response_helpers"
5
6
  require "dependabot/nuget/update_checker/repository_finder"
6
7
  require "sorbet-runtime"
7
8
 
@@ -20,11 +21,33 @@ module Dependabot
20
21
  get_package_versions_v3(dependency_name, repository_details)
21
22
  elsif repository_type == "v2"
22
23
  get_package_versions_v2(dependency_name, repository_details)
24
+ elsif repository_type == "local"
25
+ get_package_versions_local(dependency_name, repository_details)
23
26
  else
24
27
  raise "Unknown repository type: #{repository_type}"
25
28
  end
26
29
  end
27
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
+
28
51
  sig do
29
52
  params(dependency_name: String, repository_details: T::Hash[Symbol, String])
30
53
  .returns(T.nilable(T::Set[String]))
@@ -52,14 +75,15 @@ module Dependabot
52
75
  doc = execute_xml_nuget_request(repository_details.fetch(:versions_url), repository_details)
53
76
  return unless doc
54
77
 
55
- id_nodes = doc.xpath("/feed/entry/properties/Id")
78
+ # v2 APIs can differ, but all tested have this title value set to the name of the package
79
+ title_nodes = doc.xpath("/feed/entry/title")
56
80
  matching_versions = Set.new
57
- id_nodes.each do |id_node|
58
- return nil unless id_node.text
81
+ title_nodes.each do |title_node|
82
+ return nil unless title_node.text
59
83
 
60
- next unless id_node.text.casecmp?(dependency_name)
84
+ next unless title_node.text.casecmp?(dependency_name)
61
85
 
62
- version_node = id_node.parent.xpath("Version")
86
+ version_node = title_node.parent.xpath("properties/Version")
63
87
  matching_versions << version_node.text if version_node && version_node.text
64
88
  end
65
89
 
@@ -131,6 +155,7 @@ module Dependabot
131
155
  &.find { |d| d.fetch("id").casecmp(dependency_name.downcase).zero? }
132
156
  &.fetch("versions")
133
157
  &.map { |d| d.fetch("version") }
158
+ &.to_set
134
159
  end
135
160
 
136
161
  sig do
@@ -162,7 +187,7 @@ module Dependabot
162
187
  )
163
188
  return unless response.status == 200
164
189
 
165
- body = remove_wrapping_zero_width_chars(response.body)
190
+ body = HttpResponseHelpers.remove_wrapping_zero_width_chars(response.body)
166
191
  JSON.parse(body)
167
192
  end
168
193
 
@@ -193,13 +218,6 @@ module Dependabot
193
218
 
194
219
  raise PrivateSourceTimedOut, repo_url
195
220
  end
196
-
197
- sig { params(string: String).returns(String) }
198
- private_class_method def self.remove_wrapping_zero_width_chars(string)
199
- string.force_encoding("UTF-8").encode
200
- .gsub(/\A[\u200B-\u200D\uFEFF]/, "")
201
- .gsub(/[\u200B-\u200D\uFEFF]\Z/, "")
202
- end
203
221
  end
204
222
  end
205
223
  end
@@ -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]