dependabot-nuget 0.237.0 → 0.239.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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/nuget/cache_manager.rb +22 -0
  3. data/lib/dependabot/nuget/file_fetcher/import_paths_finder.rb +15 -0
  4. data/lib/dependabot/nuget/file_fetcher.rb +61 -64
  5. data/lib/dependabot/nuget/file_parser/dotnet_tools_json_parser.rb +2 -1
  6. data/lib/dependabot/nuget/file_parser/global_json_parser.rb +2 -1
  7. data/lib/dependabot/nuget/file_parser/packages_config_parser.rb +22 -4
  8. data/lib/dependabot/nuget/file_parser/project_file_parser.rb +287 -15
  9. data/lib/dependabot/nuget/file_parser/property_value_finder.rb +24 -52
  10. data/lib/dependabot/nuget/file_parser.rb +4 -1
  11. data/lib/dependabot/nuget/file_updater.rb +123 -117
  12. data/lib/dependabot/nuget/native_helpers.rb +94 -0
  13. data/lib/dependabot/nuget/requirement.rb +5 -1
  14. data/lib/dependabot/nuget/update_checker/compatibility_checker.rb +85 -0
  15. data/lib/dependabot/nuget/update_checker/dependency_finder.rb +228 -0
  16. data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +119 -0
  17. data/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +83 -0
  18. data/lib/dependabot/nuget/update_checker/property_updater.rb +30 -3
  19. data/lib/dependabot/nuget/update_checker/repository_finder.rb +36 -10
  20. data/lib/dependabot/nuget/update_checker/tfm_comparer.rb +31 -0
  21. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +127 -0
  22. data/lib/dependabot/nuget/update_checker/version_finder.rb +47 -6
  23. data/lib/dependabot/nuget/update_checker.rb +42 -8
  24. data/lib/dependabot/nuget.rb +2 -0
  25. metadata +35 -9
  26. data/lib/dependabot/nuget/file_updater/packages_config_declaration_finder.rb +0 -70
  27. data/lib/dependabot/nuget/file_updater/project_file_declaration_finder.rb +0 -183
@@ -5,15 +5,19 @@ require "nokogiri"
5
5
 
6
6
  require "dependabot/dependency"
7
7
  require "dependabot/nuget/file_parser"
8
+ require "dependabot/nuget/update_checker"
9
+ require "dependabot/nuget/cache_manager"
8
10
 
9
11
  # For details on how dotnet handles version constraints, see:
10
12
  # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
11
13
  module Dependabot
12
14
  module Nuget
13
15
  class FileParser
16
+ # rubocop:disable Metrics/ClassLength
14
17
  class ProjectFileParser
15
18
  require "dependabot/file_parsers/base/dependency_set"
16
19
  require_relative "property_value_finder"
20
+ require_relative "../update_checker/repository_finder"
17
21
 
18
22
  DEPENDENCY_SELECTOR = "ItemGroup > PackageReference, " \
19
23
  "ItemGroup > GlobalPackageReference, " \
@@ -21,15 +25,64 @@ module Dependabot
21
25
  "ItemGroup > Dependency, " \
22
26
  "ItemGroup > DevelopmentDependency"
23
27
 
28
+ PROJECT_REFERENCE_SELECTOR = "ItemGroup > ProjectReference"
29
+
30
+ PACKAGE_REFERENCE_SELECTOR = "ItemGroup > PackageReference, " \
31
+ "ItemGroup > GlobalPackageReference"
32
+
33
+ PACKAGE_VERSION_SELECTOR = "ItemGroup > PackageVersion"
34
+
24
35
  PROJECT_SDK_REGEX = %r{^([^/]+)/(\d+(?:[.]\d+(?:[.]\d+)?)?(?:[+-].*)?)$}
25
36
  PROPERTY_REGEX = /\$\((?<property>.*?)\)/
26
37
  ITEM_REGEX = /\@\((?<property>.*?)\)/
27
38
 
