dependabot-nuget 0.236.0 → 0.238.0

Sign up to get free protection for your applications and to get access to all the features.
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 +72 -63
  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/metadata_finder.rb +4 -1
  13. data/lib/dependabot/nuget/native_helpers.rb +94 -0
  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 +114 -0
  17. data/lib/dependabot/nuget/update_checker/nuspec_fetcher.rb +86 -0
  18. data/lib/dependabot/nuget/update_checker/property_updater.rb +30 -3
  19. data/lib/dependabot/nuget/update_checker/repository_finder.rb +32 -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 +49 -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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7aefa443941f4d0d0ef21fe0d1378243669f27ca62dd32731fb0881120ffeea5
4
- data.tar.gz: 5b8a246fc596421b5a44949a669038a2f209cad056a79e55a8310c3157b301f1
3
+ metadata.gz: 4d913e6e79cd35b4a00551789e2d9fbea3a71476b76a12f64adfc3427cf419b1
4
+ data.tar.gz: 103b3ed625f8a89133b2c53b6a49941826cf3663de6897e9baad943a78fa37c7
5
5
  SHA512:
6
- metadata.gz: 669395354dfa0e41d0d6e71ee23f8003eb092bbb8946595fa86453e8210363c2d14460dcaa348804fefd4445533024275792124e54a32de52d19ed39d4ce8f01
7
- data.tar.gz: e9e6716d91819085aea460e7ade8b02bf88cabf3e5710bb1f6438d4a1974e5f1cc31f9e4a6e642705a5fa80d9782af7cb074f5f2cfc67b61d8bcfd2ec99001f8
6
+ metadata.gz: c820263ed7825b8f7270693513d8ebab3ee73169318ca5113c75cad689887b5b42c1731fd6496e12033ba7a3ff0d3a59752b7bc512e64ae93baf4a520d485a83
7
+ data.tar.gz: 7ce94d621ef880f809124364816e19aa8cb1fe1975967d8eb907cf40b1573ff2c3fe4c8fad91631f5f545433813252736c3c1db17228366dea8950f0ab50ac9a
@@ -0,0 +1,22 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/file_fetchers"
5
+ require "dependabot/file_fetchers/base"
6
+ require "set"
7
+
8
+ module Dependabot
9
+ module Nuget
10
+ class CacheManager
11
+ def self.caching_disabled?
12
+ ENV["DEPENDABOT_NUGET_CACHE_DISABLED"] == "true"
13
+ end
14
+
15
+ def self.cache(name)
16
+ @cache ||= {}
17
+ @cache[name] ||= {}
18
+ @cache[name]
19
+ end
20
+ end
21
+ end
22
+ end
@@ -39,6 +39,21 @@ module Dependabot
39
39
  nodes.compact
40
40
  end
41
41
 
42
+ def project_file_paths
43
+ doc = Nokogiri::XML(project_file.content)
44
+ doc.remove_namespaces!
45
+ nodes = doc.xpath("/Project/ItemGroup/ProjectFile").map do |node|
46
+ attribute = node.attribute("Include")
47
+ next unless attribute
48
+
49
+ path = attribute.value.strip.tr("\\", "/")
50
+ path = File.join(current_dir, path) unless current_dir.nil?
51
+ Pathname.new(path).cleanpath.to_path
52
+ end
53
+
54
+ nodes.compact
55
+ end
56
+
42
57
  private
43
58
 
44
59
  attr_reader :project_file
@@ -4,30 +4,38 @@
4
4
  require "dependabot/file_fetchers"
5
5
  require "dependabot/file_fetchers/base"
6
6
  require "set"
7
+ require "sorbet-runtime"
7
8
 
8
9
  module Dependabot
9
10
  module Nuget
10
11
  class FileFetcher < Dependabot::FileFetchers::Base
12
+ extend T::Sig
13
+ extend T::Helpers
14
+
11
15
  require_relative "file_fetcher/import_paths_finder"
12
16
  require_relative "file_fetcher/sln_project_paths_finder"
13
17
 
18
+ BUILD_FILE_NAMES = /^Directory\.Build\.(props|targets)$/i # Directory.Build.props, Directory.Build.targets
19
+
14
20
  def self.required_files_in?(filenames)
