Fix gradle bumping dependency substitutions issue - #16042
Conversation
There was a problem hiding this comment.
Pull request overview
Prevents Gradle dependency substitution rules from being parsed or rewritten as dependencies.
Changes:
- Strips
dependencySubstitutionblocks during parsing. - Skips single-line substitution rules during updates.
- Adds parser/updater regression coverage and a fixture.
Show a summary per file
| File | Description |
|---|---|
gradle/lib/dependabot/gradle/file_parser.rb |
Excludes substitution blocks from parsing. |
gradle/lib/dependabot/gradle/file_updater.rb |
Avoids rewriting detected substitution lines. |
gradle/spec/dependabot/gradle/file_parser_spec.rb |
Tests parser behavior. |
gradle/spec/dependabot/gradle/file_updater_spec.rb |
Tests preserving substitution rules. |
gradle/spec/fixtures/buildfiles/dependency_substitution.gradle |
Provides regression fixture. |
Review details
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
Review details
Suppressed comments (1)
gradle/lib/dependabot/gradle/file_updater.rb:267
- This excludes whole lines rather than only the substitution block. Gradle permits compact statements such as
resolutionStrategy.dependencySubstitution { ... }; dependencies { implementation "g:a:1" }; the parser preserves and discovers the real declaration after removing the exact block range, but the updater skips that shared line and raisesExpected content to change!. Please retain the character offsets and inspect/update only the portions outside substitution blocks so valid declarations on a boundary line remain updateable without rewriting the rule.
content.lines.each_with_index.filter_map do |line, index|
next if substitution_ranges.any? { |range| range.cover?(index) }
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Balanced
There was a problem hiding this comment.
Review details
Suppressed comments (4)
Previously missed (2) — in code that hasn't changed since the last review.
gradle/lib/dependabot/gradle/file_parser.rb:47
- This unbounded match also recognizes suffixes of unrelated identifiers, so a valid custom closure such as
notdependencySubstitution { implementation("g:n:1") }is deleted and its real dependency disappears from parsing. Require an identifier boundary before the Gradle API name while still allowing the dottedresolutionStrategy.dependencySubstitutionform.
This issue also appears on line 121 of the same file.
DEPENDENCY_SUBSTITUTION_DECLARATION_REGEX = /dependencySubstitution\s*\{/
gradle/lib/dependabot/gradle/file_updater.rb:26
- This has the same suffix false positive as the parser regex: a custom block named
notdependencySubstitutioncauses every declaration on its covered lines to be skipped by the updater. Add an identifier boundary so only the GradledependencySubstitutionmember is recognized.
This issue also appears on line 266 of the same file.
SUBSTITUTION_BLOCK_START_REGEX = /dependencySubstitution\s*\{/
gradle/lib/dependabot/gradle/file_updater.rb:267
- This line-based exclusion also drops real declarations that happen to share a line with the substitution block. For example, a valid compact script such as
configurations.all { resolutionStrategy.dependencySubstitution { ... } }; dependencies { implementation("g:n:1") }is parsed successfully, but the updater skips the entire line and raisesExpected content to change!. Track substitution character offsets and exclude only matches within those offsets rather than excluding whole lines.
content.lines.each_with_index.filter_map do |line, index|
next if substitution_ranges.any? { |range| range.cover?(index) }
gradle/lib/dependabot/gradle/file_parser.rb:123
- The first matching triple-quote sequence is not necessarily the delimiter: Groovy triple-quoted strings can contain an escaped delimiter such as
\""". In a substitution reason, this closes the mask early, exposes any following brace, and can make the block boundary fail so substitution coordinates are parsed or rewritten again. Locate an unescaped closing delimiter (accounting for an odd backslash run) instead of using the first rawindexmatch.
if three == '"""' || three == "'''"
close = content.index(three, index + 3)
close ? close + 3 : length
- Files reviewed: 5/5 changed files
- Comments generated: 0 new
- Review effort level: Balanced
59bcf64 to
ac40f26
Compare
There was a problem hiding this comment.
Copilot review overview
Review tier: Balanced
Findings: 1
New issues introduced by this change (3)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_parser.rb — This scanner combines incompatible Groovy and Kotlin lexical rules without knowing the build-file… |
|
gradle/lib/dependabot/gradle/file_parser.rb — This suffix match also treats unrelated custom Gradle closure names such as… |
|
gradle/lib/dependabot/gradle/file_updater.rb — This suffix match also marks unrelated custom Gradle closures such as `mydependencySubstitution {… |
Suppressed comments (2)
gradle/lib/dependabot/gradle/file_updater.rb:267
- The exclusion is line-based, so it also drops declarations outside the block when they share its opening or closing line. For example,
resolutionStrategy.dependencySubstitution {}; dependencies { implementation("g:a:1") }is valid and the parser still findsg:a:1, but this updater finds no declaration and raisesExpected content to change!. Exclude only the exact block spans (or mask those spans while preserving the remaining text on each line).
content.lines.each_with_index.filter_map do |line, index|
next if substitution_ranges.any? { |range| range.cover?(index) }
gradle/lib/dependabot/gradle/file_parser.rb:692
- Nested matches produce overlapping ranges, but deleting the inner range first shifts the string before the outer range is applied. The outer range then consumes content after its actual closing brace, potentially removing a following real dependency from parser input. Since removing an outer substitution block already removes nested occurrences, skip matches whose start is covered by a previously recorded outer range before mutating the string.
block_ranges.reverse_each { |range| result[range] = "" }
|
Fixed |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 2
New issues introduced by this change (2)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_updater.rb — This scan also runs for non-script requirements because buildfiles contains every non-support… |
|
gradle/lib/dependabot/gradle/file_parser.rb — This heuristic misclassifies valid no-parentheses Groovy calls such as because /reason {/: the… |
Issues resolved since last review (3)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_updater.rb — This suffix match also marks unrelated custom Gradle closures such as `mydependencySubstitution {… View resolved comment |
|
gradle/lib/dependabot/gradle/file_parser.rb — This suffix match also treats unrelated custom Gradle closure names such as… View resolved comment |
|
gradle/lib/dependabot/gradle/file_parser.rb — This scanner combines incompatible Groovy and Kotlin lexical rules without knowing the build-file… View resolved comment |
Suppressed comments (1)
gradle/lib/dependabot/gradle/file_updater.rb:267
- A range marks the entire opening and closing lines, not just the block's offsets. Valid one-line Groovy such as
resolutionStrategy.dependencySubstitution {}; dependencies { implementation "g:a:1" }is parsed as a real dependency after the block is removed, but this updater skips its only declaration and raisesExpected content to change!. Track/mask character spans on each line and update only declarations outside those spans; simply retaining the whole line would still let the later global replacement modify the substitution text.
next if substitution_ranges.any? { |range| range.cover?(index) }
kbukum1
left a comment
There was a problem hiding this comment.
I think fileparser now becamee more complicated. Not sure if there is better way for doing all parsing on the file. Maybe we should have helper or something instead of having bunch of if-elses. I am approving. If it is feasable it will be great to have follow-up to kind of proper helper utility where it helps for parsing properly.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_updater.rb — Skipping the entire covered line also skips real declarations that appear after a substitution… |
Issues resolved since last review (2)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_parser.rb — This heuristic misclassifies valid no-parentheses Groovy calls such as because /reason {/: the… View resolved comment |
|
gradle/lib/dependabot/gradle/file_updater.rb — This scan also runs for non-script requirements because buildfiles contains every non-support… View resolved comment |
Suppressed comments (1)
gradle/lib/dependabot/gradle/file_parser.rb:226
- This previous-character heuristic misses valid Groovy slashy strings after keywords. In
def reason = { return /tracking {/ }inside a substitution block, the opening slash follows theninreturn, so its{remains structural; the closing slash is then mistaken for a new unterminated slashy string, masking the real block close. Both callers consequently fail to exclude the substitution coordinates. Please recognize value-introducing keyword contexts such asreturn(or use a lexical scanner) and cover this case.
def self.slashy_string_start?(content, index)
position = index - 1
position -= 1 while position >= 0 && (content[position] == " " || content[position] == "\t")
return true if position.negative?
!T.must(content[position]).match?(/[\w)\]}]/)
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Balanced
Findings: 1
New issues introduced by this change (3)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_parser.rb — Valid Groovy call syntax also permits the closure inside parentheses, e.g.… |
|
gradle/lib/dependabot/gradle/file_updater.rb — The updater has the same gap for the valid Groovy form `resolutionStrategy.dependencySubstitution({… |
|
gradle/lib/dependabot/gradle/file_parser.rb — This treats any Groovy / after a newline as the start of a slashy string because newline is not… |
Pre-existing issues (1)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_updater.rb — Skipping the entire covered line also skips real declarations that appear after a substitution… View comment |
| DEPENDENCY_SET_ENTRY_REGEX = /entry\s+['"](?<name>#{PART})['"]/o | ||
| PLUGIN_BLOCK_DECLARATION_REGEX = /(?:^|\s)plugins\s*\{/ | ||
| PLUGIN_ID_REGEX = /['"](?<id>#{PART})['"]/o | ||
| DEPENDENCY_SUBSTITUTION_DECLARATION_REGEX = /\bdependencySubstitution\s*\{/ |
| # resolutionStrategy.dependencySubstitution { | ||
| # Coordinates inside such a block are substitution targets, not real | ||
| # dependency declarations, and must never be rewritten. | ||
| SUBSTITUTION_BLOCK_START_REGEX = /\bdependencySubstitution\s*\{/ |
| position = index - 1 | ||
| position -= 1 while position >= 0 && (content[position] == " " || content[position] == "\t") | ||
| return true if position.negative? | ||
|
|
||
| !T.must(content[position]).match?(/[\w)\]}]/) |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Balanced
Findings: 3
Pre-existing issues (3)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_parser.rb — This treats any Groovy / after a newline as the start of a slashy string because newline is not… View comment |
|
gradle/lib/dependabot/gradle/file_updater.rb — The updater has the same gap for the valid Groovy form `resolutionStrategy.dependencySubstitution({… View comment |
|
gradle/lib/dependabot/gradle/file_parser.rb — Valid Groovy call syntax also permits the closure inside parentheses, e.g.… View comment |
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
gradle/lib/dependabot/gradle/file_updater.rb — Skipping the entire covered line also skips real declarations that appear after a substitution… View resolved comment |
Suppressed comments (3)
gradle/lib/dependabot/gradle/file_parser.rb:47
- Valid Gradle/Groovy DSL can pass the closure in parentheses (
resolutionStrategy.dependencySubstitution({ ... })). This regex does not match the({form, so those substitution coordinates remain inprepared_contentand are still parsed as dependencies. Accept an optional opening parenthesis before the block brace.
DEPENDENCY_SUBSTITUTION_DECLARATION_REGEX = /\bdependencySubstitution\s*\{/
gradle/lib/dependabot/gradle/file_updater.rb:26
- The parser-side fix also needs to be mirrored here:
dependencySubstitution({ ... })is valid Gradle/Groovy syntax, but this pattern misses it. The updater will therefore treat lines in that block as ordinary declarations and can still rewrite the source coordinate.
SUBSTITUTION_BLOCK_START_REGEX = /\bdependencySubstitution\s*\{/
gradle/lib/dependabot/gradle/file_updater.rb:286
- Returning an allowed line does not confine the later replacement to that occurrence:
update_version_in_buildfilecallscontent.gsub(declaration, ...)globally. With valid multiline forms such asimplementation(\n "g:a:1"\n)andsubstitute module(\n "g:a:1"\n), identically indented coordinate lines cause the real declaration selected here to rewrite the matching line inside the excluded substitution block too. Carry occurrence offsets/ranges into the replacement (or mask protected ranges during replacement) instead of returning raw line text for a globalgsub.
line if evaluated.include?(T.must(requirement.requirement_string))


What are you trying to accomplish?
Fixes #15423.
Gradle
resolutionStrategy.dependencySubstitutionrules use the same coordinate syntax as real dependencies, so Dependabot treated the substituted (source) coordinate as a dependency. A rule like:got rewritten to
substitute module("...:2.0.3") using module("...:2.0.3")— collapsing the substitution into a no-op.This PR makes Dependabot ignore substitution rules:
prepared_contentstripsdependencySubstitution { ... }blocks, so no coordinate inside them is parsed as a dependency.original_buildfile_declarationsskips declarations whose source line is inside a substitution block, so a real dependency sharing the same coordinate/version isn't rewritten into the substitution rule.Both share
FileParser.mask_literals_and_comments, which blanks the contents of string literals and comments (preserving length and newline positions) so brace counting finds the real block boundaries. It handles quoted strings, Groovy slashy/dollar-slashy strings, and line/nested block comments, and blocks are removed before the legacy comment stripping runs.Anything you want to highlight for special attention from reviewers?
resolutionStrategy.dependencySubstitution {.substitute(...).using(...)formatting is handled, not just single-line rules.gsub, which also handles emptydependencySubstitution {}blocks.How will you know you've accomplished your goal?
//inside a string, comment braces, a nested block comment, and an empty block.Checklist