dependabot-nuget 0.237.0 → 0.239.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 +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