15
21
  return true if filenames.any? { |f| f.match?(/^packages\.config$/i) }
16
22
  return true if filenames.any? { |f| f.end_with?(".sln") }
17
23
  return true if filenames.any? { |f| f.match?("^src$") }
24
+ return true if filenames.any? { |f| f.end_with?(".proj") }
18
25
 
19
26
  filenames.any? { |name| name.match?(%r{^[^/]*\.[a-z]{2}proj$}) }
20
27
  end
21
28
 
22
29
  def self.required_files_message
23
- "Repo must contain a .(cs|vb|fs)proj file or a packages.config."
30
+ "Repo must contain a .proj file, .(cs|vb|fs)proj file, or a packages.config."
24
31
  end
25
32
 
26
- private
27
-
33
+ # rubocop:disable Metrics/AbcSize
34
+ sig { override.returns(T::Array[DependencyFile]) }
28
35
  def fetch_files
29
36
  fetched_files = []
30
37
  fetched_files += project_files
38
+ fetched_files += project_files.filter_map { |f| directory_packages_props_file_from_project_file(f) }
31
39
  fetched_files += directory_build_files
32
40
  fetched_files += imported_property_files
33
41
 
@@ -37,7 +45,10 @@ module Dependabot
37
45
  fetched_files << dotnet_tools_json if dotnet_tools_json
38
46
  fetched_files << packages_props if packages_props
39
47
 
40
- fetched_files = fetched_files.uniq
48
+ # dedup files based on their absolute path
49
+ fetched_files = fetched_files.uniq do |fetched_file|
50
+ Pathname.new(File.join(fetched_file.directory, fetched_file.name)).cleanpath.to_path
51
+ end
41
52
 
42
53
  if project_files.none? && packages_config_files.none?
43
54
  raise @missing_sln_project_file_errors.first if @missing_sln_project_file_errors&.any?
@@ -50,15 +61,17 @@ module Dependabot
50
61
 
51
62
  fetched_files
52
63
  end
64
+ # rubocop:enable Metrics/AbcSize
65
+
66
+ private
53
67
 
54
68
  def project_files
55
69
  @project_files ||=
56
70
  begin
57
71
  project_files = []
58
- project_files << csproj_file if csproj_file
59
- project_files << vbproj_file if vbproj_file
60
- project_files << fsproj_file if fsproj_file
61
- project_files << directory_packages_props_file if directory_packages_props_file
72
+ project_files += csproj_file
73
+ project_files += vbproj_file
74
+ project_files += fsproj_file
62
75
 
63
76
  project_files += sln_project_files
64
77
  project_files
@@ -107,49 +120,37 @@ module Dependabot
107
120
  @directory_build_files ||= fetch_directory_build_files
108
121
  end
109
122
 
123
+ # rubocop:disable Metrics/AbcSize
110
124
  def fetch_directory_build_files
111
- attempted_paths = []
125
+ attempted_dirs = []
112
126
  directory_build_files = []
113
-
114
- # Don't need to insert "." here, because Directory.Build.props files
115
- # can only be used by project files (not packages.config ones)
116
- project_files.map { |f| File.dirname(f.name) }.uniq.map do |dir|
117
- possible_paths = dir.split("/").flat_map.with_index do |_, i|
118
- base = dir.split("/").first(i + 1).join("/")
119
- possible_build_file_paths(base)
127
+ directory_path = Pathname.new(directory)
128
+
129
+ # find all build files (Directory.Build.props/.targets) relative to the given project file
130
+ project_files.map { |f| File.dirname(File.join(f.directory, f.name)) }.uniq.each do |dir|
131
+ # Simulate MSBuild walking up the directory structure looking for a file
132
+ possible_dirs = dir.split("/").map.with_index do |_, i|
133
+ candidate_dir = dir.split("/").first(i + 1).join("/")
134
+ candidate_dir = "/#{candidate_dir}" unless candidate_dir.start_with?("/")
135
+ candidate_dir
120
136
  end.reverse
121
137
 
