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
@@ -1,19 +1,22 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "dependabot/dependency_file"
4
5
  require "dependabot/file_updaters"
5
6
  require "dependabot/file_updaters/base"
7
+ require "dependabot/nuget/native_helpers"
6
8
 
7
9
  module Dependabot
8
10
  module Nuget
9
11
  class FileUpdater < Dependabot::FileUpdaters::Base
10
- require_relative "file_updater/packages_config_declaration_finder"
11
- require_relative "file_updater/project_file_declaration_finder"
12
12
  require_relative "file_updater/property_value_updater"
13
+ require_relative "file_parser/project_file_parser"
14
+ require_relative "file_parser/dotnet_tools_json_parser"
15
+ require_relative "file_parser/packages_config_parser"
13
16
 
14
17
  def self.updated_files_regex
15
18
  [
16
- %r{^[^/]*\.[a-z]{2}proj$},
19
+ %r{^[^/]*\.([a-z]{2})?proj$},
17
20
  /^packages\.config$/i,
18
21
  /^global\.json$/i,
19
22
  /^dotnet-tools\.json$/i,
@@ -24,156 +27,159 @@ module Dependabot
24
27
  end
25
28
 
26
29
  def updated_dependency_files
27
- updated_files = T.let(dependency_files.dup, T.untyped)
28
-
29
- # Loop through each of the changed requirements, applying changes to
30
- # all files for that change. Note that the logic is different here
31
- # to other languages because donet has property inheritance across
32
- # files
33
30
  dependencies.each do |dependency|
34
- updated_files = update_files_for_dependency(
35
- files: updated_files,
36
- dependency: dependency
37
- )
31
+ try_update_projects(dependency) || try_update_json(dependency)
38
32
  end
39
33
 
40
- updated_files.reject! { |f| dependency_files.include?(f) }
34
+ # update all with content from disk
35
+ updated_files = dependency_files.filter_map do |f|
36
+ updated_content = File.read(dependency_file_path(f))
37
+ next if updated_content == f.content
41
38
 
42
- raise "No files changed!" if updated_files.none?
39
+ normalized_content = normalize_content(f, updated_content)
40
+ next if normalized_content == f.content
41
+
42
+ puts "The contents of file [#{f.name}] were updated."
43
+
44
+ updated_file(file: f, content: normalized_content)
45
+ end
46
+
47
+ # reset repo files
48
+ SharedHelpers.reset_git_repo(T.cast(repo_contents_path, String)) if repo_contents_path
43
49
 
44
50
  updated_files
45
51
  end
46
52
 
47
53
  private
48
54
 
49
- def project_files
50
- dependency_files.select { |df| df.name.match?(/\.[a-z]{2}proj$|[Dd]irectory.[Pp]ackages.props/) }
55
+ def try_update_projects(dependency)
56
+ update_ran = T.let(false, T::Boolean)
57
+
58
+ # run update for each project file
59
+ project_files.each do |project_file|
60
+ project_dependencies = project_dependencies(project_file)
61
+ proj_path = dependency_file_path(project_file)
62
+
63
+ next unless project_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
64
+
65
+ NativeHelpers.run_nuget_updater_tool(repo_contents_path, proj_path, dependency, !dependency.top_level?)
66
+ update_ran = true
67
+ end
68
+
69
+ update_ran
51
70
  end
52
71
 
53
- def packages_config_files
54
- dependency_files.select do |f|
55
- T.must(T.must(f.name.split("/").last).casecmp("packages.config")).zero?
72
+ def try_update_json(dependency)
73
+ if dotnet_tools_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? } ||
74
+ global_json_dependencies.any? { |dep| dep.name.casecmp(dependency.name).zero? }
75
+
76
+ # We just need to feed the updater a project file, grab the first
77
+ project_file = project_files.first
78
+ proj_path = dependency_file_path(project_file)
79
+
80
+ NativeHelpers.run_nuget_updater_tool(repo_contents_path, proj_path, dependency, !dependency.top_level?)
81
+ return true
56
82
  end
