dependabot-gradle 0.394.0 → 0.396.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f0d3b612852682c36ce3d0d29211249cd1b7e1a115e24c4a1578095eac1ca6ad
4
- data.tar.gz: '081d98ee31543ed861252a52278c594d71ee08d041db794325f1f080b3dfae4f'
3
+ metadata.gz: '091e3f48c1ef779b93c429cdbcbae77eb751854ebdea7b2f354283d14e76b0ea'
4
+ data.tar.gz: bd6a1439e622474c62d724cac0161f953c41451b8509b0b84abde59aeb8c3cf6
5
5
  SHA512:
6
- metadata.gz: '0018bf3c1476bddff7bf4871dfec0c31556f2b9e57a517db8e1c14aa8a86d3b73441a9a91f0aebddd6c2049087414e432c1517fd3cd2f6ee76596103753c6a86'
7
- data.tar.gz: 1f2006691565c7ba348cd2256250f941ec5f27a982c1cda3b2669d362b6522c3765a1a9c4839f76302d8c577bae454be330906a7778b2b8f7a491c73db1791d2
6
+ metadata.gz: 3dea639bb879078a4f3f373e6e8cf30cdc2abe5b993e66eb40c24e8d27ce3f788e4929e7b8c3fd0635c677c86fa6d8b863eee1b998b2eceee120fb7b7ddf9be3
7
+ data.tar.gz: 4a188fc94783c49e1900f67d4390309876d6080ad41b5ab5ef4f527b7f730291d285c6d34e23ec23880f7a23b0c7b4709326a572eddda702bd33847e9ab74b47
@@ -91,17 +91,12 @@ module Dependabot
91
91
  files += subproject_buildfiles(root_dir)
92
92
  files += subproject_lockfiles(root_dir)
93
93
  files += dependency_script_plugins(root_dir)
94
- files += convention_plugin_source_files(files, root_dir) if fetch_plugin_sources_for_lockfiles?
94
+ files += convention_plugin_source_files(files, root_dir)
95
95
 
96
96
  files + included_builds(root_dir)
97
97
  .flat_map { |dir| all_buildfiles_in_build(dir) }
98
98
  end
99
99
 
100
- sig { returns(T::Boolean) }
101
- def fetch_plugin_sources_for_lockfiles?
102
- Dependabot::Experiments.enabled?(:gradle_lockfile_updater)
103
- end
104
-
105
100
  # Only fetch source trees for *nested* project directories (e.g. "build-logic/convention"
106
101
  # or "included/convention"), plus the root of a non-top-level build (e.g. "build-logic" or
107
102
  # "buildSrc" itself), never the top-level repo root ("app" style modules of the outermost