122
- possible_paths += [
123
- "Directory.Build.props",
124
- "Directory.build.props",
125
- "Directory.Packages.props",
126
- "Directory.packages.props",
127
- "Directory.Build.targets",
128
- "Directory.build.targets"
129
- ]
130
-
131
- possible_paths.each do |path|
132
- break if attempted_paths.include?(path)
133
-
134
- attempted_paths << path
135
- file = fetch_file_if_present(path)
136
- directory_build_files << file if file
138
+ possible_dirs.each do |possible_dir|
139
+ break if attempted_dirs.include?(possible_dir)
140
+
141
+ attempted_dirs << possible_dir
142
+ relative_possible_dir = Pathname.new(possible_dir).relative_path_from(directory_path).to_s
143
+ build_files = repo_contents(dir: relative_possible_dir).select { |f| f.name.match?(BUILD_FILE_NAMES) }
144
+ directory_build_files += build_files.map do |file|
145
+ possible_file = File.join(relative_possible_dir, file.name).delete_prefix("/")
146
+ fetch_file_from_host(possible_file)
147
+ end
137
148
  end
138
149
  end
139
150
 
140
151
  directory_build_files
141
152
  end
142
-
143
- def possible_build_file_paths(base)
144
- [
145
- Pathname.new(base + "/Directory.Build.props").cleanpath.to_path,
146
- Pathname.new(base + "/Directory.build.props").cleanpath.to_path,
147
- Pathname.new(base + "/Directory.Packages.props").cleanpath.to_path,
148
- Pathname.new(base + "/Directory.packages.props").cleanpath.to_path,
149
- Pathname.new(base + "/Directory.Build.targets").cleanpath.to_path,
150
- Pathname.new(base + "/Directory.build.targets").cleanpath.to_path
151
- ]
152
- end
153
+ # rubocop:enable Metrics/AbcSize
153
154
 
154
155
  def sln_project_files
155
156
  return [] unless sln_files
@@ -184,35 +185,42 @@ module Dependabot
184
185
  end
185
186
 
186
187
  def csproj_file
187
- @csproj_file ||=
188
- begin
189
- file = repo_contents.find { |f| f.name.end_with?(".csproj") }
190
- fetch_file_from_host(file.name) if file
191
- end
188
+ @csproj_file ||= find_and_fetch_with_suffix(".csproj")
192
189
  end
193
190
 
194
191
  def vbproj_file
195
- @vbproj_file ||=
196
- begin
197
- file = repo_contents.find { |f| f.name.end_with?(".vbproj") }
198
- fetch_file_from_host(file.name) if file
199
- end
192
+ @vbproj_file ||= find_and_fetch_with_suffix(".vbproj")
200
193
  end
201
194
 
202
195
  def fsproj_file
203
- @fsproj_file ||=
204
- begin
205
- file = repo_contents.find { |f| f.name.end_with?(".fsproj") }
206
- fetch_file_from_host(file.name) if file
207
- end
196
+ @fsproj_file ||= find_and_fetch_with_suffix(".fsproj")
208
197
  end
209
198
 
210
- def directory_packages_props_file
211
- @directory_packages_props_file ||=
212
- begin
213
- file = repo_contents.find { |f| f.name.casecmp?("directory.packages.props") }
214
- fetch_file_from_host(file.name) if file
199
+ def directory_packages_props_file_from_project_file(project_file)
200
+ # walk up the tree from each project file stopping at the first `Directory.Packages.props` file found
201
+ # https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management#central-package-management-rules
202
+
203
+ found_directory_packages_props_file = nil
204
+ directory_path = Pathname.new(directory)
205
+ full_project_dir = File.dirname(File.join(project_file.directory, project_file.name))
206
+ full_project_dir.split("/").each.with_index do |_, i|
207
+ break if found_directory_packages_props_file
208
+
209
+ base = full_project_dir.split("/").first(i + 1).join("/")
210
+ candidate_file_path = Pathname.new(base + "/Directory.Packages.props").cleanpath.to_path
211
+ candidate_directory = Pathname.new(File.dirname(candidate_file_path))
212
+ relative_candidate_directory = candidate_directory.relative_path_from(directory_path)
213
+ candidate_file = repo_contents(dir: relative_candidate_directory).find do |f|
214
+ f.name.casecmp?("Directory.Packages.props")
215
215
  end