83
+
84
+ false
57
85
  end
58
86
 
59
- def global_json
60
- dependency_files.find { |f| T.must(f.name.casecmp("global.json")).zero? }
87
+ def project_dependencies(project_file)
88
+ # Collect all dependencies from the project file and associated packages.config
89
+ dependencies = project_file_parser.dependency_set(project_file: project_file).dependencies
90
+ packages_config = find_packages_config(project_file)
91
+ return dependencies unless packages_config
92
+
93
+ dependencies + FileParser::PackagesConfigParser.new(packages_config: packages_config)
94
+ .dependency_set.dependencies
61
95
  end
62
96
 
63
- def dotnet_tools_json
64
- dependency_files.find { |f| T.must(f.name.casecmp(".config/dotnet-tools.json")).zero? }
97
+ def find_packages_config(project_file)
98
+ project_file_name = File.basename(project_file.name)
99
+ packages_config_path = project_file.name.gsub(project_file_name, "packages.config")
100
+ packages_config_files.find { |f| f.name == packages_config_path }
65
101
  end
66
102
 
67
- def check_required_files
68
- return if project_files.any? || packages_config_files.any?
103
+ def project_file_parser
104
+ @project_file_parser ||=
105
+ FileParser::ProjectFileParser.new(
106
+ dependency_files: dependency_files,
107
+ credentials: credentials
108
+ )
109
+ end
69
110
 
70
- raise "No project file or packages.config!"
111
+ def global_json_dependencies
112
+ return [] unless global_json
113
+
114
+ @global_json_dependencies ||=
115
+ FileParser::GlobalJsonParser.new(global_json: global_json).dependency_set.dependencies
71
116
  end
72
117
 
73
- def update_files_for_dependency(files:, dependency:)
74
- # The UpdateChecker ensures the order of requirements is preserved
75
- # when updating, so we can zip them together in new/old pairs.
76
- reqs = dependency.requirements.zip(dependency.previous_requirements)
77
- .reject { |new_req, old_req| new_req == old_req }
118
+ def dotnet_tools_json_dependencies
119
+ return [] unless dotnet_tools_json
78
120
 
79
- # Loop through each changed requirement and update the files
80
- reqs.each do |new_req, old_req|
81
- raise "Bad req match" unless new_req[:file] == old_req[:file]
82
- next if new_req[:requirement] == old_req[:requirement]
121
+ @dotnet_tools_json_dependencies ||=
122
+ FileParser::DotNetToolsJsonParser.new(dotnet_tools_json: dotnet_tools_json).dependency_set.dependencies
123
+ end
83
124
 
84
- file = files.find { |f| f.name == new_req.fetch(:file) }
125
+ def normalize_content(dependency_file, updated_content)
126
+ # Fix up line endings
127
+ if dependency_file.content.include?("\r\n") && updated_content.match?(/(?<!\r)\n/)
128
+ # The original content contain windows style newlines.
129
+ # Ensure the updated content also uses windows style newlines.
130
+ updated_content = updated_content.gsub(/(?<!\r)\n/, "\r\n")
131
+ puts "Fixing mismatched Windows line endings for [#{dependency_file.name}]."
132
+ elsif updated_content.include?("\r\n")
133
+ # The original content does not contain windows style newlines.
134
+ # Ensure the updated content uses unix style newlines.
135
+ updated_content = updated_content.gsub("\r\n", "\n")
136
+ puts "Fixing mismatched Unix line endings for [#{dependency_file.name}]."
137
+ end
85
138
 