@@ -44,6 +44,7 @@ module Dependabot
44
44
  DEPENDENCY_SET_ENTRY_REGEX = /entry\s+['"](?<name>#{PART})['"]/o
45
45
  PLUGIN_BLOCK_DECLARATION_REGEX = /(?:^|\s)plugins\s*\{/
46
46
  PLUGIN_ID_REGEX = /['"](?<id>#{PART})['"]/o
47
+ DEPENDENCY_SUBSTITUTION_DECLARATION_REGEX = /\bdependencySubstitution\s*\{/
47
48
 
48
49
  sig { override.returns(T::Array[Dependabot::Dependency]) }
49
50
  def parse
@@ -85,6 +86,153 @@ module Dependabot
85
86
  .filter_map { |f| dependency_files.find { |bf| bf.name == f } }
86
87
  end
87
88
 
89
+ # Replaces the contents of string literals and comments with spaces,
90
+ # preserving the overall length and newline positions. This lets callers
91
+ # locate structural `{`/`}` braces without being confused by braces that
92
+ # appear inside quoted strings (e.g. `because("see issue {")`), Groovy
93
+ # slashy/dollar-slashy strings (`/.../`, `$/.../$`) or comments.
94
+ #
95
+ # `kotlin` selects the build-file dialect: Kotlin (`.gradle.kts`) allows
96
+ # nested block comments but has no slashy strings, whereas Groovy
97
+ # (`.gradle`) has slashy strings but non-nested block comments.
98
+ sig { params(content: String, kotlin: T::Boolean).returns(String) }
99
+ def self.mask_literals_and_comments(content, kotlin: false)
100
+ masked = content.dup
101
+ index = 0
102
+ length = content.length
103
+
104
+ while index < length
105
+ stop = literal_or_comment_end(content, index, length, kotlin: kotlin)
106
+ if stop
107
+ mask_region!(masked, content, index, stop)
108
+ index = stop
109
+ else
110
+ index += 1
111
+ end
112
+ end
113
+
114
+ masked
115
+ end
116
+
117
+ # Returns the index just past a string literal or comment that starts at
118
+ # `index`, or `nil` when `index` is not the start of one.
119
+ sig { params(content: String, index: Integer, length: Integer, kotlin: T::Boolean).returns(T.nilable(Integer)) }
120
+ def self.literal_or_comment_end(content, index, length, kotlin: false) # rubocop:disable Metrics/PerceivedComplexity
121
+ three = content[index, 3]
122
+ two = content[index, 2]
123
+ char = T.must(content[index])
124
+
125
+ if three == '"""' || three == "'''"
126
+ close = content.index(three, index + 3)
127
+ close ? close + 3 : length
128
+ elsif !kotlin && two == "$/"
129
+ dollar_slashy_string_end(content, index, length)
130
+ elsif char == '"' || char == "'"
131
+ single_quote_string_end(content, index, char, length)
132
+ elsif two == "//"
133
+ content.index("\n", index) || length
134
+ elsif two == "/*"
135
+ block_comment_end(content, index, length, nested: kotlin)
136
+ elsif !kotlin && char == "/" && slashy_string_start?(content, index)
137
+ slashy_string_end(content, index, length)
138
+ end
139
+ end
140
+
141
+ # Finds the index just past the closing `*/` of a block comment starting at
142
+ # `start_index`. Kotlin (`.gradle.kts`) permits nested `/*`/`*/` pairs;
143
+ # Groovy closes at the first `*/`.
144
+ sig { params(content: String, start_index: Integer, length: Integer, nested: T::Boolean).returns(Integer) }
145
+ def self.block_comment_end(content, start_index, length, nested: false)
146
+ depth = 1
147
+ stop = start_index + 2
148
+ while stop < length
149
+ pair = content[stop, 2]
150
+ if nested && pair == "/*"
151
+ depth += 1
152
+ stop += 2
153
+ elsif pair == "*/"
154
+ depth -= 1
155
+ stop += 2
156
+ return stop if depth.zero?
157
+ else
158
+ stop += 1
159
+ end
160
+ end
161
+ length
162
+ end
163
+
164
+ # Finds the index just past the closing quote of a single-quoted (`'` or
165
+ # `"`) string literal starting at `start_index`, honouring backslash escapes.
166
+ sig { params(content: String, start_index: Integer, quote: String, length: Integer).returns(Integer) }
167
+ def self.single_quote_string_end(content, start_index, quote, length)
168
+ stop = start_index + 1
169
+ while stop < length
170
+ char = content[stop]
171
+ if char == "\\"
172
+ stop += 2
173
+ elsif char == quote
174
+ return stop + 1
175
+ else
176
+ stop += 1
177
+ end
178
+ end
179
+ stop
180
+ end
181
+
182
+ # Finds the index just past the closing `/` of a Groovy slashy string
183
+ # (`/.../`) starting at `start_index`. Only `\/` escapes the delimiter.
184
+ sig { params(content: String, start_index: Integer, length: Integer).returns(Integer) }
185
+ def self.slashy_string_end(content, start_index, length)
186
+ stop = start_index + 1
187
+ while stop < length
188
+ if content[stop] == "\\" && content[stop + 1] == "/"
189
+ stop += 2
190
+ elsif content[stop] == "/"
191
+ return stop + 1
192
+ else
193
+ stop += 1
194
+ end
195
+ end
196
+ stop
197
+ end
198
+
199
+ # Finds the index just past the closing `/$` of a Groovy dollar-slashy
200
+ # string (`$/.../$`) starting at `start_index`. `$$` and `$/` are escapes.
201
+ sig { params(content: String, start_index: Integer, length: Integer).returns(Integer) }
202
+ def self.dollar_slashy_string_end(content, start_index, length)
203
+ stop = start_index + 2
204
+ while stop < length
205
+ pair = content[stop, 2]
206
+ if pair == "$$" || pair == "$/"
207
+ stop += 2
208
+ elsif pair == "/$"
209
+ return stop + 2
210
+ else
211
+ stop += 1
212
+ end
213
+ end
214
+ stop
215
+ end
216
+
217
+ # A `/` begins a slashy string (rather than a division operator) only when
218
+ # a value is expected, i.e. the previous non-space character is not the end
219
+ # of an identifier, number, or closing bracket.
220
+ sig { params(content: String, index: Integer).returns(T::Boolean) }
221
+ def self.slashy_string_start?(content, index)
222
+ position = index - 1
223
+ position -= 1 while position >= 0 && (content[position] == " " || content[position] == "\t")
224
+ return true if position.negative?
225
+
226
+ !T.must(content[position]).match?(/[\w)\]}]/)
227
+ end
228
+
229
+ sig { params(masked: String, original: String, start_index: Integer, stop_index: Integer).void }
230
+ def self.mask_region!(masked, original, start_index, stop_index)
231
+ (start_index...stop_index).each do |position|
232
+ masked[position] = original[position] == "\n" ? "\n" : " "
233
+ end
234
+ end
235
+
88
236
  sig { returns(Ecosystem) }
89
237
  def ecosystem
90
238
  @ecosystem ||= T.let(
@@ -510,11 +658,22 @@ module Dependabot
510
658
 
511
659
  sig { params(buildfile: Dependabot::DependencyFile).returns(String) }
512
660
  def prepared_content(buildfile)
661
+ # Remove any dependencySubstitution blocks first, before the comment
662
+ # regexes below run. The coordinates inside `substitute module(...) using
663
+ # module(...)` rules are substitution targets, not real dependency
664
+ # declarations, and must not be updated. Braces inside strings/comments
665
+ # are masked so the matching closing brace is located correctly, and each
666
+ # block is deleted by its exact offsets. Doing this before the comment
667
+ # stripping keeps string literals intact so the masker can see them.
668
+ prepared_content = remove_dependency_substitution_blocks(
669
+ T.must(buildfile.content),
670
+ kotlin: buildfile.name.end_with?(".kts")
671
+ )
672
+
513
673
  # Remove any comments
514
- prepared_content =
515
- T.must(buildfile.content)
516
- .gsub(%r{(?<=^|\s)//.*$}, "\n")
517
- .gsub(%r{(?<=^|\s)/\*.*?\*/}m, "")
674
+ prepared_content = prepared_content
675
+ .gsub(%r{(?<=^|\s)//.*$}, "\n")
676
+ .gsub(%r{(?<=^|\s)/\*.*?\*/}m, "")
518
677
 
519
678
  # Remove the dependencyVerification section added by Gradle Witness
520
679
  # (TODO: Support updating this in the FileUpdater)
@@ -527,6 +686,21 @@ module Dependabot
527
686
  prepared_content
528
687
  end
529
688
 
689
+ sig { params(content: String, kotlin: T::Boolean).returns(String) }
690
+ def remove_dependency_substitution_blocks(content, kotlin: false)
691
+ result = content.dup
692
+ masked = FileParser.mask_literals_and_comments(content, kotlin: kotlin)
693
+ block_ranges = T.let([], T::Array[T::Range[Integer]])
694
+ masked.to_enum(:scan, DEPENDENCY_SUBSTITUTION_DECLARATION_REGEX).each do
695
+ mtch = T.must(Regexp.last_match)
696
+ start_index = mtch.begin(0)
697
+ end_index = mtch.end(0) + closing_bracket_index(T.must(masked[mtch.end(0)..]))
698
+ block_ranges << (start_index..end_index)
699
+ end
700
+ block_ranges.reverse_each { |range| result[range] = "" }
701
+ result
702
+ end
703
+
530
704
  sig { params(string: String).returns(Integer) }
531
705
  def closing_bracket_index(string)
532
706
  closes_required = 1
@@ -19,6 +19,12 @@ module Dependabot
19
19
 
20
20
  SUPPORTED_BUILD_FILE_NAMES = %w(build.gradle build.gradle.kts).freeze
21
21
 
22
+ # Matches the start of a Gradle dependency substitution block, e.g.
23
+ # resolutionStrategy.dependencySubstitution {
24
+ # Coordinates inside such a block are substitution targets, not real
25
+ # dependency declarations, and must never be rewritten.
26
+ SUBSTITUTION_BLOCK_START_REGEX = /\bdependencySubstitution\s*\{/
27
+
22
28
  sig { override.returns(T::Array[::Dependabot::DependencyFile]) }
23
29
  def updated_dependency_files
24
30
  updated_files = buildfiles.dup
@@ -108,9 +114,7 @@ module Dependabot
108
114
  updated_files = wrapper_updater.update_files(buildfile)
109
115
  replace_updated_files(files, updated_files)
110
116
  end
111
- if Dependabot::Experiments.enabled?(:gradle_lockfile_updater)
112
- update_lockfiles_for_buildfiles(files, buildfiles_processed)
113
- end
117
+ update_lockfiles_for_buildfiles(files, buildfiles_processed)
114
118
 
115
119
  files
116
120
  end
@@ -254,31 +258,70 @@ module Dependabot
254
258
  # single line.
255
259
  file = T.must(requirement.file)
256
260
  buildfile = T.must(buildfiles.find { |f| f.name == file })
261
+ content = T.must(buildfile.content)
262
+ substitution_ranges = substitution_block_line_ranges(content, kotlin: file.end_with?(".kts"))
263
+
264
+ content.lines.each_with_index.filter_map do |line, index|
265
+ next if substitution_ranges.any? { |range| range.cover?(index) }
257
266
 
258
- T.must(buildfile.content).lines.select do |line|
259
- line = evaluate_properties(line, buildfile)
260
- line = line.gsub(%r{(?<=^|\s)//.*$}, "")
267
+ evaluated = evaluate_properties(line, buildfile)
268
+ evaluated = evaluated.gsub(%r{(?<=^|\s)//.*$}, "")
261
269
 
262
270
  if dependency.name.include?(":")
263
271
  dep_parts = dependency.name.split(":")
264
- next false unless line.include?(T.must(dep_parts.first)) || line.include?(T.must(dep_parts.last))
272
+ next unless evaluated.include?(T.must(dep_parts.first)) || evaluated.include?(T.must(dep_parts.last))
265
273
  elsif file.end_with?(".properties")
266
274
  property = requirement.source_string("property")
267
- next false unless property && line.start_with?(property)
275
+ next unless property && evaluated.start_with?(property)
268
276
  elsif file.end_with?(".toml")
269
- next false unless line.include?(dependency.name)
277
+ next unless evaluated.include?(dependency.name)
270
278
  else
271
279
  name_regex_value = /['"]#{Regexp.quote(dependency.name)}['"]/
272
280
  name_regex = /(id|kotlin)(\s+#{name_regex_value}|\(#{name_regex_value}\))/
273
- next false unless line.match?(name_regex)
281
+ next unless evaluated.match?(name_regex)
274
282
  end
275
283
 
276
- line.include?(T.must(requirement.requirement_string))
284
+ line if evaluated.include?(T.must(requirement.requirement_string))
277
285
  end
278
286
  end
279
287
  # rubocop:enable Metrics/AbcSize
280
288
  # rubocop:enable Metrics/PerceivedComplexity
281
289
 
290
+ # Returns the (0-based) line-index ranges covered by dependencySubstitution
291
+ # blocks, so lines inside them can be excluded regardless of formatting.
292
+ # Braces inside strings/comments are masked so the matching closing brace
293
+ # is located correctly.
294
+ sig { params(content: String, kotlin: T::Boolean).returns(T::Array[T::Range[Integer]]) }
295
+ def substitution_block_line_ranges(content, kotlin: false)
296
+ ranges = T.let([], T::Array[T::Range[Integer]])
297
+ masked = Gradle::FileParser.mask_literals_and_comments(content, kotlin: kotlin)
298
+
299
+ masked.to_enum(:scan, SUBSTITUTION_BLOCK_START_REGEX).each do
300
+ match = T.must(Regexp.last_match)
301
+ start_offset = match.begin(0)
302
+ close_offset = match.end(0) + closing_bracket_index(T.must(masked[match.end(0)..]))
303
+
304
+ start_line = T.must(masked[0...start_offset]).count("\n")
305
+ end_line = T.must(masked[0..close_offset]).count("\n")
306
+ ranges << (start_line..end_line)
307
+ end
308
+
309
+ ranges
310
+ end
311
+
312
+ sig { params(string: String).returns(Integer) }
313
+ def closing_bracket_index(string)
314
+ closes_required = 1
315
+
316
+ string.chars.each_with_index do |char, index|
317
+ closes_required += 1 if char == "{"
318
+ closes_required -= 1 if char == "}"
319
+ return index if closes_required.zero?
320
+ end
321
+
322
+ 0
323
+ end
324
+
282
325
  sig { params(string: String, buildfile: Dependabot::DependencyFile).returns(String) }
283
326
  def evaluate_properties(string, buildfile)
284
327
  result = string.dup
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dependabot-gradle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.394.0
4
+ version: 0.396.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
@@ -15,28 +15,28 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.394.0
18
+ version: 0.396.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.394.0
25
+ version: 0.396.0
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dependabot-maven
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
30
  - - '='
31
31
  - !ruby/object:Gem::Version
32
- version: 0.394.0
32
+ version: 0.396.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - '='
38
38
  - !ruby/object:Gem::Version
39
- version: 0.394.0
39
+ version: 0.396.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: debug
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -192,19 +192,19 @@ dependencies:
192
192
  - !ruby/object:Gem::Version
193
193
  version: '1.1'
194
194
  - !ruby/object:Gem::Dependency
195
- name: turbo_tests
195
+ name: turbo_tests2
196
196
  requirement: !ruby/object:Gem::Requirement
197
197
  requirements:
198
198
  - - "~>"
199
199
  - !ruby/object:Gem::Version
200
- version: 2.2.5
200
+ version: 3.2.7
201
201
  type: :development
202
202
  prerelease: false
203
203
  version_requirements: !ruby/object:Gem::Requirement
204
204
  requirements:
205
205
  - - "~>"
206
206
  - !ruby/object:Gem::Version
207
- version: 2.2.5
207
+ version: 3.2.7
208
208
  - !ruby/object:Gem::Dependency
209
209
  name: vcr
210
210
  requirement: !ruby/object:Gem::Requirement
@@ -291,7 +291,7 @@ licenses:
291
291
  - MIT
292
292
  metadata:
293
293
  bug_tracker_uri: https://github.com/dependabot/dependabot-core/issues
294
- changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.394.0
294
+ changelog_uri: https://github.com/dependabot/dependabot-core/releases/tag/v0.396.0
295
295
  rdoc_options: []
296
296
  require_paths:
297
297
  - lib