dependabot-gradle 0.394.0 → 0.395.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: 5217463d5379b1769d013ce040af9eac03e0eb47e524505c86e1d3d67fc6de48
4
+ data.tar.gz: 739c83f7271742fe92942c8394c874c06c3d3b470003f6f636703d7ec76eeee6
5
5
  SHA512:
6
- metadata.gz: '0018bf3c1476bddff7bf4871dfec0c31556f2b9e57a517db8e1c14aa8a86d3b73441a9a91f0aebddd6c2049087414e432c1517fd3cd2f6ee76596103753c6a86'
7
- data.tar.gz: 1f2006691565c7ba348cd2256250f941ec5f27a982c1cda3b2669d362b6522c3765a1a9c4839f76302d8c577bae454be330906a7778b2b8f7a491c73db1791d2
6
+ metadata.gz: 2745a7a32084148fd05b96a323c0cc8ec428af6b06de8a8e12ad756b11c27f18f967f97475aa1eb1a143ffffe70e1ca06ca090651d4ed8b7ec85c9f9722b9ebf
7
+ data.tar.gz: 9273e6463194c037fab4594e48e92db6d727e1c7316501b43cca19930e6244aa2c9bdb3acff72e50f42fdb54b815d495381782ec1b527eac8d2d96dadba2441f
@@ -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
@@ -254,31 +260,70 @@ module Dependabot
254
260
  # single line.
255
261
  file = T.must(requirement.file)
256
262
  buildfile = T.must(buildfiles.find { |f| f.name == file })
263
+ content = T.must(buildfile.content)
264
+ substitution_ranges = substitution_block_line_ranges(content, kotlin: file.end_with?(".kts"))
265
+
266
+ content.lines.each_with_index.filter_map do |line, index|
267
+ next if substitution_ranges.any? { |range| range.cover?(index) }
257
268
 
258
- T.must(buildfile.content).lines.select do |line|
259
- line = evaluate_properties(line, buildfile)
260
- line = line.gsub(%r{(?<=^|\s)//.*$}, "")
269
+ evaluated = evaluate_properties(line, buildfile)
270
+ evaluated = evaluated.gsub(%r{(?<=^|\s)//.*$}, "")
261
271
 
262
272
  if dependency.name.include?(":")
263
273
  dep_parts = dependency.name.split(":")
264
- next false unless line.include?(T.must(dep_parts.first)) || line.include?(T.must(dep_parts.last))
274
+ next unless evaluated.include?(T.must(dep_parts.first)) || evaluated.include?(T.must(dep_parts.last))
265
275
  elsif file.end_with?(".properties")
266
276
  property = requirement.source_string("property")
267
- next false unless property && line.start_with?(property)
277
+ next unless property && evaluated.start_with?(property)
268
278
  elsif file.end_with?(".toml")
269
- next false unless line.include?(dependency.name)
279
+ next unless evaluated.include?(dependency.name)
270
280
  else
271
281
  name_regex_value = /['"]#{Regexp.quote(dependency.name)}['"]/
272
282
  name_regex = /(id|kotlin)(\s+#{name_regex_value}|\(#{name_regex_value}\))/
273
- next false unless line.match?(name_regex)
283
+ next unless evaluated.match?(name_regex)
274
284
  end
275
285
 
276
- line.include?(T.must(requirement.requirement_string))
286
+ line if evaluated.include?(T.must(requirement.requirement_string))
277
287
  end
278
288
  end
279
289
  # rubocop:enable Metrics/AbcSize
280
290
  # rubocop:enable Metrics/PerceivedComplexity
281
291
 
292
+ # Returns the (0-based) line-index ranges covered by dependencySubstitution
293
+ # blocks, so lines inside them can be excluded regardless of formatting.
294
+ # Braces inside strings/comments are masked so the matching closing brace
295
+ # is located correctly.
296
+ sig { params(content: String, kotlin: T::Boolean).returns(T::Array[T::Range[Integer]]) }
297
+ def substitution_block_line_ranges(content, kotlin: false)
298
+ ranges = T.let([], T::Array[T::Range[Integer]])
299
+ masked = Gradle::FileParser.mask_literals_and_comments(content, kotlin: kotlin)
300
+
301
+ masked.to_enum(:scan, SUBSTITUTION_BLOCK_START_REGEX).each do
302
+ match = T.must(Regexp.last_match)
303
+ start_offset = match.begin(0)
304
+ close_offset = match.end(0) + closing_bracket_index(T.must(masked[match.end(0)..]))
305
+
306
+ start_line = T.must(masked[0...start_offset]).count("\n")
307
+ end_line = T.must(masked[0..close_offset]).count("\n")
308
+ ranges << (start_line..end_line)
309
+ end
310
+
311
+ ranges
312
+ end
313
+
314
+ sig { params(string: String).returns(Integer) }
315
+ def closing_bracket_index(string)
316
+ closes_required = 1
317
+
318
+ string.chars.each_with_index do |char, index|
319
+ closes_required += 1 if char == "{"
320
+ closes_required -= 1 if char == "}"
321
+ return index if closes_required.zero?
322
+ end
323
+
324
+ 0
325
+ end
326
+
282
327
  sig { params(string: String, buildfile: Dependabot::DependencyFile).returns(String) }
283
328
  def evaluate_properties(string, buildfile)
284
329
  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.395.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.395.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.395.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.395.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.395.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.395.0
295
295
  rdoc_options: []
296
296
  require_paths:
297
297
  - lib