28
- def initialize(dependency_files:)
29
- @dependency_files = dependency_files
39
+ def self.dependency_set_cache
40
+ CacheManager.cache("project_file_dependency_set")
41
+ end
42
+
43
+ def self.dependency_url_search_cache
44
+ CacheManager.cache("dependency_url_search_cache")
45
+ end
46
+
47
+ def initialize(dependency_files:, credentials:)
48
+ @dependency_files = dependency_files
49
+ @credentials = credentials
30
50
  end
31
51
 
32
52
  def dependency_set(project_file:)
53
+ return parse_dependencies(project_file) if CacheManager.caching_disabled?
54
+
55
+ key = "#{project_file.name.downcase}::#{project_file.content.hash}"
56
+ cache = ProjectFileParser.dependency_set_cache
57
+
58
+ cache[key] ||= parse_dependencies(project_file)
59
+
60
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
61
+ dependency_set += cache[key]
62
+ dependency_set
63
+ end
64
+
65
+ def target_frameworks(project_file:)
66
+ target_framework = details_for_property("TargetFramework", project_file)
67
+ return [target_framework&.fetch(:value)] if target_framework
68
+
69
+ target_frameworks = details_for_property("TargetFrameworks", project_file)
70
+ return target_frameworks&.fetch(:value)&.split(";") if target_frameworks
71
+
72
+ target_framework = details_for_property("TargetFrameworkVersion", project_file)
73
+ return [] unless target_framework
74
+
75
+ # TargetFrameworkVersion is a string like "v4.7.2"
76
+ value = target_framework&.fetch(:value)
77
+ # convert it to a string like "net472"
78
+ ["net#{value[1..-1].delete('.')}"]
79
+ end
80
+
81
+ private
82
+
83
+ attr_reader :dependency_files, :credentials
84
+
85
+ def parse_dependencies(project_file)
33
86
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
34
87
 
35
88
  doc = Nokogiri::XML(project_file.content)
@@ -46,6 +99,10 @@ module Dependabot
46
99
  dependency_set << dependency if dependency
47
100
  end
48
101
 
102
+ add_global_package_references(dependency_set)
103
+
104
+ add_transitive_dependencies(project_file, doc, dependency_set)
105
+
49
106
  # Look for SDK references; see:
50
107
  # https://docs.microsoft.com/en-us/visualstudio/msbuild/how-to-use-project-sdk
51
108
  add_sdk_references(doc, dependency_set, project_file)
@@ -53,9 +110,86 @@ module Dependabot
53
110
  dependency_set
54
111
  end
55
112
 
56
- private
113
+ def add_global_package_references(dependency_set)
114
+ project_import_files.each do |file|
115
+ doc = Nokogiri::XML(file.content)
116
+ doc.remove_namespaces!
57
117
 