86
- files =
87
- if new_req.dig(:metadata, :property_name)
88
- update_property_value(files, file, new_req)
89
- else
90
- update_declaration(files, dependency, file, old_req, new_req)
91
- end
139
+ # Fix up BOM
140
+ if !dependency_file.content.start_with?("\uFEFF") && updated_content.start_with?("\uFEFF")
141
+ updated_content = updated_content.delete_prefix("\uFEFF")
142
+ puts "Removing BOM from [#{dependency_file.name}]."
143
+ elsif dependency_file.content.start_with?("\uFEFF") && !updated_content.start_with?("\uFEFF")
144
+ updated_content = "\uFEFF" + updated_content
145
+ puts "Adding BOM to [#{dependency_file.name}]."
92
146
  end
93
147
 
94
- files
148
+ updated_content
95
149
  end
96
150
 
97
- def update_property_value(files, file, req)
98
- files = files.dup
99
- property_name = req.fetch(:metadata).fetch(:property_name)
100
-
101
- PropertyValueUpdater
102
- .new(dependency_files: files)
103
- .update_files_for_property_change(
104
- property_name: property_name,
105
- updated_value: req.fetch(:requirement),
106
- callsite_file: file
107
- )
151
+ def dependency_file_path(dependency_file)
152
+ if dependency_file.directory.start_with?(repo_contents_path)
153
+ File.join(dependency_file.directory, dependency_file.name)
154
+ else
155
+ file_directory = dependency_file.directory
156
+ file_directory = file_directory[1..-1] if file_directory.start_with?("/")
157
+ File.join(repo_contents_path || "", file_directory, dependency_file.name)
158
+ end
108
159
  end
109
160
 
110
- def update_declaration(files, dependency, file, old_req, new_req)
111
- files = files.dup
112
-
113
- updated_content = file.content
161
+ def project_files
162
+ dependency_files.select { |df| df.name.match?(/\.([a-z]{2})?proj$/) }
163
+ end
114
164
 
115
- original_declarations(dependency, old_req).each do |old_dec|
116
- updated_content = updated_content.gsub(
117
- old_dec,
118
- updated_declaration(old_dec, old_req, new_req)
119
- )
165
+ def packages_config_files
166
+ dependency_files.select do |f|
167
+ T.must(T.must(f.name.split("/").last).casecmp("packages.config")).zero?
120
168
  end
169
+ end
121
170
 