216
+ found_directory_packages_props_file = fetch_file_from_host(candidate_file.name) if candidate_file
217
+ end
218
+
219
+ found_directory_packages_props_file
220
+ end
221
+
222
+ def find_and_fetch_with_suffix(suffix)
223
+ repo_contents.select { |f| f.name.end_with?(suffix) }.map { |f| fetch_file_from_host(f.name) }
216
224
  end
217
225
 
218
226
  def nuget_config_files
@@ -281,7 +289,8 @@ module Dependabot
281
289
  def fetch_imported_property_files(file:, previously_fetched_files:)
282
290
  paths =
283
291
  ImportPathsFinder.new(project_file: file).import_paths +
284
- ImportPathsFinder.new(project_file: file).project_reference_paths
292
+ ImportPathsFinder.new(project_file: file).project_reference_paths +
293
+ ImportPathsFinder.new(project_file: file).project_file_paths
285
294
 
286
295
  paths.flat_map do |path|
287
296
  next if previously_fetched_files.map(&:name).include?(path)
@@ -51,7 +51,8 @@ module Dependabot
51
51
  attr_reader :dotnet_tools_json
52
52
 
53
53
  def parsed_dotnet_tools_json
54
- @parsed_dotnet_tools_json ||= JSON.parse(dotnet_tools_json.content)
54
+ # Remove BOM if present as JSON should be UTF-8
55
+ @parsed_dotnet_tools_json ||= JSON.parse(dotnet_tools_json.content.delete_prefix("\uFEFF"))
55
56
  rescue JSON::ParserError
56
57
  raise Dependabot::DependencyFileNotParseable, dotnet_tools_json.path
57
58
  end
@@ -48,7 +48,8 @@ module Dependabot
48
48
  attr_reader :global_json
49
49
 
50
50
  def parsed_global_json
51
- @parsed_global_json ||= JSON.parse(global_json.content)
51
+ # Remove BOM if present as JSON should be UTF-8
52
+ @parsed_global_json ||= JSON.parse(global_json.content.delete_prefix("\uFEFF"))
52
53
  rescue JSON::ParserError
53
54
  raise Dependabot::DependencyFileNotParseable, global_json.path
54
55
  end
@@ -5,6 +5,7 @@ require "nokogiri"
5
5
 
6
6
  require "dependabot/dependency"
7
7
  require "dependabot/nuget/file_parser"
8
+ require "dependabot/nuget/cache_manager"
8
9
 
9
10
  # For details on packages.config files see:
10
11
  # https://docs.microsoft.com/en-us/nuget/reference/packages-config
@@ -16,11 +17,32 @@ module Dependabot
16
17
 
17
18
  DEPENDENCY_SELECTOR = "packages > package"
18
19
 
20
+ def self.dependency_set_cache
21
+ CacheManager.cache("packages_config_dependency_set")
22
+ end
23
+
19
24
  def initialize(packages_config:)
20
25
  @packages_config = packages_config
21
26
  end
22
27
 
23
28
  def dependency_set
29
+ return parse_dependencies if CacheManager.caching_disabled?
30
+
31
+ key = "#{packages_config.name.downcase}::#{packages_config.content.hash}"
32
+ cache = PackagesConfigParser.dependency_set_cache
33
+
34
+ cache[key] ||= parse_dependencies
35
+
36
+ dependency_set = Dependabot::FileParsers::Base::DependencySet.new
37
+ dependency_set += cache[key]
38
+ dependency_set
39
+ end
40
+
41
+ private
42
+
43
+ attr_reader :packages_config
44
+
45
+ def parse_dependencies
24
46
  dependency_set = Dependabot::FileParsers::Base::DependencySet.new
25
47
 
26
48
  doc = Nokogiri::XML(packages_config.content)
@@ -43,10 +65,6 @@ module Dependabot
43
65
  dependency_set
44
66
  end
45
67
 
46
- private
47
-
48
- attr_reader :packages_config
49
-
50
68
  def dependency_name(dependency_node)
51
69
  dependency_node.attribute("id")&.value&.strip ||
52
70
  dependency_node.at_xpath("./id")&.content&.strip