58
- attr_reader :dependency_files
118
+ doc.css(PACKAGE_REFERENCE_SELECTOR).each do |dependency_node|
119
+ name = dependency_name(dependency_node, file)
120
+ req = dependency_requirement(dependency_node, file)
121
+ version = dependency_version(dependency_node, file)
122
+ prop_name = req_property_name(dependency_node)
123
+
124
+ dependency = build_dependency(name, req, version, prop_name, file)
125
+ dependency_set << dependency if dependency
126
+ end
127
+ end
128
+ end
129
+
130
+ def add_transitive_dependencies(project_file, doc, dependency_set)
131
+ add_transitive_dependencies_from_packages(dependency_set)
132
+ add_transitive_dependencies_from_project_references(project_file, doc, dependency_set)
133
+ end
134
+
135
+ def add_transitive_dependencies_from_project_references(project_file, doc, dependency_set)
136
+ project_file_directory = File.dirname(project_file.name)
137
+ is_rooted = project_file_directory.start_with?("/")
138
+ # Root the directory path to avoid expand_path prepending the working directory
139
+ project_file_directory = "/" + project_file_directory unless is_rooted
140
+
141
+ # Look for regular project references
142
+ doc.css(PROJECT_REFERENCE_SELECTOR).each do |reference_node|
143
+ relative_path = dependency_name(reference_node, project_file)
144
+ # This could result from a <ProjectReference Remove="..." /> item.
145
+ next unless relative_path
146
+
147
+ # normalize path separators
148
+ relative_path = relative_path.tr("\\", "/")
149
+ # path is relative to the project file directory
150
+ relative_path = File.join(project_file_directory, relative_path)
151
+
152
+ # get absolute path
153
+ full_path = File.expand_path(relative_path)
154
+ full_path = full_path[1..-1] unless is_rooted
155
+
156
+ referenced_file = dependency_files.find { |f| f.name == full_path }
157
+ next unless referenced_file
158
+
159
+ dependency_set(project_file: referenced_file).dependencies.each do |dep|
160
+ dependency = Dependency.new(
161
+ name: dep.name,
162
+ version: dep.version,
163
+ package_manager: dep.package_manager,
164
+ requirements: []
165
+ )
166
+ dependency_set << dependency
167
+ end
168
+ end
169
+ end
170
+
171
+ def add_transitive_dependencies_from_packages(dependency_set)
172
+ transitive_dependencies_from_packages(dependency_set.dependencies).each { |dep| dependency_set << dep }
173
+ end
174
+
175
+ def transitive_dependencies_from_packages(dependencies)
176
+ transitive_dependencies = {}
177
+
178
+ dependencies.each do |dependency|
179
+ UpdateChecker::DependencyFinder.new(
180
+ dependency: dependency,
181
+ dependency_files: dependency_files,
182
+ credentials: credentials
183
+ ).transitive_dependencies.each do |transitive_dep|
184
+ visited_dep = transitive_dependencies[transitive_dep.name.downcase]
185
+ next if !visited_dep.nil? && visited_dep.numeric_version > transitive_dep.numeric_version
186
+
187
+ transitive_dependencies[transitive_dep.name.downcase] = transitive_dep
188
+ end
189
+ end
190
+
191
+ transitive_dependencies.values
192
+ end
59
193
 
60
194
  def add_sdk_references(doc, dependency_set, project_file)
61
195
  # These come in 3 flavours:
@@ -133,21 +267,88 @@ module Dependabot
133
267
  requirement[:metadata] = { property_name: root_prop_name }
134
268
  end
135
269
 