122
- raise "Expected content to change!" if updated_content == file.content
123
-
124
- files[files.index(file)] =
125
- updated_file(file: file, content: updated_content)
126
- files
127
- end
128
-
129
- def original_declarations(dependency, requirement)
130
- if requirement.fetch(:file).casecmp("global.json").zero?
131
- [
132
- global_json.content.match(
133
- /"#{Regexp.escape(dependency.name)}"\s*:\s*
134
- "#{Regexp.escape(dependency.previous_version)}"/x
135
- ).to_s
136
- ]
137
- elsif requirement.fetch(:file).casecmp(".config/dotnet-tools.json").zero?
138
- [
139
- dotnet_tools_json.content.match(
140
- /"#{Regexp.escape(dependency.name)}"\s*:\s*{\s*"version"\s*:\s*
141
- "#{Regexp.escape(dependency.previous_version)}"/xm
142
- ).to_s
143
- ]
144
- else
145
- declaration_finder(dependency, requirement).declaration_strings
146
- end
171
+ def global_json
172
+ dependency_files.find { |f| T.must(f.name.casecmp("global.json")).zero? }
147
173
  end
148
174
 
149
- def declaration_finder(dependency, requirement)
150
- @declaration_finders ||= {}
151
-
152
- requirement_fn = requirement.fetch(:file)
153
- @declaration_finders[dependency.hash + requirement.hash] ||=
154
- if requirement_fn.split("/").last.casecmp("packages.config").zero?
155
- PackagesConfigDeclarationFinder.new(
156
- dependency_name: dependency.name,
157
- declaring_requirement: requirement,
158
- packages_config:
159
- packages_config_files.find { |f| f.name == requirement_fn }
160
- )
161
- else
162
- ProjectFileDeclarationFinder.new(
163
- dependency_name: dependency.name,
164
- declaring_requirement: requirement,
165
- dependency_files: dependency_files
166
- )
167
- end
168
- end
169
-
170
- def updated_declaration(old_declaration, previous_req, requirement)
171
- original_req_string = previous_req.fetch(:requirement)
172
-
173
- old_declaration.gsub(
174
- original_req_string,
175
- requirement.fetch(:requirement)
176
- )
175
+ def dotnet_tools_json
176
+ dependency_files.find { |f| T.must(f.name.casecmp(".config/dotnet-tools.json")).zero? }
177
+ end
178
+
179
+ def check_required_files
180
+ return if project_files.any? || packages_config_files.any?
181
+
182
+ raise "No project file or packages.config!"
177
183
  end
178
184
  end
179
185
  end
@@ -0,0 +1,94 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Dependabot
5
+ module Nuget
6
+ module NativeHelpers
7
+ def self.native_helpers_root
8
+ helpers_root = ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", nil)
9
+ return File.join(helpers_root, "nuget") unless helpers_root.nil?
10
+
11
+ File.expand_path("../../../helpers", __dir__)
12
+ end
13
+
14
+ def self.run_nuget_framework_check(project_tfms, package_tfms)
15
+ exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
16
+ command = [
17
+ exe_path,
18
+ "framework-check",
19
+ "--project-tfms",
20
+ *project_tfms,
21
+ "--package-tfms",
22
+ *package_tfms,
23
+ "--verbose"
24
+ ].join(" ")
25
+
26
+ fingerprint = [
27
+ exe_path,
28
+ "framework-check",
29
+ "--project-tfms",
30
+ "<project-tfms>",
31
+ "--package-tfms",
32
+ "<package-tfms>",
33
+ "--verbose"
34
+ ].join(" ")
35
+
36
+ puts "running NuGet updater:\n" + command
37
+
38
+ output = SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
39
+ puts output
40
+
41
+ # Exit code == 0 means that all project frameworks are compatible
42
+ true
43
+ rescue Dependabot::SharedHelpers::HelperSubprocessFailed
44
+ # Exit code != 0 means that not all project frameworks are compatible
45
+ false
46
+ end
47
+
48
+ # rubocop:disable Metrics/MethodLength
49
+ def self.run_nuget_updater_tool(repo_root, proj_path, dependency, is_transitive)
50
+ exe_path = File.join(native_helpers_root, "NuGetUpdater", "NuGetUpdater.Cli")
51
+ command = [
52
+ exe_path,
53
+ "update",
54
+ "--repo-root",
55
+ repo_root,
56
+ "--solution-or-project",
57
+ proj_path,
58
+ "--dependency",
59
+ dependency.name,
60
+ "--new-version",
61
+ dependency.version,
62
+ "--previous-version",
63
+ dependency.previous_version,
64
+ is_transitive ? "--transitive" : "",
65
+ "--verbose"
66
+ ].join(" ")
67
+
68
+ fingerprint = [
69
+ exe_path,
70
+ "update",
71
+ "--repo-root",
72
+ "<repo-root>",
73
+ "--solution-or-project",
74
+ "<path-to-solution-or-project>",
75
+ "--dependency",
76
+ "<dependency-name>",
77
+ "--new-version",
78
+ "<new-version>",
79
+ "--previous-version",
80
+ "<previous-version>",
81
+ is_transitive ? "--transitive" : "",
82
+ "--verbose"
83
+ ].join(" ")
84
+
85
+ puts "running NuGet updater:\n" + command
86
+
87
+ output = SharedHelpers.run_shell_command(command, fingerprint: fingerprint)
88
+
89
+ puts output
90
+ end
91
+ # rubocop:enable Metrics/MethodLength
92
+ end
93
+ end
94
+ end
@@ -1,6 +1,9 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
6
+ require "dependabot/requirement"
4
7
  require "dependabot/utils"
5
8
  require "dependabot/nuget/version"
6
9
 
@@ -8,7 +11,7 @@ require "dependabot/nuget/version"
8
11
  # https://docs.microsoft.com/en-us/nuget/reference/package-versioning
9
12
  module Dependabot
10
13
  module Nuget
11
- class Requirement < Gem::Requirement
14
+ class Requirement < Dependabot::Requirement
12
15
  def self.parse(obj)
13
16
  return ["=", Nuget::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
14
17
 
@@ -25,6 +28,7 @@ module Dependabot
25
28
  # For consistency with other languages, we define a requirements array.
26
29
  # Dotnet doesn't have an `OR` separator for requirements, so it always
27
30
  # contains a single element.
31
+ sig { override.params(requirement_string: T.nilable(String)).returns(T::Array[Requirement]) }
28
32
  def self.requirements_array(requirement_string)
29
33
  [new(requirement_string)]
30
34
  end
@@ -0,0 +1,85 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "dependabot/nuget/update_checker"
5
+
6
+ module Dependabot
7
+ module Nuget
8
+ class UpdateChecker
9
+ class CompatibilityChecker
10
+ require_relative "nuspec_fetcher"
11
+ require_relative "nupkg_fetcher"
12
+ require_relative "tfm_finder"
13
+ require_relative "tfm_comparer"
14
+
15
+ def initialize(dependency_urls:, dependency:, tfm_finder:)
16
+ @dependency_urls = dependency_urls
17
+ @dependency = dependency
18
+ @tfm_finder = tfm_finder
19
+ end
20
+
21
+ def compatible?(version)
22
+ nuspec_xml = NuspecFetcher.fetch_nuspec(dependency_urls, dependency.name, version)
23
+ return false unless nuspec_xml
24
+
25
+ # development dependencies are packages such as analyzers which need to be
26
+ # compatible with the compiler not the project itself.
27
+ return true if development_dependency?(nuspec_xml)
28
+
29
+ package_tfms = parse_package_tfms(nuspec_xml)
30
+ package_tfms = fetch_package_tfms(version) if package_tfms.empty?
31
+ # nil is a special return value that indicates that the package is likely a development dependency
32
+ return true if package_tfms.nil?
33
+ return false if package_tfms.empty?
34
+
35
+ return false if project_tfms.nil? || project_tfms.empty?
36
+
37
+ TfmComparer.are_frameworks_compatible?(project_tfms, package_tfms)
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :dependency_urls, :dependency, :tfm_finder
43
+
44
+ def development_dependency?(nuspec_xml)
45
+ contents = nuspec_xml.at_xpath("package/metadata/developmentDependency")&.content&.strip
46
+ return false unless contents
47
+
48
+ contents.casecmp("true").zero?
49
+ end
50
+
51
+ def parse_package_tfms(nuspec_xml)
52
+ nuspec_xml.xpath("//dependencies/group").map do |group|
53
+ group.attribute("targetFramework")
54
+ end
55
+ end
56
+
57
+ def project_tfms
58
+ return @project_tfms if defined?(@project_tfms)
59
+
60
+ @project_tfms = tfm_finder.frameworks(dependency)
61
+ end
62
+
63
+ def fetch_package_tfms(dependency_version)
64
+ nupkg_buffer = NupkgFetcher.fetch_nupkg_buffer(dependency_urls, dependency.name, dependency_version)
65
+ return [] unless nupkg_buffer
66
+
67
+ # Parse tfms from the folders beneath the lib folder
68
+ folder_name = "lib/"
69
+ tfms = Set.new
70
+ Zip::File.open_buffer(nupkg_buffer) do |zip|
71
+ lib_file_entries = zip.select { |entry| entry.name.start_with?(folder_name) }
72
+ # If there is no lib folder in this package, assume it is a development dependency
73
+ return nil if lib_file_entries.empty?
74
+
75
+ lib_file_entries.each do |entry|
76
+ _, tfm = entry.name.split("/").first(2)
77
+ tfms << tfm
78
+ end
79
+ end
80
+ tfms.to_a
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end