dependabot-gradle 0.84.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/gradle/file_parser"
4
+
5
+ module Dependabot
6
+ module Gradle
7
+ class FileParser
8
+ class RepositoriesFinder
9
+ # The Central Repo doesn't have special status for Gradle, but until
10
+ # we're confident we're selecting repos correctly it's wise to include
11
+ # it as a default.
12
+ CENTRAL_REPO_URL = "https://repo.maven.apache.org/maven2"
13
+
14
+ REPOSITORIES_BLOCK_START = /(?:^|\s)repositories\s*\{/.freeze
15
+ MAVEN_REPO_REGEX =
16
+ /maven\s*\{[^\}]*\surl[\s\(]\s*['"](?<url>[^'"]+)['"]/.freeze
17
+
18
+ def initialize(dependency_files:, target_dependency_file:)
19
+ @dependency_files = dependency_files
20
+ @target_dependency_file = target_dependency_file
21
+ raise "No target file!" unless target_dependency_file
22
+ end
23
+
24
+ def repository_urls
25
+ repository_urls = []
26
+ repository_urls += inherited_repository_urls
27
+ repository_urls += own_buildfile_repository_urls
28
+ repository_urls = repository_urls.uniq
29
+
30
+ return repository_urls unless repository_urls.empty?
31
+
32
+ [CENTRAL_REPO_URL]
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :dependency_files, :target_dependency_file
38
+
39
+ def inherited_repository_urls
40
+ return [] unless top_level_buildfile
41
+
42
+ buildfile_content = comment_free_content(top_level_buildfile)
43
+ subproject_blocks = []
44
+
45
+ buildfile_content.scan(/(?:^|\s)allprojects\s*\{/) do
46
+ mtch = Regexp.last_match
47
+ subproject_blocks <<
48
+ mtch.post_match[0..closing_bracket_index(mtch.post_match)]
49
+ end
50
+
51
+ if top_level_buildfile != target_dependency_file
52
+ buildfile_content.scan(/(?:^|\s)subprojects\s*\{/) do
53
+ mtch = Regexp.last_match
54
+ subproject_blocks <<
55
+ mtch.post_match[0..closing_bracket_index(mtch.post_match)]
56
+ end
57
+ end
58
+
59
+ repository_urls_from(subproject_blocks.join("\n"))
60
+ end
61
+
62
+ def own_buildfile_repository_urls
63
+ buildfile_content = comment_free_content(target_dependency_file)
64
+
65
+ buildfile_content.dup.scan(/(?:^|\s)subprojects\s*\{/) do
66
+ mtch = Regexp.last_match
67
+ buildfile_content.gsub!(
68
+ mtch.post_match[0..closing_bracket_index(mtch.post_match)],
69
+ ""
70
+ )
71
+ end
72
+
73
+ repository_urls_from(buildfile_content)
74
+ end
75
+
76
+ def repository_urls_from(buildfile_content)
77
+ repository_urls = []
78
+
79
+ repository_blocks = []
80
+ buildfile_content.scan(REPOSITORIES_BLOCK_START) do
81
+ mtch = Regexp.last_match
82
+ repository_blocks <<
83
+ mtch.post_match[0..closing_bracket_index(mtch.post_match)]
84
+ end
85
+
86
+ repository_blocks.each do |block|
87
+ if block.include?(" google(")
88
+ repository_urls << "https://maven.google.com/"
89
+ end
90
+
91
+ if block.include?(" mavenCentral(")
92
+ repository_urls << "https://repo.maven.apache.org/maven2/"
93
+ end
94
+
95
+ if block.include?(" jcenter(")
96
+ repository_urls << "https://jcenter.bintray.com/"
97
+ end
98
+
99
+ block.scan(MAVEN_REPO_REGEX) do
100
+ repository_urls << Regexp.last_match.named_captures.fetch("url")
101
+ end
102
+ end
103
+
104
+ repository_urls.
105
+ map { |url| url.strip.gsub(%r{/$}, "") }.
106
+ select { |url| valid_url?(url) }.
107
+ uniq
108
+ end
109
+
110
+ def closing_bracket_index(string)
111
+ closes_required = 1
112
+
113
+ string.chars.each_with_index do |char, index|
114
+ closes_required += 1 if char == "{"
115
+ closes_required -= 1 if char == "}"
116
+ return index if closes_required.zero?
117
+ end
118
+ end
119
+
120
+ def valid_url?(url)
121
+ # Reject non-http URLs because they're probably parsing mistakes
122
+ return false unless url.start_with?("http")
123
+
124
+ URI.parse(url)
125
+ true
126
+ rescue URI::InvalidURIError
127
+ false
128
+ end
129
+
130
+ def comment_free_content(buildfile)
131
+ buildfile.content.
132
+ gsub(%r{(?<=^|\s)//.*$}, "\n").
133
+ gsub(%r{(?<=^|\s)/\*.*?\*/}m, "")
134
+ end
135
+
136
+ def top_level_buildfile
137
+ @top_level_buildfile ||=
138
+ dependency_files.find { |f| f.name == "build.gradle" }
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/file_updaters"
4
+ require "dependabot/file_updaters/base"
5
+ require "dependabot/gradle/file_parser"
6
+
7
+ module Dependabot
8
+ module Gradle
9
+ class FileUpdater < Dependabot::FileUpdaters::Base
10
+ require_relative "file_updater/dependency_set_updater"
11
+ require_relative "file_updater/property_value_updater"
12
+
13
+ def self.updated_files_regex
14
+ [/^build\.gradle$/, %r{/build\.gradle$}]
15
+ end
16
+
17
+ def updated_dependency_files
18
+ updated_files = buildfiles.dup
19
+
20
+ # Loop through each of the changed requirements, applying changes to
21
+ # all buildfiles for that change. Note that the logic is different
22
+ # here to other languages because Java has property inheritance across
23
+ # files (although we're not supporting it for gradle yet).
24
+ dependencies.each do |dependency|
25
+ updated_files = update_buildfiles_for_dependency(
26
+ buildfiles: updated_files,
27
+ dependency: dependency
28
+ )
29
+ end
30
+
31
+ updated_files = updated_files.reject { |f| buildfiles.include?(f) }
32
+
33
+ raise "No files changed!" if updated_files.none?
34
+
35
+ updated_files
36
+ end
37
+
38
+ private
39
+
40
+ def check_required_files
41
+ raise "No build.gradle!" unless get_original_file("build.gradle")
42
+ end
43
+
44
+ def update_buildfiles_for_dependency(buildfiles:, dependency:)
45
+ files = buildfiles.dup
46
+
47
+ # The UpdateChecker ensures the order of requirements is preserved
48
+ # when updating, so we can zip them together in new/old pairs.
49
+ reqs = dependency.requirements.zip(dependency.previous_requirements).
50
+ reject { |new_req, old_req| new_req == old_req }
51
+
52
+ # Loop through each changed requirement and update the buildfiles
53
+ reqs.each do |new_req, old_req|
54
+ raise "Bad req match" unless new_req[:file] == old_req[:file]
55
+ next if new_req[:requirement] == old_req[:requirement]
56
+
57
+ buildfile = files.find { |f| f.name == new_req.fetch(:file) }
58
+
59
+ if new_req.dig(:metadata, :property_name)
60
+ files = update_files_for_property_change(files, old_req, new_req)
61
+ elsif new_req.dig(:metadata, :dependency_set)
62
+ files = update_files_for_dep_set_change(files, old_req, new_req)
63
+ else
64
+ files[files.index(buildfile)] =
65
+ update_version_in_buildfile(
66
+ dependency,
67
+ buildfile,
68
+ old_req,
69
+ new_req
70
+ )
71
+ end
72
+ end
73
+
74
+ files
75
+ end
76
+
77
+ def update_files_for_property_change(buildfiles, old_req, new_req)
78
+ files = buildfiles.dup
79
+ property_name = new_req.fetch(:metadata).fetch(:property_name)
80
+ buildfile = files.find { |f| f.name == new_req.fetch(:file) }
81
+
82
+ PropertyValueUpdater.new(dependency_files: files).
83
+ update_files_for_property_change(
84
+ property_name: property_name,
85
+ callsite_buildfile: buildfile,
86
+ previous_value: old_req.fetch(:requirement),
87
+ updated_value: new_req.fetch(:requirement)
88
+ )
89
+ end
90
+
91
+ def update_files_for_dep_set_change(buildfiles, old_req, new_req)
92
+ files = buildfiles.dup
93
+ dependency_set = new_req.fetch(:metadata).fetch(:dependency_set)
94
+ buildfile = files.find { |f| f.name == new_req.fetch(:file) }
95
+
96
+ DependencySetUpdater.new(dependency_files: files).
97
+ update_files_for_dep_set_change(
98
+ dependency_set: dependency_set,
99
+ buildfile: buildfile,
100
+ previous_requirement: old_req.fetch(:requirement),
101
+ updated_requirement: new_req.fetch(:requirement)
102
+ )
103
+ end
104
+
105
+ def update_version_in_buildfile(dependency, buildfile, previous_req,
106
+ requirement)
107
+ updated_content =
108
+ buildfile.content.gsub(
109
+ original_buildfile_declaration(dependency, previous_req),
110
+ updated_buildfile_declaration(
111
+ dependency,
112
+ previous_req,
113
+ requirement
114
+ )
115
+ )
116
+
117
+ if updated_content == buildfile.content
118
+ raise "Expected content to change!"
119
+ end
120
+
121
+ updated_file(file: buildfile, content: updated_content)
122
+ end
123
+
124
+ def original_buildfile_declaration(dependency, requirement)
125
+ # This implementation is limited to declarations that appear on a
126
+ # single line.
127
+ buildfile = buildfiles.find { |f| f.name == requirement.fetch(:file) }
128
+ buildfile.content.lines.find do |line|
129
+ line = evaluate_properties(line, buildfile)
130
+ next false unless line.include?(dependency.name.split(":").first)
131
+ next false unless line.include?(dependency.name.split(":").last)
132
+
133
+ line.include?(requirement.fetch(:requirement))
134
+ end
135
+ end
136
+
137
+ def evaluate_properties(string, buildfile)
138
+ result = string.dup
139
+
140
+ string.scan(Gradle::FileParser::PROPERTY_REGEX) do
141
+ prop_name = Regexp.last_match.named_captures.fetch("property_name")
142
+ property_value = property_value_finder.property_value(
143
+ property_name: prop_name,
144
+ callsite_buildfile: buildfile
145
+ )
146
+ next unless property_value
147
+
148
+ result.sub!(Regexp.last_match.to_s, property_value)
149
+ end
150
+
151
+ result
152
+ end
153
+
154
+ def property_value_finder
155
+ @property_value_finder ||=
156
+ Gradle::FileParser::PropertyValueFinder.
157
+ new(dependency_files: dependency_files)
158
+ end
159
+
160
+ def updated_buildfile_declaration(dependency, previous_req, requirement)
161
+ original_req_string = previous_req.fetch(:requirement)
162
+
163
+ original_buildfile_declaration(dependency, previous_req).gsub(
164
+ original_req_string,
165
+ requirement.fetch(:requirement)
166
+ )
167
+ end
168
+
169
+ def buildfiles
170
+ @buildfiles ||=
171
+ dependency_files.select { |f| f.name.end_with?("build.gradle") }
172
+ end
173
+ end
174
+ end
175
+ end
176
+
177
+ Dependabot::FileUpdaters.register("gradle", Dependabot::Gradle::FileUpdater)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/gradle/file_parser"
4
+ require "dependabot/gradle/file_updater"
5
+
6
+ module Dependabot
7
+ module Gradle
8
+ class FileUpdater
9
+ class DependencySetUpdater
10
+ def initialize(dependency_files:)
11
+ @dependency_files = dependency_files
12
+ end
13
+
14
+ def update_files_for_dep_set_change(dependency_set:,
15
+ buildfile:,
16
+ previous_requirement:,
17
+ updated_requirement:)
18
+ declaration_string =
19
+ original_declaration_string(dependency_set, buildfile)
20
+
21
+ return dependency_files unless declaration_string
22
+
23
+ updated_content = buildfile.content.sub(
24
+ declaration_string,
25
+ declaration_string.sub(
26
+ previous_requirement,
27
+ updated_requirement
28
+ )
29
+ )
30
+
31
+ updated_files = dependency_files.dup
32
+ updated_files[updated_files.index(buildfile)] =
33
+ update_file(file: buildfile, content: updated_content)
34
+
35
+ updated_files
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :dependency_files
41
+
42
+ def original_declaration_string(dependency_set, buildfile)
43
+ regex = Gradle::FileParser::DEPENDENCY_SET_DECLARATION_REGEX
44
+ dependency_sets = []
45
+ buildfile.content.scan(regex) do
46
+ dependency_sets << Regexp.last_match.to_s
47
+ end
48
+
49
+ dependency_sets.find do |mtch|
50
+ next unless mtch.include?(dependency_set[:group])
51
+
52
+ mtch.include?(dependency_set[:version])
53
+ end
54
+ end
55
+
56
+ def update_file(file:, content:)
57
+ updated_file = file.dup
58
+ updated_file.content = content
59
+ updated_file
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/gradle/file_updater"
4
+ require "dependabot/gradle/file_parser/property_value_finder"
5
+
6
+ module Dependabot
7
+ module Gradle
8
+ class FileUpdater
9
+ class PropertyValueUpdater
10
+ def initialize(dependency_files:)
11
+ @dependency_files = dependency_files
12
+ end
13
+
14
+ def update_files_for_property_change(property_name:,
15
+ callsite_buildfile:,
16
+ previous_value:,
17
+ updated_value:)
18
+ declaration_details = property_value_finder.property_details(
19
+ property_name: property_name,
20
+ callsite_buildfile: callsite_buildfile
21
+ )
22
+ declaration_string = declaration_details.fetch(:declaration_string)
23
+ filename = declaration_details.fetch(:file)
24
+
25
+ file_to_update = dependency_files.find { |f| f.name == filename }
26
+ updated_content = file_to_update.content.sub(
27
+ declaration_string,
28
+ declaration_string.sub(previous_value, updated_value)
29
+ )
30
+
31
+ updated_files = dependency_files.dup
32
+ updated_files[updated_files.index(file_to_update)] =
33
+ update_file(file: file_to_update, content: updated_content)
34
+
35
+ updated_files
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :dependency_files
41
+
42
+ def property_value_finder
43
+ @property_value_finder ||=
44
+ Gradle::FileParser::PropertyValueFinder.
45
+ new(dependency_files: dependency_files)
46
+ end
47
+
48
+ def update_file(file:, content:)
49
+ updated_file = file.dup
50
+ updated_file.content = content
51
+ updated_file
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "nokogiri"
4
+ require "dependabot/metadata_finders/base"
5
+ require "dependabot/file_fetchers/base"
6
+ require "dependabot/gradle/file_parser/repositories_finder"
7
+
8
+ module Dependabot
9
+ module Gradle
10
+ class MetadataFinder < Dependabot::MetadataFinders::Base
11
+ DOT_SEPARATOR_REGEX = %r{\.(?:(?!\d+[.\/])+)}.freeze
12
+ PROPERTY_REGEX = /\$\{(?<property>.*?)\}/.freeze
13
+
14
+ private
15
+
16
+ def look_up_source
17
+ tmp_source = look_up_source_in_pom(dependency_pom_file)
18
+ return tmp_source if tmp_source
19
+
20
+ return unless (parent = parent_pom_file(dependency_pom_file))
21
+
22
+ tmp_source = look_up_source_in_pom(parent)
23
+ return unless tmp_source
24
+
25
+ artifact = dependency.name.split(":").last
26
+ return tmp_source if tmp_source.repo.end_with?(artifact)
27
+ return tmp_source if repo_has_subdir_for_dep?(tmp_source)
28
+ end
29
+
30
+ def repo_has_subdir_for_dep?(tmp_source)
31
+ @repo_has_subdir_for_dep ||= {}
32
+ if @repo_has_subdir_for_dep.key?(tmp_source)
33
+ return @repo_has_subdir_for_dep[tmp_source]
34
+ end
35
+
36
+ artifact = dependency.name.split(":").last
37
+ fetcher =
38
+ FileFetchers::Base.new(source: tmp_source, credentials: credentials)
39
+
40
+ @repo_has_subdir_for_dep[tmp_source] =
41
+ fetcher.send(:repo_contents, raise_errors: false).
42
+ select { |f| f.type == "dir" }.
43
+ any? { |f| artifact.end_with?(f.name) }
44
+ rescue Dependabot::RepoNotFound
45
+ @repo_has_subdir_for_dep[tmp_source] = false
46
+ end
47
+
48
+ def look_up_source_in_pom(pom)
49
+ potential_source_urls = [
50
+ pom.at_css("project > url")&.content,
51
+ pom.at_css("project > scm > url")&.content,
52
+ pom.at_css("project > issueManagement > url")&.content
53
+ ].compact
54
+
55
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
56
+ source_url ||= source_from_anywhere_in_pom(pom)
57
+ source_url = substitute_property_in_source_url(source_url, pom)
58
+
59
+ Source.from_url(source_url)
60
+ end
61
+
62
+ def substitute_property_in_source_url(source_url, pom)
63
+ return unless source_url
64
+ return source_url unless source_url.include?("${")
65
+
66
+ regex = PROPERTY_REGEX
67
+ property_name = source_url.match(regex).named_captures["property"]
68
+ doc = pom.dup
69
+ doc.remove_namespaces!
70
+ nm = property_name.sub(/^pom\./, "").sub(/^project\./, "")
71
+ property_value =
72
+ loop do
73
+ candidate_node =
74
+ doc.at_xpath("/project/#{nm}") ||
75
+ doc.at_xpath("/project/properties/#{nm}") ||
76
+ doc.at_xpath("/project/profiles/profile/properties/#{nm}")
77
+ break candidate_node.content if candidate_node
78
+ break unless nm.match?(DOT_SEPARATOR_REGEX)
79
+
80
+ nm = nm.sub(DOT_SEPARATOR_REGEX, "/")
81
+ end
82
+
83
+ source_url.gsub("${#{property_name}}", property_value)
84
+ end
85
+
86
+ def source_from_anywhere_in_pom(pom)
87
+ github_urls = []
88
+ pom.to_s.scan(Source::SOURCE_REGEX) do
89
+ github_urls << Regexp.last_match.to_s
90
+ end
91
+
92
+ github_urls.find do |url|
93
+ repo = Source.from_url(url).repo
94
+ repo.end_with?(dependency.name.split(":").last)
95
+ end
96
+ end
97
+
98
+ def dependency_pom_file
99
+ return @dependency_pom_file unless @dependency_pom_file.nil?
100
+
101
+ artifact_id = dependency.name.split(":").last
102
+ response = Excon.get(
103
+ "#{maven_repo_dependency_url}/"\
104
+ "#{dependency.version}/"\
105
+ "#{artifact_id}-#{dependency.version}.pom",
106
+ headers: auth_details,
107
+ idempotent: true,
108
+ **SharedHelpers.excon_defaults
109
+ )
110
+
111
+ @dependency_pom_file = Nokogiri::XML(response.body)
112
+ rescue Excon::Error::Timeout
113
+ @dependency_pom_file = Nokogiri::XML("")
114
+ end
115
+
116
+ def parent_pom_file(pom)
117
+ doc = pom.dup
118
+ doc.remove_namespaces!
119
+ group_id = doc.at_xpath("/project/parent/groupId")&.content&.strip
120
+ artifact_id =
121
+ doc.at_xpath("/project/parent/artifactId")&.content&.strip
122
+ version = doc.at_xpath("/project/parent/version")&.content&.strip
123
+
124
+ return unless artifact_id && group_id && version
125
+
126
+ response = Excon.get(
127
+ "#{maven_repo_url}/#{group_id.tr('.', '/')}/#{artifact_id}/"\
128
+ "#{version}/"\
129
+ "#{artifact_id}-#{version}.pom",
130
+ headers: auth_details,
131
+ idempotent: true,
132
+ **SharedHelpers.excon_defaults
133
+ )
134
+
135
+ Nokogiri::XML(response.body)
136
+ end
137
+
138
+ def maven_repo_url
139
+ source = dependency.requirements.
140
+ find { |r| r&.fetch(:source) }&.fetch(:source)
141
+
142
+ source&.fetch(:url, nil) ||
143
+ source&.fetch("url") ||
144
+ Gradle::FileParser::RepositoriesFinder::CENTRAL_REPO_URL
145
+ end
146
+
147
+ def maven_repo_dependency_url
148
+ group_id, artifact_id = dependency.name.split(":")
149
+
150
+ "#{maven_repo_url}/#{group_id.tr('.', '/')}/#{artifact_id}"
151
+ end
152
+
153
+ def auth_details
154
+ cred =
155
+ credentials.select { |c| c["type"] == "maven_repository" }.
156
+ find do |c|
157
+ cred_url = c.fetch("url").gsub(%r{/+$}, "")
158
+ next false unless cred_url == maven_repo_url
159
+
160
+ c.fetch("username", nil)
161
+ end
162
+
163
+ return {} unless cred
164
+
165
+ token = cred.fetch("username") + ":" + cred.fetch("password")
166
+ encoded_token = Base64.encode64(token).delete("\n")
167
+ { "Authorization" => "Basic #{encoded_token}" }
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ Dependabot::MetadataFinders.
174
+ register("gradle", Dependabot::Gradle::MetadataFinder)