136
- Dependency.new(
270
+ dependency = Dependency.new(
137
271
  name: name,
138
272
  version: version,
139
273
  package_manager: "nuget",
140
274
  requirements: [requirement]
141
275
  )
276
+
277
+ # only include dependency if one of the sources has it
278
+ return unless dependency_has_search_results?(dependency)
279
+
280
+ dependency
281
+ end
282
+
283
+ def dependency_has_search_results?(dependency)
284
+ nuget_configs = dependency_files.select { |f| f.name.casecmp?("nuget.config") }
285
+ dependency_urls = UpdateChecker::RepositoryFinder.new(
286
+ dependency: dependency,
287
+ credentials: credentials,
288
+ config_files: nuget_configs
289
+ ).dependency_urls
290
+ if dependency_urls.empty?
291
+ dependency_urls = [UpdateChecker::RepositoryFinder.get_default_repository_details(dependency.name)]
292
+ end
293
+ dependency_urls.any? do |dependency_url|
294
+ dependency_url_has_matching_result?(dependency.name, dependency_url)
295
+ end
296
+ end
297
+
298
+ def dependency_url_has_matching_result?(dependency_name, dependency_url)
299
+ repository_type = dependency_url.fetch(:repository_type)
300
+ if repository_type == "v3"
301
+ dependency_url_has_matching_result_v3?(dependency_name, dependency_url)
302
+ elsif repository_type == "v2"
303
+ dependency_url_has_matching_result_v2?(dependency_name, dependency_url)
304
+ else
305
+ raise "Unknown repository type: #{repository_type}"
306
+ end
307
+ end
308
+
309
+ def dependency_url_has_matching_result_v3?(dependency_name, dependency_url)
310
+ url = dependency_url.fetch(:search_url)
311
+ auth_header = dependency_url.fetch(:auth_header)
312
+ response = execute_search_for_dependency_url(url, auth_header)
313
+ return false unless response.status == 200
314
+
315
+ body = JSON.parse(response.body)
316
+ data = body["data"]
317
+ return false unless data.length.positive?
318
+
319
+ data.any? { |result| result["id"].casecmp?(dependency_name) }
320
+ end
321
+
322
+ def dependency_url_has_matching_result_v2?(dependency_name, dependency_url)
323
+ url = dependency_url.fetch(:versions_url)
324
+ auth_header = dependency_url.fetch(:auth_header)
325
+ response = execute_search_for_dependency_url(url, auth_header)
326
+ return false unless response.status == 200
327
+
328
+ doc = Nokogiri::XML(response.body)
329
+ doc.remove_namespaces!
330
+ id_nodes = doc.xpath("/feed/entry/properties/Id")
331
+ found_matching_result = id_nodes.any? do |id_node|
332
+ return false unless id_node.text
333
+
334
+ id_node.text.casecmp?(dependency_name)
335
+ end
336
+ found_matching_result
337
+ end
338
+
339
+ def execute_search_for_dependency_url(url, auth_header)
340
+ cache = ProjectFileParser.dependency_url_search_cache
341
+ cache[url] ||= Dependabot::RegistryClient.get(
342
+ url: url,
343
+ headers: auth_header
344
+ )
345
+
346
+ cache[url]
142
347
  end
143
348
 
144
- # rubocop:disable Metrics/PerceivedComplexity
145
349
  def dependency_name(dependency_node, project_file)
146
- raw_name =
147
- dependency_node.attribute("Include")&.value&.strip ||
148
- dependency_node.at_xpath("./Include")&.content&.strip ||
149
- dependency_node.attribute("Update")&.value&.strip ||
150
- dependency_node.at_xpath("./Update")&.content&.strip
350
+ raw_name = get_attribute_value(dependency_node, "Include") ||
351
+ get_attribute_value(dependency_node, "Update")
151
352
  return unless raw_name
152
353
 
153
354
  # If the item contains @(ItemGroup) then ignore as it
@@ -156,15 +357,51 @@ module Dependabot
156
357
 
157
358
  evaluated_value(raw_name, project_file)
158
359
  end
159
- # rubocop:enable Metrics/PerceivedComplexity
160
360
 
161
361
  def dependency_requirement(dependency_node, project_file)
162
- raw_requirement = get_node_version_value(dependency_node)
362
+ raw_requirement = get_node_version_value(dependency_node) ||
363
+ find_package_version(dependency_node, project_file)
163
364
  return unless raw_requirement
164
365
 
165
366
  evaluated_value(raw_requirement, project_file)
166
367
  end
167
368
 
369
+ def find_package_version(dependency_node, project_file)
370
+ name = dependency_name(dependency_node, project_file)
371
+ return unless name
372
+
373
+ package_version_string = package_versions[name].to_s
374
+ return unless package_version_string != ""
375
+
376
+ package_version_string
377
+ end
378
+
379
+ def package_versions
380
+ @package_versions ||= begin
381
+ package_versions = {}
382
+ directory_packages_props_files.each do |file|
383
+ doc = Nokogiri::XML(file.content)
384
+ doc.remove_namespaces!
385
+ doc.css(PACKAGE_VERSION_SELECTOR).each do |package_node|
386
+ name = dependency_name(package_node, file)
387
+ version = dependency_version(package_node, file)
388
+ next unless name && version
389
+
390
+ version = Version.new(version)
391
+ existing_version = package_versions[name]
392
+ next if existing_version && existing_version > version
393
+
394
+ package_versions[name] = version
395
+ end
396
+ end
397
+ package_versions
398
+ end
399
+ end
400
+
401
+ def directory_packages_props_files
402
+ dependency_files.select { |df| df.name.match?(/[Dd]irectory.[Pp]ackages.props/) }
403
+ end
404
+
168
405
  def dependency_version(dependency_node, project_file)
169
406
  requirement = dependency_requirement(dependency_node, project_file)
170
407
  return unless requirement
@@ -191,9 +428,12 @@ module Dependabot
191
428
  .named_captures.fetch("property")
192
429
  end
193
430
 
194
- # rubocop:disable Metrics/PerceivedComplexity
195
431
  def get_node_version_value(node)
196
- attribute = "Version"
432
+ get_attribute_value(node, "Version") || get_attribute_value(node, "VersionOverride")
433
+ end
434
+
435
+ # rubocop:disable Metrics/PerceivedComplexity
436
+ def get_attribute_value(node, attribute)
197
437
  value =
198
438
  node.attribute(attribute)&.value&.strip ||
199
439
  node.at_xpath("./#{attribute}")&.content&.strip ||
@@ -230,7 +470,39 @@ module Dependabot
230
470
  @property_value_finder ||=
231
471
  PropertyValueFinder.new(dependency_files: dependency_files)
232
472
  end
473
+
474
+ def project_import_files
475
+ dependency_files -
476
+ project_files -
477
+ packages_config_files -
478
+ nuget_configs -
479
+ [global_json] -
480
+ [dotnet_tools_json]
481
+ end
482
+
483
+ def project_files
484
+ dependency_files.select { |f| f.name.match?(/\.[a-z]{2}proj$/) }
485
+ end
486
+
487
+ def packages_config_files
488
+ dependency_files.select do |f|
489
+ f.name.split("/").last.casecmp("packages.config").zero?
490
+ end
491
+ end
492
+
493
+ def nuget_configs
494
+ dependency_files.select { |f| f.name.match?(/nuget\.config$/i) }
495
+ end
496
+
497
+ def global_json
498
+ dependency_files.find { |f| f.name.casecmp("global.json").zero? }
499
+ end
500
+
501
+ def dotnet_tools_json
502
+ dependency_files.find { |f| f.name.casecmp(".config/dotnet-tools.json").zero? }
503
+ end
233
504
  end
505
+ # rubocop:enable Metrics/ClassLength
234
506
  end
235
507
  end
236
508
  end
@@ -39,7 +39,7 @@ module Dependabot
39
39
  )
