dependabot-nuget 0.237.0 → 0.238.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (26) 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 +65 -61
  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/update_checker/compatibility_checker.rb +85 -0
  14. data/lib/dependabot/nuget/update_checker/dependency_finder.rb +228 -0
  15. data/lib/dependabot/nuget/update_checker/nupkg_fetcher.rb +114 -0
  16. data/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +86 -0
  17. data/lib/dependabot/nuget/update_checker/property_updater.rb +30 -3
  18. data/lib/dependabot/nuget/update_checker/repository_finder.rb +32 -10
  19. data/lib/dependabot/nuget/update_checker/tfm_comparer.rb +31 -0
  20. data/lib/dependabot/nuget/update_checker/tfm_finder.rb +127 -0
  21. data/lib/dependabot/nuget/update_checker/version_finder.rb +47 -6
  22. data/lib/dependabot/nuget/update_checker.rb +42 -8
  23. data/lib/dependabot/nuget.rb +2 -0
  24. metadata +33 -7
  25. data/lib/dependabot/nuget/file_updater/packages_config_declaration_finder.rb +0 -70
  26. 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