40
40
 
41
41
  node_details ||=
42
- find_property_in_directory_build_packages(
42
+ find_property_in_directory_packages_props(
43
43
  property: property_name,
44
44
  callsite_file: callsite_file
45
45
  )
@@ -101,24 +101,18 @@ module Dependabot
101
101
  end
102
102
 
103
103
  def find_property_in_directory_build_targets(property:, callsite_file:)
104
- file = build_targets_file_for_project(callsite_file)
105
- return unless file
106
-
107
- deep_find_prop_node(property: property, file: file)
104
+ find_property_in_up_tree_files(property: property, callsite_file: callsite_file,
105
+ expected_file_name: "Directory.Build.targets")
108
106
  end
109
107
 
110
108
  def find_property_in_directory_build_props(property:, callsite_file:)
111
- file = build_props_file_for_project(callsite_file)
112
- return unless file
113
-
114
- deep_find_prop_node(property: property, file: file)
109
+ find_property_in_up_tree_files(property: property, callsite_file: callsite_file,
110
+ expected_file_name: "Directory.Build.props")
115
111
  end
116
112
 
117
- def find_property_in_directory_build_packages(property:, callsite_file:)
118
- file = build_packages_file_for_project(callsite_file)
119
- return unless file
120
-
121
- deep_find_prop_node(property: property, file: file)
113
+ def find_property_in_directory_packages_props(property:, callsite_file:)
114
+ find_property_in_up_tree_files(property: property, callsite_file: callsite_file,
115
+ expected_file_name: "Directory.Packages.props")
122
116
  end
123
117
 
124
118
  def find_property_in_packages_props(property:)
@@ -128,53 +122,29 @@ module Dependabot
128
122
  deep_find_prop_node(property: property, file: file)
129
123
  end
130
124
 
131
- def build_targets_file_for_project(project_file)
132
- dir = File.dirname(project_file.name)
133
-
134
- # Nuget walks up the directory structure looking for a
135
- # Directory.Build.targets file
136
- possible_paths = dir.split("/").map.with_index do |_, i|
137
- base = dir.split("/").first(i + 1).join("/")
138
- Pathname.new(base + "/Directory.Build.targets").cleanpath.to_path
139
- end.reverse + ["Directory.Build.targets"]
140
-
141
- path = possible_paths.uniq
142
- .find { |p| dependency_files.find { |f| f.name == p } }
125
+ def find_property_in_up_tree_files(property:, callsite_file:, expected_file_name:)
126
+ files = up_tree_files_for_project(callsite_file, expected_file_name)
127
+ return unless files
128
+ return if files.empty?
143
129
 
144
- dependency_files.find { |f| f.name == path }
130
+ # first file where we were able to find the node
131
+ files.reduce(nil) { |acc, file| acc || deep_find_prop_node(property: property, file: file) }
145
132
  end
146
133
 
147
- def build_props_file_for_project(project_file)
134
+ def up_tree_files_for_project(project_file, expected_file_name)
148
135
  dir = File.dirname(project_file.name)
149
136
 
150
- # Nuget walks up the directory structure looking for a
151
- # Directory.Build.props file
137
+ # Simulate MSBuild walking up the directory structure looking for a file
152
138
  possible_paths = dir.split("/").map.with_index do |_, i|
153
139
  base = dir.split("/").first(i + 1).join("/")
154
- Pathname.new(base + "/Directory.Build.props").cleanpath.to_path
155
- end.reverse + ["Directory.Build.props"]
140
+ Pathname.new(base + "/#{expected_file_name}").cleanpath.to_path
141
+ end.reverse + [expected_file_name]
156
142
 
157
- path =
143
+ paths =
158
144
  possible_paths.uniq
159
- .find { |p| dependency_files.find { |f| f.name.casecmp(p).zero? } }
160
-
161
- dependency_files.find { |f| f.name == path }
162
- end
163
-
164
- def build_packages_file_for_project(project_file)
165
- dir = File.dirname(project_file.name)
166
-
167
- # Nuget walks up the directory structure looking for a
168
- # Directory.Packages.props file
169
- possible_paths = dir.split("/").map.with_index do |_, i|
170
- base = dir.split("/").first(i + 1).join("/")
171
- Pathname.new(base + "/Directory.Packages.props").cleanpath.to_path
172
- end.reverse + ["Directory.Packages.props"]
173
-
174
- path = possible_paths.uniq
175
- .find { |p| dependency_files.find { |f| f.name == p } }
145
+ .select { |p| dependency_files.find { |f| f.name.casecmp(p).zero? } }
176
146
 
177
- dependency_files.find { |f| f.name == path }
147
+ dependency_files.select { |f| paths.include?(f.name) }
178
148
  end
179
149
 
180
150
  def packages_props_file
@@ -182,7 +152,9 @@ module Dependabot
182
152
  end
183
153
 
184
154
  def property_xpath(property_name)
185
- "/Project/PropertyGroup/#{property_name}"
155
+ # only return properties that don't have a `Condition` attribute or the `Condition` attribute is checking for
156
+ # an empty string, e.g., Condition="$(SomeProperty) == ''"
157
+ %{/Project/PropertyGroup/#{property_name}[not(@Condition) or @Condition="$(#{property_name}) == ''"]}
186
158
  end
187
159
 
188
160
  def node_details(file:, node:, property:)
@@ -67,7 +67,10 @@ module Dependabot
67
67
 
68
68
  def project_file_parser
69
69
  @project_file_parser ||=
70
- ProjectFileParser.new(dependency_files: dependency_files)
70
+ ProjectFileParser.new(
71
+ dependency_files: dependency_files,
72
+ credentials: credentials
73
+ )
71
74
  end
72
75
 
73
76
  def project_files